VizTooltips: Optimize performance (#80102)

This commit is contained in:
Leon Sorokin 2024-01-05 16:07:04 -06:00 committed by GitHub
parent 90fb6a0122
commit 200c71f5d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 69 additions and 264 deletions

View File

@ -5,12 +5,20 @@ import { LabelValue } from './types';
interface Props {
headerLabel: LabelValue;
isPinned: boolean;
}
export const HeaderLabel = ({ headerLabel }: Props) => {
export const HeaderLabel = ({ headerLabel, isPinned }: Props) => {
const { label, value, color, colorIndicator } = headerLabel;
return (
<VizTooltipRow label={label} value={value} color={color} colorIndicator={colorIndicator} marginRight={'22px'} />
<VizTooltipRow
label={label}
value={value}
color={color}
colorIndicator={colorIndicator}
marginRight={'22px'}
isPinned={isPinned}
/>
);
};

View File

@ -1,43 +0,0 @@
import React from 'react';
import { GraphSeriesValue } from '@grafana/data';
import { VizTooltipRow } from './VizTooltipRow';
import { ColorIndicator } from './types';
export interface SeriesListProps {
series: SingleSeriesProps[];
}
// Based on SeriesTable, with new styling
export const SeriesList = ({ series }: SeriesListProps) => {
return (
<>
{series.map((series, index) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const label = series.label as string;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const value = series.value as string;
return (
<VizTooltipRow
key={`${series.label}-${index}`}
label={label}
value={value}
color={series.color}
colorIndicator={ColorIndicator.series}
isActive={series.isActive}
justify={'space-between'}
/>
);
})}
</>
);
};
export interface SingleSeriesProps {
color?: string;
label?: React.ReactNode;
value?: string | GraphSeriesValue;
isActive?: boolean;
colorIndicator?: ColorIndicator;
}

View File

@ -11,9 +11,10 @@ import { LabelValue } from './types';
interface Props {
contentLabelValue: LabelValue[];
customContent?: ReactElement[];
isPinned: boolean;
}
export const VizTooltipContent = ({ contentLabelValue, customContent }: Props) => {
export const VizTooltipContent = ({ contentLabelValue, customContent, isPinned }: Props) => {
const styles = useStyles2(getStyles);
return (
@ -31,6 +32,7 @@ export const VizTooltipContent = ({ contentLabelValue, customContent }: Props) =
colorPlacement={colorPlacement}
isActive={isActive}
justify={'space-between'}
isPinned={isPinned}
/>
);
})}

View File

@ -13,14 +13,15 @@ interface Props {
headerLabel: LabelValue;
keyValuePairs?: LabelValue[];
customValueDisplay?: ReactElement | null;
isPinned: boolean;
}
export const VizTooltipHeader = ({ headerLabel, keyValuePairs, customValueDisplay }: Props) => {
export const VizTooltipHeader = ({ headerLabel, keyValuePairs, customValueDisplay, isPinned }: Props) => {
const styles = useStyles2(getStyles);
return (
<div className={styles.wrapper}>
<HeaderLabel headerLabel={headerLabel} />
{customValueDisplay || <VizTooltipHeaderLabelValue keyValuePairs={keyValuePairs} />}
<HeaderLabel headerLabel={headerLabel} isPinned={isPinned} />
{customValueDisplay || <VizTooltipHeaderLabelValue keyValuePairs={keyValuePairs} isPinned={isPinned} />}
</div>
);
};

View File

@ -5,9 +5,10 @@ import { LabelValue } from './types';
interface Props {
keyValuePairs?: LabelValue[];
isPinned: boolean;
}
export const VizTooltipHeaderLabelValue = ({ keyValuePairs }: Props) => (
export const VizTooltipHeaderLabelValue = ({ keyValuePairs, isPinned }: Props) => (
<>
{keyValuePairs?.map((keyValuePair, i) => (
<VizTooltipRow
@ -17,6 +18,7 @@ export const VizTooltipHeaderLabelValue = ({ keyValuePairs }: Props) => (
color={keyValuePair.color}
colorIndicator={keyValuePair.colorIndicator!}
justify={'space-between'}
isPinned={isPinned}
/>
))}
</>

View File

@ -13,6 +13,7 @@ interface Props extends LabelValue {
justify?: string;
isActive?: boolean; // for series list
marginRight?: string;
isPinned: boolean;
}
export const VizTooltipRow = ({
@ -24,6 +25,7 @@ export const VizTooltipRow = ({
justify = 'flex-start',
isActive = false,
marginRight = '0px',
isPinned,
}: Props) => {
const styles = useStyles2(getStyles, justify, marginRight);
@ -53,15 +55,19 @@ export const VizTooltipRow = ({
{color && colorPlacement === ColorPlacement.first && (
<VizTooltipColorIndicator color={color} colorIndicator={colorIndicator} />
)}
<Tooltip content={label} interactive={false} show={showLabelTooltip}>
<div
className={cx(styles.label, isActive && styles.activeSeries)}
onMouseEnter={onMouseEnterLabel}
onMouseLeave={onMouseLeaveLabel}
>
{label}
</div>
</Tooltip>
{!isPinned ? (
<div className={cx(styles.label, isActive && styles.activeSeries)}>{label}</div>
) : (
<Tooltip content={label} interactive={false} show={showLabelTooltip}>
<div
className={cx(styles.label, isActive && styles.activeSeries)}
onMouseEnter={onMouseEnterLabel}
onMouseLeave={onMouseLeaveLabel}
>
{label}
</div>
</Tooltip>
)}
</div>
)}
@ -69,11 +75,20 @@ export const VizTooltipRow = ({
{color && colorPlacement === ColorPlacement.leading && (
<VizTooltipColorIndicator color={color} colorIndicator={colorIndicator} />
)}
<Tooltip content={value ? value.toString() : ''} interactive={false} show={showValueTooltip}>
<div className={cx(styles.value, isActive)} onMouseEnter={onMouseEnterValue} onMouseLeave={onMouseLeaveValue}>
{value}
</div>
</Tooltip>
{!isPinned ? (
<div className={cx(styles.value, isActive)}>{value}</div>
) : (
<Tooltip content={value ? value.toString() : ''} interactive={false} show={showValueTooltip}>
<div
className={cx(styles.value, isActive)}
onMouseEnter={onMouseEnterValue}
onMouseLeave={onMouseLeaveValue}
>
{value}
</div>
</Tooltip>
)}
{color && colorPlacement === ColorPlacement.trailing && (
<>
&nbsp;
@ -86,14 +101,6 @@ export const VizTooltipRow = ({
};
const getStyles = (theme: GrafanaTheme2, justify: string, marginRight: string) => ({
wrapper: css({
display: 'flex',
flexDirection: 'column',
flex: 1,
gap: 4,
borderTop: `1px solid ${theme.colors.border.medium}`,
padding: theme.spacing(1),
}),
contentWrapper: css({
display: 'flex',
alignItems: 'center',
@ -101,9 +108,6 @@ const getStyles = (theme: GrafanaTheme2, justify: string, marginRight: string) =
flexWrap: 'wrap',
marginRight: marginRight,
}),
customContentPadding: css({
padding: `${theme.spacing(1)} 0`,
}),
label: css({
color: theme.colors.text.secondary,
fontWeight: 400,

View File

@ -382,8 +382,12 @@ const HeatmapHoverCell = ({
return (
<div className={styles.wrapper}>
<VizTooltipHeader headerLabel={getHeaderLabel()} />
<VizTooltipContent contentLabelValue={getContentLabelValue()} customContent={getCustomContent()} />
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
<VizTooltipContent
contentLabelValue={getContentLabelValue()}
customContent={getCustomContent()}
isPinned={isPinned}
/>
{isPinned && <VizTooltipFooter dataLinks={links} canAnnotate={canAnnotate} />}
</div>
);

View File

@ -180,8 +180,8 @@ export const StateTimelineTooltip2 = ({
return (
<div className={styles.wrapper}>
<VizTooltipHeader headerLabel={getHeaderLabel()} />
<VizTooltipContent contentLabelValue={getContentLabelValue()} />
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
<VizTooltipContent contentLabelValue={getContentLabelValue()} isPinned={isPinned} />
{isPinned && <VizTooltipFooter dataLinks={links} canAnnotate={false} />}
</div>
);

View File

@ -142,8 +142,8 @@ export const StatusHistoryTooltip2 = ({
return (
<div className={styles.wrapper}>
<VizTooltipHeader headerLabel={getHeaderLabel()} />
<VizTooltipContent contentLabelValue={getContentLabelValue()} />
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
<VizTooltipContent contentLabelValue={getContentLabelValue()} isPinned={isPinned} />
{isPinned && <VizTooltipFooter dataLinks={links} canAnnotate={false} />}
</div>
);

View File

@ -133,7 +133,7 @@ export const TimeSeriesTooltip = ({
const getHeaderLabel = (): LabelValue => {
return {
label: '',
label: xField.type === FieldType.time ? '' : getFieldDisplayName(xField, seriesFrame, frames),
value: xVal,
};
};
@ -145,8 +145,8 @@ export const TimeSeriesTooltip = ({
return (
<div>
<div className={styles.wrapper}>
<VizTooltipHeader headerLabel={getHeaderLabel()} />
<VizTooltipContent contentLabelValue={getContentLabelValue()} />
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
<VizTooltipContent contentLabelValue={getContentLabelValue()} isPinned={isPinned} />
{isPinned && <VizTooltipFooter dataLinks={links} canAnnotate={false} />}
</div>
</div>

View File

@ -10,9 +10,9 @@ import { preparePlotFrame } from 'app/core/components/GraphNG/utils';
import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries';
import { findFieldIndex } from 'app/features/dimensions';
import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip';
import { prepareGraphableFields, regenerateLinksSupplier } from '../timeseries/utils';
import { TrendTooltip } from './TrendTooltip';
import { Options } from './panelcfg.gen';
export const TrendPanel = ({
@ -132,13 +132,13 @@ export const TrendPanel = ({
}
render={(u, dataIdxs, seriesIdx, isPinned = false) => {
return (
<TrendTooltip
<TimeSeriesTooltip
frames={info.frames!}
data={alignedDataFrame}
mode={options.tooltip.mode}
sortOrder={options.tooltip.sort}
seriesFrame={alignedDataFrame}
dataIdxs={dataIdxs}
seriesIdx={seriesIdx}
mode={options.tooltip.mode}
sortOrder={options.tooltip.sort}
isPinned={isPinned}
/>
);

View File

@ -1,173 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import {
arrayUtils,
DashboardCursorSync,
DataFrame,
FALLBACK_COLOR,
Field,
FieldType,
formattedValueToString,
getDisplayProcessor,
getFieldDisplayName,
GrafanaTheme2,
LinkModel,
} from '@grafana/data';
import { TooltipDisplayMode, SortOrder } from '@grafana/schema';
import { SeriesTableRowProps, useStyles2, useTheme2 } from '@grafana/ui';
import { SeriesList } from '@grafana/ui/src/components/VizTooltip/SeriesList';
import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter';
import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader';
import { LabelValue } from '@grafana/ui/src/components/VizTooltip/types';
import { DEFAULT_TOOLTIP_WIDTH } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
interface TrendTooltipProps {
frames?: DataFrame[];
// aligned data frame
data: DataFrame;
// config: UPlotConfigBuilder;
mode?: TooltipDisplayMode;
sortOrder?: SortOrder;
sync?: () => DashboardCursorSync;
// hovered points
dataIdxs: Array<number | null>;
// closest/hovered series
seriesIdx: number | null;
isPinned: boolean;
}
export const TrendTooltip = ({
frames,
data,
mode = TooltipDisplayMode.Single,
sortOrder = SortOrder.None,
dataIdxs,
seriesIdx,
isPinned,
}: TrendTooltipProps) => {
const theme = useTheme2();
const styles = useStyles2(getStyles);
const xField = data.fields[0];
if (!xField) {
return null;
}
const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, theme });
let xVal = xFieldFmt(xField!.values[dataIdxs[0]!]).text;
let tooltip: React.ReactNode = null;
const links: Array<LinkModel<Field>> = [];
const linkLookup = new Set<string>();
// Single mode
if (mode === TooltipDisplayMode.Single || isPinned) {
const field = data.fields[seriesIdx!];
if (!field) {
return null;
}
const dataIdx = dataIdxs[seriesIdx!]!;
xVal = xFieldFmt(xField!.values[dataIdx]).text;
const fieldFmt = field.display || getDisplayProcessor({ field, theme });
const display = fieldFmt(field.values[dataIdx]);
if (field.getLinks) {
const v = field.values[dataIdx];
const disp = field.display ? field.display(v) : { text: `${v}`, numeric: +v };
field.getLinks({ calculatedValue: disp, valueRowIndex: dataIdx }).forEach((link) => {
const key = `${link.title}/${link.href}`;
if (!linkLookup.has(key)) {
links.push(link);
linkLookup.add(key);
}
});
}
tooltip = (
<SeriesList
series={[
{
color: display.color || FALLBACK_COLOR,
label: getFieldDisplayName(field, data, frames),
value: display ? formattedValueToString(display) : null,
},
]}
/>
);
}
if (mode === TooltipDisplayMode.Multi && !isPinned) {
let series: SeriesTableRowProps[] = [];
const frame = data;
const fields = frame.fields;
const sortIdx: unknown[] = [];
for (let i = 0; i < fields.length; i++) {
const field = frame.fields[i];
if (
!field ||
field === xField ||
field.type === FieldType.time ||
field.type !== FieldType.number ||
field.config.custom?.hideFrom?.tooltip ||
field.config.custom?.hideFrom?.viz
) {
continue;
}
const v = data.fields[i].values[dataIdxs[i]!];
const display = field.display!(v);
sortIdx.push(v);
series.push({
color: display.color || FALLBACK_COLOR,
label: field.state?.displayName ?? field.name,
value: display ? formattedValueToString(display) : null,
isActive: seriesIdx === i,
});
}
if (sortOrder !== SortOrder.None) {
// create sort reference series array, as Array.sort() mutates the original array
const sortRef = [...series];
const sortFn = arrayUtils.sortValues(sortOrder);
series.sort((a, b) => {
// get compared values indices to retrieve raw values from sortIdx
const aIdx = sortRef.indexOf(a);
const bIdx = sortRef.indexOf(b);
return sortFn(sortIdx[aIdx], sortIdx[bIdx]);
});
}
tooltip = <SeriesList series={series} />;
}
const getHeaderLabel = (): LabelValue => {
return {
label: getFieldDisplayName(xField, data),
value: xVal,
};
};
return (
<div>
<div className={styles.wrapper}>
<VizTooltipHeader headerLabel={getHeaderLabel()} customValueDisplay={tooltip} />
{isPinned && <VizTooltipFooter dataLinks={links} canAnnotate={false} />}
</div>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css({
display: 'flex',
flexDirection: 'column',
width: DEFAULT_TOOLTIP_WIDTH,
}),
});

View File

@ -120,8 +120,8 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, allSeries, dismiss,
return (
<div className={styles.wrapper}>
<VizTooltipHeader headerLabel={getHeaderLabel()} />
<VizTooltipContent contentLabelValue={getContentLabel()} />
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
<VizTooltipContent contentLabelValue={getContentLabel()} isPinned={isPinned} />
{isPinned && <VizTooltipFooter dataLinks={getLinks()} canAnnotate={false} />}
</div>
);