diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index 01e0258be9d..76cd2eca5e3 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -172,7 +172,7 @@ export const TooltipPlugin2 = ({ config, hoverMode, render, clientZoom = false, if (pendingPinned) { _style = { pointerEvents: _isPinned ? 'all' : 'none' }; - domRef.current!.closest('.react-grid-item')?.classList.toggle('context-menu-open', _isPinned); + domRef.current?.closest('.react-grid-item')?.classList.toggle('context-menu-open', _isPinned); // @ts-ignore _plot!.cursor._lock = _isPinned; diff --git a/public/app/core/components/TimelineChart/timeline.ts b/public/app/core/components/TimelineChart/timeline.ts index 6143ffe5763..442c2679a28 100644 --- a/public/app/core/components/TimelineChart/timeline.ts +++ b/public/app/core/components/TimelineChart/timeline.ts @@ -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)', diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index 28605270859..f12f6af50a8 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -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 ( <> - + {config.featureToggles.newVizTooltips ? ( + <> + {options.tooltip.mode !== TooltipDisplayMode.None && ( + { + return ( + + ); + }} + /> + )} + + ) : ( + <> + + + {data.annotations && ( + + )} - + {enableAnnotationCreation ? ( + + {({ startAnnotating }) => { + if (options.tooltip.mode === TooltipDisplayMode.None) { + return null; + } - {data.annotations && ( - - )} + if (focusedPointIdx === null || (!isActive && sync && sync() === DashboardCursorSync.Crosshair)) { + return null; + } - {enableAnnotationCreation ? ( - - {({ startAnnotating }) => { - if (options.tooltip.mode === TooltipDisplayMode.None) { - return null; - } - - if (focusedPointIdx === null || (!isActive && sync && sync() === DashboardCursorSync.Crosshair)) { - return null; - } - - return ( - - {hover && coords && focusedSeriesIdx && ( - - {renderCustomTooltip(alignedFrame, focusedSeriesIdx, focusedPointIdx, () => { - startAnnotating({ coords: { plotCanvas: coords.canvas, viewport: coords.viewport } }); - onCloseToolTip(); - })} - - )} - - ); - }} - - ) : ( - - {options.tooltip.mode !== TooltipDisplayMode.None && hover && coords && ( - - {renderCustomTooltip(alignedFrame, focusedSeriesIdx, focusedPointIdx)} - + return ( + + {hover && coords && focusedSeriesIdx && ( + + {renderCustomTooltip(alignedFrame, focusedSeriesIdx, focusedPointIdx, () => { + startAnnotating({ coords: { plotCanvas: coords.canvas, viewport: coords.viewport } }); + onCloseToolTip(); + })} + + )} + + ); + }} + + ) : ( + + {options.tooltip.mode !== TooltipDisplayMode.None && hover && coords && ( + + {renderCustomTooltip(alignedFrame, focusedSeriesIdx, focusedPointIdx)} + + )} + )} - + )} ); diff --git a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx new file mode 100644 index 00000000000..a24fd1bf2a0 --- /dev/null +++ b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx @@ -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; + 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> = 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 ( +
+ + + {isPinned && } +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + wrapper: css({ + display: 'flex', + flexDirection: 'column', + width: DEFAULT_TOOLTIP_WIDTH, + }), +});