diff --git a/public/app/features/explore/TraceView/components/utils/color-generator.test.ts b/public/app/features/explore/TraceView/components/utils/color-generator.test.ts index 86aecb1476f..a4a0958cd8a 100644 --- a/public/app/features/explore/TraceView/components/utils/color-generator.test.ts +++ b/public/app/features/explore/TraceView/components/utils/color-generator.test.ts @@ -51,8 +51,8 @@ it('should not allow colors with a contrast ratio < 3 in light mode', () => { }); it('should not allow colors with a contrast ratio < 3 in dark mode', () => { - expect(colorsToFilter.indexOf('#890F02')).toBe(11); - expect(colorsToFilter.indexOf('#0A437C')).toBe(12); + expect(colorsToFilter.indexOf('#890F02')).toBe(12); + expect(colorsToFilter.indexOf('#0A437C')).toBe(13); const filteredColors = getFilteredColors(colorsToFilter, createTheme({ colors: { mode: 'dark' } })); expect(filteredColors.indexOf('#890F02')).toBe(-1); expect(filteredColors.indexOf('#0A437C')).toBe(-1); diff --git a/public/app/features/explore/TraceView/components/utils/color-generator.tsx b/public/app/features/explore/TraceView/components/utils/color-generator.tsx index cc108cb038d..8e885b23ade 100644 --- a/public/app/features/explore/TraceView/components/utils/color-generator.tsx +++ b/public/app/features/explore/TraceView/components/utils/color-generator.tsx @@ -132,19 +132,20 @@ export function getRgbColorByKey(key: string, theme: GrafanaTheme2): [number, nu } export function getFilteredColors(colorsHex: string[], theme: GrafanaTheme2) { + const filtered = [...colorsHex]; // Remove red as a span color because it looks like an error - const redIndex = colorsHex.indexOf('#E24D42'); + const redIndex = filtered.indexOf('#E24D42'); if (redIndex > -1) { - colorsHex.splice(redIndex, 1); + filtered.splice(redIndex, 1); } const redIndex2 = colorsHex.indexOf('#BF1B00'); if (redIndex2 > -1) { - colorsHex.splice(redIndex2, 1); + filtered.splice(redIndex2, 1); } // Only add colors that have a contrast ratio >= 3 for the current theme let filteredColors = []; - for (const color of colorsHex) { + for (const color of filtered) { if (tinycolor.readability(theme.colors.background.primary, color) >= 3) { filteredColors.push(color); } diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/FlameGraph.test.tsx b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/FlameGraph.test.tsx index 06b838e0416..fc152e96b00 100644 --- a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/FlameGraph.test.tsx +++ b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/FlameGraph.test.tsx @@ -3,6 +3,8 @@ import React from 'react'; import { createDataFrame } from '@grafana/data'; +import { ColorScheme } from '../types'; + import FlameGraph from './FlameGraph'; import { FlameGraphDataContainer } from './dataTransform'; import { data } from './testData/dataNestedSet'; @@ -45,6 +47,7 @@ describe('FlameGraph', () => { onSandwich={onSandwich} onFocusPillClick={onFocusPillClick} onSandwichPillClick={onSandwichPillClick} + colorScheme={ColorScheme.ValueBased} /> ); return { diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/FlameGraph.tsx b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/FlameGraph.tsx index 5ddb3d467d1..56f928a2919 100644 --- a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/FlameGraph.tsx +++ b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/FlameGraph.tsx @@ -23,7 +23,7 @@ import { useMeasure } from 'react-use'; import { Icon, useStyles2 } from '@grafana/ui'; import { PIXELS_PER_LEVEL } from '../../constants'; -import { ClickedItemData, TextAlign } from '../types'; +import { ClickedItemData, ColorScheme, TextAlign } from '../types'; import FlameGraphContextMenu from './FlameGraphContextMenu'; import FlameGraphMetadata from './FlameGraphMetadata'; @@ -46,6 +46,7 @@ type Props = { onSandwich: (label: string) => void; onFocusPillClick: () => void; onSandwichPillClick: () => void; + colorScheme: ColorScheme; }; const FlameGraph = ({ @@ -62,6 +63,7 @@ const FlameGraph = ({ sandwichItem, onFocusPillClick, onSandwichPillClick, + colorScheme, }: Props) => { const styles = useStyles2(getStyles); @@ -96,6 +98,7 @@ const FlameGraph = ({ search, textAlign, totalTicks, + colorScheme, focusedItemData ); diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/colors.test.ts b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/colors.test.ts new file mode 100644 index 00000000000..662221fe1b4 --- /dev/null +++ b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/colors.test.ts @@ -0,0 +1,25 @@ +import { createTheme } from '@grafana/data'; + +import { getBarColorByPackage, getBarColorByValue } from './colors'; + +describe('getBarColorByValue', () => { + it('converts value to color', () => { + expect(getBarColorByValue(1, 100, 0, 1).toHslString()).toBe('hsl(50, 100%, 65%)'); + expect(getBarColorByValue(100, 100, 0, 1).toHslString()).toBe('hsl(0, 100%, 72%)'); + expect(getBarColorByValue(10, 100, 0, 0.1).toHslString()).toBe('hsl(0, 100%, 72%)'); + }); +}); + +describe('getBarColorByPackage', () => { + it('converts package to color', () => { + const theme = createTheme(); + const c = getBarColorByPackage('net/http.HandlerFunc.ServeHTTP', theme); + expect(c.toHslString()).toBe('hsl(246, 40%, 65%)'); + // same package should have same color + expect(getBarColorByPackage('net/http.(*conn).serve', theme).toHslString()).toBe(c.toHslString()); + + expect(getBarColorByPackage('github.com/grafana/phlare/pkg/util.Log.Wrap.func1', theme).toHslString()).toBe( + 'hsl(105, 40%, 76%)' + ); + }); +}); diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/colors.ts b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/colors.ts new file mode 100644 index 00000000000..851922094a7 --- /dev/null +++ b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/colors.ts @@ -0,0 +1,95 @@ +import color from 'tinycolor2'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import murmurhash3_32_gc from './murmur3'; + +// Colors taken from pyroscope, they should be from Grafana originally, but I didn't find from where exactly. +const packageColors = [ + color({ h: 24, s: 69, l: 60 }), + color({ h: 34, s: 65, l: 65 }), + color({ h: 194, s: 52, l: 61 }), + color({ h: 163, s: 45, l: 55 }), + color({ h: 211, s: 48, l: 60 }), + color({ h: 246, s: 40, l: 65 }), + color({ h: 305, s: 63, l: 79 }), + color({ h: 47, s: 100, l: 73 }), + + color({ r: 183, g: 219, b: 171 }), + color({ r: 244, g: 213, b: 152 }), + color({ r: 78, g: 146, b: 249 }), + color({ r: 249, g: 186, b: 143 }), + color({ r: 242, g: 145, b: 145 }), + color({ r: 130, g: 181, b: 216 }), + color({ r: 229, g: 168, b: 226 }), + color({ r: 174, g: 162, b: 224 }), + color({ r: 154, g: 196, b: 138 }), + color({ r: 242, g: 201, b: 109 }), + color({ r: 101, g: 197, b: 219 }), + color({ r: 249, g: 147, b: 78 }), + color({ r: 234, g: 100, b: 96 }), + color({ r: 81, g: 149, b: 206 }), + color({ r: 214, g: 131, b: 206 }), + color({ r: 128, g: 110, b: 183 }), +]; + +const byValueMinColor = getBarColorByValue(1, 100, 0, 1); +const byValueMaxColor = getBarColorByValue(100, 100, 0, 1); +export const byValueGradient = `linear-gradient(90deg, ${byValueMinColor} 0%, ${byValueMaxColor} 100%)`; + +// Handpicked some vaguely rainbow-ish colors +export const byPackageGradient = `linear-gradient(90deg, ${packageColors[0]} 0%, ${packageColors[2]} 30%, ${packageColors[6]} 50%, ${packageColors[7]} 70%, ${packageColors[8]} 100%)`; + +export function getBarColorByValue(value: number, totalTicks: number, rangeMin: number, rangeMax: number) { + // / (rangeMax - rangeMin) here so when you click a bar it will adjust the top (clicked)bar to the most 'intense' color + const intensity = Math.min(1, value / totalTicks / (rangeMax - rangeMin)); + const h = 50 - 50 * intensity; + const l = 65 + 7 * intensity; + + return color({ h, s: 100, l }); +} + +export function getBarColorByPackage(label: string, theme: GrafanaTheme2) { + const packageName = getPackageName(label); + // TODO: similar thing happens in trace view with selecting colors of the spans, so maybe this could be unified. + const hash = murmurhash3_32_gc(packageName || '', 0); + const colorIndex = hash % packageColors.length; + let packageColor = packageColors[colorIndex]; + if (theme.isLight) { + packageColor = packageColor.clone().brighten(15); + } + return packageColor; +} + +// const getColors = memoizeOne((theme) => getFilteredColors(colors, theme)); + +// Different regexes to get the package name and function name from the label. We may at some point get an info about +// the language from the backend and use the right regex but right now we just try all of them from most to least +// specific. +const matchers = [ + ['phpspy', /^(?(.*\/)*)(?.*\.php+)(?.*)$/], + ['pyspy', /^(?(.*\/)*)(?.*\.py+)(?.*)$/], + ['rbspy', /^(?(.*\/)*)(?.*\.rb+)(?.*)$/], + [ + 'nodespy', + /^(\.\/node_modules\/)?(?[^/]*)(?.*\.?(jsx?|tsx?)?):(?.*):(?.*)$/, + ], + ['gospy', /^(?.*?\/.*?\.|.*?\.|.+)(?.*)$/], // also 'scrape' + ['javaspy', /^(?.+\/)(?.+\.)(?.+)$/], + ['dotnetspy', /^(?.+)\.(.+)\.(.+)\(.*\)$/], + ['tracing', /^(?.+?):.*$/], + ['pyroscope-rs', /^(?[^::]+)/], + ['ebpfspy', /^(?.+)$/], + ['unknown', /^(?.+)$/], +]; + +// Get the package name from the symbol. Try matchers from the list and return first one that matches. +function getPackageName(name: string): string | undefined { + for (const [_, matcher] of matchers) { + const match = name.match(matcher); + if (match) { + return match.groups?.packageName || ''; + } + } + return undefined; +} diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/murmur3.ts b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/murmur3.ts new file mode 100644 index 00000000000..3847798d66e --- /dev/null +++ b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/murmur3.ts @@ -0,0 +1,84 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* + +Copyright (c) 2011 Gary Court + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +/* eslint-disable no-plusplus */ +/* eslint-disable prefer-const */ +/* eslint-disable no-bitwise */ +/* eslint-disable camelcase */ + +export default function murmurhash3_32_gc(key: string, seed = 0) { + let remainder; + let bytes; + let h1; + let h1b; + let c1; + let c2; + let k1; + let i; + + remainder = key.length & 3; // key.length % 4 + bytes = key.length - remainder; + h1 = seed; + c1 = 0xcc9e2d51; + c2 = 0x1b873593; + i = 0; + + while (i < bytes) { + k1 = + (key.charCodeAt(i) & 0xff) | + ((key.charCodeAt(++i) & 0xff) << 8) | + ((key.charCodeAt(++i) & 0xff) << 16) | + ((key.charCodeAt(++i) & 0xff) << 24); + ++i; + + k1 = ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff; + k1 = (k1 << 15) | (k1 >>> 17); + k1 = ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff; + + h1 ^= k1; + h1 = (h1 << 13) | (h1 >>> 19); + h1b = ((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 0xffffffff; + h1 = (h1b & 0xffff) + 0x6b64 + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16); + } + + k1 = 0; + + switch (remainder) { + case 3: + k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16; + // fall through + case 2: + k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8; + // fall through + case 1: + k1 ^= key.charCodeAt(i) & 0xff; + // fall through + default: + k1 = ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff; + k1 = (k1 << 15) | (k1 >>> 17); + k1 = ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff; + h1 ^= k1; + } + + h1 ^= key.length; + + h1 ^= h1 >>> 16; + h1 = ((h1 & 0xffff) * 0x85ebca6b + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff; + h1 ^= h1 >>> 13; + h1 = ((h1 & 0xffff) * 0xc2b2ae35 + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) & 0xffffffff; + h1 ^= h1 >>> 16; + + return h1 >>> 0; +} diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/rendering.ts b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/rendering.ts index 1a68968ba65..3e782d761bd 100644 --- a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/rendering.ts +++ b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/rendering.ts @@ -1,7 +1,8 @@ import uFuzzy from '@leeoniya/ufuzzy'; import { RefObject, useEffect, useMemo, useState } from 'react'; -import { colors } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; +import { colors, useTheme2 } from '@grafana/ui'; import { BAR_BORDER_WIDTH, @@ -11,8 +12,9 @@ import { LABEL_THRESHOLD, PIXELS_PER_LEVEL, } from '../../constants'; -import { ClickedItemData, TextAlign } from '../types'; +import { ClickedItemData, ColorScheme, TextAlign } from '../types'; +import { getBarColorByPackage, getBarColorByValue } from './colors'; import { FlameGraphDataContainer, LevelItem } from './dataTransform'; const ufuzzy = new uFuzzy(); @@ -27,6 +29,7 @@ export function useFlameRender( search: string, textAlign: TextAlign, totalTicks: number, + colorScheme: ColorScheme, focusedItemData?: ClickedItemData ) { const foundLabels = useMemo(() => { @@ -47,6 +50,7 @@ export function useFlameRender( }, [search, data]); const ctx = useSetupCanvas(canvasRef, wrapperWidth, levels.length); + const theme = useTheme2(); useEffect(() => { if (!ctx) { @@ -63,7 +67,19 @@ export function useFlameRender( for (const rect of dimensions) { const focusedLevel = focusedItemData ? focusedItemData.level : 0; // Render each rectangle based on the computed dimensions - renderRect(ctx, rect, totalTicks, rangeMin, rangeMax, levelIndex, focusedLevel, foundLabels, textAlign); + renderRect( + ctx, + rect, + totalTicks, + rangeMin, + rangeMax, + levelIndex, + focusedLevel, + foundLabels, + textAlign, + colorScheme, + theme + ); } } }, [ @@ -78,6 +94,8 @@ export function useFlameRender( foundLabels, textAlign, totalTicks, + colorScheme, + theme, ]); } @@ -175,7 +193,9 @@ export function renderRect( levelIndex: number, topLevelIndex: number, foundNames: Set | undefined, - textAlign: TextAlign + textAlign: TextAlign, + colorScheme: ColorScheme, + theme: GrafanaTheme2 ) { if (rect.width < HIDE_THRESHOLD) { return; @@ -184,28 +204,36 @@ export function renderRect( ctx.beginPath(); ctx.rect(rect.x + (rect.collapsed ? 0 : BAR_BORDER_WIDTH), rect.y, rect.width, rect.height); - // / (rangeMax - rangeMin) here so when you click a bar it will adjust the top (clicked)bar to the most 'intense' color - const intensity = Math.min(1, rect.ticks / totalTicks / (rangeMax - rangeMin)); - const h = 50 - 50 * intensity; - const l = 65 + 7 * intensity; + const color = + colorScheme === ColorScheme.ValueBased + ? getBarColorByValue(rect.ticks, totalTicks, rangeMin, rangeMax) + : getBarColorByPackage(rect.label, theme); - const name = rect.label; - - if (!rect.collapsed) { - ctx.stroke(); - - if (foundNames) { - ctx.fillStyle = foundNames.has(name) ? getBarColor(h, l) : colors[55]; - } else { - ctx.fillStyle = levelIndex > topLevelIndex - 1 ? getBarColor(h, l) : getBarColor(h, l + 15); - } + if (foundNames) { + // Means we are searching, we use color for matches and gray the rest + ctx.fillStyle = foundNames.has(rect.label) ? color.toHslString() : colors[55]; } else { - ctx.fillStyle = foundNames && foundNames.has(name) ? getBarColor(h, l) : colors[55]; + // No search + if (rect.collapsed) { + // Collapsed are always grayed + ctx.fillStyle = colors[55]; + } else { + // Mute if we are above the focused symbol + ctx.fillStyle = levelIndex > topLevelIndex - 1 ? color.toHslString() : color.lighten(15).toHslString(); + } } + + if (rect.collapsed) { + // Only fill the collapsed rects + ctx.fill(); + return; + } + + ctx.stroke(); ctx.fill(); - if (!rect.collapsed && rect.width >= LABEL_THRESHOLD) { - renderLabel(ctx, name, rect, textAlign); + if (rect.width >= LABEL_THRESHOLD) { + renderLabel(ctx, rect.label, rect, textAlign); } } @@ -250,7 +278,3 @@ function renderLabel(ctx: CanvasRenderingContext2D, name: string, rect: RectData export function getBarX(offset: number, totalTicks: number, rangeMin: number, pixelsPerTick: number) { return (offset - totalTicks * rangeMin) * pixelsPerTick; } - -function getBarColor(h: number, l: number) { - return `hsl(${h}, 100%, ${l}%)`; -} diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraphContainer.tsx b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraphContainer.tsx index bce165ab256..d87b8157b8c 100644 --- a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraphContainer.tsx +++ b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraphContainer.tsx @@ -12,7 +12,7 @@ import FlameGraph from './FlameGraph/FlameGraph'; import { FlameGraphDataContainer } from './FlameGraph/dataTransform'; import FlameGraphHeader from './FlameGraphHeader'; import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer'; -import { ClickedItemData, SelectedView, TextAlign } from './types'; +import { ClickedItemData, ColorScheme, SelectedView, TextAlign } from './types'; type Props = { data?: DataFrame; @@ -30,6 +30,7 @@ const FlameGraphContainer = (props: Props) => { const [textAlign, setTextAlign] = useState('left'); // 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(); + const [colorScheme, setColorScheme] = useState(ColorScheme.ValueBased); const theme = useTheme2(); @@ -86,6 +87,8 @@ const FlameGraphContainer = (props: Props) => { textAlign={textAlign} onTextAlignChange={setTextAlign} showResetButton={Boolean(focusedItemData || sandwichItem)} + colorScheme={colorScheme} + onColorSchemeChange={setColorScheme} />
@@ -102,7 +105,6 @@ const FlameGraphContainer = (props: Props) => { grafana_version: config.buildInfo.version, }); setSearch(symbol); - resetFocus(); } }} height={selectedView === SelectedView.TopTable ? 600 : undefined} @@ -127,6 +129,7 @@ const FlameGraphContainer = (props: Props) => { }} onFocusPillClick={resetFocus} onSandwichPillClick={resetSandwich} + colorScheme={colorScheme} /> )}
diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraphHeader.test.tsx b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraphHeader.test.tsx index d9933254284..520ff81b54b 100644 --- a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraphHeader.test.tsx +++ b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraphHeader.test.tsx @@ -6,13 +6,14 @@ import React from 'react'; import { CoreApp } from '@grafana/data'; import FlameGraphHeader from './FlameGraphHeader'; -import { SelectedView } from './types'; +import { ColorScheme, SelectedView } from './types'; describe('FlameGraphHeader', () => { function setup(props: Partial> = {}) { const setSearch = jest.fn(); const setSelectedView = jest.fn(); const onReset = jest.fn(); + const onSchemeChange = jest.fn(); const renderResult = render( { onTextAlignChange={jest.fn()} textAlign={'left'} showResetButton={true} + colorScheme={ColorScheme.ValueBased} + onColorSchemeChange={onSchemeChange} {...props} /> ); @@ -36,6 +39,7 @@ describe('FlameGraphHeader', () => { setSearch, setSelectedView, onReset, + onSchemeChange, }, }; } @@ -55,4 +59,17 @@ 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); + }); }); diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraphHeader.tsx b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraphHeader.tsx index 0859a3cc0f1..4f3523e4764 100644 --- a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraphHeader.tsx +++ b/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraphHeader.tsx @@ -1,16 +1,17 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import React, { useEffect, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import usePrevious from 'react-use/lib/usePrevious'; import { CoreApp, GrafanaTheme2, SelectableValue } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; -import { Button, Input, RadioButtonGroup, useStyles2 } from '@grafana/ui'; +import { Button, Dropdown, Input, Menu, RadioButtonGroup, useStyles2 } from '@grafana/ui'; import { config } from '../../../../../core/config'; import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants'; -import { SelectedView, TextAlign } from './types'; +import { byPackageGradient, byValueGradient } from './FlameGraph/colors'; +import { ColorScheme, SelectedView, TextAlign } from './types'; type Props = { app: CoreApp; @@ -23,6 +24,8 @@ type Props = { textAlign: TextAlign; onTextAlignChange: (align: TextAlign) => void; showResetButton: boolean; + colorScheme: ColorScheme; + onColorSchemeChange: (colorScheme: ColorScheme) => void; }; const FlameGraphHeader = ({ @@ -36,6 +39,8 @@ const FlameGraphHeader = ({ textAlign, onTextAlignChange, showResetButton, + colorScheme, + onColorSchemeChange, }: Props) => { const styles = useStyles2((theme) => getStyles(theme, app)); function interaction(name: string, context: Record) { @@ -93,7 +98,7 @@ const FlameGraphHeader = ({ aria-label={'Reset focus and sandwich state'} /> )} - + size="sm" disabled={selectedView === SelectedView.TopTable} @@ -119,6 +124,42 @@ const FlameGraphHeader = ({ ); }; +type ColorSchemeButtonProps = { + app: CoreApp; + value: ColorScheme; + onChange: (colorScheme: ColorScheme) => void; +}; +function ColorSchemeButton(props: ColorSchemeButtonProps) { + const styles = useStyles2((theme) => getStyles(theme, props.app)); + const menu = ( + + props.onChange(ColorScheme.ValueBased)} /> + props.onChange(ColorScheme.PackageBased)} /> + + ); + return ( + + + + ); +} + const alignOptions: Array> = [ { value: 'left', description: 'Align text left', icon: 'align-left' }, { value: 'right', description: 'Align text right', icon: 'align-right' }, @@ -209,6 +250,21 @@ const getStyles = (theme: GrafanaTheme2, app: CoreApp) => ({ padding: 0 5px; color: ${theme.colors.text.disabled}; `, + colorDot: css` + label: colorDot; + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + `, + colorDotByValue: css` + label: colorDotByValue; + background: ${byValueGradient}; + `, + colorDotByPackage: css` + label: colorDotByPackage; + background: ${byPackageGradient}; + `, }); export default FlameGraphHeader; diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/types.ts b/public/app/plugins/panel/flamegraph/flamegraphV2/components/types.ts index 0b317431b3c..ea811266e89 100644 --- a/public/app/plugins/panel/flamegraph/flamegraphV2/components/types.ts +++ b/public/app/plugins/panel/flamegraph/flamegraphV2/components/types.ts @@ -42,4 +42,9 @@ export type TopTableValue = { unitValue: string; }; +export enum ColorScheme { + ValueBased = 'valueBased', + PackageBased = 'packageBased', +} + export type TextAlign = 'left' | 'right';