mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 01:15:46 +08:00
FlameGraph: Simplify the data needed for context menu and item focusing (#69006)
This commit is contained in:
@ -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'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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',
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -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'}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
|
@ -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()}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 (
|
||||||
|
@ -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 = {
|
||||||
|
Reference in New Issue
Block a user