Add call tree to flame graph container
This commit is contained in:
@@ -58,6 +58,7 @@
|
||||
"d3": "^7.8.5",
|
||||
"lodash": "4.17.21",
|
||||
"react": "18.3.1",
|
||||
"react-table": "^7.8.0",
|
||||
"react-use": "17.6.0",
|
||||
"react-virtualized-auto-sizer": "1.0.26",
|
||||
"tinycolor2": "1.6.0",
|
||||
@@ -81,6 +82,7 @@
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/react-virtualized-auto-sizer": "1.0.8",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"babel-jest": "29.7.0",
|
||||
|
||||
@@ -13,6 +13,7 @@ const meta: Meta<typeof FlameGraphCallTreeContainer> = {
|
||||
component: FlameGraphCallTreeContainer,
|
||||
args: {
|
||||
colorScheme: ColorScheme.PackageBased,
|
||||
search: '',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTable, useSortBy, useExpanded, Column, Row, UseExpandedRowProps } fr
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, Input, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { Button, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { getBarColorByDiff, getBarColorByPackage, getBarColorByValue } from '../FlameGraph/colors';
|
||||
import { FlameGraphDataContainer } from '../FlameGraph/dataTransform';
|
||||
@@ -23,10 +23,12 @@ type Props = {
|
||||
onSandwich: (str?: string) => void;
|
||||
onTableSort?: (sort: string) => void;
|
||||
colorScheme: ColorScheme | ColorSchemeDiff;
|
||||
search: string;
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
const FlameGraphCallTreeContainer = memo(
|
||||
({ data, onSymbolClick, sandwichItem, onSandwich, onTableSort, colorScheme: initialColorScheme }: Props) => {
|
||||
({ data, onSymbolClick, sandwichItem, onSandwich, onTableSort, colorScheme: initialColorScheme, search, compact = false }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const theme = useTheme2();
|
||||
|
||||
@@ -42,8 +44,8 @@ const FlameGraphCallTreeContainer = memo(
|
||||
// Callers state - track which function's callers we're showing
|
||||
const [callersNodeLabel, setCallersNodeLabel] = useState<string | undefined>(undefined);
|
||||
|
||||
// Search state
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
// Search state - use search from parent (shared with TopTable and FlameGraph)
|
||||
const searchQuery = search;
|
||||
const [currentMatchIndex, setCurrentMatchIndex] = useState<number>(0);
|
||||
const [searchError, setSearchError] = useState<string | undefined>(undefined);
|
||||
|
||||
@@ -235,12 +237,6 @@ const FlameGraphCallTreeContainer = memo(
|
||||
}
|
||||
};
|
||||
|
||||
const clearSearch = () => {
|
||||
setSearchQuery('');
|
||||
setCurrentMatchIndex(-1);
|
||||
setSearchError(undefined);
|
||||
};
|
||||
|
||||
// Get current search match node ID
|
||||
const currentSearchMatchId = useMemo(() => {
|
||||
if (searchNodes.length > 0 && currentMatchIndex >= 0 && currentMatchIndex < searchNodes.length) {
|
||||
@@ -349,7 +345,7 @@ const FlameGraphCallTreeContainer = memo(
|
||||
// Define columns
|
||||
const columns = useMemo<Column<CallTreeNode>[]>(() => {
|
||||
if (data.isDiffFlamegraph()) {
|
||||
return [
|
||||
const cols: Column<CallTreeNode>[] = [
|
||||
{
|
||||
Header: '',
|
||||
id: 'actions',
|
||||
@@ -381,12 +377,16 @@ const FlameGraphCallTreeContainer = memo(
|
||||
onSymbolClick={onSymbolClick}
|
||||
styles={styles}
|
||||
allNodes={nodes}
|
||||
compact={compact}
|
||||
/>
|
||||
),
|
||||
minWidth: 200,
|
||||
width: undefined,
|
||||
},
|
||||
{
|
||||
];
|
||||
|
||||
if (!compact) {
|
||||
cols.push({
|
||||
Header: '',
|
||||
id: 'colorBar',
|
||||
Cell: ({ row }: { row: Row<CallTreeNode> }) => (
|
||||
@@ -395,7 +395,10 @@ const FlameGraphCallTreeContainer = memo(
|
||||
minWidth: 200,
|
||||
width: 200,
|
||||
disableSortBy: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
cols.push(
|
||||
{
|
||||
Header: 'Baseline %',
|
||||
accessor: 'selfPercent',
|
||||
@@ -418,10 +421,12 @@ const FlameGraphCallTreeContainer = memo(
|
||||
),
|
||||
sortType: 'basic',
|
||||
width: 100,
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
return cols;
|
||||
} else {
|
||||
return [
|
||||
const cols: Column<CallTreeNode>[] = [
|
||||
{
|
||||
Header: '',
|
||||
id: 'actions',
|
||||
@@ -453,58 +458,67 @@ const FlameGraphCallTreeContainer = memo(
|
||||
onSymbolClick={onSymbolClick}
|
||||
styles={styles}
|
||||
allNodes={nodes}
|
||||
compact={compact}
|
||||
/>
|
||||
),
|
||||
minWidth: 200,
|
||||
width: undefined,
|
||||
},
|
||||
{
|
||||
Header: '',
|
||||
id: 'colorBar',
|
||||
Cell: ({ row }: { row: Row<CallTreeNode> }) => (
|
||||
<ColorBarCell node={row.original} data={data} colorScheme={colorScheme} theme={theme} styles={styles} focusedNode={focusedNode} callersNode={callersNode} />
|
||||
),
|
||||
minWidth: 200,
|
||||
width: 200,
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Header: 'Self',
|
||||
accessor: 'self',
|
||||
Cell: ({ row }: { row: Row<CallTreeNode> }) => {
|
||||
const displaySelf = data.getSelfDisplay([row.original.levelItem.itemIndexes[0]]);
|
||||
const formattedValue = displaySelf.suffix ? displaySelf.text + displaySelf.suffix : displaySelf.text;
|
||||
return (
|
||||
<div className={styles.valueCell}>
|
||||
<span className={styles.valueNumber}>{formattedValue}</span>
|
||||
<span className={styles.percentNumber}>{row.original.selfPercent.toFixed(2)}%</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
sortType: 'basic',
|
||||
minWidth: 120,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
Header: 'Total',
|
||||
accessor: 'total',
|
||||
Cell: ({ row }: { row: Row<CallTreeNode> }) => {
|
||||
const displayValue = data.valueDisplayProcessor(row.original.total);
|
||||
const formattedValue = displayValue.suffix ? displayValue.text + displayValue.suffix : displayValue.text;
|
||||
return (
|
||||
<div className={styles.valueCell}>
|
||||
<span className={styles.valueNumber}>{formattedValue}</span>
|
||||
<span className={styles.percentNumber}>{row.original.totalPercent.toFixed(2)}%</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
sortType: 'basic',
|
||||
minWidth: 120,
|
||||
width: 120,
|
||||
},
|
||||
];
|
||||
|
||||
if (!compact) {
|
||||
cols.push(
|
||||
{
|
||||
Header: '',
|
||||
id: 'colorBar',
|
||||
Cell: ({ row }: { row: Row<CallTreeNode> }) => (
|
||||
<ColorBarCell node={row.original} data={data} colorScheme={colorScheme} theme={theme} styles={styles} focusedNode={focusedNode} callersNode={callersNode} />
|
||||
),
|
||||
minWidth: 200,
|
||||
width: 200,
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Header: 'Self',
|
||||
accessor: 'self',
|
||||
Cell: ({ row }: { row: Row<CallTreeNode> }) => {
|
||||
const displaySelf = data.getSelfDisplay([row.original.levelItem.itemIndexes[0]]);
|
||||
const formattedValue = displaySelf.suffix ? displaySelf.text + displaySelf.suffix : displaySelf.text;
|
||||
return (
|
||||
<div className={styles.valueCell}>
|
||||
<span className={styles.valueNumber}>{formattedValue}</span>
|
||||
<span className={styles.percentNumber}>{row.original.selfPercent.toFixed(2)}%</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
sortType: 'basic',
|
||||
minWidth: 120,
|
||||
width: 120,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
cols.push({
|
||||
Header: 'Total',
|
||||
accessor: 'total',
|
||||
Cell: ({ row }: { row: Row<CallTreeNode> }) => {
|
||||
const displayValue = data.valueDisplayProcessor(row.original.total);
|
||||
const formattedValue = displayValue.suffix ? displayValue.text + displayValue.suffix : displayValue.text;
|
||||
return (
|
||||
<div className={styles.valueCell}>
|
||||
<span className={styles.valueNumber}>{formattedValue}</span>
|
||||
<span className={styles.percentNumber}>{row.original.totalPercent.toFixed(2)}%</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
sortType: 'basic',
|
||||
minWidth: 120,
|
||||
width: 120,
|
||||
});
|
||||
|
||||
return cols;
|
||||
}
|
||||
}, [data, onSymbolClick, colorScheme, theme, styles, focusedNode, callersNode, callersNodeLabel]);
|
||||
}, [data, onSymbolClick, colorScheme, theme, styles, focusedNode, callersNode, callersNodeLabel, compact]);
|
||||
|
||||
// toggleRowExpanded is used in the Cell renderers but doesn't need to be in the dependencies
|
||||
// because it's accessed at render time, not definition time
|
||||
@@ -541,56 +555,38 @@ const FlameGraphCallTreeContainer = memo(
|
||||
{/* Toolbar */}
|
||||
<div className={styles.toolbar}>
|
||||
<div className={styles.toolbarLeft}>
|
||||
<div className={styles.searchContainer}>
|
||||
<Input
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.currentTarget.value)}
|
||||
placeholder="Search..."
|
||||
className={styles.searchInput}
|
||||
suffix={
|
||||
searchQuery && (
|
||||
{searchQuery && (
|
||||
<div className={styles.searchContainer}>
|
||||
{searchNodes.length > 0 && (
|
||||
<div className={styles.searchNavigation}>
|
||||
<span className={styles.searchCounter}>
|
||||
{currentMatchIndex + 1} of {searchNodes.length}
|
||||
{searchNodes.length >= 50 && '+'}
|
||||
</span>
|
||||
<Button
|
||||
icon="times"
|
||||
icon="angle-up"
|
||||
fill="text"
|
||||
size="sm"
|
||||
onClick={clearSearch}
|
||||
tooltip="Clear search"
|
||||
aria-label="Clear search"
|
||||
onClick={navigateToPrevMatch}
|
||||
tooltip="Previous match"
|
||||
aria-label="Previous match"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{searchNodes.length > 0 && (
|
||||
<div className={styles.searchNavigation}>
|
||||
<span className={styles.searchCounter}>
|
||||
{currentMatchIndex + 1} of {searchNodes.length}
|
||||
{searchNodes.length >= 50 && '+'}
|
||||
</span>
|
||||
<Button
|
||||
icon="angle-up"
|
||||
fill="text"
|
||||
size="sm"
|
||||
onClick={navigateToPrevMatch}
|
||||
tooltip="Previous match"
|
||||
aria-label="Previous match"
|
||||
/>
|
||||
<Button
|
||||
icon="angle-down"
|
||||
fill="text"
|
||||
size="sm"
|
||||
onClick={navigateToNextMatch}
|
||||
tooltip="Next match"
|
||||
aria-label="Next match"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{searchQuery && searchNodes.length === 0 && !searchError && (
|
||||
<span className={styles.searchNoResults}>No matches found</span>
|
||||
)}
|
||||
{searchError && (
|
||||
<span className={styles.searchError}>{searchError}</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
icon="angle-down"
|
||||
fill="text"
|
||||
size="sm"
|
||||
onClick={navigateToNextMatch}
|
||||
tooltip="Next match"
|
||||
aria-label="Next match"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{searchQuery && searchNodes.length === 0 && !searchError && (
|
||||
<span className={styles.searchNoResults}>No matches found</span>
|
||||
)}
|
||||
{searchError && <span className={styles.searchError}>{searchError}</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{focusedNode && (
|
||||
@@ -848,6 +844,7 @@ function FunctionCellWithExpander({
|
||||
onSymbolClick,
|
||||
styles,
|
||||
allNodes,
|
||||
compact = false,
|
||||
}: {
|
||||
row: Row<CallTreeNode> & UseExpandedRowProps<CallTreeNode>;
|
||||
value: string;
|
||||
@@ -858,6 +855,7 @@ function FunctionCellWithExpander({
|
||||
onSymbolClick: (symbol: string) => void;
|
||||
styles: any;
|
||||
allNodes: CallTreeNode[];
|
||||
compact?: boolean;
|
||||
}) {
|
||||
const handleClick = () => {
|
||||
if (hasChildren) {
|
||||
@@ -968,7 +966,7 @@ function FunctionCellWithExpander({
|
||||
<Button fill="text" size="sm" onClick={handleClick} className={styles.functionButton}>
|
||||
{value}
|
||||
</Button>
|
||||
{row.original.childCount > 0 && (
|
||||
{!compact && row.original.childCount > 0 && (
|
||||
<span className={styles.nodeBadge}>
|
||||
{row.original.childCount} {row.original.childCount === 1 ? 'child' : 'children'}, {row.original.subtreeSize} {row.original.subtreeSize === 1 ? 'node' : 'nodes'}
|
||||
</span>
|
||||
@@ -1077,9 +1075,9 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
}),
|
||||
toolbarLeft: css({
|
||||
flexGrow: 1,
|
||||
minWidth: '150px',
|
||||
maxWidth: '350px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
toolbarRight: css({
|
||||
display: 'flex',
|
||||
@@ -1087,14 +1085,10 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
flexWrap: 'wrap',
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
searchInput: css({
|
||||
width: '100%',
|
||||
}),
|
||||
searchContainer: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
width: '100%',
|
||||
flexWrap: 'wrap',
|
||||
}),
|
||||
searchNavigation: css({
|
||||
|
||||
@@ -43,10 +43,13 @@ describe('FlameGraph', () => {
|
||||
setRangeMax={setRangeMax}
|
||||
onItemFocused={onItemFocused}
|
||||
textAlign={'left'}
|
||||
onTextAlignChange={jest.fn()}
|
||||
onSandwich={onSandwich}
|
||||
onFocusPillClick={onFocusPillClick}
|
||||
onSandwichPillClick={onSandwichPillClick}
|
||||
colorScheme={ColorScheme.ValueBased}
|
||||
onColorSchemeChange={jest.fn()}
|
||||
isDiffMode={false}
|
||||
selectedView={SelectedView.FlameGraph}
|
||||
search={''}
|
||||
collapsedMap={container.getCollapsedMap()}
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Icon } from '@grafana/ui';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Button, ButtonGroup, Dropdown, Icon, Menu, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { byPackageGradient, byValueGradient, diffColorBlindGradient, diffDefaultGradient } from './colors';
|
||||
import { PIXELS_PER_LEVEL } from '../constants';
|
||||
import { ClickedItemData, ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from '../types';
|
||||
|
||||
@@ -39,11 +41,14 @@ type Props = {
|
||||
onItemFocused: (data: ClickedItemData) => void;
|
||||
focusedItemData?: ClickedItemData;
|
||||
textAlign: TextAlign;
|
||||
onTextAlignChange: (align: TextAlign) => void;
|
||||
sandwichItem?: string;
|
||||
onSandwich: (label: string) => void;
|
||||
onFocusPillClick: () => void;
|
||||
onSandwichPillClick: () => void;
|
||||
colorScheme: ColorScheme | ColorSchemeDiff;
|
||||
onColorSchemeChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
|
||||
isDiffMode: boolean;
|
||||
showFlameGraphOnly?: boolean;
|
||||
getExtraContextMenuButtons?: GetExtraContextMenuButtonsFunction;
|
||||
collapsing?: boolean;
|
||||
@@ -63,11 +68,14 @@ const FlameGraph = ({
|
||||
onItemFocused,
|
||||
focusedItemData,
|
||||
textAlign,
|
||||
onTextAlignChange,
|
||||
onSandwich,
|
||||
sandwichItem,
|
||||
onFocusPillClick,
|
||||
onSandwichPillClick,
|
||||
colorScheme,
|
||||
onColorSchemeChange,
|
||||
isDiffMode,
|
||||
showFlameGraphOnly,
|
||||
getExtraContextMenuButtons,
|
||||
collapsing,
|
||||
@@ -76,7 +84,7 @@ const FlameGraph = ({
|
||||
collapsedMap,
|
||||
setCollapsedMap,
|
||||
}: Props) => {
|
||||
const styles = getStyles();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const [levels, setLevels] = useState<LevelItem[][]>();
|
||||
const [levelsCallers, setLevelsCallers] = useState<LevelItem[][]>();
|
||||
@@ -175,28 +183,183 @@ const FlameGraph = ({
|
||||
);
|
||||
}
|
||||
|
||||
const alignOptions: Array<SelectableValue<TextAlign>> = [
|
||||
{ value: 'left', description: 'Align text left', icon: 'align-left' },
|
||||
{ value: 'right', description: 'Align text right', icon: 'align-right' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.graph}>
|
||||
<FlameGraphMetadata
|
||||
data={data}
|
||||
focusedItem={focusedItemData}
|
||||
sandwichedLabel={sandwichItem}
|
||||
totalTicks={totalViewTicks}
|
||||
onFocusPillClick={onFocusPillClick}
|
||||
onSandwichPillClick={onSandwichPillClick}
|
||||
/>
|
||||
<div className={styles.toolbar}>
|
||||
<FlameGraphMetadata
|
||||
data={data}
|
||||
focusedItem={focusedItemData}
|
||||
sandwichedLabel={sandwichItem}
|
||||
totalTicks={totalViewTicks}
|
||||
onFocusPillClick={onFocusPillClick}
|
||||
onSandwichPillClick={onSandwichPillClick}
|
||||
/>
|
||||
<div className={styles.controls}>
|
||||
<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'}
|
||||
/>
|
||||
<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'}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<RadioButtonGroup<TextAlign>
|
||||
size="sm"
|
||||
options={alignOptions}
|
||||
value={textAlign}
|
||||
onChange={onTextAlignChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{canvas}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = () => ({
|
||||
type ColorSchemeButtonProps = {
|
||||
value: ColorScheme | ColorSchemeDiff;
|
||||
onChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
|
||||
isDiffMode: boolean;
|
||||
};
|
||||
|
||||
function ColorSchemeButton(props: ColorSchemeButtonProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
let menu = (
|
||||
<Menu>
|
||||
<Menu.Item label="By package name" onClick={() => props.onChange(ColorScheme.PackageBased)} />
|
||||
<Menu.Item label="By value" onClick={() => props.onChange(ColorScheme.ValueBased)} />
|
||||
</Menu>
|
||||
);
|
||||
|
||||
// Show a bit different gradient as a way to indicate selected value
|
||||
const colorDotStyle =
|
||||
{
|
||||
[ColorScheme.ValueBased]: styles.colorDotByValue,
|
||||
[ColorScheme.PackageBased]: styles.colorDotByPackage,
|
||||
[ColorSchemeDiff.DiffColorBlind]: styles.colorDotDiffColorBlind,
|
||||
[ColorSchemeDiff.Default]: styles.colorDotDiffDefault,
|
||||
}[props.value] || styles.colorDotByValue;
|
||||
|
||||
let contents = <span className={cx(styles.colorDot, colorDotStyle)} />;
|
||||
|
||||
if (props.isDiffMode) {
|
||||
menu = (
|
||||
<Menu>
|
||||
<Menu.Item label="Default (green to red)" onClick={() => props.onChange(ColorSchemeDiff.Default)} />
|
||||
<Menu.Item label="Color blind (blue to red)" onClick={() => props.onChange(ColorSchemeDiff.DiffColorBlind)} />
|
||||
</Menu>
|
||||
);
|
||||
|
||||
contents = (
|
||||
<div className={cx(styles.colorDotDiff, colorDotStyle)}>
|
||||
<div>-100% (removed)</div>
|
||||
<div>0%</div>
|
||||
<div>+100% (added)</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown overlay={menu}>
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
fill={'outline'}
|
||||
size={'sm'}
|
||||
tooltip={'Change color scheme'}
|
||||
onClick={() => {}}
|
||||
className={styles.buttonSpacing}
|
||||
aria-label={'Change color scheme'}
|
||||
>
|
||||
{contents}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
graph: css({
|
||||
label: 'graph',
|
||||
overflow: 'auto',
|
||||
flexGrow: 1,
|
||||
flexBasis: '50%',
|
||||
}),
|
||||
toolbar: css({
|
||||
label: 'toolbar',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.spacing(1),
|
||||
}),
|
||||
controls: css({
|
||||
label: 'controls',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
buttonSpacing: css({
|
||||
label: 'buttonSpacing',
|
||||
marginRight: theme.spacing(1),
|
||||
}),
|
||||
colorDot: css({
|
||||
label: 'colorDot',
|
||||
display: 'inline-block',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
borderRadius: theme.shape.radius.circle,
|
||||
}),
|
||||
colorDotDiff: css({
|
||||
label: 'colorDotDiff',
|
||||
display: 'flex',
|
||||
width: '200px',
|
||||
height: '12px',
|
||||
color: 'white',
|
||||
fontSize: 9,
|
||||
lineHeight: 1.3,
|
||||
fontWeight: 300,
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 2px',
|
||||
// We have a specific sizing for this so probably makes sense to use hardcoded value here
|
||||
// eslint-disable-next-line @grafana/no-border-radius-literal
|
||||
borderRadius: '2px',
|
||||
}),
|
||||
colorDotByValue: css({
|
||||
label: 'colorDotByValue',
|
||||
background: byValueGradient,
|
||||
}),
|
||||
colorDotByPackage: css({
|
||||
label: 'colorDotByPackage',
|
||||
background: byPackageGradient,
|
||||
}),
|
||||
colorDotDiffDefault: css({
|
||||
label: 'colorDotDiffDefault',
|
||||
background: diffDefaultGradient,
|
||||
}),
|
||||
colorDotDiffColorBlind: css({
|
||||
label: 'colorDotDiffColorBlind',
|
||||
background: diffColorBlindGradient,
|
||||
}),
|
||||
sandwichCanvasWrapper: css({
|
||||
label: 'sandwichCanvasWrapper',
|
||||
display: 'flex',
|
||||
|
||||
@@ -7,13 +7,14 @@ import { useMeasure } from 'react-use';
|
||||
import { DataFrame, GrafanaTheme2, escapeStringForRegex } from '@grafana/data';
|
||||
import { ThemeContext } from '@grafana/ui';
|
||||
|
||||
import FlameGraphCallTreeContainer from './CallTree/FlameGraphCallTreeContainer';
|
||||
import FlameGraph from './FlameGraph/FlameGraph';
|
||||
import { GetExtraContextMenuButtonsFunction } from './FlameGraph/FlameGraphContextMenu';
|
||||
import { CollapsedMap, FlameGraphDataContainer } from './FlameGraph/dataTransform';
|
||||
import FlameGraphHeader from './FlameGraphHeader';
|
||||
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
|
||||
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants';
|
||||
import { ClickedItemData, ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
|
||||
import { ClickedItemData, ColorScheme, ColorSchemeDiff, PaneView, SelectedView, TextAlign, ViewMode } from './types';
|
||||
import { getAssistantContextFromDataFrame } from './utils';
|
||||
|
||||
const ufuzzy = new uFuzzy();
|
||||
@@ -110,6 +111,10 @@ const FlameGraphContainer = ({
|
||||
const [rangeMax, setRangeMax] = useState(1);
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedView, setSelectedView] = useState(SelectedView.Both);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>(ViewMode.Split);
|
||||
const [leftPaneView, setLeftPaneView] = useState<PaneView>(PaneView.TopTable);
|
||||
const [rightPaneView, setRightPaneView] = useState<PaneView>(PaneView.FlameGraph);
|
||||
const [singleView, setSingleView] = useState<PaneView>(PaneView.FlameGraph);
|
||||
const [sizeRef, { width: containerWidth }] = useMeasure<HTMLDivElement>();
|
||||
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
|
||||
@@ -217,6 +222,10 @@ const FlameGraphContainer = ({
|
||||
onItemFocused={(data) => setFocusedItemData(data)}
|
||||
focusedItemData={focusedItemData}
|
||||
textAlign={textAlign}
|
||||
onTextAlignChange={(align) => {
|
||||
setTextAlign(align);
|
||||
onTextAlignSelected?.(align);
|
||||
}}
|
||||
sandwichItem={sandwichItem}
|
||||
onSandwich={(label: string) => {
|
||||
resetFocus();
|
||||
@@ -225,6 +234,8 @@ const FlameGraphContainer = ({
|
||||
onFocusPillClick={resetFocus}
|
||||
onSandwichPillClick={resetSandwich}
|
||||
colorScheme={colorScheme}
|
||||
onColorSchemeChange={setColorScheme}
|
||||
isDiffMode={dataContainer.isDiffFlamegraph()}
|
||||
showFlameGraphOnly={showFlameGraphOnly}
|
||||
collapsing={!disableCollapsing}
|
||||
getExtraContextMenuButtons={getExtraContextMenuButtons}
|
||||
@@ -255,26 +266,69 @@ const FlameGraphContainer = ({
|
||||
/>
|
||||
);
|
||||
|
||||
// Use compact mode for CallTree when in split view
|
||||
const isCallTreeInSplitView =
|
||||
selectedView === SelectedView.Both &&
|
||||
viewMode === ViewMode.Split &&
|
||||
(leftPaneView === PaneView.CallTree || rightPaneView === PaneView.CallTree);
|
||||
|
||||
const callTree = (
|
||||
<FlameGraphCallTreeContainer
|
||||
data={dataContainer}
|
||||
onSymbolClick={onSymbolClick}
|
||||
sandwichItem={sandwichItem}
|
||||
onSandwich={setSandwichItem}
|
||||
onTableSort={onTableSort}
|
||||
colorScheme={colorScheme}
|
||||
search={search}
|
||||
compact={isCallTreeInSplitView}
|
||||
/>
|
||||
);
|
||||
|
||||
// Helper function to render a pane based on its view type
|
||||
const renderPane = (paneView: PaneView) => {
|
||||
switch (paneView) {
|
||||
case PaneView.TopTable:
|
||||
// TopTable uses AutoSizer which needs a parent with defined height
|
||||
return <div className={styles.tableContainer}>{table}</div>;
|
||||
case PaneView.FlameGraph:
|
||||
return flameGraph;
|
||||
case PaneView.CallTree:
|
||||
// CallTree also uses AutoSizer which needs a parent with defined height
|
||||
return <div className={styles.tableContainer}>{callTree}</div>;
|
||||
default:
|
||||
return flameGraph;
|
||||
}
|
||||
};
|
||||
|
||||
let body;
|
||||
if (showFlameGraphOnly || selectedView === SelectedView.FlameGraph) {
|
||||
body = flameGraph;
|
||||
} else if (selectedView === SelectedView.TopTable) {
|
||||
body = <div className={styles.tableContainer}>{table}</div>;
|
||||
} else if (selectedView === SelectedView.CallTree) {
|
||||
body = <div className={styles.tableContainer}>{callTree}</div>;
|
||||
} else if (selectedView === SelectedView.Both) {
|
||||
if (vertical) {
|
||||
body = (
|
||||
<div>
|
||||
<div className={styles.verticalGraphContainer}>{flameGraph}</div>
|
||||
<div className={styles.verticalTableContainer}>{table}</div>
|
||||
</div>
|
||||
);
|
||||
// New view model: support split view with independent pane selections
|
||||
if (viewMode === ViewMode.Split) {
|
||||
if (vertical) {
|
||||
body = (
|
||||
<div>
|
||||
<div className={styles.verticalPaneContainer}>{renderPane(leftPaneView)}</div>
|
||||
<div className={styles.verticalPaneContainer}>{renderPane(rightPaneView)}</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
body = (
|
||||
<div className={styles.horizontalContainer}>
|
||||
<div className={styles.horizontalPaneContainer}>{renderPane(leftPaneView)}</div>
|
||||
<div className={styles.horizontalPaneContainer}>{renderPane(rightPaneView)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
body = (
|
||||
<div className={styles.horizontalContainer}>
|
||||
<div className={styles.horizontalTableContainer}>{table}</div>
|
||||
<div className={styles.horizontalGraphContainer}>{flameGraph}</div>
|
||||
</div>
|
||||
);
|
||||
// Single view mode
|
||||
body = <div className={styles.singlePaneContainer}>{renderPane(singleView)}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,25 +346,23 @@ const FlameGraphContainer = ({
|
||||
setSelectedView(view);
|
||||
onViewSelected?.(view);
|
||||
}}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
leftPaneView={leftPaneView}
|
||||
setLeftPaneView={setLeftPaneView}
|
||||
rightPaneView={rightPaneView}
|
||||
setRightPaneView={setRightPaneView}
|
||||
singleView={singleView}
|
||||
setSingleView={setSingleView}
|
||||
containerWidth={containerWidth}
|
||||
onReset={() => {
|
||||
resetFocus();
|
||||
resetSandwich();
|
||||
}}
|
||||
textAlign={textAlign}
|
||||
onTextAlignChange={(align) => {
|
||||
setTextAlign(align);
|
||||
onTextAlignSelected?.(align);
|
||||
}}
|
||||
showResetButton={Boolean(focusedItemData || sandwichItem)}
|
||||
colorScheme={colorScheme}
|
||||
onColorSchemeChange={setColorScheme}
|
||||
stickyHeader={Boolean(stickyHeader)}
|
||||
extraHeaderElements={extraHeaderElements}
|
||||
vertical={vertical}
|
||||
isDiffMode={dataContainer.isDiffFlamegraph()}
|
||||
setCollapsedMap={setCollapsedMap}
|
||||
collapsedMap={collapsedMap}
|
||||
assistantContext={data && showAnalyzeWithAssistant ? getAssistantContextFromDataFrame(data) : undefined}
|
||||
/>
|
||||
)}
|
||||
@@ -435,20 +487,20 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
width: '100%',
|
||||
}),
|
||||
|
||||
horizontalGraphContainer: css({
|
||||
flexBasis: '50%',
|
||||
}),
|
||||
|
||||
horizontalTableContainer: css({
|
||||
horizontalPaneContainer: css({
|
||||
label: 'horizontalPaneContainer',
|
||||
flexBasis: '50%',
|
||||
maxHeight: 800,
|
||||
}),
|
||||
|
||||
verticalGraphContainer: css({
|
||||
verticalPaneContainer: css({
|
||||
label: 'verticalPaneContainer',
|
||||
marginBottom: theme.spacing(1),
|
||||
height: 800,
|
||||
}),
|
||||
|
||||
verticalTableContainer: css({
|
||||
singlePaneContainer: css({
|
||||
label: 'singlePaneContainer',
|
||||
height: 800,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -3,9 +3,8 @@ import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import * as React from 'react';
|
||||
|
||||
import { CollapsedMap } from './FlameGraph/dataTransform';
|
||||
import FlameGraphHeader from './FlameGraphHeader';
|
||||
import { ColorScheme, SelectedView } from './types';
|
||||
import { PaneView, SelectedView, ViewMode } from './types';
|
||||
|
||||
jest.mock('@grafana/assistant', () => ({
|
||||
useAssistant: jest.fn().mockReturnValue({
|
||||
@@ -20,8 +19,11 @@ describe('FlameGraphHeader', () => {
|
||||
function setup(props: Partial<React.ComponentProps<typeof FlameGraphHeader>> = {}) {
|
||||
const setSearch = jest.fn();
|
||||
const setSelectedView = jest.fn();
|
||||
const setViewMode = jest.fn();
|
||||
const setLeftPaneView = jest.fn();
|
||||
const setRightPaneView = jest.fn();
|
||||
const setSingleView = jest.fn();
|
||||
const onReset = jest.fn();
|
||||
const onSchemeChange = jest.fn();
|
||||
|
||||
const renderResult = render(
|
||||
<FlameGraphHeader
|
||||
@@ -29,17 +31,18 @@ describe('FlameGraphHeader', () => {
|
||||
setSearch={setSearch}
|
||||
selectedView={SelectedView.Both}
|
||||
setSelectedView={setSelectedView}
|
||||
viewMode={ViewMode.Split}
|
||||
setViewMode={setViewMode}
|
||||
leftPaneView={PaneView.TopTable}
|
||||
setLeftPaneView={setLeftPaneView}
|
||||
rightPaneView={PaneView.FlameGraph}
|
||||
setRightPaneView={setRightPaneView}
|
||||
singleView={PaneView.FlameGraph}
|
||||
setSingleView={setSingleView}
|
||||
containerWidth={1600}
|
||||
onReset={onReset}
|
||||
onTextAlignChange={jest.fn()}
|
||||
textAlign={'left'}
|
||||
showResetButton={true}
|
||||
colorScheme={ColorScheme.ValueBased}
|
||||
onColorSchemeChange={onSchemeChange}
|
||||
stickyHeader={false}
|
||||
isDiffMode={false}
|
||||
setCollapsedMap={() => {}}
|
||||
collapsedMap={new CollapsedMap()}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -50,7 +53,6 @@ describe('FlameGraphHeader', () => {
|
||||
setSearch,
|
||||
setSelectedView,
|
||||
onReset,
|
||||
onSchemeChange,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -70,27 +72,4 @@ describe('FlameGraphHeader', () => {
|
||||
await userEvent.click(resetButton);
|
||||
expect(handlers.onReset).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls on color scheme change when clicked', async () => {
|
||||
const { handlers } = setup();
|
||||
const changeButton = screen.getByLabelText(/Change color scheme/);
|
||||
expect(changeButton).toBeInTheDocument();
|
||||
await userEvent.click(changeButton);
|
||||
|
||||
const byPackageButton = screen.getByText(/By package name/);
|
||||
expect(byPackageButton).toBeInTheDocument();
|
||||
await userEvent.click(byPackageButton);
|
||||
|
||||
expect(handlers.onSchemeChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shows diff color scheme switch when diff', async () => {
|
||||
setup({ isDiffMode: true });
|
||||
const changeButton = screen.getByLabelText(/Change color scheme/);
|
||||
expect(changeButton).toBeInTheDocument();
|
||||
await userEvent.click(changeButton);
|
||||
|
||||
expect(screen.getByText(/Default/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Color blind/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,30 +5,29 @@ import { useDebounce, usePrevious } from 'react-use';
|
||||
|
||||
import { ChatContextItem, OpenAssistantButton } from '@grafana/assistant';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Button, ButtonGroup, Dropdown, Input, Menu, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||
import { Button, Input, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||
|
||||
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 { ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
|
||||
import { PaneView, SelectedView, ViewMode } from './types';
|
||||
|
||||
type Props = {
|
||||
search: string;
|
||||
setSearch: (search: string) => void;
|
||||
selectedView: SelectedView;
|
||||
setSelectedView: (view: SelectedView) => void;
|
||||
viewMode: ViewMode;
|
||||
setViewMode: (mode: ViewMode) => void;
|
||||
leftPaneView: PaneView;
|
||||
setLeftPaneView: (view: PaneView) => void;
|
||||
rightPaneView: PaneView;
|
||||
setRightPaneView: (view: PaneView) => void;
|
||||
singleView: PaneView;
|
||||
setSingleView: (view: PaneView) => void;
|
||||
containerWidth: number;
|
||||
onReset: () => void;
|
||||
textAlign: TextAlign;
|
||||
onTextAlignChange: (align: TextAlign) => void;
|
||||
showResetButton: boolean;
|
||||
colorScheme: ColorScheme | ColorSchemeDiff;
|
||||
onColorSchemeChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
|
||||
stickyHeader: boolean;
|
||||
vertical?: boolean;
|
||||
isDiffMode: boolean;
|
||||
setCollapsedMap: (collapsedMap: CollapsedMap) => void;
|
||||
collapsedMap: CollapsedMap;
|
||||
|
||||
extraHeaderElements?: React.ReactNode;
|
||||
|
||||
@@ -40,19 +39,20 @@ const FlameGraphHeader = ({
|
||||
setSearch,
|
||||
selectedView,
|
||||
setSelectedView,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
leftPaneView,
|
||||
setLeftPaneView,
|
||||
rightPaneView,
|
||||
setRightPaneView,
|
||||
singleView,
|
||||
setSingleView,
|
||||
containerWidth,
|
||||
onReset,
|
||||
textAlign,
|
||||
onTextAlignChange,
|
||||
showResetButton,
|
||||
colorScheme,
|
||||
onColorSchemeChange,
|
||||
stickyHeader,
|
||||
extraHeaderElements,
|
||||
vertical,
|
||||
isDiffMode,
|
||||
setCollapsedMap,
|
||||
collapsedMap,
|
||||
assistantContext,
|
||||
}: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
@@ -87,6 +87,25 @@ const FlameGraphHeader = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedView === SelectedView.Both && viewMode === ViewMode.Split && (
|
||||
<div className={styles.middleContainer}>
|
||||
<RadioButtonGroup<PaneView>
|
||||
size="sm"
|
||||
options={paneViewOptions}
|
||||
value={leftPaneView}
|
||||
onChange={setLeftPaneView}
|
||||
className={styles.buttonSpacing}
|
||||
/>
|
||||
<RadioButtonGroup<PaneView>
|
||||
size="sm"
|
||||
options={paneViewOptions}
|
||||
value={rightPaneView}
|
||||
onChange={setRightPaneView}
|
||||
className={styles.buttonSpacing}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.rightContainer}>
|
||||
{!!assistantContext?.length && (
|
||||
<div className={styles.buttonSpacing}>
|
||||
@@ -111,129 +130,62 @@ const FlameGraphHeader = ({
|
||||
aria-label={'Reset focus and sandwich state'}
|
||||
/>
|
||||
)}
|
||||
<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}
|
||||
{selectedView === SelectedView.Both ? (
|
||||
<>
|
||||
<RadioButtonGroup<ViewMode>
|
||||
size="sm"
|
||||
options={viewModeOptions}
|
||||
value={viewMode}
|
||||
onChange={setViewMode}
|
||||
className={styles.buttonSpacing}
|
||||
/>
|
||||
{viewMode === ViewMode.Single && (
|
||||
<RadioButtonGroup<PaneView>
|
||||
size="sm"
|
||||
options={paneViewOptions}
|
||||
value={singleView}
|
||||
onChange={setSingleView}
|
||||
className={styles.buttonSpacing}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<RadioButtonGroup<SelectedView>
|
||||
size="sm"
|
||||
options={getViewOptions(containerWidth, vertical)}
|
||||
value={selectedView}
|
||||
onChange={setSelectedView}
|
||||
/>
|
||||
<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>
|
||||
size="sm"
|
||||
disabled={selectedView === SelectedView.TopTable}
|
||||
options={alignOptions}
|
||||
value={textAlign}
|
||||
onChange={onTextAlignChange}
|
||||
className={styles.buttonSpacing}
|
||||
/>
|
||||
<RadioButtonGroup<SelectedView>
|
||||
size="sm"
|
||||
options={getViewOptions(containerWidth, vertical)}
|
||||
value={selectedView}
|
||||
onChange={setSelectedView}
|
||||
/>
|
||||
)}
|
||||
{extraHeaderElements && <div className={styles.extraElements}>{extraHeaderElements}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type ColorSchemeButtonProps = {
|
||||
value: ColorScheme | ColorSchemeDiff;
|
||||
onChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
|
||||
isDiffMode: boolean;
|
||||
};
|
||||
function ColorSchemeButton(props: ColorSchemeButtonProps) {
|
||||
// TODO: probably create separate getStyles
|
||||
const styles = useStyles2(getStyles);
|
||||
let menu = (
|
||||
<Menu>
|
||||
<Menu.Item label="By package name" onClick={() => props.onChange(ColorScheme.PackageBased)} />
|
||||
<Menu.Item label="By value" onClick={() => props.onChange(ColorScheme.ValueBased)} />
|
||||
</Menu>
|
||||
);
|
||||
const viewModeOptions: Array<SelectableValue<ViewMode>> = [
|
||||
{ value: ViewMode.Single, label: 'Single', description: 'Single view' },
|
||||
{ value: ViewMode.Split, label: 'Split', description: 'Split view' },
|
||||
];
|
||||
|
||||
// Show a bit different gradient as a way to indicate selected value
|
||||
const colorDotStyle =
|
||||
{
|
||||
[ColorScheme.ValueBased]: styles.colorDotByValue,
|
||||
[ColorScheme.PackageBased]: styles.colorDotByPackage,
|
||||
[ColorSchemeDiff.DiffColorBlind]: styles.colorDotDiffColorBlind,
|
||||
[ColorSchemeDiff.Default]: styles.colorDotDiffDefault,
|
||||
}[props.value] || styles.colorDotByValue;
|
||||
|
||||
let contents = <span className={cx(styles.colorDot, colorDotStyle)} />;
|
||||
|
||||
if (props.isDiffMode) {
|
||||
menu = (
|
||||
<Menu>
|
||||
<Menu.Item label="Default (green to red)" onClick={() => props.onChange(ColorSchemeDiff.Default)} />
|
||||
<Menu.Item label="Color blind (blue to red)" onClick={() => props.onChange(ColorSchemeDiff.DiffColorBlind)} />
|
||||
</Menu>
|
||||
);
|
||||
|
||||
contents = (
|
||||
<div className={cx(styles.colorDotDiff, colorDotStyle)}>
|
||||
<div>-100% (removed)</div>
|
||||
<div>0%</div>
|
||||
<div>+100% (added)</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown overlay={menu}>
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
fill={'outline'}
|
||||
size={'sm'}
|
||||
tooltip={'Change color scheme'}
|
||||
onClick={() => {}}
|
||||
className={styles.buttonSpacing}
|
||||
aria-label={'Change color scheme'}
|
||||
>
|
||||
{contents}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
const alignOptions: Array<SelectableValue<TextAlign>> = [
|
||||
{ value: 'left', description: 'Align text left', icon: 'align-left' },
|
||||
{ value: 'right', description: 'Align text right', icon: 'align-right' },
|
||||
const paneViewOptions: Array<SelectableValue<PaneView>> = [
|
||||
{ value: PaneView.TopTable, label: 'Table' },
|
||||
{ value: PaneView.FlameGraph, label: 'Flame' },
|
||||
{ value: PaneView.CallTree, label: 'Tree' },
|
||||
];
|
||||
|
||||
function getViewOptions(width: number, vertical?: boolean): Array<SelectableValue<SelectedView>> {
|
||||
let viewOptions: Array<{ value: SelectedView; label: string; description: string }> = [
|
||||
{ value: SelectedView.TopTable, label: 'Top Table', description: 'Only show top table' },
|
||||
{ value: SelectedView.FlameGraph, label: 'Flame Graph', description: 'Only show flame graph' },
|
||||
{ value: SelectedView.CallTree, label: 'Call Tree', description: 'Only show call tree' },
|
||||
];
|
||||
|
||||
if (width >= MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH || vertical) {
|
||||
viewOptions.push({
|
||||
value: SelectedView.Both,
|
||||
label: 'Both',
|
||||
description: 'Show both the top table and flame graph',
|
||||
description: 'Show split or single view with multiple visualizations',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -273,10 +225,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
width: '100%',
|
||||
top: 0,
|
||||
gap: theme.spacing(1),
|
||||
marginTop: theme.spacing(1),
|
||||
position: 'relative',
|
||||
}),
|
||||
stickyHeader: css({
|
||||
zIndex: theme.zIndex.navbarFixed,
|
||||
@@ -285,10 +239,20 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
}),
|
||||
inputContainer: css({
|
||||
label: 'inputContainer',
|
||||
flexGrow: 1,
|
||||
flexGrow: 0,
|
||||
minWidth: '150px',
|
||||
maxWidth: '350px',
|
||||
}),
|
||||
middleContainer: css({
|
||||
label: 'middleContainer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: theme.spacing(1),
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
}),
|
||||
rightContainer: css({
|
||||
label: 'rightContainer',
|
||||
display: 'flex',
|
||||
@@ -309,44 +273,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
padding: '0 5px',
|
||||
color: theme.colors.text.disabled,
|
||||
}),
|
||||
colorDot: css({
|
||||
label: 'colorDot',
|
||||
display: 'inline-block',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
borderRadius: theme.shape.radius.circle,
|
||||
}),
|
||||
colorDotDiff: css({
|
||||
label: 'colorDotDiff',
|
||||
display: 'flex',
|
||||
width: '200px',
|
||||
height: '12px',
|
||||
color: 'white',
|
||||
fontSize: 9,
|
||||
lineHeight: 1.3,
|
||||
fontWeight: 300,
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 2px',
|
||||
// We have a specific sizing for this so probably makes sense to use hardcoded value here
|
||||
// eslint-disable-next-line @grafana/no-border-radius-literal
|
||||
borderRadius: '2px',
|
||||
}),
|
||||
colorDotByValue: css({
|
||||
label: 'colorDotByValue',
|
||||
background: byValueGradient,
|
||||
}),
|
||||
colorDotByPackage: css({
|
||||
label: 'colorDotByPackage',
|
||||
background: byPackageGradient,
|
||||
}),
|
||||
colorDotDiffDefault: css({
|
||||
label: 'colorDotDiffDefault',
|
||||
background: diffDefaultGradient,
|
||||
}),
|
||||
colorDotDiffColorBlind: css({
|
||||
label: 'colorDotDiffColorBlind',
|
||||
background: diffColorBlindGradient,
|
||||
}),
|
||||
extraElements: css({
|
||||
label: 'extraElements',
|
||||
marginLeft: theme.spacing(1),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { default as FlameGraph, type Props } from './FlameGraphContainer';
|
||||
export { default as FlameGraphCallTreeContainer } from './CallTree/FlameGraphCallTreeContainer';
|
||||
export { checkFields, getMessageCheckFieldsResult } from './FlameGraph/dataTransform';
|
||||
export { data } from './FlameGraph/testData/dataNestedSet';
|
||||
|
||||
@@ -21,6 +21,18 @@ export enum SelectedView {
|
||||
TopTable = 'topTable',
|
||||
FlameGraph = 'flameGraph',
|
||||
Both = 'both',
|
||||
CallTree = 'callTree',
|
||||
}
|
||||
|
||||
export enum ViewMode {
|
||||
Single = 'single',
|
||||
Split = 'split',
|
||||
}
|
||||
|
||||
export enum PaneView {
|
||||
TopTable = 'topTable',
|
||||
FlameGraph = 'flameGraph',
|
||||
CallTree = 'callTree',
|
||||
}
|
||||
|
||||
export interface TableData {
|
||||
|
||||
@@ -3507,6 +3507,7 @@ __metadata:
|
||||
"@types/lodash": "npm:4.17.20"
|
||||
"@types/node": "npm:24.10.1"
|
||||
"@types/react": "npm:18.3.18"
|
||||
"@types/react-table": "npm:^7.7.20"
|
||||
"@types/react-virtualized-auto-sizer": "npm:1.0.8"
|
||||
"@types/tinycolor2": "npm:1.4.6"
|
||||
babel-jest: "npm:29.7.0"
|
||||
@@ -3517,6 +3518,7 @@ __metadata:
|
||||
jest-canvas-mock: "npm:2.5.2"
|
||||
lodash: "npm:4.17.21"
|
||||
react: "npm:18.3.1"
|
||||
react-table: "npm:^7.8.0"
|
||||
react-use: "npm:17.6.0"
|
||||
react-virtualized-auto-sizer: "npm:1.0.26"
|
||||
rollup: "npm:^4.22.4"
|
||||
@@ -11159,7 +11161,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react-table@npm:7.7.20":
|
||||
"@types/react-table@npm:7.7.20, @types/react-table@npm:^7.7.20":
|
||||
version: 7.7.20
|
||||
resolution: "@types/react-table@npm:7.7.20"
|
||||
dependencies:
|
||||
@@ -29371,7 +29373,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-table@npm:7.8.0":
|
||||
"react-table@npm:7.8.0, react-table@npm:^7.8.0":
|
||||
version: 7.8.0
|
||||
resolution: "react-table@npm:7.8.0"
|
||||
peerDependencies:
|
||||
|
||||
Reference in New Issue
Block a user