mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
FlameGraph: adds ability to add context menu items (#81675)
* pyroscope: adds ability to add context menu items * moves things around * removes console.log * improvements * Change the extra context button API shape * Add test * lint --------- Co-authored-by: Andrej Ocenas <mr.ocenas@gmail.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { createDataFrame } from '@grafana/data';
|
import { createDataFrame } from '@grafana/data';
|
||||||
|
|
||||||
import { ColorScheme } from '../types';
|
import { ColorScheme, SelectedView } from '../types';
|
||||||
|
|
||||||
import FlameGraph from './FlameGraph';
|
import FlameGraph from './FlameGraph';
|
||||||
import { FlameGraphDataContainer } from './dataTransform';
|
import { FlameGraphDataContainer } from './dataTransform';
|
||||||
@@ -23,7 +23,7 @@ jest.mock('react-use', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('FlameGraph', () => {
|
describe('FlameGraph', () => {
|
||||||
function setup() {
|
function setup(props?: Partial<React.ComponentProps<typeof FlameGraph>>) {
|
||||||
const flameGraphData = createDataFrame(data);
|
const flameGraphData = createDataFrame(data);
|
||||||
const container = new FlameGraphDataContainer(flameGraphData, { collapsing: true });
|
const container = new FlameGraphDataContainer(flameGraphData, { collapsing: true });
|
||||||
|
|
||||||
@@ -47,6 +47,9 @@ describe('FlameGraph', () => {
|
|||||||
onFocusPillClick={onFocusPillClick}
|
onFocusPillClick={onFocusPillClick}
|
||||||
onSandwichPillClick={onSandwichPillClick}
|
onSandwichPillClick={onSandwichPillClick}
|
||||||
colorScheme={ColorScheme.ValueBased}
|
colorScheme={ColorScheme.ValueBased}
|
||||||
|
selectedView={SelectedView.FlameGraph}
|
||||||
|
search={''}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -80,18 +83,31 @@ describe('FlameGraph', () => {
|
|||||||
expect(screen.getByText('16.5 Bil | 16.5 Bil samples (Count)')).toBeDefined();
|
expect(screen.getByText('16.5 Bil | 16.5 Bil samples (Count)')).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render context menu', async () => {
|
it('should render context menu + extra items', async () => {
|
||||||
const event = new MouseEvent('click', { bubbles: true });
|
const event = new MouseEvent('click', { bubbles: true });
|
||||||
Object.defineProperty(event, 'offsetX', { get: () => 10 });
|
Object.defineProperty(event, 'offsetX', { get: () => 10 });
|
||||||
Object.defineProperty(event, 'offsetY', { get: () => 10 });
|
Object.defineProperty(event, 'offsetY', { get: () => 10 });
|
||||||
Object.defineProperty(HTMLCanvasElement.prototype, 'clientWidth', { configurable: true, value: 500 });
|
Object.defineProperty(HTMLCanvasElement.prototype, 'clientWidth', { configurable: true, value: 500 });
|
||||||
|
|
||||||
setup();
|
setup({
|
||||||
|
getExtraContextMenuButtons: (clickedItemData, data, state) => {
|
||||||
|
expect(clickedItemData).toMatchObject({ posX: 0, posY: 0, label: 'total' });
|
||||||
|
expect(data.length).toEqual(1101);
|
||||||
|
expect(state).toEqual({
|
||||||
|
selectedView: SelectedView.FlameGraph,
|
||||||
|
isDiff: false,
|
||||||
|
search: '',
|
||||||
|
collapseConfig: undefined,
|
||||||
|
});
|
||||||
|
return [{ label: 'test extra item', icon: 'eye', onClick: () => {} }];
|
||||||
|
},
|
||||||
|
});
|
||||||
const canvas = screen.getByTestId('flameGraph') as HTMLCanvasElement;
|
const canvas = screen.getByTestId('flameGraph') as HTMLCanvasElement;
|
||||||
expect(canvas).toBeInTheDocument();
|
expect(canvas).toBeInTheDocument();
|
||||||
expect(screen.queryByTestId('contextMenu')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('contextMenu')).not.toBeInTheDocument();
|
||||||
|
|
||||||
fireEvent(canvas, event);
|
fireEvent(canvas, event);
|
||||||
expect(screen.getByTestId('contextMenu')).toBeInTheDocument();
|
expect(screen.getByTestId('contextMenu')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('test extra item')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { Icon } from '@grafana/ui';
|
import { Icon } from '@grafana/ui';
|
||||||
|
|
||||||
import { PIXELS_PER_LEVEL } from '../constants';
|
import { PIXELS_PER_LEVEL } from '../constants';
|
||||||
import { ClickedItemData, ColorScheme, ColorSchemeDiff, TextAlign } from '../types';
|
import { ClickedItemData, ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from '../types';
|
||||||
|
|
||||||
import FlameGraphCanvas from './FlameGraphCanvas';
|
import FlameGraphCanvas from './FlameGraphCanvas';
|
||||||
|
import { GetExtraContextMenuButtonsFunction } from './FlameGraphContextMenu';
|
||||||
import FlameGraphMetadata from './FlameGraphMetadata';
|
import FlameGraphMetadata from './FlameGraphMetadata';
|
||||||
import { CollapsedMap, FlameGraphDataContainer, LevelItem } from './dataTransform';
|
import { CollapsedMap, FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||||
|
|
||||||
@@ -45,7 +46,10 @@ type Props = {
|
|||||||
onSandwichPillClick: () => void;
|
onSandwichPillClick: () => void;
|
||||||
colorScheme: ColorScheme | ColorSchemeDiff;
|
colorScheme: ColorScheme | ColorSchemeDiff;
|
||||||
showFlameGraphOnly?: boolean;
|
showFlameGraphOnly?: boolean;
|
||||||
|
getExtraContextMenuButtons?: GetExtraContextMenuButtonsFunction;
|
||||||
collapsing?: boolean;
|
collapsing?: boolean;
|
||||||
|
selectedView: SelectedView;
|
||||||
|
search: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FlameGraph = ({
|
const FlameGraph = ({
|
||||||
@@ -64,7 +68,10 @@ const FlameGraph = ({
|
|||||||
onSandwichPillClick,
|
onSandwichPillClick,
|
||||||
colorScheme,
|
colorScheme,
|
||||||
showFlameGraphOnly,
|
showFlameGraphOnly,
|
||||||
|
getExtraContextMenuButtons,
|
||||||
collapsing,
|
collapsing,
|
||||||
|
selectedView,
|
||||||
|
search,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const styles = getStyles();
|
const styles = getStyles();
|
||||||
|
|
||||||
@@ -122,7 +129,10 @@ const FlameGraph = ({
|
|||||||
showFlameGraphOnly,
|
showFlameGraphOnly,
|
||||||
collapsedMap,
|
collapsedMap,
|
||||||
setCollapsedMap,
|
setCollapsedMap,
|
||||||
|
getExtraContextMenuButtons,
|
||||||
collapsing,
|
collapsing,
|
||||||
|
search,
|
||||||
|
selectedView,
|
||||||
};
|
};
|
||||||
const canvas = levelsCallers ? (
|
const canvas = levelsCallers ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import React, { MouseEvent as ReactMouseEvent, useCallback, useEffect, useRef, u
|
|||||||
import { useMeasure } from 'react-use';
|
import { useMeasure } from 'react-use';
|
||||||
|
|
||||||
import { PIXELS_PER_LEVEL } from '../constants';
|
import { PIXELS_PER_LEVEL } from '../constants';
|
||||||
import { ClickedItemData, ColorScheme, ColorSchemeDiff, TextAlign } from '../types';
|
import { ClickedItemData, ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from '../types';
|
||||||
|
|
||||||
import FlameGraphContextMenu from './FlameGraphContextMenu';
|
import FlameGraphContextMenu, { GetExtraContextMenuButtonsFunction } from './FlameGraphContextMenu';
|
||||||
import FlameGraphTooltip from './FlameGraphTooltip';
|
import FlameGraphTooltip from './FlameGraphTooltip';
|
||||||
import { CollapseConfig, CollapsedMap, FlameGraphDataContainer, LevelItem } from './dataTransform';
|
import { CollapseConfig, CollapsedMap, FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||||
import { getBarX, useFlameRender } from './rendering';
|
import { getBarX, useFlameRender } from './rendering';
|
||||||
@@ -37,6 +37,10 @@ type Props = {
|
|||||||
collapsedMap: CollapsedMap;
|
collapsedMap: CollapsedMap;
|
||||||
setCollapsedMap: (collapsedMap: CollapsedMap) => void;
|
setCollapsedMap: (collapsedMap: CollapsedMap) => void;
|
||||||
collapsing?: boolean;
|
collapsing?: boolean;
|
||||||
|
getExtraContextMenuButtons?: GetExtraContextMenuButtonsFunction;
|
||||||
|
|
||||||
|
selectedView: SelectedView;
|
||||||
|
search: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FlameGraphCanvas = ({
|
const FlameGraphCanvas = ({
|
||||||
@@ -61,6 +65,9 @@ const FlameGraphCanvas = ({
|
|||||||
collapsedMap,
|
collapsedMap,
|
||||||
setCollapsedMap,
|
setCollapsedMap,
|
||||||
collapsing,
|
collapsing,
|
||||||
|
getExtraContextMenuButtons,
|
||||||
|
selectedView,
|
||||||
|
search,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const styles = getStyles();
|
const styles = getStyles();
|
||||||
|
|
||||||
@@ -186,6 +193,7 @@ const FlameGraphCanvas = ({
|
|||||||
/>
|
/>
|
||||||
{!showFlameGraphOnly && clickedItemData && (
|
{!showFlameGraphOnly && clickedItemData && (
|
||||||
<FlameGraphContextMenu
|
<FlameGraphContextMenu
|
||||||
|
data={data}
|
||||||
itemData={clickedItemData}
|
itemData={clickedItemData}
|
||||||
collapsing={collapsing}
|
collapsing={collapsing}
|
||||||
collapseConfig={collapsedMap.get(clickedItemData.item)}
|
collapseConfig={collapsedMap.get(clickedItemData.item)}
|
||||||
@@ -214,6 +222,9 @@ const FlameGraphCanvas = ({
|
|||||||
}}
|
}}
|
||||||
allGroupsCollapsed={Array.from(collapsedMap.values()).every((i) => i.collapsed)}
|
allGroupsCollapsed={Array.from(collapsedMap.values()).every((i) => i.collapsed)}
|
||||||
allGroupsExpanded={Array.from(collapsedMap.values()).every((i) => !i.collapsed)}
|
allGroupsExpanded={Array.from(collapsedMap.values()).every((i) => !i.collapsed)}
|
||||||
|
getExtraContextMenuButtons={getExtraContextMenuButtons}
|
||||||
|
selectedView={selectedView}
|
||||||
|
search={search}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { MenuItem, MenuGroup, ContextMenu } from '@grafana/ui';
|
import { DataFrame } from '@grafana/data';
|
||||||
|
import { MenuItem, MenuGroup, ContextMenu, IconName } from '@grafana/ui';
|
||||||
|
|
||||||
import { ClickedItemData } from '../types';
|
import { ClickedItemData, SelectedView } from '../types';
|
||||||
|
|
||||||
import { CollapseConfig } from './dataTransform';
|
import { CollapseConfig, FlameGraphDataContainer } from './dataTransform';
|
||||||
|
|
||||||
|
export type GetExtraContextMenuButtonsFunction = (
|
||||||
|
clickedItemData: ClickedItemData,
|
||||||
|
data: DataFrame,
|
||||||
|
state: { selectedView: SelectedView; isDiff: boolean; search: string; collapseConfig?: CollapseConfig }
|
||||||
|
) => ExtraContextMenuButton[];
|
||||||
|
|
||||||
|
export type ExtraContextMenuButton = {
|
||||||
|
label: string;
|
||||||
|
icon: IconName;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
data: FlameGraphDataContainer;
|
||||||
itemData: ClickedItemData;
|
itemData: ClickedItemData;
|
||||||
onMenuItemClick: () => void;
|
onMenuItemClick: () => void;
|
||||||
onItemFocus: () => void;
|
onItemFocus: () => void;
|
||||||
@@ -15,13 +29,17 @@ type Props = {
|
|||||||
onCollapseGroup: () => void;
|
onCollapseGroup: () => void;
|
||||||
onExpandAllGroups: () => void;
|
onExpandAllGroups: () => void;
|
||||||
onCollapseAllGroups: () => void;
|
onCollapseAllGroups: () => void;
|
||||||
|
getExtraContextMenuButtons?: GetExtraContextMenuButtonsFunction;
|
||||||
collapseConfig?: CollapseConfig;
|
collapseConfig?: CollapseConfig;
|
||||||
collapsing?: boolean;
|
collapsing?: boolean;
|
||||||
allGroupsCollapsed?: boolean;
|
allGroupsCollapsed?: boolean;
|
||||||
allGroupsExpanded?: boolean;
|
allGroupsExpanded?: boolean;
|
||||||
|
selectedView: SelectedView;
|
||||||
|
search: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FlameGraphContextMenu = ({
|
const FlameGraphContextMenu = ({
|
||||||
|
data,
|
||||||
itemData,
|
itemData,
|
||||||
onMenuItemClick,
|
onMenuItemClick,
|
||||||
onItemFocus,
|
onItemFocus,
|
||||||
@@ -31,11 +49,21 @@ const FlameGraphContextMenu = ({
|
|||||||
onCollapseGroup,
|
onCollapseGroup,
|
||||||
onExpandAllGroups,
|
onExpandAllGroups,
|
||||||
onCollapseAllGroups,
|
onCollapseAllGroups,
|
||||||
|
getExtraContextMenuButtons,
|
||||||
collapsing,
|
collapsing,
|
||||||
allGroupsExpanded,
|
allGroupsExpanded,
|
||||||
allGroupsCollapsed,
|
allGroupsCollapsed,
|
||||||
|
selectedView,
|
||||||
|
search,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
function renderItems() {
|
function renderItems() {
|
||||||
|
const extraButtons =
|
||||||
|
getExtraContextMenuButtons?.(itemData, data.data, {
|
||||||
|
selectedView,
|
||||||
|
isDiff: data.isDiffFlamegraph(),
|
||||||
|
search,
|
||||||
|
collapseConfig,
|
||||||
|
}) || [];
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -63,7 +91,9 @@ const FlameGraphContextMenu = ({
|
|||||||
onMenuItemClick();
|
onMenuItemClick();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{extraButtons.map(({ label, icon, onClick }) => {
|
||||||
|
return <MenuItem label={label} icon={icon} onClick={() => onClick()} key={label} />;
|
||||||
|
})}
|
||||||
{collapsing && (
|
{collapsing && (
|
||||||
<MenuGroup label={'Grouping'}>
|
<MenuGroup label={'Grouping'}>
|
||||||
{collapseConfig ? (
|
{collapseConfig ? (
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ export class FlameGraphDataContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isDiffFlamegraph() {
|
isDiffFlamegraph() {
|
||||||
return this.valueRightField && this.selfRightField;
|
return Boolean(this.valueRightField && this.selfRightField);
|
||||||
}
|
}
|
||||||
|
|
||||||
getLabel(index: number) {
|
getLabel(index: number) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
|||||||
import { ThemeContext } from '@grafana/ui';
|
import { ThemeContext } from '@grafana/ui';
|
||||||
|
|
||||||
import FlameGraph from './FlameGraph/FlameGraph';
|
import FlameGraph from './FlameGraph/FlameGraph';
|
||||||
|
import { GetExtraContextMenuButtonsFunction } from './FlameGraph/FlameGraphContextMenu';
|
||||||
import { FlameGraphDataContainer } from './FlameGraph/dataTransform';
|
import { FlameGraphDataContainer } from './FlameGraph/dataTransform';
|
||||||
import FlameGraphHeader from './FlameGraphHeader';
|
import FlameGraphHeader from './FlameGraphHeader';
|
||||||
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
|
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
|
||||||
@@ -52,6 +53,11 @@ export type Props = {
|
|||||||
*/
|
*/
|
||||||
extraHeaderElements?: React.ReactNode;
|
extraHeaderElements?: React.ReactNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra buttons that will be shown in the context menu when user clicks on a Node.
|
||||||
|
*/
|
||||||
|
getExtraContextMenuButtons?: GetExtraContextMenuButtonsFunction;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If true the flamegraph will be rendered on top of the table.
|
* If true the flamegraph will be rendered on top of the table.
|
||||||
*/
|
*/
|
||||||
@@ -80,6 +86,7 @@ const FlameGraphContainer = ({
|
|||||||
vertical,
|
vertical,
|
||||||
showFlameGraphOnly,
|
showFlameGraphOnly,
|
||||||
disableCollapsing,
|
disableCollapsing,
|
||||||
|
getExtraContextMenuButtons,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [focusedItemData, setFocusedItemData] = useState<ClickedItemData>();
|
const [focusedItemData, setFocusedItemData] = useState<ClickedItemData>();
|
||||||
|
|
||||||
@@ -169,6 +176,9 @@ const FlameGraphContainer = ({
|
|||||||
colorScheme={colorScheme}
|
colorScheme={colorScheme}
|
||||||
showFlameGraphOnly={showFlameGraphOnly}
|
showFlameGraphOnly={showFlameGraphOnly}
|
||||||
collapsing={!disableCollapsing}
|
collapsing={!disableCollapsing}
|
||||||
|
getExtraContextMenuButtons={getExtraContextMenuButtons}
|
||||||
|
selectedView={selectedView}
|
||||||
|
search={search}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -238,7 +248,7 @@ const FlameGraphContainer = ({
|
|||||||
stickyHeader={Boolean(stickyHeader)}
|
stickyHeader={Boolean(stickyHeader)}
|
||||||
extraHeaderElements={extraHeaderElements}
|
extraHeaderElements={extraHeaderElements}
|
||||||
vertical={vertical}
|
vertical={vertical}
|
||||||
isDiffMode={Boolean(dataContainer.isDiffFlamegraph())}
|
isDiffMode={dataContainer.isDiffFlamegraph()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { LevelItem } from './FlameGraph/dataTransform';
|
import { LevelItem } from './FlameGraph/dataTransform';
|
||||||
|
|
||||||
|
export { type FlameGraphDataContainer } from './FlameGraph/dataTransform';
|
||||||
|
|
||||||
|
export { type ExtraContextMenuButton } from './FlameGraph/FlameGraphContextMenu';
|
||||||
|
|
||||||
export type ClickedItemData = {
|
export type ClickedItemData = {
|
||||||
posX: number;
|
posX: number;
|
||||||
posY: number;
|
posY: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user