mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
345 lines
10 KiB
TypeScript
345 lines
10 KiB
TypeScript
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 { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
|
import { Button, ButtonGroup, Dropdown, Input, Menu, 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';
|
|
|
|
type Props = {
|
|
search: string;
|
|
setSearch: (search: string) => void;
|
|
selectedView: SelectedView;
|
|
setSelectedView: (view: SelectedView) => 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;
|
|
};
|
|
|
|
const FlameGraphHeader = ({
|
|
search,
|
|
setSearch,
|
|
selectedView,
|
|
setSelectedView,
|
|
containerWidth,
|
|
onReset,
|
|
textAlign,
|
|
onTextAlignChange,
|
|
showResetButton,
|
|
colorScheme,
|
|
onColorSchemeChange,
|
|
stickyHeader,
|
|
extraHeaderElements,
|
|
vertical,
|
|
isDiffMode,
|
|
setCollapsedMap,
|
|
collapsedMap,
|
|
}: Props) => {
|
|
const styles = useStyles2(getStyles);
|
|
const [localSearch, setLocalSearch] = useSearchInput(search, setSearch);
|
|
|
|
const suffix =
|
|
localSearch !== '' ? (
|
|
<Button
|
|
icon="times"
|
|
fill="text"
|
|
size="sm"
|
|
onClick={() => {
|
|
// We could set only one and wait them to sync but there is no need to debounce this.
|
|
setSearch('');
|
|
setLocalSearch('');
|
|
}}
|
|
>
|
|
Clear
|
|
</Button>
|
|
) : null;
|
|
|
|
return (
|
|
<div className={cx(styles.header, { [styles.stickyHeader]: stickyHeader })}>
|
|
<div className={styles.inputContainer}>
|
|
<Input
|
|
value={localSearch || ''}
|
|
onChange={(v) => {
|
|
setLocalSearch(v.currentTarget.value);
|
|
}}
|
|
placeholder={'Search...'}
|
|
suffix={suffix}
|
|
/>
|
|
</div>
|
|
|
|
<div className={styles.rightContainer}>
|
|
{showResetButton && (
|
|
<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'}
|
|
/>
|
|
)}
|
|
<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}
|
|
/>
|
|
<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>
|
|
);
|
|
|
|
// 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' },
|
|
];
|
|
|
|
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' },
|
|
];
|
|
|
|
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: string,
|
|
setSearch: (search: string) => void
|
|
): [string | undefined, (search: string) => void] {
|
|
const [localSearchState, setLocalSearchState] = useState(search);
|
|
const prevSearch = usePrevious(search);
|
|
|
|
// Debouncing cause changing parent search triggers rerender on both the flamegraph and table
|
|
useDebounce(
|
|
() => {
|
|
setSearch(localSearchState);
|
|
},
|
|
250,
|
|
[localSearchState]
|
|
);
|
|
|
|
// Make sure we still handle updates from parent (from clicking on a table item for example). We check if the parent
|
|
// search value changed to something that isn't our local value.
|
|
useEffect(() => {
|
|
if (prevSearch !== search && search !== localSearchState) {
|
|
setLocalSearchState(search);
|
|
}
|
|
}, [search, prevSearch, localSearchState]);
|
|
|
|
return [localSearchState, setLocalSearchState];
|
|
}
|
|
|
|
const getStyles = (theme: GrafanaTheme2) => ({
|
|
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 default FlameGraphHeader;
|