From 33154abcd976365f2f94ef62be971814a25c4a6d Mon Sep 17 00:00:00 2001 From: Dmitry Filimonov Date: Thu, 14 Mar 2024 03:00:30 -0700 Subject: [PATCH] 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 --- .../src/FlameGraph/FlameGraph.test.tsx | 24 ++++++++++-- .../src/FlameGraph/FlameGraph.tsx | 12 +++++- .../src/FlameGraph/FlameGraphCanvas.tsx | 15 +++++++- .../src/FlameGraph/FlameGraphContextMenu.tsx | 38 +++++++++++++++++-- .../src/FlameGraph/dataTransform.ts | 2 +- .../src/FlameGraphContainer.tsx | 12 +++++- packages/grafana-flamegraph/src/types.ts | 4 ++ 7 files changed, 94 insertions(+), 13 deletions(-) diff --git a/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.test.tsx b/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.test.tsx index 93fb6b71eb3..90fdf831406 100644 --- a/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.test.tsx +++ b/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { createDataFrame } from '@grafana/data'; -import { ColorScheme } from '../types'; +import { ColorScheme, SelectedView } from '../types'; import FlameGraph from './FlameGraph'; import { FlameGraphDataContainer } from './dataTransform'; @@ -23,7 +23,7 @@ jest.mock('react-use', () => { }); describe('FlameGraph', () => { - function setup() { + function setup(props?: Partial>) { const flameGraphData = createDataFrame(data); const container = new FlameGraphDataContainer(flameGraphData, { collapsing: true }); @@ -47,6 +47,9 @@ describe('FlameGraph', () => { onFocusPillClick={onFocusPillClick} onSandwichPillClick={onSandwichPillClick} colorScheme={ColorScheme.ValueBased} + selectedView={SelectedView.FlameGraph} + search={''} + {...props} /> ); return { @@ -80,18 +83,31 @@ describe('FlameGraph', () => { 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 }); Object.defineProperty(event, 'offsetX', { get: () => 10 }); Object.defineProperty(event, 'offsetY', { get: () => 10 }); 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; expect(canvas).toBeInTheDocument(); expect(screen.queryByTestId('contextMenu')).not.toBeInTheDocument(); fireEvent(canvas, event); expect(screen.getByTestId('contextMenu')).toBeInTheDocument(); + expect(screen.getByText('test extra item')).toBeInTheDocument(); }); }); diff --git a/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx b/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx index 6bd6bb1ab9f..0a207cb94ea 100644 --- a/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx +++ b/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx @@ -22,9 +22,10 @@ import React, { useEffect, useState } from 'react'; import { Icon } from '@grafana/ui'; 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 { GetExtraContextMenuButtonsFunction } from './FlameGraphContextMenu'; import FlameGraphMetadata from './FlameGraphMetadata'; import { CollapsedMap, FlameGraphDataContainer, LevelItem } from './dataTransform'; @@ -45,7 +46,10 @@ type Props = { onSandwichPillClick: () => void; colorScheme: ColorScheme | ColorSchemeDiff; showFlameGraphOnly?: boolean; + getExtraContextMenuButtons?: GetExtraContextMenuButtonsFunction; collapsing?: boolean; + selectedView: SelectedView; + search: string; }; const FlameGraph = ({ @@ -64,7 +68,10 @@ const FlameGraph = ({ onSandwichPillClick, colorScheme, showFlameGraphOnly, + getExtraContextMenuButtons, collapsing, + selectedView, + search, }: Props) => { const styles = getStyles(); @@ -122,7 +129,10 @@ const FlameGraph = ({ showFlameGraphOnly, collapsedMap, setCollapsedMap, + getExtraContextMenuButtons, collapsing, + search, + selectedView, }; const canvas = levelsCallers ? ( <> diff --git a/packages/grafana-flamegraph/src/FlameGraph/FlameGraphCanvas.tsx b/packages/grafana-flamegraph/src/FlameGraph/FlameGraphCanvas.tsx index c5b067abea2..3d4f43a644e 100644 --- a/packages/grafana-flamegraph/src/FlameGraph/FlameGraphCanvas.tsx +++ b/packages/grafana-flamegraph/src/FlameGraph/FlameGraphCanvas.tsx @@ -3,9 +3,9 @@ import React, { MouseEvent as ReactMouseEvent, useCallback, useEffect, useRef, u import { useMeasure } from 'react-use'; 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 { CollapseConfig, CollapsedMap, FlameGraphDataContainer, LevelItem } from './dataTransform'; import { getBarX, useFlameRender } from './rendering'; @@ -37,6 +37,10 @@ type Props = { collapsedMap: CollapsedMap; setCollapsedMap: (collapsedMap: CollapsedMap) => void; collapsing?: boolean; + getExtraContextMenuButtons?: GetExtraContextMenuButtonsFunction; + + selectedView: SelectedView; + search: string; }; const FlameGraphCanvas = ({ @@ -61,6 +65,9 @@ const FlameGraphCanvas = ({ collapsedMap, setCollapsedMap, collapsing, + getExtraContextMenuButtons, + selectedView, + search, }: Props) => { const styles = getStyles(); @@ -186,6 +193,7 @@ const FlameGraphCanvas = ({ /> {!showFlameGraphOnly && clickedItemData && ( i.collapsed)} allGroupsExpanded={Array.from(collapsedMap.values()).every((i) => !i.collapsed)} + getExtraContextMenuButtons={getExtraContextMenuButtons} + selectedView={selectedView} + search={search} /> )} diff --git a/packages/grafana-flamegraph/src/FlameGraph/FlameGraphContextMenu.tsx b/packages/grafana-flamegraph/src/FlameGraph/FlameGraphContextMenu.tsx index 3a370f5dc86..c84d4810f42 100644 --- a/packages/grafana-flamegraph/src/FlameGraph/FlameGraphContextMenu.tsx +++ b/packages/grafana-flamegraph/src/FlameGraph/FlameGraphContextMenu.tsx @@ -1,12 +1,26 @@ 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 = { + data: FlameGraphDataContainer; itemData: ClickedItemData; onMenuItemClick: () => void; onItemFocus: () => void; @@ -15,13 +29,17 @@ type Props = { onCollapseGroup: () => void; onExpandAllGroups: () => void; onCollapseAllGroups: () => void; + getExtraContextMenuButtons?: GetExtraContextMenuButtonsFunction; collapseConfig?: CollapseConfig; collapsing?: boolean; allGroupsCollapsed?: boolean; allGroupsExpanded?: boolean; + selectedView: SelectedView; + search: string; }; const FlameGraphContextMenu = ({ + data, itemData, onMenuItemClick, onItemFocus, @@ -31,11 +49,21 @@ const FlameGraphContextMenu = ({ onCollapseGroup, onExpandAllGroups, onCollapseAllGroups, + getExtraContextMenuButtons, collapsing, allGroupsExpanded, allGroupsCollapsed, + selectedView, + search, }: Props) => { function renderItems() { + const extraButtons = + getExtraContextMenuButtons?.(itemData, data.data, { + selectedView, + isDiff: data.isDiffFlamegraph(), + search, + collapseConfig, + }) || []; return ( <> - + {extraButtons.map(({ label, icon, onClick }) => { + return onClick()} key={label} />; + })} {collapsing && ( {collapseConfig ? ( diff --git a/packages/grafana-flamegraph/src/FlameGraph/dataTransform.ts b/packages/grafana-flamegraph/src/FlameGraph/dataTransform.ts index 1fb84619476..142500c926e 100644 --- a/packages/grafana-flamegraph/src/FlameGraph/dataTransform.ts +++ b/packages/grafana-flamegraph/src/FlameGraph/dataTransform.ts @@ -272,7 +272,7 @@ export class FlameGraphDataContainer { } isDiffFlamegraph() { - return this.valueRightField && this.selfRightField; + return Boolean(this.valueRightField && this.selfRightField); } getLabel(index: number) { diff --git a/packages/grafana-flamegraph/src/FlameGraphContainer.tsx b/packages/grafana-flamegraph/src/FlameGraphContainer.tsx index df8435c0dad..c4962e5b181 100644 --- a/packages/grafana-flamegraph/src/FlameGraphContainer.tsx +++ b/packages/grafana-flamegraph/src/FlameGraphContainer.tsx @@ -7,6 +7,7 @@ import { DataFrame, GrafanaTheme2 } from '@grafana/data'; import { ThemeContext } from '@grafana/ui'; import FlameGraph from './FlameGraph/FlameGraph'; +import { GetExtraContextMenuButtonsFunction } from './FlameGraph/FlameGraphContextMenu'; import { FlameGraphDataContainer } from './FlameGraph/dataTransform'; import FlameGraphHeader from './FlameGraphHeader'; import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer'; @@ -52,6 +53,11 @@ export type Props = { */ 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. */ @@ -80,6 +86,7 @@ const FlameGraphContainer = ({ vertical, showFlameGraphOnly, disableCollapsing, + getExtraContextMenuButtons, }: Props) => { const [focusedItemData, setFocusedItemData] = useState(); @@ -169,6 +176,9 @@ const FlameGraphContainer = ({ colorScheme={colorScheme} showFlameGraphOnly={showFlameGraphOnly} collapsing={!disableCollapsing} + getExtraContextMenuButtons={getExtraContextMenuButtons} + selectedView={selectedView} + search={search} /> ); @@ -238,7 +248,7 @@ const FlameGraphContainer = ({ stickyHeader={Boolean(stickyHeader)} extraHeaderElements={extraHeaderElements} vertical={vertical} - isDiffMode={Boolean(dataContainer.isDiffFlamegraph())} + isDiffMode={dataContainer.isDiffFlamegraph()} /> )} diff --git a/packages/grafana-flamegraph/src/types.ts b/packages/grafana-flamegraph/src/types.ts index c4616a26847..aa8a84de882 100644 --- a/packages/grafana-flamegraph/src/types.ts +++ b/packages/grafana-flamegraph/src/types.ts @@ -1,5 +1,9 @@ import { LevelItem } from './FlameGraph/dataTransform'; +export { type FlameGraphDataContainer } from './FlameGraph/dataTransform'; + +export { type ExtraContextMenuButton } from './FlameGraph/FlameGraphContextMenu'; + export type ClickedItemData = { posX: number; posY: number;