UNPKG

@grafana/flamegraph

Version:

Grafana flamegraph visualization component

280 lines (277 loc) • 9.13 kB
import { jsx, jsxs } from 'react/jsx-runtime'; import { css } from '@emotion/css'; import uFuzzy from '@leeoniya/ufuzzy'; import { useState, useMemo, useEffect, useCallback } from 'react'; import { useMeasure } from 'react-use'; import { ThemeContext } from '@grafana/ui'; import FlameGraph from './FlameGraph/FlameGraph.mjs'; import { CollapsedMap, FlameGraphDataContainer } from './FlameGraph/dataTransform.mjs'; import FlameGraphHeader from './FlameGraphHeader.mjs'; import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer.mjs'; import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants.mjs'; import { SelectedView, ColorSchemeDiff, ColorScheme } from './types.mjs'; const ufuzzy = new uFuzzy(); const FlameGraphContainer = ({ data, onTableSymbolClick, onViewSelected, onTextAlignSelected, onTableSort, getTheme, stickyHeader, extraHeaderElements, vertical, showFlameGraphOnly, disableCollapsing, keepFocusOnDataChange, getExtraContextMenuButtons }) => { const [focusedItemData, setFocusedItemData] = useState(); const [rangeMin, setRangeMin] = useState(0); const [rangeMax, setRangeMax] = useState(1); const [search, setSearch] = useState(""); const [selectedView, setSelectedView] = useState(SelectedView.Both); const [sizeRef, { width: containerWidth }] = useMeasure(); const [textAlign, setTextAlign] = useState("left"); const [sandwichItem, setSandwichItem] = useState(); const [collapsedMap, setCollapsedMap] = useState(new CollapsedMap()); const theme = useMemo(() => getTheme(), [getTheme]); const dataContainer = useMemo(() => { if (!data) { return; } const container = new FlameGraphDataContainer(data, { collapsing: !disableCollapsing }, theme); setCollapsedMap(container.getCollapsedMap()); return container; }, [data, theme, disableCollapsing]); const [colorScheme, setColorScheme] = useColorScheme(dataContainer); const styles = getStyles(theme); const matchedLabels = useLabelSearch(search, dataContainer); useEffect(() => { if (containerWidth > 0 && containerWidth < MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH && selectedView === SelectedView.Both && !vertical) { setSelectedView(SelectedView.FlameGraph); } }, [selectedView, setSelectedView, containerWidth, vertical]); const resetFocus = useCallback(() => { setFocusedItemData(void 0); setRangeMin(0); setRangeMax(1); }, [setFocusedItemData, setRangeMax, setRangeMin]); const resetSandwich = useCallback(() => { setSandwichItem(void 0); }, [setSandwichItem]); useEffect(() => { var _a; if (!keepFocusOnDataChange) { resetFocus(); resetSandwich(); return; } if (dataContainer && focusedItemData) { const item = (_a = dataContainer.getNodesWithLabel(focusedItemData.label)) == null ? void 0 : _a[0]; if (item) { setFocusedItemData({ ...focusedItemData, item }); const levels = dataContainer.getLevels(); const totalViewTicks = levels.length ? levels[0][0].value : 0; setRangeMin(item.start / totalViewTicks); setRangeMax((item.start + item.value) / totalViewTicks); } else { setFocusedItemData({ ...focusedItemData, item: { start: 0, value: 0, itemIndexes: [], children: [], level: 0 } }); setRangeMin(0); setRangeMax(1); } } }, [dataContainer, keepFocusOnDataChange]); const onSymbolClick = useCallback( (symbol) => { if (search === symbol) { setSearch(""); } else { onTableSymbolClick == null ? void 0 : onTableSymbolClick(symbol); setSearch(symbol); resetFocus(); } }, [setSearch, resetFocus, onTableSymbolClick, search] ); if (!dataContainer) { return null; } const flameGraph = /* @__PURE__ */ jsx( FlameGraph, { data: dataContainer, rangeMin, rangeMax, matchedLabels, setRangeMin, setRangeMax, onItemFocused: (data2) => setFocusedItemData(data2), focusedItemData, textAlign, sandwichItem, onSandwich: (label) => { resetFocus(); setSandwichItem(label); }, onFocusPillClick: resetFocus, onSandwichPillClick: resetSandwich, colorScheme, showFlameGraphOnly, collapsing: !disableCollapsing, getExtraContextMenuButtons, selectedView, search, collapsedMap, setCollapsedMap } ); const table = /* @__PURE__ */ jsx( FlameGraphTopTableContainer, { data: dataContainer, onSymbolClick, search, matchedLabels, sandwichItem, onSandwich: setSandwichItem, onSearch: setSearch, onTableSort, colorScheme } ); let body; if (showFlameGraphOnly || selectedView === SelectedView.FlameGraph) { body = flameGraph; } else if (selectedView === SelectedView.TopTable) { body = /* @__PURE__ */ jsx("div", { className: styles.tableContainer, children: table }); } else if (selectedView === SelectedView.Both) { if (vertical) { body = /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx("div", { className: styles.verticalGraphContainer, children: flameGraph }), /* @__PURE__ */ jsx("div", { className: styles.verticalTableContainer, children: table }) ] }); } else { body = /* @__PURE__ */ jsxs("div", { className: styles.horizontalContainer, children: [ /* @__PURE__ */ jsx("div", { className: styles.horizontalTableContainer, children: table }), /* @__PURE__ */ jsx("div", { className: styles.horizontalGraphContainer, children: flameGraph }) ] }); } } return ( // We add the theme context to bridge the gap if this is rendered in non grafana environment where the context // isn't already provided. /* @__PURE__ */ jsx(ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsxs("div", { ref: sizeRef, className: styles.container, children: [ !showFlameGraphOnly && /* @__PURE__ */ jsx( FlameGraphHeader, { search, setSearch, selectedView, setSelectedView: (view) => { setSelectedView(view); onViewSelected == null ? void 0 : onViewSelected(view); }, containerWidth, onReset: () => { resetFocus(); resetSandwich(); }, textAlign, onTextAlignChange: (align) => { setTextAlign(align); onTextAlignSelected == null ? void 0 : onTextAlignSelected(align); }, showResetButton: Boolean(focusedItemData || sandwichItem), colorScheme, onColorSchemeChange: setColorScheme, stickyHeader: Boolean(stickyHeader), extraHeaderElements, vertical, isDiffMode: dataContainer.isDiffFlamegraph(), setCollapsedMap, collapsedMap } ), /* @__PURE__ */ jsx("div", { className: styles.body, children: body }) ] }) }) ); }; function useColorScheme(dataContainer) { const defaultColorScheme = (dataContainer == null ? void 0 : dataContainer.isDiffFlamegraph()) ? ColorSchemeDiff.Default : ColorScheme.PackageBased; const [colorScheme, setColorScheme] = useState(defaultColorScheme); useEffect(() => { setColorScheme(defaultColorScheme); }, [defaultColorScheme]); return [colorScheme, setColorScheme]; } function useLabelSearch(search, data) { return useMemo(() => { if (search && data) { const foundLabels = /* @__PURE__ */ new Set(); let idxs = ufuzzy.filter(data.getUniqueLabels(), search); if (idxs) { for (let idx of idxs) { foundLabels.add(data.getUniqueLabels()[idx]); } } return foundLabels; } return void 0; }, [search, data]); } function getStyles(theme) { return { container: css({ label: "container", overflow: "auto", height: "100%", display: "flex", flex: "1 1 0", flexDirection: "column", minHeight: 0, gap: theme.spacing(1) }), body: css({ label: "body", flexGrow: 1 }), tableContainer: css({ // This is not ideal for dashboard panel where it creates a double scroll. In a panel it should be 100% but then // in explore we need a specific height. height: 800 }), horizontalContainer: css({ label: "horizontalContainer", display: "flex", minHeight: 0, flexDirection: "row", columnGap: theme.spacing(1), width: "100%" }), horizontalGraphContainer: css({ flexBasis: "50%" }), horizontalTableContainer: css({ flexBasis: "50%", maxHeight: 800 }), verticalGraphContainer: css({ marginBottom: theme.spacing(1) }), verticalTableContainer: css({ height: 800 }) }; } export { FlameGraphContainer as default }; //# sourceMappingURL=FlameGraphContainer.mjs.map