mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
296 lines
8.4 KiB
TypeScript
296 lines
8.4 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 { CoreApp, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
|
import { reportInteraction, config } from '@grafana/runtime';
|
|
import { Button, Dropdown, Input, Menu, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
|
|
|
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
|
|
|
|
import { byPackageGradient, byValueGradient, diffColorBlindGradient, diffDefaultGradient } from './FlameGraph/colors';
|
|
import { ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
|
|
|
|
type Props = {
|
|
app: CoreApp;
|
|
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;
|
|
isDiffMode: boolean;
|
|
};
|
|
|
|
const FlameGraphHeader = ({
|
|
app,
|
|
search,
|
|
setSearch,
|
|
selectedView,
|
|
setSelectedView,
|
|
containerWidth,
|
|
onReset,
|
|
textAlign,
|
|
onTextAlignChange,
|
|
showResetButton,
|
|
colorScheme,
|
|
onColorSchemeChange,
|
|
isDiffMode,
|
|
}: Props) => {
|
|
const styles = useStyles2((theme) => getStyles(theme, app));
|
|
function interaction(name: string, context: Record<string, string | number>) {
|
|
reportInteraction(`grafana_flamegraph_${name}`, {
|
|
app,
|
|
grafana_version: config.buildInfo.version,
|
|
...context,
|
|
});
|
|
}
|
|
|
|
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={styles.header}>
|
|
<div className={styles.inputContainer}>
|
|
<Input
|
|
value={localSearch || ''}
|
|
onChange={(v) => {
|
|
setLocalSearch(v.currentTarget.value);
|
|
}}
|
|
placeholder={'Search..'}
|
|
width={44}
|
|
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 app={app} value={colorScheme} onChange={onColorSchemeChange} isDiffMode={isDiffMode} />
|
|
<RadioButtonGroup<TextAlign>
|
|
size="sm"
|
|
disabled={selectedView === SelectedView.TopTable}
|
|
options={alignOptions}
|
|
value={textAlign}
|
|
onChange={(val) => {
|
|
interaction('text_align_selected', { align: val });
|
|
onTextAlignChange(val);
|
|
}}
|
|
className={styles.buttonSpacing}
|
|
/>
|
|
<RadioButtonGroup<SelectedView>
|
|
size="sm"
|
|
options={getViewOptions(containerWidth)}
|
|
value={selectedView}
|
|
onChange={(view) => {
|
|
interaction('view_selected', { view });
|
|
setSelectedView(view);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
type ColorSchemeButtonProps = {
|
|
app: CoreApp;
|
|
value: ColorScheme | ColorSchemeDiff;
|
|
onChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
|
|
isDiffMode: boolean;
|
|
};
|
|
function ColorSchemeButton(props: ColorSchemeButtonProps) {
|
|
const styles = useStyles2((theme) => getStyles(theme, props.app));
|
|
|
|
let menu = (
|
|
<Menu>
|
|
<Menu.Item label="By value" onClick={() => props.onChange(ColorScheme.ValueBased)} />
|
|
<Menu.Item label="By package name" onClick={() => props.onChange(ColorScheme.PackageBased)} />
|
|
</Menu>
|
|
);
|
|
|
|
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>
|
|
);
|
|
}
|
|
|
|
// 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;
|
|
|
|
return (
|
|
<Dropdown overlay={menu}>
|
|
<Button
|
|
variant={'secondary'}
|
|
fill={'outline'}
|
|
size={'sm'}
|
|
tooltip={'Change color scheme'}
|
|
onClick={() => {}}
|
|
className={styles.buttonSpacing}
|
|
aria-label={'Change color scheme'}
|
|
>
|
|
<span className={cx(styles.colorDot, colorDotStyle)} />
|
|
</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): 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) {
|
|
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, app: CoreApp) => ({
|
|
header: css`
|
|
label: header;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
background: ${theme.colors.background.primary};
|
|
top: 0;
|
|
z-index: ${theme.zIndex.navbarFixed};
|
|
${app === CoreApp.Explore
|
|
? css`
|
|
position: sticky;
|
|
padding-bottom: ${theme.spacing(1)};
|
|
padding-top: ${theme.spacing(1)};
|
|
`
|
|
: ''};
|
|
`,
|
|
inputContainer: css`
|
|
label: inputContainer;
|
|
margin-right: 20px;
|
|
`,
|
|
rightContainer: css`
|
|
label: rightContainer;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
`,
|
|
buttonSpacing: css`
|
|
label: buttonSpacing;
|
|
margin-right: ${theme.spacing(1)};
|
|
`,
|
|
|
|
resetButton: css`
|
|
label: resetButton;
|
|
display: flex;
|
|
margin-right: ${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;
|
|
border-radius: 50%;
|
|
`,
|
|
colorDotByValue: css`
|
|
label: colorDotByValue;
|
|
background: ${byValueGradient};
|
|
`,
|
|
colorDotByPackage: css`
|
|
label: colorDotByPackage;
|
|
background: ${byPackageGradient};
|
|
`,
|
|
colorDotDiffDefault: css`
|
|
label: colorDotDiffDefault;
|
|
background: ${diffDefaultGradient};
|
|
`,
|
|
|
|
colorDotDiffColorBlind: css`
|
|
label: colorDotDiffColorBlind;
|
|
background: ${diffColorBlindGradient};
|
|
`,
|
|
});
|
|
|
|
export default FlameGraphHeader;
|