mirror of
https://github.com/grafana/grafana.git
synced 2025-02-13 17:15:40 -06:00
FlameGraph: Simplify the data needed for context menu and item focusing (#69006)
This commit is contained in:
parent
81264e4a77
commit
4908045fc3
@ -1,7 +1,7 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { CoreApp, createDataFrame } from '@grafana/data';
|
||||
import { createDataFrame } from '@grafana/data';
|
||||
|
||||
import { SelectedView } from '../types';
|
||||
|
||||
@ -11,17 +11,20 @@ import { data } from './testData/dataNestedSet';
|
||||
|
||||
import 'jest-canvas-mock';
|
||||
|
||||
jest.mock('react-use', () => ({
|
||||
useMeasure: () => {
|
||||
const ref = React.useRef();
|
||||
return [ref, { width: 1600 }];
|
||||
},
|
||||
}));
|
||||
jest.mock('react-use', () => {
|
||||
const reactUse = jest.requireActual('react-use');
|
||||
return {
|
||||
...reactUse,
|
||||
useMeasure: () => {
|
||||
const ref = React.useRef();
|
||||
return [ref, { width: 1600 }];
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('FlameGraph', () => {
|
||||
const FlameGraphWithProps = () => {
|
||||
const [topLevelIndex, setTopLevelIndex] = useState(0);
|
||||
const [selectedBarIndex, setSelectedBarIndex] = useState(0);
|
||||
const [focusedItemIndex, setFocusedItemIndex] = useState<number>();
|
||||
const [rangeMin, setRangeMin] = useState(0);
|
||||
const [rangeMax, setRangeMax] = useState(1);
|
||||
const [search] = useState('');
|
||||
@ -34,18 +37,17 @@ describe('FlameGraph', () => {
|
||||
return (
|
||||
<FlameGraph
|
||||
data={container}
|
||||
app={CoreApp.Explore}
|
||||
levels={levels}
|
||||
topLevelIndex={topLevelIndex}
|
||||
selectedBarIndex={selectedBarIndex}
|
||||
rangeMin={rangeMin}
|
||||
rangeMax={rangeMax}
|
||||
search={search}
|
||||
setTopLevelIndex={setTopLevelIndex}
|
||||
setSelectedBarIndex={setSelectedBarIndex}
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
selectedView={selectedView}
|
||||
onItemFocused={(itemIndex) => {
|
||||
setFocusedItemIndex(itemIndex);
|
||||
}}
|
||||
focusedItemIndex={focusedItemIndex}
|
||||
textAlign={'left'}
|
||||
/>
|
||||
);
|
||||
|
@ -21,11 +21,10 @@ import uFuzzy from '@leeoniya/ufuzzy';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
|
||||
import { CoreApp } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { PIXELS_PER_LEVEL } from '../../constants';
|
||||
import { SelectedView, ContextMenuData, TextAlign } from '../types';
|
||||
import { SelectedView, ClickedItemData, TextAlign } from '../types';
|
||||
|
||||
import FlameGraphContextMenu from './FlameGraphContextMenu';
|
||||
import FlameGraphMetadata from './FlameGraphMetadata';
|
||||
@ -35,36 +34,30 @@ import { getBarX, getRectDimensionsForLevel, renderRect } from './rendering';
|
||||
|
||||
type Props = {
|
||||
data: FlameGraphDataContainer;
|
||||
app: CoreApp;
|
||||
levels: LevelItem[][];
|
||||
topLevelIndex: number;
|
||||
selectedBarIndex: number;
|
||||
rangeMin: number;
|
||||
rangeMax: number;
|
||||
search: string;
|
||||
setTopLevelIndex: (level: number) => void;
|
||||
setSelectedBarIndex: (bar: number) => void;
|
||||
setRangeMin: (range: number) => void;
|
||||
setRangeMax: (range: number) => void;
|
||||
selectedView: SelectedView;
|
||||
style?: React.CSSProperties;
|
||||
onItemFocused: (itemIndex: number) => void;
|
||||
focusedItemIndex?: number;
|
||||
textAlign: TextAlign;
|
||||
};
|
||||
|
||||
const FlameGraph = ({
|
||||
data,
|
||||
app,
|
||||
levels,
|
||||
topLevelIndex,
|
||||
selectedBarIndex,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
search,
|
||||
setTopLevelIndex,
|
||||
setSelectedBarIndex,
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
selectedView,
|
||||
onItemFocused,
|
||||
focusedItemIndex,
|
||||
textAlign,
|
||||
}: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
@ -74,7 +67,8 @@ const FlameGraph = ({
|
||||
const graphRef = useRef<HTMLCanvasElement>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
const [tooltipItem, setTooltipItem] = useState<LevelItem>();
|
||||
const [contextMenuData, setContextMenuData] = useState<ContextMenuData>();
|
||||
|
||||
const [clickedItemData, setClickedItemData] = useState<ClickedItemData>();
|
||||
|
||||
const [ufuzzy] = useState(() => {
|
||||
return new uFuzzy();
|
||||
@ -120,22 +114,12 @@ const FlameGraph = ({
|
||||
// sometimes we collapse multiple bars into single rect.
|
||||
const dimensions = getRectDimensionsForLevel(data, level, levelIndex, totalTicks, rangeMin, pixelsPerTick);
|
||||
for (const rect of dimensions) {
|
||||
const focusedLevel = focusedItemIndex ? data.getLevel(focusedItemIndex) : 0;
|
||||
// Render each rectangle based on the computed dimensions
|
||||
renderRect(
|
||||
ctx,
|
||||
rect,
|
||||
totalTicks,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
search,
|
||||
levelIndex,
|
||||
topLevelIndex,
|
||||
foundLabels,
|
||||
textAlign
|
||||
);
|
||||
renderRect(ctx, rect, totalTicks, rangeMin, rangeMax, search, levelIndex, focusedLevel, foundLabels, textAlign);
|
||||
}
|
||||
}
|
||||
}, [data, levels, wrapperWidth, totalTicks, rangeMin, rangeMax, search, topLevelIndex, foundLabels, textAlign]);
|
||||
}, [data, levels, wrapperWidth, totalTicks, rangeMin, rangeMax, search, focusedItemIndex, foundLabels, textAlign]);
|
||||
|
||||
useEffect(() => {
|
||||
if (graphRef.current) {
|
||||
@ -153,15 +137,22 @@ const FlameGraph = ({
|
||||
|
||||
// if clicking on a block in the canvas
|
||||
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 {
|
||||
// if clicking on the canvas but there is no block beneath the cursor
|
||||
setContextMenuData(undefined);
|
||||
setClickedItemData(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
graphRef.current!.onmousemove = (e) => {
|
||||
if (tooltipRef.current && contextMenuData === undefined) {
|
||||
if (tooltipRef.current && clickedItemData === undefined) {
|
||||
setTooltipItem(undefined);
|
||||
const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin);
|
||||
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
|
||||
@ -197,16 +188,13 @@ const FlameGraph = ({
|
||||
levels,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
topLevelIndex,
|
||||
totalTicks,
|
||||
wrapperWidth,
|
||||
setTopLevelIndex,
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
selectedView,
|
||||
setSelectedBarIndex,
|
||||
setContextMenuData,
|
||||
contextMenuData,
|
||||
setClickedItemData,
|
||||
clickedItemData,
|
||||
]);
|
||||
|
||||
// hide context menu if outside the flame graph canvas is clicked
|
||||
@ -214,38 +202,31 @@ const FlameGraph = ({
|
||||
const handleOnClick = (e: MouseEvent) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
if ((e.target as HTMLElement).parentElement?.id !== 'flameGraphCanvasContainer') {
|
||||
setContextMenuData(undefined);
|
||||
setClickedItemData(undefined);
|
||||
}
|
||||
};
|
||||
window.addEventListener('click', handleOnClick);
|
||||
return () => window.removeEventListener('click', handleOnClick);
|
||||
}, [setContextMenuData]);
|
||||
}, [setClickedItemData]);
|
||||
|
||||
return (
|
||||
<div className={styles.graph} ref={sizeRef}>
|
||||
<FlameGraphMetadata
|
||||
data={data}
|
||||
levels={levels}
|
||||
topLevelIndex={topLevelIndex}
|
||||
selectedBarIndex={selectedBarIndex}
|
||||
totalTicks={totalTicks}
|
||||
/>
|
||||
<FlameGraphMetadata data={data} focusedItemIndex={focusedItemIndex} totalTicks={totalTicks} />
|
||||
<div className={styles.canvasContainer} id="flameGraphCanvasContainer">
|
||||
<canvas ref={graphRef} data-testid="flameGraph" />
|
||||
</div>
|
||||
<FlameGraphTooltip tooltipRef={tooltipRef} item={tooltipItem} data={data} totalTicks={totalTicks} />
|
||||
{contextMenuData && (
|
||||
{clickedItemData && (
|
||||
<FlameGraphContextMenu
|
||||
data={data}
|
||||
contextMenuData={contextMenuData!}
|
||||
levels={levels}
|
||||
totalTicks={totalTicks}
|
||||
graphRef={graphRef}
|
||||
setContextMenuData={setContextMenuData}
|
||||
setTopLevelIndex={setTopLevelIndex}
|
||||
setSelectedBarIndex={setSelectedBarIndex}
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
itemData={clickedItemData}
|
||||
onMenuItemClick={() => {
|
||||
setClickedItemData(undefined);
|
||||
}}
|
||||
onItemFocus={(itemIndex) => {
|
||||
setRangeMin(clickedItemData.start / totalTicks);
|
||||
setRangeMax((clickedItemData.start + data.getValue(clickedItemData.itemIndex)) / totalTicks);
|
||||
onItemFocused(itemIndex);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -2,78 +2,47 @@ import React from 'react';
|
||||
|
||||
import { MenuItem, ContextMenu } from '@grafana/ui';
|
||||
|
||||
import { ContextMenuData } from '../types';
|
||||
|
||||
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||
import { ClickedItemData } from '../types';
|
||||
|
||||
type Props = {
|
||||
contextMenuData: ContextMenuData;
|
||||
data: FlameGraphDataContainer;
|
||||
levels: LevelItem[][];
|
||||
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;
|
||||
itemData: ClickedItemData;
|
||||
onMenuItemClick: () => void;
|
||||
onItemFocus: (itemIndex: number) => void;
|
||||
};
|
||||
|
||||
const FlameGraphContextMenu = ({
|
||||
contextMenuData,
|
||||
graphRef,
|
||||
totalTicks,
|
||||
levels,
|
||||
setContextMenuData,
|
||||
setTopLevelIndex,
|
||||
setSelectedBarIndex,
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
data,
|
||||
}: Props) => {
|
||||
const clickedItem = levels[contextMenuData.levelIndex][contextMenuData.barIndex];
|
||||
|
||||
const renderMenuItems = () => {
|
||||
const FlameGraphContextMenu = ({ itemData, onMenuItemClick, onItemFocus }: Props) => {
|
||||
function renderItems() {
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
label="Focus block"
|
||||
icon={'eye'}
|
||||
onClick={() => {
|
||||
if (graphRef.current && contextMenuData) {
|
||||
setTopLevelIndex(contextMenuData.levelIndex);
|
||||
setSelectedBarIndex(contextMenuData.barIndex);
|
||||
setRangeMin(clickedItem.start / totalTicks);
|
||||
setRangeMax((clickedItem.start + data.getValue(clickedItem.itemIndex)) / totalTicks);
|
||||
setContextMenuData(undefined);
|
||||
}
|
||||
onItemFocus(itemData.itemIndex);
|
||||
onMenuItemClick();
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
label="Copy function name"
|
||||
icon={'copy'}
|
||||
onClick={() => {
|
||||
if (graphRef.current && contextMenuData) {
|
||||
navigator.clipboard.writeText(data.getLabel(clickedItem.itemIndex)).then(() => {
|
||||
setContextMenuData(undefined);
|
||||
});
|
||||
}
|
||||
navigator.clipboard.writeText(itemData.label).then(() => {
|
||||
onMenuItemClick();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="contextMenu">
|
||||
{contextMenuData.e.clientX && contextMenuData.e.clientY && (
|
||||
<ContextMenu
|
||||
renderMenuItems={() => renderMenuItems()}
|
||||
x={contextMenuData.e.clientX + 10}
|
||||
y={contextMenuData.e.clientY}
|
||||
focusOnOpen={false}
|
||||
></ContextMenu>
|
||||
)}
|
||||
<ContextMenu
|
||||
renderMenuItems={renderItems}
|
||||
x={itemData.posX + 10}
|
||||
y={itemData.posY}
|
||||
focusOnOpen={false}
|
||||
></ContextMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ describe('should get metadata correctly', () => {
|
||||
const container = new FlameGraphDataContainer(
|
||||
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({
|
||||
percentValue: 18.83,
|
||||
unitTitle: 'RAM',
|
||||
@ -35,7 +35,7 @@ describe('should get metadata correctly', () => {
|
||||
const container = new FlameGraphDataContainer(
|
||||
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({
|
||||
percentValue: 18.83,
|
||||
unitTitle: 'Count',
|
||||
@ -48,7 +48,7 @@ describe('should get metadata correctly', () => {
|
||||
const container = new FlameGraphDataContainer(
|
||||
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({
|
||||
percentValue: 18.83,
|
||||
unitTitle: 'Count',
|
||||
@ -61,7 +61,7 @@ describe('should get metadata correctly', () => {
|
||||
const container = new FlameGraphDataContainer(
|
||||
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({
|
||||
percentValue: 18.83,
|
||||
unitTitle: 'Count',
|
||||
@ -74,7 +74,7 @@ describe('should get metadata correctly', () => {
|
||||
const container = new FlameGraphDataContainer(
|
||||
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({
|
||||
percentValue: 18.83,
|
||||
unitTitle: 'Time',
|
||||
|
@ -5,29 +5,23 @@ import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { Metadata } from '../types';
|
||||
|
||||
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||
import { FlameGraphDataContainer } from './dataTransform';
|
||||
|
||||
type Props = {
|
||||
data: FlameGraphDataContainer;
|
||||
levels: LevelItem[][];
|
||||
topLevelIndex: number;
|
||||
selectedBarIndex: 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);
|
||||
if (levels[topLevelIndex] && levels[topLevelIndex][selectedBarIndex]) {
|
||||
const bar = levels[topLevelIndex][selectedBarIndex];
|
||||
const metadata = getMetadata(data, bar, totalTicks);
|
||||
const metadataText = `${metadata?.unitValue} (${metadata?.percentValue}%) of ${metadata?.samples} total samples (${metadata?.unitTitle})`;
|
||||
return <>{<div className={styles.metadata}>{metadataText}</div>}</>;
|
||||
}
|
||||
return <></>;
|
||||
const metadata = getMetadata(data, focusedItemIndex || 0, totalTicks);
|
||||
const metadataText = `${metadata?.unitValue} (${metadata?.percentValue}%) of ${metadata?.samples} total samples (${metadata?.unitTitle})`;
|
||||
return <>{<div className={styles.metadata}>{metadataText}</div>}</>;
|
||||
});
|
||||
|
||||
export const getMetadata = (data: FlameGraphDataContainer, bar: LevelItem, totalTicks: number): Metadata => {
|
||||
const displayValue = data.getValueDisplay(bar.itemIndex);
|
||||
export const getMetadata = (data: FlameGraphDataContainer, itemIndex: number, totalTicks: number): Metadata => {
|
||||
const displayValue = data.getValueDisplay(itemIndex);
|
||||
const percentValue = Math.round(10000 * (displayValue.numeric / totalTicks)) / 100;
|
||||
let unitValue = displayValue.text + displayValue.suffix;
|
||||
|
||||
|
@ -21,6 +21,7 @@ describe('getRectDimensionsForLevel', () => {
|
||||
{
|
||||
width: 999,
|
||||
height: 22,
|
||||
itemIndex: 0,
|
||||
x: 0,
|
||||
y: 22,
|
||||
collapsed: false,
|
||||
@ -42,9 +43,29 @@ describe('getRectDimensionsForLevel', () => {
|
||||
);
|
||||
const result = getRectDimensionsForLevel(container, level, 2, 100, 0, 10);
|
||||
expect(result).toEqual([
|
||||
{ width: 999, height: 22, x: 0, y: 44, collapsed: false, ticks: 100, label: '1', unitLabel: '100' },
|
||||
{ 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: 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',
|
||||
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);
|
||||
expect(result).toEqual([
|
||||
{ width: 99, height: 22, x: 0, y: 44, collapsed: false, ticks: 100, label: '1', unitLabel: '100' },
|
||||
{ width: 3, height: 22, x: 100, y: 44, collapsed: true, ticks: 3, label: '2', unitLabel: '2' },
|
||||
{ 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', itemIndex: 1 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -21,6 +21,7 @@ type RectData = {
|
||||
ticks: number;
|
||||
label: string;
|
||||
unitLabel: string;
|
||||
itemIndex: number;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -67,6 +68,7 @@ export function getRectDimensionsForLevel(
|
||||
ticks: curBarTicks,
|
||||
label: data.getLabel(item.itemIndex),
|
||||
unitLabel: unit,
|
||||
itemIndex: item.itemIndex,
|
||||
});
|
||||
}
|
||||
return coordinatesLevel;
|
||||
|
@ -3,6 +3,7 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
|
||||
import { DataFrame, CoreApp, GrafanaTheme2 } from '@grafana/data';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { useStyles2, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
|
||||
@ -19,8 +20,8 @@ type Props = {
|
||||
};
|
||||
|
||||
const FlameGraphContainer = (props: Props) => {
|
||||
const [topLevelIndex, setTopLevelIndex] = useState(0);
|
||||
const [selectedBarIndex, setSelectedBarIndex] = useState(0);
|
||||
const [focusedItemIndex, setFocusedItemIndex] = useState<number>();
|
||||
|
||||
const [rangeMin, setRangeMin] = useState(0);
|
||||
const [rangeMax, setRangeMax] = useState(1);
|
||||
const [search, setSearch] = useState('');
|
||||
@ -56,8 +57,7 @@ const FlameGraphContainer = (props: Props) => {
|
||||
}, [selectedView, setSelectedView, containerWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
setTopLevelIndex(0);
|
||||
setSelectedBarIndex(0);
|
||||
setFocusedItemIndex(undefined);
|
||||
setRangeMin(0);
|
||||
setRangeMax(1);
|
||||
}, [props.data]);
|
||||
@ -68,15 +68,16 @@ const FlameGraphContainer = (props: Props) => {
|
||||
<div ref={sizeRef} className={styles.container}>
|
||||
<FlameGraphHeader
|
||||
app={props.app}
|
||||
setTopLevelIndex={setTopLevelIndex}
|
||||
setSelectedBarIndex={setSelectedBarIndex}
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
selectedView={selectedView}
|
||||
setSelectedView={setSelectedView}
|
||||
containerWidth={containerWidth}
|
||||
onReset={() => {
|
||||
setRangeMin(0);
|
||||
setRangeMax(1);
|
||||
setFocusedItemIndex(undefined);
|
||||
}}
|
||||
textAlign={textAlign}
|
||||
onTextAlignChange={setTextAlign}
|
||||
/>
|
||||
@ -87,31 +88,35 @@ const FlameGraphContainer = (props: Props) => {
|
||||
data={dataContainer}
|
||||
app={props.app}
|
||||
totalLevels={levels.length}
|
||||
selectedView={selectedView}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
setTopLevelIndex={setTopLevelIndex}
|
||||
setSelectedBarIndex={setSelectedBarIndex}
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
onSymbolClick={(symbol) => {
|
||||
if (search === symbol) {
|
||||
setSearch('');
|
||||
} else {
|
||||
reportInteraction('grafana_flamegraph_table_item_selected', {
|
||||
app: props.app,
|
||||
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 && (
|
||||
<FlameGraph
|
||||
data={dataContainer}
|
||||
app={props.app}
|
||||
levels={levels}
|
||||
topLevelIndex={topLevelIndex}
|
||||
selectedBarIndex={selectedBarIndex}
|
||||
rangeMin={rangeMin}
|
||||
rangeMax={rangeMax}
|
||||
search={search}
|
||||
setTopLevelIndex={setTopLevelIndex}
|
||||
setSelectedBarIndex={setSelectedBarIndex}
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
selectedView={selectedView}
|
||||
onItemFocused={(itemIndex) => setFocusedItemIndex(itemIndex)}
|
||||
focusedItemIndex={focusedItemIndex}
|
||||
textAlign={textAlign}
|
||||
/>
|
||||
)}
|
||||
|
@ -18,13 +18,12 @@ describe('FlameGraphHeader', () => {
|
||||
app={CoreApp.Explore}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
setTopLevelIndex={jest.fn()}
|
||||
setSelectedBarIndex={jest.fn()}
|
||||
setRangeMin={jest.fn()}
|
||||
setRangeMax={jest.fn()}
|
||||
selectedView={selectedView}
|
||||
setSelectedView={setSelectedView}
|
||||
containerWidth={1600}
|
||||
onReset={() => {
|
||||
setSearch('');
|
||||
}}
|
||||
onTextAlignChange={jest.fn()}
|
||||
textAlign={'left'}
|
||||
/>
|
||||
|
@ -15,14 +15,11 @@ import { SelectedView, TextAlign } from './types';
|
||||
type Props = {
|
||||
app: CoreApp;
|
||||
search: string;
|
||||
setTopLevelIndex: (level: number) => void;
|
||||
setSelectedBarIndex: (bar: number) => void;
|
||||
setRangeMin: (range: number) => void;
|
||||
setRangeMax: (range: number) => void;
|
||||
setSearch: (search: string) => void;
|
||||
selectedView: SelectedView;
|
||||
setSelectedView: (view: SelectedView) => void;
|
||||
containerWidth: number;
|
||||
onReset: () => void;
|
||||
textAlign: TextAlign;
|
||||
onTextAlignChange: (align: TextAlign) => void;
|
||||
};
|
||||
@ -30,14 +27,11 @@ type Props = {
|
||||
const FlameGraphHeader = ({
|
||||
app,
|
||||
search,
|
||||
setTopLevelIndex,
|
||||
setSelectedBarIndex,
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
setSearch,
|
||||
selectedView,
|
||||
setSelectedView,
|
||||
containerWidth,
|
||||
onReset,
|
||||
textAlign,
|
||||
onTextAlignChange,
|
||||
}: 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);
|
||||
|
||||
return (
|
||||
@ -75,7 +59,16 @@ const FlameGraphHeader = ({
|
||||
width={44}
|
||||
/>
|
||||
</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
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -1,19 +1,15 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { CoreApp, createDataFrame } from '@grafana/data';
|
||||
|
||||
import { FlameGraphDataContainer, nestedSetToLevels } from '../FlameGraph/dataTransform';
|
||||
import { data } from '../FlameGraph/testData/dataNestedSet';
|
||||
import { SelectedView } from '../types';
|
||||
|
||||
import FlameGraphTopTableContainer from './FlameGraphTopTableContainer';
|
||||
|
||||
describe('FlameGraphTopTableContainer', () => {
|
||||
const FlameGraphTopTableContainerWithProps = () => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedView, _] = useState(SelectedView.Both);
|
||||
|
||||
const flameGraphData = createDataFrame(data);
|
||||
const container = new FlameGraphDataContainer(flameGraphData);
|
||||
const levels = nestedSetToLevels(container);
|
||||
@ -23,13 +19,7 @@ describe('FlameGraphTopTableContainer', () => {
|
||||
data={container}
|
||||
app={CoreApp.Explore}
|
||||
totalLevels={levels.length}
|
||||
selectedView={selectedView}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
setTopLevelIndex={jest.fn()}
|
||||
setSelectedBarIndex={jest.fn()}
|
||||
setRangeMin={jest.fn()}
|
||||
setRangeMax={jest.fn()}
|
||||
onSymbolClick={jest.fn()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -8,52 +8,18 @@ import { Table, TableSortByFieldState, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { PIXELS_PER_LEVEL, TOP_TABLE_COLUMN_WIDTH } from '../../constants';
|
||||
import { FlameGraphDataContainer } from '../FlameGraph/dataTransform';
|
||||
import { SelectedView, TableData } from '../types';
|
||||
import { TableData } from '../types';
|
||||
|
||||
type Props = {
|
||||
data: FlameGraphDataContainer;
|
||||
app: CoreApp;
|
||||
totalLevels: number;
|
||||
selectedView: SelectedView;
|
||||
search: string;
|
||||
setSearch: (search: string) => void;
|
||||
setTopLevelIndex: (level: number) => void;
|
||||
setSelectedBarIndex: (bar: number) => void;
|
||||
setRangeMin: (range: number) => void;
|
||||
setRangeMax: (range: number) => void;
|
||||
onSymbolClick: (symbol: string) => void;
|
||||
};
|
||||
|
||||
const FlameGraphTopTableContainer = ({
|
||||
data,
|
||||
app,
|
||||
totalLevels,
|
||||
selectedView,
|
||||
search,
|
||||
setSearch,
|
||||
setTopLevelIndex,
|
||||
setSelectedBarIndex,
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
}: Props) => {
|
||||
const FlameGraphTopTableContainer = ({ data, app, totalLevels, onSymbolClick }: Props) => {
|
||||
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 }]);
|
||||
|
||||
return (
|
||||
|
@ -1,7 +1,9 @@
|
||||
export type ContextMenuData = {
|
||||
e: MouseEvent;
|
||||
levelIndex: number;
|
||||
barIndex: number;
|
||||
export type ClickedItemData = {
|
||||
posX: number;
|
||||
posY: number;
|
||||
itemIndex: number;
|
||||
label: string;
|
||||
start: number;
|
||||
};
|
||||
|
||||
export type Metadata = {
|
||||
|
Loading…
Reference in New Issue
Block a user