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:
parent
b3b044b54b
commit
27cdd25917
@ -26,17 +26,18 @@ import { Flamegraph } from '@grafana/flamegraph';
|
||||
|
||||
#### Props
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 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. |
|
||||
| getTheme | () => GrafanaTheme2 | Provides a theme for the visualization on which colors and some sizes are based. |
|
||||
| onTableSymbolClick | (symbol: string) => void | Interaction hook that can be used to report on the interaction. Fires when user click on a name in the table. |
|
||||
| onViewSelected | (view: string) => void | Interaction hook that can be used to report on the interaction. Fires when user changes the view to show (table/graph/both) |
|
||||
| onTextAlignSelected | (align: string) => void | Interaction hook that can be used to report on the interaction. Fires when user changes the text align. |
|
||||
| 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. |
|
||||
| vertical | boolean | If true the flamegraph will be rendered on top of the table. |
|
||||
| Name | Type | Description |
|
||||
| --------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 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. |
|
||||
| getTheme | () => GrafanaTheme2 | Provides a theme for the visualization on which colors and some sizes are based. |
|
||||
| onTableSymbolClick | (symbol: string) => void | Interaction hook that can be used to report on the interaction. Fires when user click on a name in the table. |
|
||||
| onViewSelected | (view: string) => void | Interaction hook that can be used to report on the interaction. Fires when user changes the view to show (table/graph/both) |
|
||||
| onTextAlignSelected | (align: string) => void | Interaction hook that can be used to report on the interaction. Fires when user changes the text align. |
|
||||
| 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. |
|
||||
| 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
|
||||
|
||||
|
@ -136,40 +136,46 @@ const FlameGraph = ({
|
||||
search,
|
||||
selectedView,
|
||||
};
|
||||
const canvas = levelsCallers ? (
|
||||
<>
|
||||
<div className={styles.sandwichCanvasWrapper}>
|
||||
<div className={styles.sandwichMarker}>
|
||||
Callers
|
||||
<Icon className={styles.sandwichMarkerIcon} name={'arrow-down'} />
|
||||
</div>
|
||||
<FlameGraphCanvas
|
||||
{...commonCanvasProps}
|
||||
root={levelsCallers[levelsCallers.length - 1][0]}
|
||||
depth={levelsCallers.length}
|
||||
direction={'parents'}
|
||||
// We do not support collapsing in sandwich view for now.
|
||||
collapsing={false}
|
||||
/>
|
||||
</div>
|
||||
let canvas = null;
|
||||
|
||||
<div className={styles.sandwichCanvasWrapper}>
|
||||
<div className={cx(styles.sandwichMarker, styles.sandwichMarkerCalees)}>
|
||||
<Icon className={styles.sandwichMarkerIcon} name={'arrow-up'} />
|
||||
Callees
|
||||
if (levelsCallers?.length) {
|
||||
canvas = (
|
||||
<>
|
||||
<div className={styles.sandwichCanvasWrapper}>
|
||||
<div className={styles.sandwichMarker}>
|
||||
Callers
|
||||
<Icon className={styles.sandwichMarkerIcon} name={'arrow-down'} />
|
||||
</div>
|
||||
<FlameGraphCanvas
|
||||
{...commonCanvasProps}
|
||||
root={levelsCallers[levelsCallers.length - 1][0]}
|
||||
depth={levelsCallers.length}
|
||||
direction={'parents'}
|
||||
// We do not support collapsing in sandwich view for now.
|
||||
collapsing={false}
|
||||
/>
|
||||
</div>
|
||||
<FlameGraphCanvas
|
||||
{...commonCanvasProps}
|
||||
root={levels[0][0]}
|
||||
depth={levels.length}
|
||||
direction={'children'}
|
||||
collapsing={false}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<FlameGraphCanvas {...commonCanvasProps} root={levels[0][0]} depth={levels.length} direction={'children'} />
|
||||
);
|
||||
|
||||
<div className={styles.sandwichCanvasWrapper}>
|
||||
<div className={cx(styles.sandwichMarker, styles.sandwichMarkerCalees)}>
|
||||
<Icon className={styles.sandwichMarkerIcon} name={'arrow-up'} />
|
||||
Callees
|
||||
</div>
|
||||
<FlameGraphCanvas
|
||||
{...commonCanvasProps}
|
||||
root={levels[0][0]}
|
||||
depth={levels.length}
|
||||
direction={'children'}
|
||||
collapsing={false}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else if (levels?.length) {
|
||||
canvas = (
|
||||
<FlameGraphCanvas {...commonCanvasProps} root={levels[0][0]} depth={levels.length} direction={'children'} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.graph}>
|
||||
|
@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { memo, ReactNode } from 'react';
|
||||
|
||||
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';
|
||||
|
||||
@ -42,43 +42,50 @@ const FlameGraphMetadata = memo(
|
||||
|
||||
if (sandwichedLabel) {
|
||||
parts.push(
|
||||
<span key={'sandwich'}>
|
||||
<Icon size={'sm'} name={'angle-right'} />
|
||||
<div className={styles.metadataPill}>
|
||||
<Icon size={'sm'} name={'gf-show-context'} />{' '}
|
||||
<span className={styles.metadataPillName}>
|
||||
{sandwichedLabel.substring(sandwichedLabel.lastIndexOf('/') + 1)}
|
||||
</span>
|
||||
<IconButton
|
||||
className={styles.pillCloseButton}
|
||||
name={'times'}
|
||||
size={'sm'}
|
||||
onClick={onSandwichPillClick}
|
||||
tooltip={'Remove sandwich view'}
|
||||
aria-label={'Remove sandwich view'}
|
||||
/>
|
||||
<Tooltip key={'sandwich'} content={sandwichedLabel} placement="top">
|
||||
<div>
|
||||
<Icon size={'sm'} name={'angle-right'} />
|
||||
<div className={styles.metadataPill}>
|
||||
<Icon size={'sm'} name={'gf-show-context'} />{' '}
|
||||
<span className={styles.metadataPillName}>
|
||||
{sandwichedLabel.substring(sandwichedLabel.lastIndexOf('/') + 1)}
|
||||
</span>
|
||||
<IconButton
|
||||
className={styles.pillCloseButton}
|
||||
name={'times'}
|
||||
size={'sm'}
|
||||
onClick={onSandwichPillClick}
|
||||
tooltip={'Remove sandwich view'}
|
||||
aria-label={'Remove sandwich view'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
<span key={'focus'}>
|
||||
<Icon size={'sm'} name={'angle-right'} />
|
||||
<div className={styles.metadataPill}>
|
||||
<Icon size={'sm'} name={'eye'} /> {percentValue}% of total
|
||||
<IconButton
|
||||
className={styles.pillCloseButton}
|
||||
name={'times'}
|
||||
size={'sm'}
|
||||
onClick={onFocusPillClick}
|
||||
tooltip={'Remove focus'}
|
||||
aria-label={'Remove focus'}
|
||||
/>
|
||||
<Tooltip key={'focus'} content={focusedItem.label} placement="top">
|
||||
<div>
|
||||
<Icon size={'sm'} name={'angle-right'} />
|
||||
<div className={styles.metadataPill}>
|
||||
<Icon size={'sm'} name={iconName} />
|
||||
{percentValue}% of total
|
||||
<IconButton
|
||||
className={styles.pillCloseButton}
|
||||
name={'times'}
|
||||
size={'sm'}
|
||||
onClick={onFocusPillClick}
|
||||
tooltip={'Remove focus'}
|
||||
aria-label={'Remove focus'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@ -107,8 +114,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
margin: theme.spacing(0, 0.5),
|
||||
}),
|
||||
metadata: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
margin: '8px 0',
|
||||
textAlign: 'center',
|
||||
}),
|
||||
metadataPillName: css({
|
||||
label: 'metadataPillName',
|
||||
|
@ -73,6 +73,10 @@ export type Props = {
|
||||
* Disable behaviour where similar items in the same stack will be collapsed into single item.
|
||||
*/
|
||||
disableCollapsing?: boolean;
|
||||
/**
|
||||
* Whether or not to keep any focused item when the profile data changes.
|
||||
*/
|
||||
keepFocusOnDataChange?: boolean;
|
||||
};
|
||||
|
||||
const FlameGraphContainer = ({
|
||||
@ -87,6 +91,7 @@ const FlameGraphContainer = ({
|
||||
vertical,
|
||||
showFlameGraphOnly,
|
||||
disableCollapsing,
|
||||
keepFocusOnDataChange,
|
||||
getExtraContextMenuButtons,
|
||||
}: Props) => {
|
||||
const [focusedItemData, setFocusedItemData] = useState<ClickedItemData>();
|
||||
@ -133,14 +138,44 @@ const FlameGraphContainer = ({
|
||||
setRangeMax(1);
|
||||
}, [setFocusedItemData, setRangeMax, setRangeMin]);
|
||||
|
||||
function resetSandwich() {
|
||||
const resetSandwich = useCallback(() => {
|
||||
setSandwichItem(undefined);
|
||||
}
|
||||
}, [setSandwichItem]);
|
||||
|
||||
useEffect(() => {
|
||||
resetFocus();
|
||||
resetSandwich();
|
||||
}, [data, resetFocus]);
|
||||
if (!keepFocusOnDataChange) {
|
||||
resetFocus();
|
||||
resetSandwich();
|
||||
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(
|
||||
(symbol: string) => {
|
||||
|
Loading…
Reference in New Issue
Block a user