@grafana/flamegraph
Version:
Grafana flamegraph visualization component
614 lines (607 loc) • 20.6 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var jsxRuntime = require('react/jsx-runtime');
var react = require('react');
var css = require('@emotion/css');
var uFuzzy = require('@leeoniya/ufuzzy');
var reactUse = require('react-use');
var data = require('@grafana/data');
var ui = require('@grafana/ui');
var FlameGraph = require('./FlameGraph/FlameGraph.cjs');
var dataTransform = require('./FlameGraph/dataTransform.cjs');
var FlameGraphHeader = require('./FlameGraphHeader.cjs');
var FlameGraphPane = require('./FlameGraphPane.cjs');
var FlameGraphTopTableContainer = require('./TopTable/FlameGraphTopTableContainer.cjs');
var constants = require('./constants.cjs');
var hooks = require('./hooks.cjs');
var types = require('./types.cjs');
var utils = require('./utils.cjs');
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
var uFuzzy__default = /*#__PURE__*/_interopDefaultCompat(uFuzzy);
"use strict";
const ufuzzy = new uFuzzy__default.default();
const FlameGraphContainer = ({
data,
onTableSymbolClick,
onViewSelected,
onTextAlignSelected,
onTableSort,
getTheme,
stickyHeader,
extraHeaderElements,
vertical,
showFlameGraphOnly,
disableCollapsing,
keepFocusOnDataChange,
getExtraContextMenuButtons,
showAnalyzeWithAssistant = true,
enableNewUI
}) => {
const theme = react.useMemo(() => getTheme(), [getTheme]);
if (enableNewUI) {
return /* @__PURE__ */ jsxRuntime.jsx(
NewUIContainer,
{
data,
onTableSymbolClick,
onViewSelected,
onTextAlignSelected,
onTableSort,
theme,
stickyHeader,
extraHeaderElements,
vertical,
showFlameGraphOnly,
disableCollapsing,
keepFocusOnDataChange,
getExtraContextMenuButtons,
showAnalyzeWithAssistant
}
);
}
return /* @__PURE__ */ jsxRuntime.jsx(
LegacyContainer,
{
data,
onTableSymbolClick,
onViewSelected,
onTextAlignSelected,
onTableSort,
theme,
stickyHeader,
extraHeaderElements,
vertical,
showFlameGraphOnly,
disableCollapsing,
keepFocusOnDataChange,
getExtraContextMenuButtons,
showAnalyzeWithAssistant
}
);
};
const LegacyContainer = ({
data: data$1,
onTableSymbolClick,
onViewSelected,
onTextAlignSelected,
onTableSort,
theme,
stickyHeader,
extraHeaderElements,
vertical,
showFlameGraphOnly,
disableCollapsing,
keepFocusOnDataChange,
getExtraContextMenuButtons,
showAnalyzeWithAssistant
}) => {
const [focusedItemData, setFocusedItemData] = react.useState();
const [rangeMin, setRangeMin] = react.useState(0);
const [rangeMax, setRangeMax] = react.useState(1);
const [search, setSearch] = react.useState("");
const [selectedView, setSelectedView] = react.useState(types.SelectedView.Both);
const [sizeRef, { width: containerWidth }] = reactUse.useMeasure();
const [textAlign, setTextAlign] = react.useState("left");
const [sandwichItem, setSandwichItem] = react.useState();
const [collapsedMap, setCollapsedMap] = react.useState(new dataTransform.CollapsedMap());
const onTableSymbolClickRef = react.useRef(onTableSymbolClick);
const onTableSortRef = react.useRef(onTableSort);
onTableSymbolClickRef.current = onTableSymbolClick;
onTableSortRef.current = onTableSort;
const dataContainer = react.useMemo(() => {
if (!data$1) {
return;
}
const container = new dataTransform.FlameGraphDataContainer(data$1, { collapsing: !disableCollapsing }, theme);
setCollapsedMap(container.getCollapsedMap());
return container;
}, [data$1, theme, disableCollapsing]);
const [colorScheme, setColorScheme] = hooks.useColorScheme(dataContainer);
const styles = getStyles(theme);
const matchedLabels = useLabelSearch(search, dataContainer);
react.useEffect(() => {
if (containerWidth > 0 && containerWidth < constants.MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH && selectedView === types.SelectedView.Both && !vertical) {
setSelectedView(types.SelectedView.FlameGraph);
}
}, [selectedView, setSelectedView, containerWidth, vertical]);
const resetFocus = react.useCallback(() => {
setFocusedItemData(void 0);
setRangeMin(0);
setRangeMax(1);
}, [setFocusedItemData, setRangeMax, setRangeMin]);
const resetSandwich = react.useCallback(() => {
setSandwichItem(void 0);
}, [setSandwichItem]);
react.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 = react.useCallback(
(symbol) => {
var _a;
const anchored = `^${data.escapeStringForRegex(symbol)}$`;
if (search === anchored) {
setSearch("");
} else {
(_a = onTableSymbolClickRef.current) == null ? void 0 : _a.call(onTableSymbolClickRef, symbol);
setSearch(anchored);
resetFocus();
}
},
[setSearch, resetFocus, search]
);
const onSearch = react.useCallback(
(str) => {
if (!str) {
setSearch("");
return;
}
setSearch(`^${data.escapeStringForRegex(str)}$`);
},
[setSearch]
);
const onSandwich = react.useCallback(
(label) => {
resetFocus();
setSandwichItem(label);
},
[resetFocus, setSandwichItem]
);
const onTableSortStable = react.useCallback((sort) => {
var _a;
(_a = onTableSortRef.current) == null ? void 0 : _a.call(onTableSortRef, sort);
}, []);
if (!dataContainer) {
return null;
}
const flameGraph = /* @__PURE__ */ jsxRuntime.jsx(
FlameGraph,
{
data: dataContainer,
rangeMin,
rangeMax,
matchedLabels,
setRangeMin,
setRangeMax,
onItemFocused: (data2) => setFocusedItemData(data2),
focusedItemData,
textAlign,
sandwichItem,
onSandwich,
onFocusPillClick: resetFocus,
onSandwichPillClick: resetSandwich,
colorScheme,
showFlameGraphOnly,
collapsing: !disableCollapsing,
getExtraContextMenuButtons,
selectedView,
search,
collapsedMap,
setCollapsedMap
}
);
const table = /* @__PURE__ */ jsxRuntime.jsx(
FlameGraphTopTableContainer.default,
{
data: dataContainer,
onSymbolClick,
search,
matchedLabels,
sandwichItem,
onSandwich: setSandwichItem,
onSearch,
onTableSort: onTableSortStable,
colorScheme
}
);
let body;
if (showFlameGraphOnly || selectedView === types.SelectedView.FlameGraph) {
body = flameGraph;
} else if (selectedView === types.SelectedView.TopTable) {
body = /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.tableContainer, children: table });
} else if (selectedView === types.SelectedView.Both) {
if (vertical) {
body = /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
/* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.verticalGraphContainer, children: flameGraph }),
/* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.verticalTableContainer, children: table })
] });
} else {
body = /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.horizontalContainer, children: [
/* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.horizontalTableContainer, children: table }),
/* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(ui.ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: sizeRef, className: styles.container, children: [
!showFlameGraphOnly && /* @__PURE__ */ jsxRuntime.jsx(
FlameGraphHeader.default,
{
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,
assistantContext: data$1 && showAnalyzeWithAssistant ? utils.getAssistantContextFromDataFrame(data$1) : void 0
}
),
/* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.body, children: body })
] }) })
);
};
const NewUIContainer = ({
data,
onTableSymbolClick,
onViewSelected,
onTextAlignSelected,
onTableSort,
theme,
stickyHeader,
extraHeaderElements,
vertical,
showFlameGraphOnly,
disableCollapsing,
keepFocusOnDataChange,
getExtraContextMenuButtons,
showAnalyzeWithAssistant
}) => {
const [search, setSearch] = react.useState("");
const [viewMode, setViewMode] = react.useState(types.ViewMode.Split);
const [leftPaneView, setLeftPaneView] = react.useState(types.PaneView.TopTable);
const [rightPaneView, setRightPaneView] = react.useState(types.PaneView.FlameGraph);
const [singleView, setSingleView] = react.useState(types.PaneView.FlameGraph);
const [panesSwapped, setPanesSwapped] = react.useState(false);
const [sizeRef, { width: containerWidth }] = reactUse.useMeasure();
const [resetKey, setResetKey] = react.useState(0);
const [focusedItemIndexes, setFocusedItemIndexes] = react.useState(void 0);
const [sharedSandwichItem, setSharedSandwichItem] = react.useState(void 0);
const canShowSplitView = containerWidth > 0 && (containerWidth >= constants.MIN_WIDTH_FOR_SPLIT_VIEW || Boolean(vertical));
const onTableSymbolClickRef = react.useRef(onTableSymbolClick);
const onTextAlignSelectedRef = react.useRef(onTextAlignSelected);
const onTableSortRef = react.useRef(onTableSort);
react.useEffect(() => {
onTableSymbolClickRef.current = onTableSymbolClick;
onTextAlignSelectedRef.current = onTextAlignSelected;
onTableSortRef.current = onTableSort;
});
const stableOnTableSymbolClick = react.useCallback((symbol) => {
var _a;
(_a = onTableSymbolClickRef.current) == null ? void 0 : _a.call(onTableSymbolClickRef, symbol);
}, []);
const stableOnTextAlignSelected = react.useCallback((align) => {
var _a;
(_a = onTextAlignSelectedRef.current) == null ? void 0 : _a.call(onTextAlignSelectedRef, align);
}, []);
const stableOnTableSort = react.useCallback((sort) => {
var _a;
(_a = onTableSortRef.current) == null ? void 0 : _a.call(onTableSortRef, sort);
}, []);
const dataContainer = react.useMemo(() => {
if (!data) {
return;
}
return new dataTransform.FlameGraphDataContainer(data, { collapsing: !disableCollapsing }, theme);
}, [data, theme, disableCollapsing]);
const styles = getStyles(theme);
const matchedLabels = useLabelSearch(search, dataContainer);
const effectiveViewMode = canShowSplitView ? viewMode : types.ViewMode.Single;
const prevViewMode = reactUse.usePrevious(viewMode);
react.useEffect(() => {
if (prevViewMode === void 0) {
return;
}
if (prevViewMode === types.ViewMode.Split && viewMode === types.ViewMode.Single) {
setSingleView(rightPaneView);
} else if (prevViewMode === types.ViewMode.Single && viewMode === types.ViewMode.Split) {
setRightPaneView(singleView);
}
}, [viewMode, prevViewMode, rightPaneView, singleView]);
if (!dataContainer) {
return null;
}
const commonPaneProps = {
dataContainer,
search,
matchedLabels,
onTableSymbolClick: stableOnTableSymbolClick,
onTextAlignSelected: stableOnTextAlignSelected,
onTableSort: stableOnTableSort,
showFlameGraphOnly,
disableCollapsing,
getExtraContextMenuButtons,
setSearch,
resetKey,
keepFocusOnDataChange,
focusedItemIndexes,
setFocusedItemIndexes
};
let body;
if (showFlameGraphOnly) {
body = /* @__PURE__ */ jsxRuntime.jsx(
FlameGraphPane,
{
...commonPaneProps,
paneView: types.PaneView.FlameGraph,
viewMode: effectiveViewMode,
paneViewForContextMenu: types.PaneView.FlameGraph,
sharedSandwichItem,
setSharedSandwichItem
}
);
} else if (effectiveViewMode === types.ViewMode.Single) {
body = /* @__PURE__ */ jsxRuntime.jsx(
FlameGraphPane,
{
...commonPaneProps,
paneView: singleView,
viewMode: types.ViewMode.Single,
paneViewForContextMenu: singleView,
sharedSandwichItem,
setSharedSandwichItem
}
);
} else {
const shouldSyncSandwich = leftPaneView !== rightPaneView;
const leftPane = /* @__PURE__ */ react.createElement(
FlameGraphPane,
{
...commonPaneProps,
key: "left-pane",
paneView: leftPaneView,
viewMode: types.ViewMode.Split,
paneViewForContextMenu: leftPaneView,
sharedSandwichItem: shouldSyncSandwich ? sharedSandwichItem : void 0,
setSharedSandwichItem: shouldSyncSandwich ? setSharedSandwichItem : void 0
}
);
const rightPane = /* @__PURE__ */ react.createElement(
FlameGraphPane,
{
...commonPaneProps,
key: "right-pane",
paneView: rightPaneView,
viewMode: types.ViewMode.Split,
paneViewForContextMenu: rightPaneView,
sharedSandwichItem: shouldSyncSandwich ? sharedSandwichItem : void 0,
setSharedSandwichItem: shouldSyncSandwich ? setSharedSandwichItem : void 0
}
);
if (vertical) {
body = /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.verticalContainer, children: [
/* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.verticalPaneContainer, style: { order: panesSwapped ? 2 : 1 }, children: leftPane }),
/* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.verticalPaneContainer, style: { order: panesSwapped ? 1 : 2 }, children: rightPane })
] });
} else {
body = /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.horizontalContainer, children: [
/* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.horizontalPaneContainer, style: { order: panesSwapped ? 2 : 1 }, children: leftPane }),
/* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.horizontalPaneContainer, style: { order: panesSwapped ? 1 : 2 }, children: rightPane })
] });
}
}
return /* @__PURE__ */ jsxRuntime.jsx(ui.ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: sizeRef, className: styles.container, children: [
!showFlameGraphOnly && /* @__PURE__ */ jsxRuntime.jsx(
FlameGraphHeader.default,
{
enableNewUI: true,
search,
setSearch,
viewMode,
setViewMode: (mode) => {
setViewMode(mode);
onViewSelected == null ? void 0 : onViewSelected(mode === types.ViewMode.Split ? "split" : singleView);
},
canShowSplitView,
containerWidth,
leftPaneView: panesSwapped ? rightPaneView : leftPaneView,
setLeftPaneView: panesSwapped ? setRightPaneView : setLeftPaneView,
rightPaneView: panesSwapped ? leftPaneView : rightPaneView,
setRightPaneView: panesSwapped ? setLeftPaneView : setRightPaneView,
singleView,
setSingleView: (view) => {
setSingleView(view);
if (viewMode === types.ViewMode.Single) {
onViewSelected == null ? void 0 : onViewSelected(view);
}
},
onSwapPanes: () => setPanesSwapped((s) => !s),
onReset: () => {
setSearch("");
setFocusedItemIndexes(void 0);
setSharedSandwichItem(void 0);
setResetKey((k) => k + 1);
},
showResetButton: Boolean(search),
stickyHeader: Boolean(stickyHeader),
extraHeaderElements,
assistantContext: data && showAnalyzeWithAssistant ? utils.getAssistantContextFromDataFrame(data) : void 0
}
),
/* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.body, children: body })
] }) });
};
function useLabelSearch(search, data) {
return react.useMemo(() => {
if (!search || !data) {
return void 0;
}
return labelSearch(search, data);
}, [search, data]);
}
function labelSearch(search, data) {
const foundLabels = /* @__PURE__ */ new Set();
const terms = search.split(",");
const regexFilter = (labels, pattern) => {
let regex;
try {
regex = new RegExp(pattern);
} catch (e) {
return false;
}
let foundMatch = false;
for (let label of labels) {
if (!regex.test(label)) {
continue;
}
foundLabels.add(label);
foundMatch = true;
}
return foundMatch;
};
const fuzzyFilter = (labels, term) => {
let idxs = ufuzzy.filter(labels, term);
if (!idxs) {
return false;
}
let foundMatch = false;
for (let idx of idxs) {
foundLabels.add(labels[idx]);
foundMatch = true;
}
return foundMatch;
};
for (let term of terms) {
if (!term) {
continue;
}
const found = regexFilter(data.getUniqueLabels(), term);
if (!found) {
fuzzyFilter(data.getUniqueLabels(), term);
}
}
return foundLabels;
}
function getStyles(theme) {
return {
container: css.css({
label: "container",
overflow: "auto",
height: "100%",
display: "flex",
flex: "1 1 0",
flexDirection: "column",
minHeight: 0,
gap: theme.spacing(1)
}),
body: css.css({
label: "body",
flexGrow: 1
}),
tableContainer: css.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: constants.FLAMEGRAPH_CONTAINER_HEIGHT
}),
horizontalContainer: css.css({
label: "horizontalContainer",
display: "flex",
minHeight: 0,
flexDirection: "row",
columnGap: theme.spacing(1),
width: "100%"
}),
horizontalGraphContainer: css.css({
flexBasis: "50%"
}),
horizontalTableContainer: css.css({
flexBasis: "50%",
maxHeight: constants.FLAMEGRAPH_CONTAINER_HEIGHT
}),
verticalGraphContainer: css.css({
marginBottom: theme.spacing(1)
}),
verticalTableContainer: css.css({
height: constants.FLAMEGRAPH_CONTAINER_HEIGHT
}),
verticalContainer: css.css({
label: "verticalContainer",
display: "flex",
flexDirection: "column"
}),
horizontalPaneContainer: css.css({
label: "horizontalPaneContainer",
flexBasis: "50%",
maxHeight: constants.FLAMEGRAPH_CONTAINER_HEIGHT,
minWidth: 0,
overflow: "auto"
}),
verticalPaneContainer: css.css({
label: "verticalPaneContainer",
marginBottom: theme.spacing(1),
height: constants.FLAMEGRAPH_CONTAINER_HEIGHT
})
};
}
exports.default = FlameGraphContainer;
exports.labelSearch = labelSearch;
exports.useLabelSearch = useLabelSearch;
//# sourceMappingURL=FlameGraphContainer.cjs.map