Flamegraph: Add collapse and expand group buttons to toolbar (#87395)
This commit is contained in:
@@ -49,6 +49,8 @@ describe('FlameGraph', () => {
|
|||||||
colorScheme={ColorScheme.ValueBased}
|
colorScheme={ColorScheme.ValueBased}
|
||||||
selectedView={SelectedView.FlameGraph}
|
selectedView={SelectedView.FlameGraph}
|
||||||
search={''}
|
search={''}
|
||||||
|
collapsedMap={container.getCollapsedMap()}
|
||||||
|
setCollapsedMap={() => {}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ type Props = {
|
|||||||
collapsing?: boolean;
|
collapsing?: boolean;
|
||||||
selectedView: SelectedView;
|
selectedView: SelectedView;
|
||||||
search: string;
|
search: string;
|
||||||
|
collapsedMap: CollapsedMap;
|
||||||
|
setCollapsedMap: (collapsedMap: CollapsedMap) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FlameGraph = ({
|
const FlameGraph = ({
|
||||||
@@ -72,10 +74,11 @@ const FlameGraph = ({
|
|||||||
collapsing,
|
collapsing,
|
||||||
selectedView,
|
selectedView,
|
||||||
search,
|
search,
|
||||||
|
collapsedMap,
|
||||||
|
setCollapsedMap,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const styles = getStyles();
|
const styles = getStyles();
|
||||||
|
|
||||||
const [collapsedMap, setCollapsedMap] = useState<CollapsedMap>(new Map());
|
|
||||||
const [levels, setLevels] = useState<LevelItem[][]>();
|
const [levels, setLevels] = useState<LevelItem[][]>();
|
||||||
const [levelsCallers, setLevelsCallers] = useState<LevelItem[][]>();
|
const [levelsCallers, setLevelsCallers] = useState<LevelItem[][]>();
|
||||||
const [totalProfileTicks, setTotalProfileTicks] = useState<number>(0);
|
const [totalProfileTicks, setTotalProfileTicks] = useState<number>(0);
|
||||||
@@ -84,8 +87,6 @@ const FlameGraph = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
setCollapsedMap(data.getCollapsedMap());
|
|
||||||
|
|
||||||
let levels = data.getLevels();
|
let levels = data.getLevels();
|
||||||
let totalProfileTicks = levels.length ? levels[0][0].value : 0;
|
let totalProfileTicks = levels.length ? levels[0][0].value : 0;
|
||||||
let totalProfileTicksRight = levels.length ? levels[0][0].valueRight : undefined;
|
let totalProfileTicksRight = levels.length ? levels[0][0].valueRight : undefined;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ClickedItemData, ColorScheme, ColorSchemeDiff, SelectedView, TextAlign
|
|||||||
|
|
||||||
import FlameGraphContextMenu, { GetExtraContextMenuButtonsFunction } from './FlameGraphContextMenu';
|
import FlameGraphContextMenu, { GetExtraContextMenuButtonsFunction } from './FlameGraphContextMenu';
|
||||||
import FlameGraphTooltip from './FlameGraphTooltip';
|
import FlameGraphTooltip from './FlameGraphTooltip';
|
||||||
import { CollapseConfig, CollapsedMap, FlameGraphDataContainer, LevelItem } from './dataTransform';
|
import { CollapsedMap, FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||||
import { getBarX, useFlameRender } from './rendering';
|
import { getBarX, useFlameRender } from './rendering';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -209,16 +209,16 @@ const FlameGraphCanvas = ({
|
|||||||
onSandwich(data.getLabel(clickedItemData.item.itemIndexes[0]));
|
onSandwich(data.getLabel(clickedItemData.item.itemIndexes[0]));
|
||||||
}}
|
}}
|
||||||
onExpandGroup={() => {
|
onExpandGroup={() => {
|
||||||
setCollapsedMap(setCollapsedStatus(collapsedMap, clickedItemData.item, false));
|
setCollapsedMap(collapsedMap.setCollapsedStatus(clickedItemData.item, false));
|
||||||
}}
|
}}
|
||||||
onCollapseGroup={() => {
|
onCollapseGroup={() => {
|
||||||
setCollapsedMap(setCollapsedStatus(collapsedMap, clickedItemData.item, true));
|
setCollapsedMap(collapsedMap.setCollapsedStatus(clickedItemData.item, true));
|
||||||
}}
|
}}
|
||||||
onExpandAllGroups={() => {
|
onExpandAllGroups={() => {
|
||||||
setCollapsedMap(setAllCollapsedStatus(collapsedMap, false));
|
setCollapsedMap(collapsedMap.setAllCollapsedStatus(false));
|
||||||
}}
|
}}
|
||||||
onCollapseAllGroups={() => {
|
onCollapseAllGroups={() => {
|
||||||
setCollapsedMap(setAllCollapsedStatus(collapsedMap, true));
|
setCollapsedMap(collapsedMap.setAllCollapsedStatus(true));
|
||||||
}}
|
}}
|
||||||
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)}
|
||||||
@@ -231,27 +231,6 @@ const FlameGraphCanvas = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function setCollapsedStatus(collapsedMap: CollapsedMap, item: LevelItem, collapsed: boolean) {
|
|
||||||
const newMap = new Map(collapsedMap);
|
|
||||||
const collapsedConfig = collapsedMap.get(item)!;
|
|
||||||
const newConfig = { ...collapsedConfig, collapsed };
|
|
||||||
for (const item of collapsedConfig.items) {
|
|
||||||
newMap.set(item, newConfig);
|
|
||||||
}
|
|
||||||
return newMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAllCollapsedStatus(collapsedMap: CollapsedMap, collapsed: boolean) {
|
|
||||||
const newMap = new Map(collapsedMap);
|
|
||||||
for (const item of collapsedMap.keys()) {
|
|
||||||
const collapsedConfig = collapsedMap.get(item)!;
|
|
||||||
const newConfig = { ...collapsedConfig, collapsed };
|
|
||||||
newMap.set(item, newConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStyles = () => ({
|
const getStyles = () => ({
|
||||||
graph: css({
|
graph: css({
|
||||||
label: 'graph',
|
label: 'graph',
|
||||||
@@ -291,7 +270,7 @@ export const convertPixelCoordinatesToBarCoordinates = (
|
|||||||
pixelsPerTick: number,
|
pixelsPerTick: number,
|
||||||
totalTicks: number,
|
totalTicks: number,
|
||||||
rangeMin: number,
|
rangeMin: number,
|
||||||
collapsedMap: Map<LevelItem, CollapseConfig>
|
collapsedMap: CollapsedMap
|
||||||
): LevelItem | undefined => {
|
): LevelItem | undefined => {
|
||||||
let next: LevelItem | undefined = root;
|
let next: LevelItem | undefined = root;
|
||||||
let currentLevel = direction === 'children' ? 0 : depth - 1;
|
let currentLevel = direction === 'children' ? 0 : depth - 1;
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { createDataFrame, FieldType } from '@grafana/data';
|
import { createDataFrame, FieldType } from '@grafana/data';
|
||||||
|
|
||||||
import { CollapsedMapContainer, FlameGraphDataContainer, LevelItem, nestedSetToLevels } from './dataTransform';
|
import {
|
||||||
|
CollapsedMapBuilder,
|
||||||
|
FlameGraphDataContainer,
|
||||||
|
LevelItem,
|
||||||
|
nestedSetToLevels,
|
||||||
|
CollapsedMap,
|
||||||
|
} from './dataTransform';
|
||||||
import { textToDataContainer } from './testHelpers';
|
import { textToDataContainer } from './testHelpers';
|
||||||
|
|
||||||
describe('nestedSetToLevels', () => {
|
describe('nestedSetToLevels', () => {
|
||||||
@@ -126,7 +132,7 @@ describe('CollapsedMapContainer', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it('groups items if they are within value threshold', () => {
|
it('groups items if they are within value threshold', () => {
|
||||||
const container = new CollapsedMapContainer();
|
const container = new CollapsedMapBuilder();
|
||||||
|
|
||||||
const child2: LevelItem = {
|
const child2: LevelItem = {
|
||||||
...defaultItem,
|
...defaultItem,
|
||||||
@@ -147,13 +153,13 @@ describe('CollapsedMapContainer', () => {
|
|||||||
|
|
||||||
container.addItem(child1, parent);
|
container.addItem(child1, parent);
|
||||||
container.addItem(child2, child1);
|
container.addItem(child2, child1);
|
||||||
expect(container.getMap().get(child1)).toMatchObject({ collapsed: true, items: [parent, child1, child2] });
|
expect(container.getCollapsedMap().get(child1)).toMatchObject({ collapsed: true, items: [parent, child1, child2] });
|
||||||
expect(container.getMap().get(child2)).toMatchObject({ collapsed: true, items: [parent, child1, child2] });
|
expect(container.getCollapsedMap().get(child2)).toMatchObject({ collapsed: true, items: [parent, child1, child2] });
|
||||||
expect(container.getMap().get(parent)).toMatchObject({ collapsed: true, items: [parent, child1, child2] });
|
expect(container.getCollapsedMap().get(parent)).toMatchObject({ collapsed: true, items: [parent, child1, child2] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't group items if they are outside value threshold", () => {
|
it("doesn't group items if they are outside value threshold", () => {
|
||||||
const container = new CollapsedMapContainer();
|
const container = new CollapsedMapBuilder();
|
||||||
|
|
||||||
const parent: LevelItem = {
|
const parent: LevelItem = {
|
||||||
...defaultItem,
|
...defaultItem,
|
||||||
@@ -166,11 +172,11 @@ describe('CollapsedMapContainer', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
container.addItem(child, parent);
|
container.addItem(child, parent);
|
||||||
expect(container.getMap().size).toBe(0);
|
expect(container.getCollapsedMap().size()).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't group items if parent has multiple children", () => {
|
it("doesn't group items if parent has multiple children", () => {
|
||||||
const container = new CollapsedMapContainer();
|
const container = new CollapsedMapBuilder();
|
||||||
|
|
||||||
const child1: LevelItem = {
|
const child1: LevelItem = {
|
||||||
...defaultItem,
|
...defaultItem,
|
||||||
@@ -190,6 +196,52 @@ describe('CollapsedMapContainer', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
container.addItem(child1, parent);
|
container.addItem(child1, parent);
|
||||||
expect(container.getMap().size).toBe(0);
|
expect(container.getCollapsedMap().size()).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CollapsedMap', () => {
|
||||||
|
function getMap() {
|
||||||
|
const container = textToDataContainer(`
|
||||||
|
[0///////////]
|
||||||
|
[1][3//][6///]
|
||||||
|
[2] [9/]
|
||||||
|
`)!;
|
||||||
|
|
||||||
|
const items = container.getLevels();
|
||||||
|
|
||||||
|
return {
|
||||||
|
map: new CollapsedMap(
|
||||||
|
new Map([
|
||||||
|
[items[1][0], { items: [items[1][0], items[2][0]], collapsed: false }],
|
||||||
|
[items[1][2], { items: [items[1][2], items[2][1]], collapsed: true }],
|
||||||
|
])
|
||||||
|
),
|
||||||
|
items,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it('collapses and expands single item', () => {
|
||||||
|
const { map: collapsedMap, items } = getMap();
|
||||||
|
let newMap = collapsedMap.setCollapsedStatus(items[1][0], true);
|
||||||
|
expect(collapsedMap.get(items[1][0])?.collapsed).toBe(false);
|
||||||
|
expect(newMap.get(items[1][0])?.collapsed).toBe(true);
|
||||||
|
|
||||||
|
newMap = collapsedMap.setCollapsedStatus(items[1][2], false);
|
||||||
|
expect(collapsedMap.get(items[1][2])?.collapsed).toBe(true);
|
||||||
|
expect(newMap.get(items[1][2])?.collapsed).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('collapses and expands all items', () => {
|
||||||
|
const { map: collapsedMap, items } = getMap();
|
||||||
|
let newMap = collapsedMap.setAllCollapsedStatus(true);
|
||||||
|
expect(collapsedMap.get(items[1][0])?.collapsed).toBe(false);
|
||||||
|
expect(newMap.get(items[1][0])?.collapsed).toBe(true);
|
||||||
|
expect(newMap.get(items[1][2])?.collapsed).toBe(true);
|
||||||
|
|
||||||
|
newMap = collapsedMap.setAllCollapsedStatus(false);
|
||||||
|
expect(collapsedMap.get(items[1][2])?.collapsed).toBe(true);
|
||||||
|
expect(newMap.get(items[1][0])?.collapsed).toBe(false);
|
||||||
|
expect(newMap.get(items[1][2])?.collapsed).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export function nestedSetToLevels(
|
|||||||
levels[currentLevel].push(newItem);
|
levels[currentLevel].push(newItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
const collapsedMapContainer = new CollapsedMapContainer(options?.collapsingThreshold);
|
const collapsedMapContainer = new CollapsedMapBuilder(options?.collapsingThreshold);
|
||||||
if (options?.collapsing) {
|
if (options?.collapsing) {
|
||||||
// We collapse similar items here, where it seems like parent and child are the same thing and so the distinction
|
// We collapse similar items here, where it seems like parent and child are the same thing and so the distinction
|
||||||
// isn't that important. We create a map of items that should be collapsed together. We need to do it with complete
|
// isn't that important. We create a map of items that should be collapsed together. We need to do it with complete
|
||||||
@@ -99,11 +99,66 @@ export function nestedSetToLevels(
|
|||||||
collapsedMapContainer.addTree(levels[0][0]);
|
collapsedMapContainer.addTree(levels[0][0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [levels, uniqueLabels, collapsedMapContainer.getMap()];
|
return [levels, uniqueLabels, collapsedMapContainer.getCollapsedMap()];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CollapsedMap = Map<LevelItem, CollapseConfig>;
|
/**
|
||||||
export class CollapsedMapContainer {
|
* Small wrapper around the map of items that should be visually collapsed in the flame graph. Reason this is a wrapper
|
||||||
|
* is that we want to make sure that when this is in the state we don't update the map directly but create a new map
|
||||||
|
* and to have a place for the methods to collapse/expand either single item or all the items.
|
||||||
|
*/
|
||||||
|
export class CollapsedMap {
|
||||||
|
// The levelItem used as a key is the item that will always be rendered in the flame graph. The config.items are all
|
||||||
|
// the items that are in the group and if the config.collapsed is true they will be hidden.
|
||||||
|
private map: Map<LevelItem, CollapseConfig> = new Map();
|
||||||
|
|
||||||
|
constructor(map?: Map<LevelItem, CollapseConfig>) {
|
||||||
|
this.map = map || new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
get(item: LevelItem) {
|
||||||
|
return this.map.get(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
keys() {
|
||||||
|
return this.map.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
values() {
|
||||||
|
return this.map.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
size() {
|
||||||
|
return this.map.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCollapsedStatus(item: LevelItem, collapsed: boolean) {
|
||||||
|
const newMap = new Map(this.map);
|
||||||
|
const collapsedConfig = this.map.get(item)!;
|
||||||
|
const newConfig = { ...collapsedConfig, collapsed };
|
||||||
|
for (const item of collapsedConfig.items) {
|
||||||
|
newMap.set(item, newConfig);
|
||||||
|
}
|
||||||
|
return new CollapsedMap(newMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAllCollapsedStatus(collapsed: boolean) {
|
||||||
|
const newMap = new Map(this.map);
|
||||||
|
for (const item of this.map.keys()) {
|
||||||
|
const collapsedConfig = this.map.get(item)!;
|
||||||
|
const newConfig = { ...collapsedConfig, collapsed };
|
||||||
|
newMap.set(item, newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CollapsedMap(newMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to CollapsedMap but this one is mutable and used during transformation of the dataFrame data into structure
|
||||||
|
* we use for rendering. This should not be passed to the React components.
|
||||||
|
*/
|
||||||
|
export class CollapsedMapBuilder {
|
||||||
private map = new Map();
|
private map = new Map();
|
||||||
private threshold = 0.99;
|
private threshold = 0.99;
|
||||||
|
|
||||||
@@ -128,10 +183,10 @@ export class CollapsedMapContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The heuristics here is pretty simple right now. Just check if it's single child and if we are within threshold.
|
||||||
|
// We assume items with small self just aren't too important while we cannot really collapse items with siblings
|
||||||
|
// as it's not clear what to do with said sibling.
|
||||||
addItem(item: LevelItem, parent?: LevelItem) {
|
addItem(item: LevelItem, parent?: LevelItem) {
|
||||||
// The heuristics here is pretty simple right now. Just check if it's single child and if we are within threshold.
|
|
||||||
// We assume items with small self just aren't too important while we cannot really collapse items with siblings
|
|
||||||
// as it's not clear what to do with said sibling.
|
|
||||||
if (parent && item.value > parent.value * this.threshold && parent.children.length === 1) {
|
if (parent && item.value > parent.value * this.threshold && parent.children.length === 1) {
|
||||||
if (this.map.has(parent)) {
|
if (this.map.has(parent)) {
|
||||||
const config = this.map.get(parent)!;
|
const config = this.map.get(parent)!;
|
||||||
@@ -145,8 +200,8 @@ export class CollapsedMapContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getMap() {
|
getCollapsedMap() {
|
||||||
return new Map(this.map);
|
return new CollapsedMap(this.map);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +280,7 @@ export class FlameGraphDataContainer {
|
|||||||
|
|
||||||
private levels: LevelItem[][] | undefined;
|
private levels: LevelItem[][] | undefined;
|
||||||
private uniqueLabelsMap: Record<string, LevelItem[]> | undefined;
|
private uniqueLabelsMap: Record<string, LevelItem[]> | undefined;
|
||||||
private collapsedMap: Map<LevelItem, CollapseConfig> | undefined;
|
private collapsedMap: CollapsedMap | undefined;
|
||||||
|
|
||||||
constructor(data: DataFrame, options: Options, theme: GrafanaTheme2 = createTheme()) {
|
constructor(data: DataFrame, options: Options, theme: GrafanaTheme2 = createTheme()) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { createDataFrame, createTheme } from '@grafana/data';
|
import { createDataFrame, createTheme } from '@grafana/data';
|
||||||
|
|
||||||
@@ -29,7 +29,8 @@ describe('FlameGraphContainer', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return <FlameGraphContainer data={flameGraphData} getTheme={() => createTheme({ colors: { mode: 'dark' } })} />;
|
const getTheme = useCallback(() => createTheme({ colors: { mode: 'dark' } }), []);
|
||||||
|
return <FlameGraphContainer data={flameGraphData} getTheme={getTheme} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should render without error', async () => {
|
it('should render without error', async () => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { ThemeContext } from '@grafana/ui';
|
|||||||
|
|
||||||
import FlameGraph from './FlameGraph/FlameGraph';
|
import FlameGraph from './FlameGraph/FlameGraph';
|
||||||
import { GetExtraContextMenuButtonsFunction } from './FlameGraph/FlameGraphContextMenu';
|
import { GetExtraContextMenuButtonsFunction } from './FlameGraph/FlameGraphContextMenu';
|
||||||
import { FlameGraphDataContainer } from './FlameGraph/dataTransform';
|
import { CollapsedMap, FlameGraphDataContainer } from './FlameGraph/dataTransform';
|
||||||
import FlameGraphHeader from './FlameGraphHeader';
|
import FlameGraphHeader from './FlameGraphHeader';
|
||||||
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
|
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
|
||||||
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants';
|
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants';
|
||||||
@@ -98,14 +98,17 @@ const FlameGraphContainer = ({
|
|||||||
const [textAlign, setTextAlign] = useState<TextAlign>('left');
|
const [textAlign, setTextAlign] = useState<TextAlign>('left');
|
||||||
// This is a label of the item because in sandwich view we group all items by label and present a merged graph
|
// This is a label of the item because in sandwich view we group all items by label and present a merged graph
|
||||||
const [sandwichItem, setSandwichItem] = useState<string>();
|
const [sandwichItem, setSandwichItem] = useState<string>();
|
||||||
|
const [collapsedMap, setCollapsedMap] = useState(new CollapsedMap());
|
||||||
|
|
||||||
const theme = getTheme();
|
const theme = useMemo(() => getTheme(), [getTheme]);
|
||||||
|
|
||||||
const dataContainer = useMemo((): FlameGraphDataContainer | undefined => {
|
const dataContainer = useMemo((): FlameGraphDataContainer | undefined => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return new FlameGraphDataContainer(data, { collapsing: !disableCollapsing }, theme);
|
|
||||||
|
const container = new FlameGraphDataContainer(data, { collapsing: !disableCollapsing }, theme);
|
||||||
|
setCollapsedMap(container.getCollapsedMap());
|
||||||
|
return container;
|
||||||
}, [data, theme, disableCollapsing]);
|
}, [data, theme, disableCollapsing]);
|
||||||
const [colorScheme, setColorScheme] = useColorScheme(dataContainer);
|
const [colorScheme, setColorScheme] = useColorScheme(dataContainer);
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
@@ -179,6 +182,8 @@ const FlameGraphContainer = ({
|
|||||||
getExtraContextMenuButtons={getExtraContextMenuButtons}
|
getExtraContextMenuButtons={getExtraContextMenuButtons}
|
||||||
selectedView={selectedView}
|
selectedView={selectedView}
|
||||||
search={search}
|
search={search}
|
||||||
|
collapsedMap={collapsedMap}
|
||||||
|
setCollapsedMap={setCollapsedMap}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -250,6 +255,8 @@ const FlameGraphContainer = ({
|
|||||||
extraHeaderElements={extraHeaderElements}
|
extraHeaderElements={extraHeaderElements}
|
||||||
vertical={vertical}
|
vertical={vertical}
|
||||||
isDiffMode={dataContainer.isDiffFlamegraph()}
|
isDiffMode={dataContainer.isDiffFlamegraph()}
|
||||||
|
setCollapsedMap={setCollapsedMap}
|
||||||
|
collapsedMap={collapsedMap}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { CollapsedMap } from './FlameGraph/dataTransform';
|
||||||
import FlameGraphHeader from './FlameGraphHeader';
|
import FlameGraphHeader from './FlameGraphHeader';
|
||||||
import { ColorScheme, SelectedView } from './types';
|
import { ColorScheme, SelectedView } from './types';
|
||||||
|
|
||||||
@@ -28,6 +29,8 @@ describe('FlameGraphHeader', () => {
|
|||||||
onColorSchemeChange={onSchemeChange}
|
onColorSchemeChange={onSchemeChange}
|
||||||
stickyHeader={false}
|
stickyHeader={false}
|
||||||
isDiffMode={false}
|
isDiffMode={false}
|
||||||
|
setCollapsedMap={() => {}}
|
||||||
|
collapsedMap={new CollapsedMap()}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import useDebounce from 'react-use/lib/useDebounce';
|
|||||||
import usePrevious from 'react-use/lib/usePrevious';
|
import usePrevious from 'react-use/lib/usePrevious';
|
||||||
|
|
||||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import { Button, Dropdown, Input, Menu, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
import { Button, ButtonGroup, Dropdown, Input, Menu, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { byPackageGradient, byValueGradient, diffColorBlindGradient, diffDefaultGradient } from './FlameGraph/colors';
|
import { byPackageGradient, byValueGradient, diffColorBlindGradient, diffDefaultGradient } from './FlameGraph/colors';
|
||||||
|
import { CollapsedMap } from './FlameGraph/dataTransform';
|
||||||
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants';
|
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants';
|
||||||
import { ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
|
import { ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
|
||||||
|
|
||||||
@@ -25,6 +26,8 @@ type Props = {
|
|||||||
stickyHeader: boolean;
|
stickyHeader: boolean;
|
||||||
vertical?: boolean;
|
vertical?: boolean;
|
||||||
isDiffMode: boolean;
|
isDiffMode: boolean;
|
||||||
|
setCollapsedMap: (collapsedMap: CollapsedMap) => void;
|
||||||
|
collapsedMap: CollapsedMap;
|
||||||
|
|
||||||
extraHeaderElements?: React.ReactNode;
|
extraHeaderElements?: React.ReactNode;
|
||||||
};
|
};
|
||||||
@@ -45,6 +48,8 @@ const FlameGraphHeader = ({
|
|||||||
extraHeaderElements,
|
extraHeaderElements,
|
||||||
vertical,
|
vertical,
|
||||||
isDiffMode,
|
isDiffMode,
|
||||||
|
setCollapsedMap,
|
||||||
|
collapsedMap,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [localSearch, setLocalSearch] = useSearchInput(search, setSearch);
|
const [localSearch, setLocalSearch] = useSearchInput(search, setSearch);
|
||||||
@@ -94,6 +99,32 @@ const FlameGraphHeader = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ColorSchemeButton value={colorScheme} onChange={onColorSchemeChange} isDiffMode={isDiffMode} />
|
<ColorSchemeButton value={colorScheme} onChange={onColorSchemeChange} isDiffMode={isDiffMode} />
|
||||||
|
<ButtonGroup className={styles.buttonSpacing}>
|
||||||
|
<Button
|
||||||
|
variant={'secondary'}
|
||||||
|
fill={'outline'}
|
||||||
|
size={'sm'}
|
||||||
|
tooltip={'Expand all groups'}
|
||||||
|
onClick={() => {
|
||||||
|
setCollapsedMap(collapsedMap.setAllCollapsedStatus(false));
|
||||||
|
}}
|
||||||
|
aria-label={'Expand all groups'}
|
||||||
|
icon={'angle-double-down'}
|
||||||
|
disabled={selectedView === SelectedView.TopTable}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant={'secondary'}
|
||||||
|
fill={'outline'}
|
||||||
|
size={'sm'}
|
||||||
|
tooltip={'Collapse all groups'}
|
||||||
|
onClick={() => {
|
||||||
|
setCollapsedMap(collapsedMap.setAllCollapsedStatus(true));
|
||||||
|
}}
|
||||||
|
aria-label={'Collapse all groups'}
|
||||||
|
icon={'angle-double-up'}
|
||||||
|
disabled={selectedView === SelectedView.TopTable}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
<RadioButtonGroup<TextAlign>
|
<RadioButtonGroup<TextAlign>
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={selectedView === SelectedView.TopTable}
|
disabled={selectedView === SelectedView.TopTable}
|
||||||
|
|||||||
Reference in New Issue
Block a user