VizTooltip: Improved StateTimeline tooltip (#79599)
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
co-authored by
Leon Sorokin
parent
b3387793f1
commit
7eea30d0e8
@@ -172,7 +172,7 @@ export const TooltipPlugin2 = ({ config, hoverMode, render, clientZoom = false,
|
|||||||
if (pendingPinned) {
|
if (pendingPinned) {
|
||||||
_style = { pointerEvents: _isPinned ? 'all' : 'none' };
|
_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
|
// @ts-ignore
|
||||||
_plot!.cursor._lock = _isPinned;
|
_plot!.cursor._lock = _isPinned;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import uPlot, { Series } from 'uplot';
|
|||||||
|
|
||||||
import { GrafanaTheme2, TimeRange } from '@grafana/data';
|
import { GrafanaTheme2, TimeRange } from '@grafana/data';
|
||||||
import { alpha } from '@grafana/data/src/themes/colorManipulator';
|
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 { FIXED_UNIT } from '@grafana/ui';
|
||||||
import { distribute, SPACE_BETWEEN } from 'app/plugins/panel/barchart/distribute';
|
import { distribute, SPACE_BETWEEN } from 'app/plugins/panel/barchart/distribute';
|
||||||
import { pointWithin, Quadtree, Rect } from 'app/plugins/panel/barchart/quadtree';
|
import { pointWithin, Quadtree, Rect } from 'app/plugins/panel/barchart/quadtree';
|
||||||
@@ -442,11 +442,8 @@ export function getConfig(opts: TimelineCoreOptions) {
|
|||||||
return hovered[seriesIdx]?.didx;
|
return hovered[seriesIdx]?.didx;
|
||||||
},
|
},
|
||||||
focus: {
|
focus: {
|
||||||
prox: 30,
|
prox: 1e3,
|
||||||
dist: (u, seriesIdx, dataIdx, valPos, curPos) => {
|
dist: (u, seriesIdx) => (hoveredAtCursor?.sidx === seriesIdx ? 0 : Infinity),
|
||||||
valPos = yMids[seriesIdx - 1] / uPlot.pxRatio;
|
|
||||||
return valPos - curPos;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
points: {
|
points: {
|
||||||
fill: 'rgba(255,255,255,0.2)',
|
fill: 'rgba(255,255,255,0.2)',
|
||||||
|
|||||||
@@ -2,16 +2,19 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|||||||
|
|
||||||
import { CartesianCoords2D, DashboardCursorSync, DataFrame, FieldType, PanelProps } from '@grafana/data';
|
import { CartesianCoords2D, DashboardCursorSync, DataFrame, FieldType, PanelProps } from '@grafana/data';
|
||||||
import { getLastStreamingDataFramePacket } from '@grafana/data/src/dataframe/StreamingDataFrame';
|
import { getLastStreamingDataFramePacket } from '@grafana/data/src/dataframe/StreamingDataFrame';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
Portal,
|
Portal,
|
||||||
TooltipDisplayMode,
|
TooltipDisplayMode,
|
||||||
|
TooltipPlugin2,
|
||||||
UPlotConfigBuilder,
|
UPlotConfigBuilder,
|
||||||
usePanelContext,
|
usePanelContext,
|
||||||
useTheme2,
|
useTheme2,
|
||||||
VizTooltipContainer,
|
VizTooltipContainer,
|
||||||
ZoomPlugin,
|
ZoomPlugin,
|
||||||
} from '@grafana/ui';
|
} 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 { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
||||||
import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart';
|
import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart';
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +29,7 @@ import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
|
|||||||
import { getTimezones } from '../timeseries/utils';
|
import { getTimezones } from '../timeseries/utils';
|
||||||
|
|
||||||
import { StateTimelineTooltip } from './StateTimelineTooltip';
|
import { StateTimelineTooltip } from './StateTimelineTooltip';
|
||||||
|
import { StateTimelineTooltip2 } from './StateTimelineTooltip2';
|
||||||
import { Options } from './panelcfg.gen';
|
import { Options } from './panelcfg.gen';
|
||||||
|
|
||||||
const TOOLTIP_OFFSET = 10;
|
const TOOLTIP_OFFSET = 10;
|
||||||
@@ -173,10 +177,10 @@ export const StateTimelinePanel = ({
|
|||||||
{...options}
|
{...options}
|
||||||
mode={TimelineMode.Changes}
|
mode={TimelineMode.Changes}
|
||||||
>
|
>
|
||||||
{(config, alignedFrame) => {
|
{(builder, alignedFrame) => {
|
||||||
if (oldConfig.current !== config) {
|
if (oldConfig.current !== builder) {
|
||||||
oldConfig.current = addTooltipSupport({
|
oldConfig.current = addTooltipSupport({
|
||||||
config,
|
config: builder,
|
||||||
onUPlotClick,
|
onUPlotClick,
|
||||||
setFocusedSeriesIdx,
|
setFocusedSeriesIdx,
|
||||||
setFocusedPointIdx,
|
setFocusedPointIdx,
|
||||||
@@ -191,55 +195,79 @@ export const StateTimelinePanel = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ZoomPlugin config={config} onZoom={onChangeTimeRange} />
|
{config.featureToggles.newVizTooltips ? (
|
||||||
|
<>
|
||||||
<OutsideRangePlugin config={config} onChangeTimeRange={onChangeTimeRange} />
|
{options.tooltip.mode !== TooltipDisplayMode.None && (
|
||||||
|
<TooltipPlugin2
|
||||||
{data.annotations && (
|
config={builder}
|
||||||
<AnnotationsPlugin annotations={data.annotations} config={config} timeZone={timeZone} />
|
hoverMode={TooltipHoverMode.xOne}
|
||||||
)}
|
queryZoom={onChangeTimeRange}
|
||||||
|
render={(u, dataIdxs, seriesIdx, isPinned) => {
|
||||||
{enableAnnotationCreation ? (
|
return (
|
||||||
<AnnotationEditorPlugin data={alignedFrame} timeZone={timeZone} config={config}>
|
<StateTimelineTooltip2
|
||||||
{({ startAnnotating }) => {
|
data={frames ?? []}
|
||||||
if (options.tooltip.mode === TooltipDisplayMode.None) {
|
dataIdxs={dataIdxs}
|
||||||
return null;
|
alignedData={alignedFrame}
|
||||||
}
|
seriesIdx={seriesIdx}
|
||||||
|
timeZone={timeZone}
|
||||||
if (focusedPointIdx === null || (!isActive && sync && sync() === DashboardCursorSync.Crosshair)) {
|
isPinned={isPinned}
|
||||||
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>
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}),
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user