FlameGraph: Simplify the data needed for context menu and item focusing (#69006)

This commit is contained in:
Andrej Ocenas
2023-05-25 18:26:14 +02:00
committed by GitHub
parent 81264e4a77
commit 4908045fc3
13 changed files with 161 additions and 237 deletions

View File

@ -1,7 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { CoreApp, createDataFrame } from '@grafana/data'; import { createDataFrame } from '@grafana/data';
import { SelectedView } from '../types'; import { SelectedView } from '../types';
@ -11,17 +11,20 @@ import { data } from './testData/dataNestedSet';
import 'jest-canvas-mock'; import 'jest-canvas-mock';
jest.mock('react-use', () => ({ jest.mock('react-use', () => {
useMeasure: () => { const reactUse = jest.requireActual('react-use');
const ref = React.useRef(); return {
return [ref, { width: 1600 }]; ...reactUse,
}, useMeasure: () => {
})); const ref = React.useRef();
return [ref, { width: 1600 }];
},
};
});
describe('FlameGraph', () => { describe('FlameGraph', () => {
const FlameGraphWithProps = () => { const FlameGraphWithProps = () => {
const [topLevelIndex, setTopLevelIndex] = useState(0); const [focusedItemIndex, setFocusedItemIndex] = useState<number>();
const [selectedBarIndex, setSelectedBarIndex] = useState(0);
const [rangeMin, setRangeMin] = useState(0); const [rangeMin, setRangeMin] = useState(0);
const [rangeMax, setRangeMax] = useState(1); const [rangeMax, setRangeMax] = useState(1);
const [search] = useState(''); const [search] = useState('');
@ -34,18 +37,17 @@ describe('FlameGraph', () => {
return ( return (
<FlameGraph <FlameGraph
data={container} data={container}
app={CoreApp.Explore}
levels={levels} levels={levels}
topLevelIndex={topLevelIndex}
selectedBarIndex={selectedBarIndex}
rangeMin={rangeMin} rangeMin={rangeMin}
rangeMax={rangeMax} rangeMax={rangeMax}
search={search} search={search}
setTopLevelIndex={setTopLevelIndex}
setSelectedBarIndex={setSelectedBarIndex}
setRangeMin={setRangeMin} setRangeMin={setRangeMin}
setRangeMax={setRangeMax} setRangeMax={setRangeMax}
selectedView={selectedView} selectedView={selectedView}
onItemFocused={(itemIndex) => {
setFocusedItemIndex(itemIndex);
}}
focusedItemIndex={focusedItemIndex}
textAlign={'left'} textAlign={'left'}
/> />
); );

View File

@ -21,11 +21,10 @@ import uFuzzy from '@leeoniya/ufuzzy';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useMeasure } from 'react-use'; import { useMeasure } from 'react-use';
import { CoreApp } from '@grafana/data';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { PIXELS_PER_LEVEL } from '../../constants'; import { PIXELS_PER_LEVEL } from '../../constants';
import { SelectedView, ContextMenuData, TextAlign } from '../types'; import { SelectedView, ClickedItemData, TextAlign } from '../types';
import FlameGraphContextMenu from './FlameGraphContextMenu'; import FlameGraphContextMenu from './FlameGraphContextMenu';
import FlameGraphMetadata from './FlameGraphMetadata'; import FlameGraphMetadata from './FlameGraphMetadata';
@ -35,36 +34,30 @@ import { getBarX, getRectDimensionsForLevel, renderRect } from './rendering';
type Props = { type Props = {
data: FlameGraphDataContainer; data: FlameGraphDataContainer;
app: CoreApp;
levels: LevelItem[][]; levels: LevelItem[][];
topLevelIndex: number;
selectedBarIndex: number;
rangeMin: number; rangeMin: number;
rangeMax: number; rangeMax: number;
search: string; search: string;
setTopLevelIndex: (level: number) => void;
setSelectedBarIndex: (bar: number) => void;
setRangeMin: (range: number) => void; setRangeMin: (range: number) => void;
setRangeMax: (range: number) => void; setRangeMax: (range: number) => void;
selectedView: SelectedView; selectedView: SelectedView;
style?: React.CSSProperties; style?: React.CSSProperties;
onItemFocused: (itemIndex: number) => void;
focusedItemIndex?: number;
textAlign: TextAlign; textAlign: TextAlign;
}; };
const FlameGraph = ({ const FlameGraph = ({
data, data,
app,
levels, levels,
topLevelIndex,
selectedBarIndex,
rangeMin, rangeMin,
rangeMax, rangeMax,
search, search,
setTopLevelIndex,
setSelectedBarIndex,
setRangeMin, setRangeMin,
setRangeMax, setRangeMax,
selectedView, selectedView,
onItemFocused,
focusedItemIndex,
textAlign, textAlign,
}: Props) => { }: Props) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@ -74,7 +67,8 @@ const FlameGraph = ({
const graphRef = useRef<HTMLCanvasElement>(null); const graphRef = useRef<HTMLCanvasElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null); const tooltipRef = useRef<HTMLDivElement>(null);
const [tooltipItem, setTooltipItem] = useState<LevelItem>(); const [tooltipItem, setTooltipItem] = useState<LevelItem>();
const [contextMenuData, setContextMenuData] = useState<ContextMenuData>();
const [clickedItemData, setClickedItemData] = useState<ClickedItemData>();
const [ufuzzy] = useState(() => { const [ufuzzy] = useState(() => {
return new uFuzzy(); return new uFuzzy();
@ -120,22 +114,12 @@ const FlameGraph = ({
// sometimes we collapse multiple bars into single rect. // sometimes we collapse multiple bars into single rect.
const dimensions = getRectDimensionsForLevel(data, level, levelIndex, totalTicks, rangeMin, pixelsPerTick); const dimensions = getRectDimensionsForLevel(data, level, levelIndex, totalTicks, rangeMin, pixelsPerTick);
for (const rect of dimensions) { for (const rect of dimensions) {
const focusedLevel = focusedItemIndex ? data.getLevel(focusedItemIndex) : 0;
// Render each rectangle based on the computed dimensions // Render each rectangle based on the computed dimensions
renderRect( renderRect(ctx, rect, totalTicks, rangeMin, rangeMax, search, levelIndex, focusedLevel, foundLabels, textAlign);
ctx,
rect,
totalTicks,
rangeMin,
rangeMax,
search,
levelIndex,
topLevelIndex,
foundLabels,
textAlign
);
} }
} }
}, [data, levels, wrapperWidth, totalTicks, rangeMin, rangeMax, search, topLevelIndex, foundLabels, textAlign]); }, [data, levels, wrapperWidth, totalTicks, rangeMin, rangeMax, search, focusedItemIndex, foundLabels, textAlign]);
useEffect(() => { useEffect(() => {
if (graphRef.current) { if (graphRef.current) {
@ -153,15 +137,22 @@ const FlameGraph = ({
// if clicking on a block in the canvas // if clicking on a block in the canvas
if (barIndex !== -1 && !isNaN(levelIndex) && !isNaN(barIndex)) { if (barIndex !== -1 && !isNaN(levelIndex) && !isNaN(barIndex)) {
setContextMenuData({ e, levelIndex, barIndex }); const item = levels[levelIndex][barIndex];
setClickedItemData({
posY: e.clientY,
posX: e.clientX,
itemIndex: item.itemIndex,
label: data.getLabel(item.itemIndex),
start: item.start,
});
} else { } else {
// if clicking on the canvas but there is no block beneath the cursor // if clicking on the canvas but there is no block beneath the cursor
setContextMenuData(undefined); setClickedItemData(undefined);
} }
}; };
graphRef.current!.onmousemove = (e) => { graphRef.current!.onmousemove = (e) => {
if (tooltipRef.current && contextMenuData === undefined) { if (tooltipRef.current && clickedItemData === undefined) {
setTooltipItem(undefined); setTooltipItem(undefined);
const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin); const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin);
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates( const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
@ -197,16 +188,13 @@ const FlameGraph = ({
levels, levels,
rangeMin, rangeMin,
rangeMax, rangeMax,
topLevelIndex,
totalTicks, totalTicks,
wrapperWidth, wrapperWidth,
setTopLevelIndex,
setRangeMin, setRangeMin,
setRangeMax, setRangeMax,
selectedView, selectedView,
setSelectedBarIndex, setClickedItemData,
setContextMenuData, clickedItemData,
contextMenuData,
]); ]);
// hide context menu if outside the flame graph canvas is clicked // hide context menu if outside the flame graph canvas is clicked
@ -214,38 +202,31 @@ const FlameGraph = ({
const handleOnClick = (e: MouseEvent) => { const handleOnClick = (e: MouseEvent) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
if ((e.target as HTMLElement).parentElement?.id !== 'flameGraphCanvasContainer') { if ((e.target as HTMLElement).parentElement?.id !== 'flameGraphCanvasContainer') {
setContextMenuData(undefined); setClickedItemData(undefined);
} }
}; };
window.addEventListener('click', handleOnClick); window.addEventListener('click', handleOnClick);
return () => window.removeEventListener('click', handleOnClick); return () => window.removeEventListener('click', handleOnClick);
}, [setContextMenuData]); }, [setClickedItemData]);
return ( return (
<div className={styles.graph} ref={sizeRef}> <div className={styles.graph} ref={sizeRef}>
<FlameGraphMetadata <FlameGraphMetadata data={data} focusedItemIndex={focusedItemIndex} totalTicks={totalTicks} />
data={data}
levels={levels}
topLevelIndex={topLevelIndex}
selectedBarIndex={selectedBarIndex}
totalTicks={totalTicks}
/>
<div className={styles.canvasContainer} id="flameGraphCanvasContainer"> <div className={styles.canvasContainer} id="flameGraphCanvasContainer">
<canvas ref={graphRef} data-testid="flameGraph" /> <canvas ref={graphRef} data-testid="flameGraph" />
</div> </div>
<FlameGraphTooltip tooltipRef={tooltipRef} item={tooltipItem} data={data} totalTicks={totalTicks} /> <FlameGraphTooltip tooltipRef={tooltipRef} item={tooltipItem} data={data} totalTicks={totalTicks} />
{contextMenuData && ( {clickedItemData && (
<FlameGraphContextMenu <FlameGraphContextMenu
data={data} itemData={clickedItemData}
contextMenuData={contextMenuData!} onMenuItemClick={() => {
levels={levels} setClickedItemData(undefined);
totalTicks={totalTicks} }}
graphRef={graphRef} onItemFocus={(itemIndex) => {
setContextMenuData={setContextMenuData} setRangeMin(clickedItemData.start / totalTicks);
setTopLevelIndex={setTopLevelIndex} setRangeMax((clickedItemData.start + data.getValue(clickedItemData.itemIndex)) / totalTicks);
setSelectedBarIndex={setSelectedBarIndex} onItemFocused(itemIndex);
setRangeMin={setRangeMin} }}
setRangeMax={setRangeMax}
/> />
)} )}
</div> </div>

View File

@ -2,78 +2,47 @@ import React from 'react';
import { MenuItem, ContextMenu } from '@grafana/ui'; import { MenuItem, ContextMenu } from '@grafana/ui';
import { ContextMenuData } from '../types'; import { ClickedItemData } from '../types';
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
type Props = { type Props = {
contextMenuData: ContextMenuData; itemData: ClickedItemData;
data: FlameGraphDataContainer; onMenuItemClick: () => void;
levels: LevelItem[][]; onItemFocus: (itemIndex: number) => void;
totalTicks: number;
graphRef: React.RefObject<HTMLCanvasElement>;
setContextMenuData: (event: ContextMenuData | undefined) => void;
setTopLevelIndex: (level: number) => void;
setSelectedBarIndex: (bar: number) => void;
setRangeMin: (range: number) => void;
setRangeMax: (range: number) => void;
}; };
const FlameGraphContextMenu = ({ const FlameGraphContextMenu = ({ itemData, onMenuItemClick, onItemFocus }: Props) => {
contextMenuData, function renderItems() {
graphRef,
totalTicks,
levels,
setContextMenuData,
setTopLevelIndex,
setSelectedBarIndex,
setRangeMin,
setRangeMax,
data,
}: Props) => {
const clickedItem = levels[contextMenuData.levelIndex][contextMenuData.barIndex];
const renderMenuItems = () => {
return ( return (
<> <>
<MenuItem <MenuItem
label="Focus block" label="Focus block"
icon={'eye'} icon={'eye'}
onClick={() => { onClick={() => {
if (graphRef.current && contextMenuData) { onItemFocus(itemData.itemIndex);
setTopLevelIndex(contextMenuData.levelIndex); onMenuItemClick();
setSelectedBarIndex(contextMenuData.barIndex);
setRangeMin(clickedItem.start / totalTicks);
setRangeMax((clickedItem.start + data.getValue(clickedItem.itemIndex)) / totalTicks);
setContextMenuData(undefined);
}
}} }}
/> />
<MenuItem <MenuItem
label="Copy function name" label="Copy function name"
icon={'copy'} icon={'copy'}
onClick={() => { onClick={() => {
if (graphRef.current && contextMenuData) { navigator.clipboard.writeText(itemData.label).then(() => {
navigator.clipboard.writeText(data.getLabel(clickedItem.itemIndex)).then(() => { onMenuItemClick();
setContextMenuData(undefined); });
});
}
}} }}
/> />
</> </>
); );
}; }
return ( return (
<div data-testid="contextMenu"> <div data-testid="contextMenu">
{contextMenuData.e.clientX && contextMenuData.e.clientY && ( <ContextMenu
<ContextMenu renderMenuItems={renderItems}
renderMenuItems={() => renderMenuItems()} x={itemData.posX + 10}
x={contextMenuData.e.clientX + 10} y={itemData.posY}
y={contextMenuData.e.clientY} focusOnOpen={false}
focusOnOpen={false} ></ContextMenu>
></ContextMenu>
)}
</div> </div>
); );
}; };

View File

@ -22,7 +22,7 @@ describe('should get metadata correctly', () => {
const container = new FlameGraphDataContainer( const container = new FlameGraphDataContainer(
makeDataFrame({ value: [1_624_078_250], level: [1], label: ['1'], self: [0] }, 'bytes') makeDataFrame({ value: [1_624_078_250], level: [1], label: ['1'], self: [0] }, 'bytes')
); );
const metadata = getMetadata(container, { itemIndex: 0, start: 0 }, 8_624_078_250); const metadata = getMetadata(container, 0, 8_624_078_250);
expect(metadata).toEqual({ expect(metadata).toEqual({
percentValue: 18.83, percentValue: 18.83,
unitTitle: 'RAM', unitTitle: 'RAM',
@ -35,7 +35,7 @@ describe('should get metadata correctly', () => {
const container = new FlameGraphDataContainer( const container = new FlameGraphDataContainer(
makeDataFrame({ value: [1_624_078_250], level: [1], label: ['1'], self: [0] }, 'none') makeDataFrame({ value: [1_624_078_250], level: [1], label: ['1'], self: [0] }, 'none')
); );
const metadata = getMetadata(container, { itemIndex: 0, start: 0 }, 8_624_078_250); const metadata = getMetadata(container, 0, 8_624_078_250);
expect(metadata).toEqual({ expect(metadata).toEqual({
percentValue: 18.83, percentValue: 18.83,
unitTitle: 'Count', unitTitle: 'Count',
@ -48,7 +48,7 @@ describe('should get metadata correctly', () => {
const container = new FlameGraphDataContainer( const container = new FlameGraphDataContainer(
makeDataFrame({ value: [1_624_078_250], level: [1], label: ['1'], self: [0] }) makeDataFrame({ value: [1_624_078_250], level: [1], label: ['1'], self: [0] })
); );
const metadata = getMetadata(container, { itemIndex: 0, start: 0 }, 8_624_078_250); const metadata = getMetadata(container, 0, 8_624_078_250);
expect(metadata).toEqual({ expect(metadata).toEqual({
percentValue: 18.83, percentValue: 18.83,
unitTitle: 'Count', unitTitle: 'Count',
@ -61,7 +61,7 @@ describe('should get metadata correctly', () => {
const container = new FlameGraphDataContainer( const container = new FlameGraphDataContainer(
makeDataFrame({ value: [1_624_078_250], level: [1], label: ['1'], self: [0] }, 'short') makeDataFrame({ value: [1_624_078_250], level: [1], label: ['1'], self: [0] }, 'short')
); );
const metadata = getMetadata(container, { itemIndex: 0, start: 0 }, 8_624_078_250); const metadata = getMetadata(container, 0, 8_624_078_250);
expect(metadata).toEqual({ expect(metadata).toEqual({
percentValue: 18.83, percentValue: 18.83,
unitTitle: 'Count', unitTitle: 'Count',
@ -74,7 +74,7 @@ describe('should get metadata correctly', () => {
const container = new FlameGraphDataContainer( const container = new FlameGraphDataContainer(
makeDataFrame({ value: [1_624_078_250], level: [1], label: ['1'], self: [0] }, 'ns') makeDataFrame({ value: [1_624_078_250], level: [1], label: ['1'], self: [0] }, 'ns')
); );
const metadata = getMetadata(container, { itemIndex: 0, start: 0 }, 8_624_078_250); const metadata = getMetadata(container, 0, 8_624_078_250);
expect(metadata).toEqual({ expect(metadata).toEqual({
percentValue: 18.83, percentValue: 18.83,
unitTitle: 'Time', unitTitle: 'Time',

View File

@ -5,29 +5,23 @@ import { useStyles2 } from '@grafana/ui';
import { Metadata } from '../types'; import { Metadata } from '../types';
import { FlameGraphDataContainer, LevelItem } from './dataTransform'; import { FlameGraphDataContainer } from './dataTransform';
type Props = { type Props = {
data: FlameGraphDataContainer; data: FlameGraphDataContainer;
levels: LevelItem[][];
topLevelIndex: number;
selectedBarIndex: number;
totalTicks: number; totalTicks: number;
focusedItemIndex?: number;
}; };
const FlameGraphMetadata = React.memo(({ data, levels, topLevelIndex, selectedBarIndex, totalTicks }: Props) => { const FlameGraphMetadata = React.memo(({ data, focusedItemIndex, totalTicks }: Props) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
if (levels[topLevelIndex] && levels[topLevelIndex][selectedBarIndex]) { const metadata = getMetadata(data, focusedItemIndex || 0, totalTicks);
const bar = levels[topLevelIndex][selectedBarIndex]; const metadataText = `${metadata?.unitValue} (${metadata?.percentValue}%) of ${metadata?.samples} total samples (${metadata?.unitTitle})`;
const metadata = getMetadata(data, bar, totalTicks); return <>{<div className={styles.metadata}>{metadataText}</div>}</>;
const metadataText = `${metadata?.unitValue} (${metadata?.percentValue}%) of ${metadata?.samples} total samples (${metadata?.unitTitle})`;
return <>{<div className={styles.metadata}>{metadataText}</div>}</>;
}
return <></>;
}); });
export const getMetadata = (data: FlameGraphDataContainer, bar: LevelItem, totalTicks: number): Metadata => { export const getMetadata = (data: FlameGraphDataContainer, itemIndex: number, totalTicks: number): Metadata => {
const displayValue = data.getValueDisplay(bar.itemIndex); const displayValue = data.getValueDisplay(itemIndex);
const percentValue = Math.round(10000 * (displayValue.numeric / totalTicks)) / 100; const percentValue = Math.round(10000 * (displayValue.numeric / totalTicks)) / 100;
let unitValue = displayValue.text + displayValue.suffix; let unitValue = displayValue.text + displayValue.suffix;

View File

@ -21,6 +21,7 @@ describe('getRectDimensionsForLevel', () => {
{ {
width: 999, width: 999,
height: 22, height: 22,
itemIndex: 0,
x: 0, x: 0,
y: 22, y: 22,
collapsed: false, collapsed: false,
@ -42,9 +43,29 @@ describe('getRectDimensionsForLevel', () => {
); );
const result = getRectDimensionsForLevel(container, level, 2, 100, 0, 10); const result = getRectDimensionsForLevel(container, level, 2, 100, 0, 10);
expect(result).toEqual([ expect(result).toEqual([
{ width: 999, height: 22, x: 0, y: 44, collapsed: false, ticks: 100, label: '1', unitLabel: '100' }, { width: 999, height: 22, x: 0, y: 44, collapsed: false, ticks: 100, label: '1', unitLabel: '100', itemIndex: 0 },
{ width: 499, height: 22, x: 1000, y: 44, collapsed: false, ticks: 50, label: '2', unitLabel: '50' }, {
{ width: 499, height: 22, x: 1500, y: 44, collapsed: false, ticks: 50, label: '3', unitLabel: '50' }, width: 499,
height: 22,
x: 1000,
y: 44,
collapsed: false,
ticks: 50,
label: '2',
unitLabel: '50',
itemIndex: 1,
},
{
width: 499,
height: 22,
x: 1500,
y: 44,
collapsed: false,
ticks: 50,
label: '3',
unitLabel: '50',
itemIndex: 2,
},
]); ]);
}); });
@ -59,8 +80,8 @@ describe('getRectDimensionsForLevel', () => {
); );
const result = getRectDimensionsForLevel(container, level, 2, 100, 0, 1); const result = getRectDimensionsForLevel(container, level, 2, 100, 0, 1);
expect(result).toEqual([ expect(result).toEqual([
{ width: 99, height: 22, x: 0, y: 44, collapsed: false, ticks: 100, label: '1', unitLabel: '100' }, { width: 99, height: 22, x: 0, y: 44, collapsed: false, ticks: 100, label: '1', unitLabel: '100', itemIndex: 0 },
{ width: 3, height: 22, x: 100, y: 44, collapsed: true, ticks: 3, label: '2', unitLabel: '2' }, { width: 3, height: 22, x: 100, y: 44, collapsed: true, ticks: 3, label: '2', unitLabel: '2', itemIndex: 1 },
]); ]);
}); });
}); });

View File

@ -21,6 +21,7 @@ type RectData = {
ticks: number; ticks: number;
label: string; label: string;
unitLabel: string; unitLabel: string;
itemIndex: number;
}; };
/** /**
@ -67,6 +68,7 @@ export function getRectDimensionsForLevel(
ticks: curBarTicks, ticks: curBarTicks,
label: data.getLabel(item.itemIndex), label: data.getLabel(item.itemIndex),
unitLabel: unit, unitLabel: unit,
itemIndex: item.itemIndex,
}); });
} }
return coordinatesLevel; return coordinatesLevel;

View File

@ -3,6 +3,7 @@ import React, { useEffect, useMemo, useState } from 'react';
import { useMeasure } from 'react-use'; import { useMeasure } from 'react-use';
import { DataFrame, CoreApp, GrafanaTheme2 } from '@grafana/data'; import { DataFrame, CoreApp, GrafanaTheme2 } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { useStyles2, useTheme2 } from '@grafana/ui'; import { useStyles2, useTheme2 } from '@grafana/ui';
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants'; import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
@ -19,8 +20,8 @@ type Props = {
}; };
const FlameGraphContainer = (props: Props) => { const FlameGraphContainer = (props: Props) => {
const [topLevelIndex, setTopLevelIndex] = useState(0); const [focusedItemIndex, setFocusedItemIndex] = useState<number>();
const [selectedBarIndex, setSelectedBarIndex] = useState(0);
const [rangeMin, setRangeMin] = useState(0); const [rangeMin, setRangeMin] = useState(0);
const [rangeMax, setRangeMax] = useState(1); const [rangeMax, setRangeMax] = useState(1);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
@ -56,8 +57,7 @@ const FlameGraphContainer = (props: Props) => {
}, [selectedView, setSelectedView, containerWidth]); }, [selectedView, setSelectedView, containerWidth]);
useEffect(() => { useEffect(() => {
setTopLevelIndex(0); setFocusedItemIndex(undefined);
setSelectedBarIndex(0);
setRangeMin(0); setRangeMin(0);
setRangeMax(1); setRangeMax(1);
}, [props.data]); }, [props.data]);
@ -68,15 +68,16 @@ const FlameGraphContainer = (props: Props) => {
<div ref={sizeRef} className={styles.container}> <div ref={sizeRef} className={styles.container}>
<FlameGraphHeader <FlameGraphHeader
app={props.app} app={props.app}
setTopLevelIndex={setTopLevelIndex}
setSelectedBarIndex={setSelectedBarIndex}
setRangeMin={setRangeMin}
setRangeMax={setRangeMax}
search={search} search={search}
setSearch={setSearch} setSearch={setSearch}
selectedView={selectedView} selectedView={selectedView}
setSelectedView={setSelectedView} setSelectedView={setSelectedView}
containerWidth={containerWidth} containerWidth={containerWidth}
onReset={() => {
setRangeMin(0);
setRangeMax(1);
setFocusedItemIndex(undefined);
}}
textAlign={textAlign} textAlign={textAlign}
onTextAlignChange={setTextAlign} onTextAlignChange={setTextAlign}
/> />
@ -87,31 +88,35 @@ const FlameGraphContainer = (props: Props) => {
data={dataContainer} data={dataContainer}
app={props.app} app={props.app}
totalLevels={levels.length} totalLevels={levels.length}
selectedView={selectedView} onSymbolClick={(symbol) => {
search={search} if (search === symbol) {
setSearch={setSearch} setSearch('');
setTopLevelIndex={setTopLevelIndex} } else {
setSelectedBarIndex={setSelectedBarIndex} reportInteraction('grafana_flamegraph_table_item_selected', {
setRangeMin={setRangeMin} app: props.app,
setRangeMax={setRangeMax} grafana_version: config.buildInfo.version,
});
setSearch(symbol);
// Reset selected level in flamegraph when selecting row in top table
setRangeMin(0);
setRangeMax(1);
}
}}
/> />
)} )}
{selectedView !== SelectedView.TopTable && ( {selectedView !== SelectedView.TopTable && (
<FlameGraph <FlameGraph
data={dataContainer} data={dataContainer}
app={props.app}
levels={levels} levels={levels}
topLevelIndex={topLevelIndex}
selectedBarIndex={selectedBarIndex}
rangeMin={rangeMin} rangeMin={rangeMin}
rangeMax={rangeMax} rangeMax={rangeMax}
search={search} search={search}
setTopLevelIndex={setTopLevelIndex}
setSelectedBarIndex={setSelectedBarIndex}
setRangeMin={setRangeMin} setRangeMin={setRangeMin}
setRangeMax={setRangeMax} setRangeMax={setRangeMax}
selectedView={selectedView} selectedView={selectedView}
onItemFocused={(itemIndex) => setFocusedItemIndex(itemIndex)}
focusedItemIndex={focusedItemIndex}
textAlign={textAlign} textAlign={textAlign}
/> />
)} )}

View File

@ -18,13 +18,12 @@ describe('FlameGraphHeader', () => {
app={CoreApp.Explore} app={CoreApp.Explore}
search={search} search={search}
setSearch={setSearch} setSearch={setSearch}
setTopLevelIndex={jest.fn()}
setSelectedBarIndex={jest.fn()}
setRangeMin={jest.fn()}
setRangeMax={jest.fn()}
selectedView={selectedView} selectedView={selectedView}
setSelectedView={setSelectedView} setSelectedView={setSelectedView}
containerWidth={1600} containerWidth={1600}
onReset={() => {
setSearch('');
}}
onTextAlignChange={jest.fn()} onTextAlignChange={jest.fn()}
textAlign={'left'} textAlign={'left'}
/> />

View File

@ -15,14 +15,11 @@ import { SelectedView, TextAlign } from './types';
type Props = { type Props = {
app: CoreApp; app: CoreApp;
search: string; search: string;
setTopLevelIndex: (level: number) => void;
setSelectedBarIndex: (bar: number) => void;
setRangeMin: (range: number) => void;
setRangeMax: (range: number) => void;
setSearch: (search: string) => void; setSearch: (search: string) => void;
selectedView: SelectedView; selectedView: SelectedView;
setSelectedView: (view: SelectedView) => void; setSelectedView: (view: SelectedView) => void;
containerWidth: number; containerWidth: number;
onReset: () => void;
textAlign: TextAlign; textAlign: TextAlign;
onTextAlignChange: (align: TextAlign) => void; onTextAlignChange: (align: TextAlign) => void;
}; };
@ -30,14 +27,11 @@ type Props = {
const FlameGraphHeader = ({ const FlameGraphHeader = ({
app, app,
search, search,
setTopLevelIndex,
setSelectedBarIndex,
setRangeMin,
setRangeMax,
setSearch, setSearch,
selectedView, selectedView,
setSelectedView, setSelectedView,
containerWidth, containerWidth,
onReset,
textAlign, textAlign,
onTextAlignChange, onTextAlignChange,
}: Props) => { }: Props) => {
@ -50,16 +44,6 @@ const FlameGraphHeader = ({
}); });
} }
const onResetView = () => {
setTopLevelIndex(0);
setSelectedBarIndex(0);
setRangeMin(0);
setRangeMax(1);
// We could set only one and wait them to sync but there is no need to debounce this.
setSearch('');
setLocalSearch('');
};
const [localSearch, setLocalSearch] = useSearchInput(search, setSearch); const [localSearch, setLocalSearch] = useSearchInput(search, setSearch);
return ( return (
@ -75,7 +59,16 @@ const FlameGraphHeader = ({
width={44} width={44}
/> />
</div> </div>
<Button type={'button'} variant="secondary" onClick={onResetView}> <Button
type={'button'}
variant="secondary"
onClick={() => {
onReset();
// We could set only one and wait them to sync but there is no need to debounce this.
setSearch('');
setLocalSearch('');
}}
>
Reset view Reset view
</Button> </Button>
</div> </div>

View File

@ -1,19 +1,15 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import React, { useState } from 'react'; import React from 'react';
import { CoreApp, createDataFrame } from '@grafana/data'; import { CoreApp, createDataFrame } from '@grafana/data';
import { FlameGraphDataContainer, nestedSetToLevels } from '../FlameGraph/dataTransform'; import { FlameGraphDataContainer, nestedSetToLevels } from '../FlameGraph/dataTransform';
import { data } from '../FlameGraph/testData/dataNestedSet'; import { data } from '../FlameGraph/testData/dataNestedSet';
import { SelectedView } from '../types';
import FlameGraphTopTableContainer from './FlameGraphTopTableContainer'; import FlameGraphTopTableContainer from './FlameGraphTopTableContainer';
describe('FlameGraphTopTableContainer', () => { describe('FlameGraphTopTableContainer', () => {
const FlameGraphTopTableContainerWithProps = () => { const FlameGraphTopTableContainerWithProps = () => {
const [search, setSearch] = useState('');
const [selectedView, _] = useState(SelectedView.Both);
const flameGraphData = createDataFrame(data); const flameGraphData = createDataFrame(data);
const container = new FlameGraphDataContainer(flameGraphData); const container = new FlameGraphDataContainer(flameGraphData);
const levels = nestedSetToLevels(container); const levels = nestedSetToLevels(container);
@ -23,13 +19,7 @@ describe('FlameGraphTopTableContainer', () => {
data={container} data={container}
app={CoreApp.Explore} app={CoreApp.Explore}
totalLevels={levels.length} totalLevels={levels.length}
selectedView={selectedView} onSymbolClick={jest.fn()}
search={search}
setSearch={setSearch}
setTopLevelIndex={jest.fn()}
setSelectedBarIndex={jest.fn()}
setRangeMin={jest.fn()}
setRangeMax={jest.fn()}
/> />
); );
}; };

View File

@ -8,52 +8,18 @@ import { Table, TableSortByFieldState, useStyles2 } from '@grafana/ui';
import { PIXELS_PER_LEVEL, TOP_TABLE_COLUMN_WIDTH } from '../../constants'; import { PIXELS_PER_LEVEL, TOP_TABLE_COLUMN_WIDTH } from '../../constants';
import { FlameGraphDataContainer } from '../FlameGraph/dataTransform'; import { FlameGraphDataContainer } from '../FlameGraph/dataTransform';
import { SelectedView, TableData } from '../types'; import { TableData } from '../types';
type Props = { type Props = {
data: FlameGraphDataContainer; data: FlameGraphDataContainer;
app: CoreApp; app: CoreApp;
totalLevels: number; totalLevels: number;
selectedView: SelectedView; onSymbolClick: (symbol: string) => void;
search: string;
setSearch: (search: string) => void;
setTopLevelIndex: (level: number) => void;
setSelectedBarIndex: (bar: number) => void;
setRangeMin: (range: number) => void;
setRangeMax: (range: number) => void;
}; };
const FlameGraphTopTableContainer = ({ const FlameGraphTopTableContainer = ({ data, app, totalLevels, onSymbolClick }: Props) => {
data,
app,
totalLevels,
selectedView,
search,
setSearch,
setTopLevelIndex,
setSelectedBarIndex,
setRangeMin,
setRangeMax,
}: Props) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const onSymbolClick = (symbol: string) => {
if (search === symbol) {
setSearch('');
} else {
reportInteraction('grafana_flamegraph_table_item_selected', {
app,
grafana_version: config.buildInfo.version,
});
setSearch(symbol);
// Reset selected level in flamegraph when selecting row in top table
setTopLevelIndex(0);
setSelectedBarIndex(0);
setRangeMin(0);
setRangeMax(1);
}
};
const [sort, setSort] = useState<TableSortByFieldState[]>([{ displayName: 'Self', desc: true }]); const [sort, setSort] = useState<TableSortByFieldState[]>([{ displayName: 'Self', desc: true }]);
return ( return (

View File

@ -1,7 +1,9 @@
export type ContextMenuData = { export type ClickedItemData = {
e: MouseEvent; posX: number;
levelIndex: number; posY: number;
barIndex: number; itemIndex: number;
label: string;
start: number;
}; };
export type Metadata = { export type Metadata = {