VizTooltip: Improved StateTimeline tooltip (#79599)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Adela Almasan 2023-12-29 17:38:40 -06:00 committed by GitHub
parent b3387793f1
commit 7eea30d0e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 224 additions and 59 deletions

View File

@ -172,7 +172,7 @@ export const TooltipPlugin2 = ({ config, hoverMode, render, clientZoom = false,
if (pendingPinned) {
_style = { pointerEvents: _isPinned ? 'all' : 'none' };
domRef.current!.closest<HTMLDivElement>('.react-grid-item')?.classList.toggle('context-menu-open', _isPinned);
domRef.current?.closest<HTMLDivElement>('.react-grid-item')?.classList.toggle('context-menu-open', _isPinned);
// @ts-ignore
_plot!.cursor._lock = _isPinned;

View File

@ -2,7 +2,7 @@ import uPlot, { Series } from 'uplot';
import { GrafanaTheme2, TimeRange } from '@grafana/data';
import { alpha } from '@grafana/data/src/themes/colorManipulator';
import { VisibilityMode, TimelineValueAlignment } from '@grafana/schema';
import { TimelineValueAlignment, VisibilityMode } from '@grafana/schema';
import { FIXED_UNIT } from '@grafana/ui';
import { distribute, SPACE_BETWEEN } from 'app/plugins/panel/barchart/distribute';
import { pointWithin, Quadtree, Rect } from 'app/plugins/panel/barchart/quadtree';
@ -442,11 +442,8 @@ export function getConfig(opts: TimelineCoreOptions) {
return hovered[seriesIdx]?.didx;
},
focus: {
prox: 30,
dist: (u, seriesIdx, dataIdx, valPos, curPos) => {
valPos = yMids[seriesIdx - 1] / uPlot.pxRatio;
return valPos - curPos;
},
prox: 1e3,
dist: (u, seriesIdx) => (hoveredAtCursor?.sidx === seriesIdx ? 0 : Infinity),
},
points: {
fill: 'rgba(255,255,255,0.2)',

View File

@ -2,16 +2,19 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
import { CartesianCoords2D, DashboardCursorSync, DataFrame, FieldType, PanelProps } from '@grafana/data';
import { getLastStreamingDataFramePacket } from '@grafana/data/src/dataframe/StreamingDataFrame';
import { config } from '@grafana/runtime';
import {
Portal,
TooltipDisplayMode,
TooltipPlugin2,
UPlotConfigBuilder,
usePanelContext,
useTheme2,
VizTooltipContainer,
ZoomPlugin,
} from '@grafana/ui';
import { HoverEvent, addTooltipSupport } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport';
import { addTooltipSupport, HoverEvent } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport';
import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart';
import {
@ -26,6 +29,7 @@ import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
import { getTimezones } from '../timeseries/utils';
import { StateTimelineTooltip } from './StateTimelineTooltip';
import { StateTimelineTooltip2 } from './StateTimelineTooltip2';
import { Options } from './panelcfg.gen';
const TOOLTIP_OFFSET = 10;
@ -173,10 +177,10 @@ export const StateTimelinePanel = ({
{...options}
mode={TimelineMode.Changes}
>
{(config, alignedFrame) => {
if (oldConfig.current !== config) {
{(builder, alignedFrame) => {
if (oldConfig.current !== builder) {
oldConfig.current = addTooltipSupport({
config,
config: builder,
onUPlotClick,
setFocusedSeriesIdx,
setFocusedPointIdx,
@ -191,55 +195,79 @@ export const StateTimelinePanel = ({
return (
<>
<ZoomPlugin config={config} onZoom={onChangeTimeRange} />
<OutsideRangePlugin config={config} onChangeTimeRange={onChangeTimeRange} />
{data.annotations && (
<AnnotationsPlugin annotations={data.annotations} config={config} timeZone={timeZone} />
)}
{enableAnnotationCreation ? (
<AnnotationEditorPlugin data={alignedFrame} timeZone={timeZone} config={config}>
{({ startAnnotating }) => {
if (options.tooltip.mode === TooltipDisplayMode.None) {
return null;
}
if (focusedPointIdx === null || (!isActive && sync && sync() === DashboardCursorSync.Crosshair)) {
return null;
}
return (
<Portal>
{hover && coords && focusedSeriesIdx && (
<VizTooltipContainer
position={{ x: coords.viewport.x, y: coords.viewport.y }}
offset={{ x: TOOLTIP_OFFSET, y: TOOLTIP_OFFSET }}
allowPointerEvents={isToolTipOpen.current}
>
{renderCustomTooltip(alignedFrame, focusedSeriesIdx, focusedPointIdx, () => {
startAnnotating({ coords: { plotCanvas: coords.canvas, viewport: coords.viewport } });
onCloseToolTip();
})}
</VizTooltipContainer>
)}
</Portal>
);
}}
</AnnotationEditorPlugin>
) : (
<Portal>
{options.tooltip.mode !== TooltipDisplayMode.None && hover && coords && (
<VizTooltipContainer
position={{ x: coords.viewport.x, y: coords.viewport.y }}
offset={{ x: TOOLTIP_OFFSET, y: TOOLTIP_OFFSET }}
allowPointerEvents={isToolTipOpen.current}
>
{renderCustomTooltip(alignedFrame, focusedSeriesIdx, focusedPointIdx)}
</VizTooltipContainer>
{config.featureToggles.newVizTooltips ? (
<>
{options.tooltip.mode !== TooltipDisplayMode.None && (
<TooltipPlugin2
config={builder}
hoverMode={TooltipHoverMode.xOne}
queryZoom={onChangeTimeRange}
render={(u, dataIdxs, seriesIdx, isPinned) => {
return (
<StateTimelineTooltip2
data={frames ?? []}
dataIdxs={dataIdxs}
alignedData={alignedFrame}
seriesIdx={seriesIdx}
timeZone={timeZone}
isPinned={isPinned}
/>
);
}}
/>
)}
</Portal>
</>
) : (
<>
<ZoomPlugin config={builder} onZoom={onChangeTimeRange} />
<OutsideRangePlugin config={builder} onChangeTimeRange={onChangeTimeRange} />
{data.annotations && (
<AnnotationsPlugin annotations={data.annotations} config={builder} timeZone={timeZone} />
)}
{enableAnnotationCreation ? (
<AnnotationEditorPlugin data={alignedFrame} timeZone={timeZone} config={builder}>
{({ startAnnotating }) => {
if (options.tooltip.mode === TooltipDisplayMode.None) {
return null;
}
if (focusedPointIdx === null || (!isActive && sync && sync() === DashboardCursorSync.Crosshair)) {
return null;
}
return (
<Portal>
{hover && coords && focusedSeriesIdx && (
<VizTooltipContainer
position={{ x: coords.viewport.x, y: coords.viewport.y }}
offset={{ x: TOOLTIP_OFFSET, y: TOOLTIP_OFFSET }}
allowPointerEvents={isToolTipOpen.current}
>
{renderCustomTooltip(alignedFrame, focusedSeriesIdx, focusedPointIdx, () => {
startAnnotating({ coords: { plotCanvas: coords.canvas, viewport: coords.viewport } });
onCloseToolTip();
})}
</VizTooltipContainer>
)}
</Portal>
);
}}
</AnnotationEditorPlugin>
) : (
<Portal>
{options.tooltip.mode !== TooltipDisplayMode.None && hover && coords && (
<VizTooltipContainer
position={{ x: coords.viewport.x, y: coords.viewport.y }}
offset={{ x: TOOLTIP_OFFSET, y: TOOLTIP_OFFSET }}
allowPointerEvents={isToolTipOpen.current}
>
{renderCustomTooltip(alignedFrame, focusedSeriesIdx, focusedPointIdx)}
</VizTooltipContainer>
)}
</Portal>
)}
</>
)}
</>
);

View File

@ -0,0 +1,140 @@
import { css } from '@emotion/css';
import React from 'react';
import {
DataFrame,
Field,
FieldType,
getDisplayProcessor,
getFieldDisplayName,
GrafanaTheme2,
LinkModel,
TimeZone,
} from '@grafana/data';
import { useStyles2, useTheme2 } from '@grafana/ui';
import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent';
import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter';
import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader';
import { ColorIndicator, ColorPlacement, LabelValue } from '@grafana/ui/src/components/VizTooltip/types';
import { DEFAULT_TOOLTIP_WIDTH } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { findNextStateIndex, fmtDuration } from 'app/core/components/TimelineChart/utils';
import { getDataLinks } from '../status-history/utils';
interface StateTimelineTooltip2Props {
data: DataFrame[];
alignedData: DataFrame;
dataIdxs: Array<number | null>;
seriesIdx: number | null | undefined;
isPinned: boolean;
timeZone?: TimeZone;
}
export const StateTimelineTooltip2 = ({
data,
alignedData,
dataIdxs,
seriesIdx,
timeZone,
isPinned,
}: StateTimelineTooltip2Props) => {
const styles = useStyles2(getStyles);
const theme = useTheme2();
const datapointIdx = seriesIdx != null ? dataIdxs[seriesIdx] : dataIdxs.find((idx) => idx != null);
if (datapointIdx == null || seriesIdx == null) {
return null;
}
const valueFieldsCount = data.reduce(
(acc, frame) => acc + frame.fields.filter((field) => field.type !== FieldType.time).length,
0
);
/**
* There could be a case when the tooltip shows a data from one of a multiple query and the other query finishes first
* from refreshing. This causes data to be out of sync. alignedData - 1 because Time field doesn't count.
* Render nothing in this case to prevent error.
* See https://github.com/grafana/support-escalations/issues/932
*/
if (
(!alignedData.meta?.transformations?.length && alignedData.fields.length - 1 !== valueFieldsCount) ||
!alignedData.fields[seriesIdx]
) {
return null;
}
const field = alignedData.fields[seriesIdx!];
const links: Array<LinkModel<Field>> = getDataLinks(field, datapointIdx);
const xField = alignedData.fields[0];
const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, timeZone, theme });
const dataFrameFieldIndex = field.state?.origin;
const fieldFmt = field.display || getDisplayProcessor({ field, timeZone, theme });
const value = field.values[datapointIdx!];
const display = fieldFmt(value);
const fieldDisplayName = dataFrameFieldIndex
? getFieldDisplayName(
data[dataFrameFieldIndex.frameIndex].fields[dataFrameFieldIndex.fieldIndex],
data[dataFrameFieldIndex.frameIndex],
data
)
: null;
const nextStateIdx = findNextStateIndex(field, datapointIdx!);
let nextStateTs;
if (nextStateIdx) {
nextStateTs = xField.values[nextStateIdx!];
}
const stateTs = xField.values[datapointIdx!];
let duration = nextStateTs && fmtDuration(nextStateTs - stateTs);
if (nextStateTs) {
duration = nextStateTs && fmtDuration(nextStateTs - stateTs);
}
const from = xFieldFmt(xField.values[datapointIdx!]).text;
const to = xFieldFmt(xField.values[nextStateIdx!]).text;
const getHeaderLabel = (): LabelValue => {
return {
label: '',
value: Boolean(to) ? to : from,
};
};
const getContentLabelValue = (): LabelValue[] => {
const durationEntry: LabelValue[] = duration ? [{ label: 'Duration', value: duration }] : [];
return [
{
label: fieldDisplayName ?? '',
value: display.text,
color: display.color,
colorIndicator: ColorIndicator.value,
colorPlacement: ColorPlacement.trailing,
},
...durationEntry,
];
};
return (
<div className={styles.wrapper}>
<VizTooltipHeader headerLabel={getHeaderLabel()} />
<VizTooltipContent contentLabelValue={getContentLabelValue()} />
{isPinned && <VizTooltipFooter dataLinks={links} canAnnotate={false} />}
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css({
display: 'flex',
flexDirection: 'column',
width: DEFAULT_TOOLTIP_WIDTH,
}),
});