import { css, cx } from '@emotion/css'; import { useEffect, useState } from 'react'; import * as React 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 !== '' ? ( ) : null; return (
{ setLocalSearch(v.currentTarget.value); }} placeholder={'Search...'} suffix={suffix} />
{showResetButton && (
); }; 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 = ( props.onChange(ColorScheme.PackageBased)} /> props.onChange(ColorScheme.ValueBased)} /> ); // 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 = ; if (props.isDiffMode) { menu = ( props.onChange(ColorSchemeDiff.Default)} /> props.onChange(ColorSchemeDiff.DiffColorBlind)} /> ); contents = (
-100% (removed)
0%
+100% (added)
); } return ( ); } const alignOptions: Array> = [ { 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> { 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;