mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
FlameGraph: Add prop to keep focused items when the profile data changes (#98356)
This commit is contained in:
@@ -27,7 +27,7 @@ import { Flamegraph } from '@grafana/flamegraph';
|
|||||||
#### Props
|
#### Props
|
||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| ------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| data | DataFrame | DataFrame with the profile data. Optional, if missing or empty the flamegraph is not rendered |
|
| data | DataFrame | DataFrame with the profile data. Optional, if missing or empty the flamegraph is not rendered |
|
||||||
| stickyHeader | boolean | Whether the header should be sticky and be always visible on the top when scrolling. |
|
| stickyHeader | boolean | Whether the header should be sticky and be always visible on the top when scrolling. |
|
||||||
| getTheme | () => GrafanaTheme2 | Provides a theme for the visualization on which colors and some sizes are based. |
|
| getTheme | () => GrafanaTheme2 | Provides a theme for the visualization on which colors and some sizes are based. |
|
||||||
@@ -37,6 +37,7 @@ import { Flamegraph } from '@grafana/flamegraph';
|
|||||||
| onTableSort | (sort: string) => void | Interaction hook that can be used to report on the interaction. Fires when user changes the teble sorting. |
|
| onTableSort | (sort: string) => void | Interaction hook that can be used to report on the interaction. Fires when user changes the teble sorting. |
|
||||||
| extraHeaderElements | React.ReactNode | Elements that will be shown in the header on the right side of the header buttons. Useful for additional functionality. |
|
| extraHeaderElements | React.ReactNode | Elements that will be shown in the header on the right side of the header buttons. Useful for additional functionality. |
|
||||||
| vertical | boolean | If true the flamegraph will be rendered on top of the table. |
|
| vertical | boolean | If true the flamegraph will be rendered on top of the table. |
|
||||||
|
| keepFocusOnDataChange | boolean | If true any focused block will stay focused when the profile data changes. Same for the sandwich view. |
|
||||||
|
|
||||||
##### DataFrame schema
|
##### DataFrame schema
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,10 @@ const FlameGraph = ({
|
|||||||
search,
|
search,
|
||||||
selectedView,
|
selectedView,
|
||||||
};
|
};
|
||||||
const canvas = levelsCallers ? (
|
let canvas = null;
|
||||||
|
|
||||||
|
if (levelsCallers?.length) {
|
||||||
|
canvas = (
|
||||||
<>
|
<>
|
||||||
<div className={styles.sandwichCanvasWrapper}>
|
<div className={styles.sandwichCanvasWrapper}>
|
||||||
<div className={styles.sandwichMarker}>
|
<div className={styles.sandwichMarker}>
|
||||||
@@ -167,9 +170,12 @@ const FlameGraph = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
);
|
||||||
|
} else if (levels?.length) {
|
||||||
|
canvas = (
|
||||||
<FlameGraphCanvas {...commonCanvasProps} root={levels[0][0]} depth={levels.length} direction={'children'} />
|
<FlameGraphCanvas {...commonCanvasProps} root={levels[0][0]} depth={levels.length} direction={'children'} />
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.graph}>
|
<div className={styles.graph}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
|||||||
import { memo, ReactNode } from 'react';
|
import { memo, ReactNode } from 'react';
|
||||||
|
|
||||||
import { getValueFormat, GrafanaTheme2 } from '@grafana/data';
|
import { getValueFormat, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Icon, IconButton, useStyles2 } from '@grafana/ui';
|
import { Icon, IconButton, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { ClickedItemData } from '../types';
|
import { ClickedItemData } from '../types';
|
||||||
|
|
||||||
@@ -42,7 +42,8 @@ const FlameGraphMetadata = memo(
|
|||||||
|
|
||||||
if (sandwichedLabel) {
|
if (sandwichedLabel) {
|
||||||
parts.push(
|
parts.push(
|
||||||
<span key={'sandwich'}>
|
<Tooltip key={'sandwich'} content={sandwichedLabel} placement="top">
|
||||||
|
<div>
|
||||||
<Icon size={'sm'} name={'angle-right'} />
|
<Icon size={'sm'} name={'angle-right'} />
|
||||||
<div className={styles.metadataPill}>
|
<div className={styles.metadataPill}>
|
||||||
<Icon size={'sm'} name={'gf-show-context'} />{' '}
|
<Icon size={'sm'} name={'gf-show-context'} />{' '}
|
||||||
@@ -58,17 +59,22 @@ const FlameGraphMetadata = memo(
|
|||||||
aria-label={'Remove sandwich view'}
|
aria-label={'Remove sandwich view'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (focusedItem) {
|
if (focusedItem) {
|
||||||
const percentValue = Math.round(10000 * (focusedItem.item.value / totalTicks)) / 100;
|
const percentValue = totalTicks > 0 ? Math.round(10000 * (focusedItem.item.value / totalTicks)) / 100 : 0;
|
||||||
|
const iconName = percentValue > 0 ? 'eye' : 'exclamation-circle';
|
||||||
|
|
||||||
parts.push(
|
parts.push(
|
||||||
<span key={'focus'}>
|
<Tooltip key={'focus'} content={focusedItem.label} placement="top">
|
||||||
|
<div>
|
||||||
<Icon size={'sm'} name={'angle-right'} />
|
<Icon size={'sm'} name={'angle-right'} />
|
||||||
<div className={styles.metadataPill}>
|
<div className={styles.metadataPill}>
|
||||||
<Icon size={'sm'} name={'eye'} /> {percentValue}% of total
|
<Icon size={'sm'} name={iconName} />
|
||||||
|
{percentValue}% of total
|
||||||
<IconButton
|
<IconButton
|
||||||
className={styles.pillCloseButton}
|
className={styles.pillCloseButton}
|
||||||
name={'times'}
|
name={'times'}
|
||||||
@@ -78,7 +84,8 @@ const FlameGraphMetadata = memo(
|
|||||||
aria-label={'Remove focus'}
|
aria-label={'Remove focus'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,8 +114,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
margin: theme.spacing(0, 0.5),
|
margin: theme.spacing(0, 0.5),
|
||||||
}),
|
}),
|
||||||
metadata: css({
|
metadata: css({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
margin: '8px 0',
|
margin: '8px 0',
|
||||||
textAlign: 'center',
|
|
||||||
}),
|
}),
|
||||||
metadataPillName: css({
|
metadataPillName: css({
|
||||||
label: 'metadataPillName',
|
label: 'metadataPillName',
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ export type Props = {
|
|||||||
* Disable behaviour where similar items in the same stack will be collapsed into single item.
|
* Disable behaviour where similar items in the same stack will be collapsed into single item.
|
||||||
*/
|
*/
|
||||||
disableCollapsing?: boolean;
|
disableCollapsing?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether or not to keep any focused item when the profile data changes.
|
||||||
|
*/
|
||||||
|
keepFocusOnDataChange?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FlameGraphContainer = ({
|
const FlameGraphContainer = ({
|
||||||
@@ -87,6 +91,7 @@ const FlameGraphContainer = ({
|
|||||||
vertical,
|
vertical,
|
||||||
showFlameGraphOnly,
|
showFlameGraphOnly,
|
||||||
disableCollapsing,
|
disableCollapsing,
|
||||||
|
keepFocusOnDataChange,
|
||||||
getExtraContextMenuButtons,
|
getExtraContextMenuButtons,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [focusedItemData, setFocusedItemData] = useState<ClickedItemData>();
|
const [focusedItemData, setFocusedItemData] = useState<ClickedItemData>();
|
||||||
@@ -133,14 +138,44 @@ const FlameGraphContainer = ({
|
|||||||
setRangeMax(1);
|
setRangeMax(1);
|
||||||
}, [setFocusedItemData, setRangeMax, setRangeMin]);
|
}, [setFocusedItemData, setRangeMax, setRangeMin]);
|
||||||
|
|
||||||
function resetSandwich() {
|
const resetSandwich = useCallback(() => {
|
||||||
setSandwichItem(undefined);
|
setSandwichItem(undefined);
|
||||||
}
|
}, [setSandwichItem]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!keepFocusOnDataChange) {
|
||||||
resetFocus();
|
resetFocus();
|
||||||
resetSandwich();
|
resetSandwich();
|
||||||
}, [data, resetFocus]);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataContainer && focusedItemData) {
|
||||||
|
const item = dataContainer.getNodesWithLabel(focusedItemData.label)?.[0];
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
setFocusedItemData({ ...focusedItemData, item });
|
||||||
|
|
||||||
|
const levels = dataContainer.getLevels();
|
||||||
|
const totalViewTicks = levels.length ? levels[0][0].value : 0;
|
||||||
|
setRangeMin(item.start / totalViewTicks);
|
||||||
|
setRangeMax((item.start + item.value) / totalViewTicks);
|
||||||
|
} else {
|
||||||
|
setFocusedItemData({
|
||||||
|
...focusedItemData,
|
||||||
|
item: {
|
||||||
|
start: 0,
|
||||||
|
value: 0,
|
||||||
|
itemIndexes: [],
|
||||||
|
children: [],
|
||||||
|
level: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setRangeMin(0);
|
||||||
|
setRangeMax(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [dataContainer, keepFocusOnDataChange]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const onSymbolClick = useCallback(
|
const onSymbolClick = useCallback(
|
||||||
(symbol: string) => {
|
(symbol: string) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user