From 03d176fae4171968815a426351b6670e382f0b31 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:48:39 -0600 Subject: [PATCH] Tooltips: Generate data links in TooltipPlugin2 (#97818) Co-authored-by: Leon Sorokin --- .../VizTooltip/VizTooltipFooter.tsx | 2 +- .../uPlot/plugins/TooltipPlugin2.tsx | 30 +++++++++++++++++-- .../plugins/panel/barchart/BarChartPanel.tsx | 6 +++- .../panel/candlestick/CandlestickPanel.tsx | 6 +++- .../state-timeline/StateTimelinePanel.tsx | 6 +++- .../state-timeline/StateTimelineTooltip2.tsx | 6 ++-- .../status-history/StatusHistoryPanel.tsx | 6 +++- .../panel/timeseries/TimeSeriesPanel.tsx | 6 +++- .../panel/timeseries/TimeSeriesTooltip.tsx | 9 +++--- public/app/plugins/panel/trend/TrendPanel.tsx | 6 +++- 10 files changed, 66 insertions(+), 17 deletions(-) diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx index b41fb29c944..05b05f7518d 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx @@ -40,7 +40,7 @@ export const VizTooltipFooter = ({ dataLinks, actions, annotate }: VizTooltipFoo return (
- {dataLinks.length > 0 &&
{renderDataLinks(dataLinks, styles)}
} + {dataLinks?.length > 0 &&
{renderDataLinks(dataLinks, styles)}
} {actions && actions.length > 0 &&
{renderActions(actions)}
} {annotate != null && (
diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index 54b6648efef..04756f2f924 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { createPortal } from 'react-dom'; import uPlot from 'uplot'; -import { GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2, LinkModel } from '@grafana/data'; import { DashboardCursorSync } from '@grafana/schema'; import { useStyles2 } from '../../../themes'; @@ -27,6 +27,8 @@ export const enum TooltipHoverMode { xyOne, } +type GeDataLinksCallback = (seriesIdx: number, dataIdx: number) => LinkModel[]; + interface TooltipPlugin2Props { config: UPlotConfigBuilder; hoverMode: TooltipHoverMode; @@ -40,6 +42,7 @@ interface TooltipPlugin2Props { clientZoom?: boolean; onSelectRange?: OnSelectRangeCallback; + getDataLinks?: GeDataLinksCallback; render: ( u: uPlot, @@ -49,7 +52,8 @@ interface TooltipPlugin2Props { dismiss: () => void, // selected time range (for annotation triggering) timeRange: TimeRange2 | null, - viaSync: boolean + viaSync: boolean, + dataLinks: LinkModel[] ) => React.ReactNode; maxWidth?: number; @@ -102,6 +106,8 @@ const MIN_ZOOM_DIST = 5; const maybeZoomAction = (e?: MouseEvent | null) => e != null && !e.ctrlKey && !e.metaKey; +const getDataLinksFallback: GeDataLinksCallback = () => []; + /** * @alpha */ @@ -115,6 +121,7 @@ export const TooltipPlugin2 = ({ maxWidth, syncMode = DashboardCursorSync.Off, syncScope = 'global', // eventsScope + getDataLinks = getDataLinksFallback, }: TooltipPlugin2Props) => { const domRef = useRef(null); const portalRoot = useRef(null); @@ -131,6 +138,9 @@ export const TooltipPlugin2 = ({ const renderRef = useRef(render); renderRef.current = render; + const getLinksRef = useRef(getDataLinks); + getLinksRef.current = getDataLinks; + useLayoutEffect(() => { sizeRef.current = { width: 0, @@ -187,6 +197,7 @@ export const TooltipPlugin2 = ({ let seriesIdxs: Array = plot?.cursor.idxs!.slice()!; let closestSeriesIdx: number | null = null; let viaSync = false; + let dataLinks: LinkModel[] = []; let pendingRender = false; let pendingPinned = false; @@ -242,7 +253,16 @@ export const TooltipPlugin2 = ({ isHovering: _isHovering, contents: _isHovering || selectedRange != null - ? renderRef.current(_plot!, seriesIdxs, closestSeriesIdx, _isPinned, dismiss, selectedRange, viaSync) + ? renderRef.current( + _plot!, + seriesIdxs, + closestSeriesIdx, + _isPinned, + dismiss, + selectedRange, + viaSync, + dataLinks + ) : null, dismiss, }; @@ -257,6 +277,8 @@ export const TooltipPlugin2 = ({ _isPinned = false; _isHovering = false; _plot!.setCursor({ left: -10, top: -10 }); + dataLinks = []; + scheduleRender(prevIsPinned); }; @@ -313,6 +335,8 @@ export const TooltipPlugin2 = ({ } // only pinnable tooltip is visible *and* is within proximity to series/point else if (_isHovering && closestSeriesIdx != null && !_isPinned) { + dataLinks = getLinksRef.current(closestSeriesIdx!, seriesIdxs[closestSeriesIdx!]!); + setTimeout(() => { _isPinned = true; scheduleRender(true); diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index ce83432e202..5b03f0fbc50 100644 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -157,7 +157,10 @@ export const BarChartPanel = (props: PanelProps) => { hoverMode={ options.tooltip.mode === TooltipDisplayMode.Single ? TooltipHoverMode.xOne : TooltipHoverMode.xAll } - render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2) => { + getDataLinks={(seriesIdx: number, dataIdx: number) => + vizSeries[0].fields[seriesIdx]!.getLinks?.({ valueRowIndex: dataIdx }) ?? [] + } + render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync, dataLinks) => { return ( ) => { isPinned={isPinned} maxHeight={options.tooltip.maxHeight} replaceVariables={replaceVariables} + dataLinks={dataLinks} /> ); }} diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index acddd0a5727..4ed97d59456 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -282,7 +282,10 @@ export const CandlestickPanel = ({ clientZoom={true} syncMode={cursorSync} syncScope={eventsScope} - render={(u, dataIdxs, seriesIdx, isPinned = false, dismiss, timeRange2, viaSync) => { + getDataLinks={(seriesIdx: number, dataIdx: number) => + alignedFrame.fields[seriesIdx]!.getLinks?.({ valueRowIndex: dataIdx }) ?? [] + } + render={(u, dataIdxs, seriesIdx, isPinned = false, dismiss, timeRange2, viaSync, dataLinks) => { if (enableAnnotationCreation && timeRange2 != null) { setNewAnnotationRange(timeRange2); dismiss(); @@ -307,6 +310,7 @@ export const CandlestickPanel = ({ annotate={enableAnnotationCreation ? annotate : undefined} maxHeight={options.tooltip.maxHeight} replaceVariables={replaceVariables} + dataLinks={dataLinks} /> ); }} diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index df2df2c811c..81e272a6b98 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -172,7 +172,10 @@ export const StateTimelinePanel = ({ queryZoom={onChangeTimeRange} syncMode={cursorSync} syncScope={eventsScope} - render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync) => { + getDataLinks={(seriesIdx: number, dataIdx: number) => + alignedFrame.fields[seriesIdx]!.getLinks?.({ valueRowIndex: dataIdx }) ?? [] + } + render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync, dataLinks) => { if (enableAnnotationCreation && timeRange2 != null) { setNewAnnotationRange(timeRange2); dismiss(); @@ -199,6 +202,7 @@ export const StateTimelinePanel = ({ withDuration={true} maxHeight={options.tooltip.maxHeight} replaceVariables={replaceVariables} + dataLinks={dataLinks} /> ); }} diff --git a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx index d4df7d88187..a2f2665a11d 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx @@ -11,7 +11,7 @@ import { VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types'; import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils'; import { findNextStateIndex, fmtDuration } from 'app/core/components/TimelineChart/utils'; -import { getDataLinks, getFieldActions } from '../status-history/utils'; +import { getFieldActions } from '../status-history/utils'; import { TimeSeriesTooltipProps } from '../timeseries/TimeSeriesTooltip'; import { isTooltipScrollable } from '../timeseries/utils'; @@ -32,6 +32,7 @@ export const StateTimelineTooltip2 = ({ withDuration, maxHeight, replaceVariables, + dataLinks, }: StateTimelineTooltip2Props) => { const xField = series.fields[0]; @@ -70,10 +71,9 @@ export const StateTimelineTooltip2 = ({ if (isPinned && seriesIdx != null) { const field = series.fields[seriesIdx]; const dataIdx = dataIdxs[seriesIdx]!; - const links = getDataLinks(field, dataIdx); const actions = getFieldActions(series, field, replaceVariables!, dataIdx); - footer = ; + footer = ; } const headerItem: VizTooltipItem = { diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx index 5f09c96ca52..fd1164c85f3 100644 --- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx +++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx @@ -104,7 +104,10 @@ export const StatusHistoryPanel = ({ queryZoom={onChangeTimeRange} syncMode={cursorSync} syncScope={eventsScope} - render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync) => { + getDataLinks={(seriesIdx: number, dataIdx: number) => + alignedFrame.fields[seriesIdx]!.getLinks?.({ valueRowIndex: dataIdx }) ?? [] + } + render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync, dataLinks) => { if (enableAnnotationCreation && timeRange2 != null) { setNewAnnotationRange(timeRange2); dismiss(); @@ -131,6 +134,7 @@ export const StatusHistoryPanel = ({ withDuration={false} maxHeight={options.tooltip.maxHeight} replaceVariables={replaceVariables} + dataLinks={dataLinks} /> ); }} diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index fb9076f8a01..9cd2ccf97a3 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -106,7 +106,10 @@ export const TimeSeriesPanel = ({ clientZoom={true} syncMode={cursorSync} syncScope={eventsScope} - render={(u, dataIdxs, seriesIdx, isPinned = false, dismiss, timeRange2, viaSync) => { + getDataLinks={(seriesIdx: number, dataIdx: number) => + alignedFrame.fields[seriesIdx]!.getLinks?.({ valueRowIndex: dataIdx }) ?? [] + } + render={(u, dataIdxs, seriesIdx, isPinned = false, dismiss, timeRange2, viaSync, dataLinks) => { if (enableAnnotationCreation && timeRange2 != null) { setNewAnnotationRange(timeRange2); dismiss(); @@ -132,6 +135,7 @@ export const TimeSeriesPanel = ({ annotate={enableAnnotationCreation ? annotate : undefined} maxHeight={options.tooltip.maxHeight} replaceVariables={replaceVariables} + dataLinks={dataLinks} /> ); }} diff --git a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx index 5ea7e95fe31..43efd330f17 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx @@ -1,6 +1,6 @@ import { ReactNode } from 'react'; -import { DataFrame, Field, FieldType, formattedValueToString, InterpolateFunction } from '@grafana/data'; +import { DataFrame, Field, FieldType, formattedValueToString, InterpolateFunction, LinkModel } from '@grafana/data'; import { SortOrder, TooltipDisplayMode } from '@grafana/schema/dist/esm/common/common.gen'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; @@ -9,7 +9,7 @@ import { VizTooltipWrapper } from '@grafana/ui/src/components/VizTooltip/VizTool import { VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types'; import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils'; -import { getDataLinks, getFieldActions } from '../status-history/utils'; +import { getFieldActions } from '../status-history/utils'; import { fmt } from '../xychart/utils'; import { isTooltipScrollable } from './utils'; @@ -37,6 +37,7 @@ export interface TimeSeriesTooltipProps { maxHeight?: number; replaceVariables?: InterpolateFunction; + dataLinks: LinkModel[]; } export const TimeSeriesTooltip = ({ @@ -50,6 +51,7 @@ export const TimeSeriesTooltip = ({ annotate, maxHeight, replaceVariables, + dataLinks, }: TimeSeriesTooltipProps) => { const xField = series.fields[0]; const xVal = formattedValueToString(xField.display!(xField.values[dataIdxs[0]!])); @@ -78,10 +80,9 @@ export const TimeSeriesTooltip = ({ if (isPinned && seriesIdx != null) { const field = series.fields[seriesIdx]; const dataIdx = dataIdxs[seriesIdx]!; - const links = getDataLinks(field, dataIdx); const actions = getFieldActions(series, field, replaceVariables!, dataIdx); - footer = ; + footer = ; } const headerItem: VizTooltipItem | null = xField.config.custom?.hideFrom?.tooltip diff --git a/public/app/plugins/panel/trend/TrendPanel.tsx b/public/app/plugins/panel/trend/TrendPanel.tsx index b787f799d92..c9175dfb246 100644 --- a/public/app/plugins/panel/trend/TrendPanel.tsx +++ b/public/app/plugins/panel/trend/TrendPanel.tsx @@ -119,7 +119,10 @@ export const TrendPanel = ({ hoverMode={ options.tooltip.mode === TooltipDisplayMode.Single ? TooltipHoverMode.xOne : TooltipHoverMode.xAll } - render={(u, dataIdxs, seriesIdx, isPinned = false) => { + getDataLinks={(seriesIdx: number, dataIdx: number) => + alignedDataFrame.fields[seriesIdx]!.getLinks?.({ valueRowIndex: dataIdx }) ?? [] + } + render={(u, dataIdxs, seriesIdx, isPinned = false, dismiss, timeRange, viaSync, dataLinks) => { return ( ); }}