@grafana/flamegraph
Version:
Grafana flamegraph visualization component
293 lines (290 loc) • 9.27 kB
JavaScript
import { jsxs, jsx } from 'react/jsx-runtime';
import { cx, css } from '@emotion/css';
import { useState, useEffect } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import usePrevious from 'react-use/lib/usePrevious';
import { useStyles2, Input, Button, ButtonGroup, RadioButtonGroup, Menu, Dropdown } from '@grafana/ui';
import { byValueGradient, byPackageGradient, diffDefaultGradient, diffColorBlindGradient } from './FlameGraph/colors.mjs';
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants.mjs';
import { SelectedView, ColorScheme, ColorSchemeDiff } from './types.mjs';
const FlameGraphHeader = ({
search,
setSearch,
selectedView,
setSelectedView,
containerWidth,
onReset,
textAlign,
onTextAlignChange,
showResetButton,
colorScheme,
onColorSchemeChange,
stickyHeader,
extraHeaderElements,
vertical,
isDiffMode,
setCollapsedMap,
collapsedMap
}) => {
const styles = useStyles2(getStyles);
const [localSearch, setLocalSearch] = useSearchInput(search, setSearch);
const suffix = localSearch !== "" ? /* @__PURE__ */ jsx(
Button,
{
icon: "times",
fill: "text",
size: "sm",
onClick: () => {
setSearch("");
setLocalSearch("");
},
children: "Clear"
}
) : null;
return /* @__PURE__ */ jsxs("div", { className: cx(styles.header, { [styles.stickyHeader]: stickyHeader }), children: [
/* @__PURE__ */ jsx("div", { className: styles.inputContainer, children: /* @__PURE__ */ jsx(
Input,
{
value: localSearch || "",
onChange: (v) => {
setLocalSearch(v.currentTarget.value);
},
placeholder: "Search...",
suffix
}
) }),
/* @__PURE__ */ jsxs("div", { className: styles.rightContainer, children: [
showResetButton && /* @__PURE__ */ jsx(
Button,
{
variant: "secondary",
fill: "outline",
size: "sm",
icon: "history-alt",
tooltip: "Reset focus and sandwich state",
onClick: () => {
onReset();
},
className: styles.buttonSpacing,
"aria-label": "Reset focus and sandwich state"
}
),
/* @__PURE__ */ jsx(ColorSchemeButton, { value: colorScheme, onChange: onColorSchemeChange, isDiffMode }),
/* @__PURE__ */ jsxs(ButtonGroup, { className: styles.buttonSpacing, children: [
/* @__PURE__ */ jsx(
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
}
),
/* @__PURE__ */ jsx(
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
}
)
] }),
/* @__PURE__ */ jsx(
RadioButtonGroup,
{
size: "sm",
disabled: selectedView === SelectedView.TopTable,
options: alignOptions,
value: textAlign,
onChange: onTextAlignChange,
className: styles.buttonSpacing
}
),
/* @__PURE__ */ jsx(
RadioButtonGroup,
{
size: "sm",
options: getViewOptions(containerWidth, vertical),
value: selectedView,
onChange: setSelectedView
}
),
extraHeaderElements && /* @__PURE__ */ jsx("div", { className: styles.extraElements, children: extraHeaderElements })
] })
] });
};
function ColorSchemeButton(props) {
const styles = useStyles2(getStyles);
let menu = /* @__PURE__ */ jsxs(Menu, { children: [
/* @__PURE__ */ jsx(Menu.Item, { label: "By package name", onClick: () => props.onChange(ColorScheme.PackageBased) }),
/* @__PURE__ */ jsx(Menu.Item, { label: "By value", onClick: () => props.onChange(ColorScheme.ValueBased) })
] });
const colorDotStyle = {
[ColorScheme.ValueBased]: styles.colorDotByValue,
[ColorScheme.PackageBased]: styles.colorDotByPackage,
[ColorSchemeDiff.DiffColorBlind]: styles.colorDotDiffColorBlind,
[ColorSchemeDiff.Default]: styles.colorDotDiffDefault
}[props.value] || styles.colorDotByValue;
let contents = /* @__PURE__ */ jsx("span", { className: cx(styles.colorDot, colorDotStyle) });
if (props.isDiffMode) {
menu = /* @__PURE__ */ jsxs(Menu, { children: [
/* @__PURE__ */ jsx(Menu.Item, { label: "Default (green to red)", onClick: () => props.onChange(ColorSchemeDiff.Default) }),
/* @__PURE__ */ jsx(Menu.Item, { label: "Color blind (blue to red)", onClick: () => props.onChange(ColorSchemeDiff.DiffColorBlind) })
] });
contents = /* @__PURE__ */ jsxs("div", { className: cx(styles.colorDotDiff, colorDotStyle), children: [
/* @__PURE__ */ jsx("div", { children: "-100% (removed)" }),
/* @__PURE__ */ jsx("div", { children: "0%" }),
/* @__PURE__ */ jsx("div", { children: "+100% (added)" })
] });
}
return /* @__PURE__ */ jsx(Dropdown, { overlay: menu, children: /* @__PURE__ */ jsx(
Button,
{
variant: "secondary",
fill: "outline",
size: "sm",
tooltip: "Change color scheme",
onClick: () => {
},
className: styles.buttonSpacing,
"aria-label": "Change color scheme",
children: contents
}
) });
}
const alignOptions = [
{ value: "left", description: "Align text left", icon: "align-left" },
{ value: "right", description: "Align text right", icon: "align-right" }
];
function getViewOptions(width, vertical) {
let viewOptions = [
{ value: SelectedView.TopTable, label: "Top Table", description: "Only show top table" },
{ value: SelectedView.FlameGraph, label: "Flame Graph", description: "Only show flame graph" }
];
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"
});
}
return viewOptions;
}
function useSearchInput(search, setSearch) {
const [localSearchState, setLocalSearchState] = useState(search);
const prevSearch = usePrevious(search);
useDebounce(
() => {
setSearch(localSearchState);
},
250,
[localSearchState]
);
useEffect(() => {
if (prevSearch !== search && search !== localSearchState) {
setLocalSearchState(search);
}
}, [search, prevSearch, localSearchState]);
return [localSearchState, setLocalSearchState];
}
const getStyles = (theme) => ({
header: css({
label: "header",
display: "flex",
flexWrap: "wrap",
justifyContent: "space-between",
width: "100%",
top: 0,
gap: theme.spacing(1),
marginTop: theme.spacing(1)
}),
stickyHeader: css({
zIndex: theme.zIndex.navbarFixed,
position: "sticky",
background: theme.colors.background.primary
}),
inputContainer: css({
label: "inputContainer",
flexGrow: 1,
minWidth: "150px",
maxWidth: "350px"
}),
rightContainer: css({
label: "rightContainer",
display: "flex",
alignItems: "flex-start",
flexWrap: "wrap"
}),
buttonSpacing: css({
label: "buttonSpacing",
marginRight: theme.spacing(1)
}),
resetButton: css({
label: "resetButton",
display: "flex",
marginRight: theme.spacing(2)
}),
resetButtonIconWrapper: css({
label: "resetButtonIcon",
padding: "0 5px",
color: theme.colors.text.disabled
}),
colorDot: css({
label: "colorDot",
display: "inline-block",
width: "10px",
height: "10px",
// eslint-disable-next-line @grafana/no-border-radius-literal
borderRadius: "50%"
}),
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)
})
});
export { FlameGraphHeader as default };
//# sourceMappingURL=FlameGraphHeader.mjs.map