Tooltips: Generate data links in TooltipPlugin2 (#97818)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Adela Almasan 2024-12-17 11:48:39 -06:00 committed by GitHub
parent 8d4e5a4e09
commit 03d176fae4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 66 additions and 17 deletions

View File

@ -40,7 +40,7 @@ export const VizTooltipFooter = ({ dataLinks, actions, annotate }: VizTooltipFoo
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
{dataLinks.length > 0 && <div className={styles.dataLinks}>{renderDataLinks(dataLinks, styles)}</div>} {dataLinks?.length > 0 && <div className={styles.dataLinks}>{renderDataLinks(dataLinks, styles)}</div>}
{actions && actions.length > 0 && <div className={styles.dataLinks}>{renderActions(actions)}</div>} {actions && actions.length > 0 && <div className={styles.dataLinks}>{renderActions(actions)}</div>}
{annotate != null && ( {annotate != null && (
<div className={styles.addAnnotations}> <div className={styles.addAnnotations}>

View File

@ -4,7 +4,7 @@ import * as React from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import uPlot from 'uplot'; import uPlot from 'uplot';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2, LinkModel } from '@grafana/data';
import { DashboardCursorSync } from '@grafana/schema'; import { DashboardCursorSync } from '@grafana/schema';
import { useStyles2 } from '../../../themes'; import { useStyles2 } from '../../../themes';
@ -27,6 +27,8 @@ export const enum TooltipHoverMode {
xyOne, xyOne,
} }
type GeDataLinksCallback = (seriesIdx: number, dataIdx: number) => LinkModel[];
interface TooltipPlugin2Props { interface TooltipPlugin2Props {
config: UPlotConfigBuilder; config: UPlotConfigBuilder;
hoverMode: TooltipHoverMode; hoverMode: TooltipHoverMode;
@ -40,6 +42,7 @@ interface TooltipPlugin2Props {
clientZoom?: boolean; clientZoom?: boolean;
onSelectRange?: OnSelectRangeCallback; onSelectRange?: OnSelectRangeCallback;
getDataLinks?: GeDataLinksCallback;
render: ( render: (
u: uPlot, u: uPlot,
@ -49,7 +52,8 @@ interface TooltipPlugin2Props {
dismiss: () => void, dismiss: () => void,
// selected time range (for annotation triggering) // selected time range (for annotation triggering)
timeRange: TimeRange2 | null, timeRange: TimeRange2 | null,
viaSync: boolean viaSync: boolean,
dataLinks: LinkModel[]
) => React.ReactNode; ) => React.ReactNode;
maxWidth?: number; maxWidth?: number;
@ -102,6 +106,8 @@ const MIN_ZOOM_DIST = 5;
const maybeZoomAction = (e?: MouseEvent | null) => e != null && !e.ctrlKey && !e.metaKey; const maybeZoomAction = (e?: MouseEvent | null) => e != null && !e.ctrlKey && !e.metaKey;
const getDataLinksFallback: GeDataLinksCallback = () => [];
/** /**
* @alpha * @alpha
*/ */
@ -115,6 +121,7 @@ export const TooltipPlugin2 = ({
maxWidth, maxWidth,
syncMode = DashboardCursorSync.Off, syncMode = DashboardCursorSync.Off,
syncScope = 'global', // eventsScope syncScope = 'global', // eventsScope
getDataLinks = getDataLinksFallback,
}: TooltipPlugin2Props) => { }: TooltipPlugin2Props) => {
const domRef = useRef<HTMLDivElement>(null); const domRef = useRef<HTMLDivElement>(null);
const portalRoot = useRef<HTMLElement | null>(null); const portalRoot = useRef<HTMLElement | null>(null);
@ -131,6 +138,9 @@ export const TooltipPlugin2 = ({
const renderRef = useRef(render); const renderRef = useRef(render);
renderRef.current = render; renderRef.current = render;
const getLinksRef = useRef(getDataLinks);
getLinksRef.current = getDataLinks;
useLayoutEffect(() => { useLayoutEffect(() => {
sizeRef.current = { sizeRef.current = {
width: 0, width: 0,
@ -187,6 +197,7 @@ export const TooltipPlugin2 = ({
let seriesIdxs: Array<number | null> = plot?.cursor.idxs!.slice()!; let seriesIdxs: Array<number | null> = plot?.cursor.idxs!.slice()!;
let closestSeriesIdx: number | null = null; let closestSeriesIdx: number | null = null;
let viaSync = false; let viaSync = false;
let dataLinks: LinkModel[] = [];
let pendingRender = false; let pendingRender = false;
let pendingPinned = false; let pendingPinned = false;
@ -242,7 +253,16 @@ export const TooltipPlugin2 = ({
isHovering: _isHovering, isHovering: _isHovering,
contents: contents:
_isHovering || selectedRange != null _isHovering || selectedRange != null
? renderRef.current(_plot!, seriesIdxs, closestSeriesIdx, _isPinned, dismiss, selectedRange, viaSync) ? renderRef.current(
_plot!,
seriesIdxs,
closestSeriesIdx,
_isPinned,
dismiss,
selectedRange,
viaSync,
dataLinks
)
: null, : null,
dismiss, dismiss,
}; };
@ -257,6 +277,8 @@ export const TooltipPlugin2 = ({
_isPinned = false; _isPinned = false;
_isHovering = false; _isHovering = false;
_plot!.setCursor({ left: -10, top: -10 }); _plot!.setCursor({ left: -10, top: -10 });
dataLinks = [];
scheduleRender(prevIsPinned); scheduleRender(prevIsPinned);
}; };
@ -313,6 +335,8 @@ export const TooltipPlugin2 = ({
} }
// only pinnable tooltip is visible *and* is within proximity to series/point // only pinnable tooltip is visible *and* is within proximity to series/point
else if (_isHovering && closestSeriesIdx != null && !_isPinned) { else if (_isHovering && closestSeriesIdx != null && !_isPinned) {
dataLinks = getLinksRef.current(closestSeriesIdx!, seriesIdxs[closestSeriesIdx!]!);
setTimeout(() => { setTimeout(() => {
_isPinned = true; _isPinned = true;
scheduleRender(true); scheduleRender(true);

View File

@ -157,7 +157,10 @@ export const BarChartPanel = (props: PanelProps<Options>) => {
hoverMode={ hoverMode={
options.tooltip.mode === TooltipDisplayMode.Single ? TooltipHoverMode.xOne : TooltipHoverMode.xAll 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 ( return (
<TimeSeriesTooltip <TimeSeriesTooltip
series={vizSeries[0]} series={vizSeries[0]}
@ -169,6 +172,7 @@ export const BarChartPanel = (props: PanelProps<Options>) => {
isPinned={isPinned} isPinned={isPinned}
maxHeight={options.tooltip.maxHeight} maxHeight={options.tooltip.maxHeight}
replaceVariables={replaceVariables} replaceVariables={replaceVariables}
dataLinks={dataLinks}
/> />
); );
}} }}

View File

@ -282,7 +282,10 @@ export const CandlestickPanel = ({
clientZoom={true} clientZoom={true}
syncMode={cursorSync} syncMode={cursorSync}
syncScope={eventsScope} 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) { if (enableAnnotationCreation && timeRange2 != null) {
setNewAnnotationRange(timeRange2); setNewAnnotationRange(timeRange2);
dismiss(); dismiss();
@ -307,6 +310,7 @@ export const CandlestickPanel = ({
annotate={enableAnnotationCreation ? annotate : undefined} annotate={enableAnnotationCreation ? annotate : undefined}
maxHeight={options.tooltip.maxHeight} maxHeight={options.tooltip.maxHeight}
replaceVariables={replaceVariables} replaceVariables={replaceVariables}
dataLinks={dataLinks}
/> />
); );
}} }}

View File

@ -172,7 +172,10 @@ export const StateTimelinePanel = ({
queryZoom={onChangeTimeRange} queryZoom={onChangeTimeRange}
syncMode={cursorSync} syncMode={cursorSync}
syncScope={eventsScope} 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) { if (enableAnnotationCreation && timeRange2 != null) {
setNewAnnotationRange(timeRange2); setNewAnnotationRange(timeRange2);
dismiss(); dismiss();
@ -199,6 +202,7 @@ export const StateTimelinePanel = ({
withDuration={true} withDuration={true}
maxHeight={options.tooltip.maxHeight} maxHeight={options.tooltip.maxHeight}
replaceVariables={replaceVariables} replaceVariables={replaceVariables}
dataLinks={dataLinks}
/> />
); );
}} }}

View File

@ -11,7 +11,7 @@ import { VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types';
import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils'; import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils';
import { findNextStateIndex, fmtDuration } from 'app/core/components/TimelineChart/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 { TimeSeriesTooltipProps } from '../timeseries/TimeSeriesTooltip';
import { isTooltipScrollable } from '../timeseries/utils'; import { isTooltipScrollable } from '../timeseries/utils';
@ -32,6 +32,7 @@ export const StateTimelineTooltip2 = ({
withDuration, withDuration,
maxHeight, maxHeight,
replaceVariables, replaceVariables,
dataLinks,
}: StateTimelineTooltip2Props) => { }: StateTimelineTooltip2Props) => {
const xField = series.fields[0]; const xField = series.fields[0];
@ -70,10 +71,9 @@ export const StateTimelineTooltip2 = ({
if (isPinned && seriesIdx != null) { if (isPinned && seriesIdx != null) {
const field = series.fields[seriesIdx]; const field = series.fields[seriesIdx];
const dataIdx = dataIdxs[seriesIdx]!; const dataIdx = dataIdxs[seriesIdx]!;
const links = getDataLinks(field, dataIdx);
const actions = getFieldActions(series, field, replaceVariables!, dataIdx); const actions = getFieldActions(series, field, replaceVariables!, dataIdx);
footer = <VizTooltipFooter dataLinks={links} annotate={annotate} actions={actions} />; footer = <VizTooltipFooter dataLinks={dataLinks} annotate={annotate} actions={actions} />;
} }
const headerItem: VizTooltipItem = { const headerItem: VizTooltipItem = {

View File

@ -104,7 +104,10 @@ export const StatusHistoryPanel = ({
queryZoom={onChangeTimeRange} queryZoom={onChangeTimeRange}
syncMode={cursorSync} syncMode={cursorSync}
syncScope={eventsScope} 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) { if (enableAnnotationCreation && timeRange2 != null) {
setNewAnnotationRange(timeRange2); setNewAnnotationRange(timeRange2);
dismiss(); dismiss();
@ -131,6 +134,7 @@ export const StatusHistoryPanel = ({
withDuration={false} withDuration={false}
maxHeight={options.tooltip.maxHeight} maxHeight={options.tooltip.maxHeight}
replaceVariables={replaceVariables} replaceVariables={replaceVariables}
dataLinks={dataLinks}
/> />
); );
}} }}

View File

@ -106,7 +106,10 @@ export const TimeSeriesPanel = ({
clientZoom={true} clientZoom={true}
syncMode={cursorSync} syncMode={cursorSync}
syncScope={eventsScope} 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) { if (enableAnnotationCreation && timeRange2 != null) {
setNewAnnotationRange(timeRange2); setNewAnnotationRange(timeRange2);
dismiss(); dismiss();
@ -132,6 +135,7 @@ export const TimeSeriesPanel = ({
annotate={enableAnnotationCreation ? annotate : undefined} annotate={enableAnnotationCreation ? annotate : undefined}
maxHeight={options.tooltip.maxHeight} maxHeight={options.tooltip.maxHeight}
replaceVariables={replaceVariables} replaceVariables={replaceVariables}
dataLinks={dataLinks}
/> />
); );
}} }}

View File

@ -1,6 +1,6 @@
import { ReactNode } from 'react'; 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 { SortOrder, TooltipDisplayMode } from '@grafana/schema/dist/esm/common/common.gen';
import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent';
import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; 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 { VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types';
import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils'; 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 { fmt } from '../xychart/utils';
import { isTooltipScrollable } from './utils'; import { isTooltipScrollable } from './utils';
@ -37,6 +37,7 @@ export interface TimeSeriesTooltipProps {
maxHeight?: number; maxHeight?: number;
replaceVariables?: InterpolateFunction; replaceVariables?: InterpolateFunction;
dataLinks: LinkModel[];
} }
export const TimeSeriesTooltip = ({ export const TimeSeriesTooltip = ({
@ -50,6 +51,7 @@ export const TimeSeriesTooltip = ({
annotate, annotate,
maxHeight, maxHeight,
replaceVariables, replaceVariables,
dataLinks,
}: TimeSeriesTooltipProps) => { }: TimeSeriesTooltipProps) => {
const xField = series.fields[0]; const xField = series.fields[0];
const xVal = formattedValueToString(xField.display!(xField.values[dataIdxs[0]!])); const xVal = formattedValueToString(xField.display!(xField.values[dataIdxs[0]!]));
@ -78,10 +80,9 @@ export const TimeSeriesTooltip = ({
if (isPinned && seriesIdx != null) { if (isPinned && seriesIdx != null) {
const field = series.fields[seriesIdx]; const field = series.fields[seriesIdx];
const dataIdx = dataIdxs[seriesIdx]!; const dataIdx = dataIdxs[seriesIdx]!;
const links = getDataLinks(field, dataIdx);
const actions = getFieldActions(series, field, replaceVariables!, dataIdx); const actions = getFieldActions(series, field, replaceVariables!, dataIdx);
footer = <VizTooltipFooter dataLinks={links} actions={actions} annotate={annotate} />; footer = <VizTooltipFooter dataLinks={dataLinks} actions={actions} annotate={annotate} />;
} }
const headerItem: VizTooltipItem | null = xField.config.custom?.hideFrom?.tooltip const headerItem: VizTooltipItem | null = xField.config.custom?.hideFrom?.tooltip

View File

@ -119,7 +119,10 @@ export const TrendPanel = ({
hoverMode={ hoverMode={
options.tooltip.mode === TooltipDisplayMode.Single ? TooltipHoverMode.xOne : TooltipHoverMode.xAll 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 ( return (
<TimeSeriesTooltip <TimeSeriesTooltip
series={alignedDataFrame} series={alignedDataFrame}
@ -130,6 +133,7 @@ export const TrendPanel = ({
isPinned={isPinned} isPinned={isPinned}
maxHeight={options.tooltip.maxHeight} maxHeight={options.tooltip.maxHeight}
replaceVariables={replaceVariables} replaceVariables={replaceVariables}
dataLinks={dataLinks}
/> />
); );
}} }}