From fcf7aa5e2f733341959755bd1032658834b07e2d Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Wed, 22 Oct 2025 03:52:08 +0200 Subject: [PATCH] Annotations: VizActions support in annotation tooltips (#112141) Co-authored-by: Leon Sorokin --- .../panel/candlestick/CandlestickPanel.tsx | 1 + .../plugins/panel/heatmap/HeatmapPanel.tsx | 1 + .../state-timeline/StateTimelinePanel.tsx | 1 + .../status-history/StatusHistoryPanel.tsx | 1 + .../app/plugins/panel/status-history/utils.ts | 27 ++++++++++--------- .../panel/timeseries/TimeSeriesPanel.tsx | 1 + .../timeseries/plugins/AnnotationsPlugin2.tsx | 18 +++++++++++-- .../annotations2/AnnotationMarker2.tsx | 14 ++++++++-- .../annotations2/AnnotationTooltip2.tsx | 7 ++--- 9 files changed, 51 insertions(+), 20 deletions(-) diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index cb17a7792fd..f66b3d554b6 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -321,6 +321,7 @@ export const CandlestickPanel = ({ /> )} )} { return links; }; -export const getAllFrameActions = (dataFrame: DataFrame) => {}; - export const getFieldActions = ( dataFrame: DataFrame, field: Field, @@ -31,19 +29,22 @@ export const getFieldActions = ( rowIndex: number ) => { const actions: Array> = []; - const actionLookup = new Set(); - const actionsModel = getActions(dataFrame, field, field.state!.scopedVars!, replaceVars, field.config.actions ?? [], { - valueRowIndex: rowIndex, - }); + if (field.state?.scopedVars) { + const actionLookup = new Set(); - actionsModel.forEach((action) => { - const key = `${action.title}`; - if (!actionLookup.has(key)) { - actions.push(action); - actionLookup.add(key); - } - }); + const actionsModel = getActions(dataFrame, field, field.state.scopedVars, replaceVars, field.config.actions ?? [], { + valueRowIndex: rowIndex, + }); + + actionsModel.forEach((action) => { + const key = `${action.title}`; + if (!actionLookup.has(key)) { + actions.push(action); + actionLookup.add(key); + } + }); + } return actions; }; diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index bc198392c05..e254fdc3e91 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -191,6 +191,7 @@ export const TimeSeriesPanel = ({ {!isVerticallyOriented && ( <> void; canvasRegionRendering?: boolean; + replaceVariables: InterpolateFunction; } // TODO: batch by color, use Path2D objects @@ -62,6 +70,7 @@ export const AnnotationsPlugin2 = ({ config, newRange, setNewRange, + replaceVariables, canvasRegionRendering = true, }: AnnotationsPluginProps) => { const [plot, setPlot] = useState(); @@ -73,6 +82,9 @@ export const AnnotationsPlugin2 = ({ const [_, forceUpdate] = useReducer((x) => x + 1, 0); + const { canExecuteActions } = usePanelContext(); + const userCanExecuteActions = canExecuteActions?.() ?? false; + const annos = useMemo(() => { let annos = annotations.filter( (frame) => frame.name !== 'exemplar' && frame.length > 0 && frame.fields.some((f) => f.name === 'time') @@ -258,6 +270,8 @@ export const AnnotationsPlugin2 = ({ key={`${frameIdx}:${i}`} exitWipEdit={isWip ? exitWipEdit : null} portalRoot={portalRoot} + canExecuteActions={userCanExecuteActions} + replaceVariables={replaceVariables} /> ); } diff --git a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx index 07ec7514b83..4b5cf62bdf7 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx @@ -5,11 +5,11 @@ import { useState } from 'react'; import * as React from 'react'; import { createPortal } from 'react-dom'; -import { DataFrame, GrafanaTheme2, LinkModel } from '@grafana/data'; +import { ActionModel, DataFrame, GrafanaTheme2, InterpolateFunction, LinkModel } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { TimeZone } from '@grafana/schema'; import { floatingUtils, useStyles2 } from '@grafana/ui'; -import { getDataLinks } from 'app/plugins/panel/status-history/utils'; +import { getDataLinks, getFieldActions } from 'app/plugins/panel/status-history/utils'; import { AnnotationEditor2 } from './AnnotationEditor2'; import { AnnotationTooltip2 } from './AnnotationTooltip2'; @@ -23,6 +23,8 @@ interface AnnoBoxProps { timeZone: TimeZone; exitWipEdit?: null | (() => void); portalRoot: HTMLElement; + canExecuteActions: boolean; + replaceVariables: InterpolateFunction; } const STATE_DEFAULT = 0; @@ -38,6 +40,8 @@ export const AnnotationMarker2 = ({ exitWipEdit, timeZone, portalRoot, + replaceVariables, + canExecuteActions, }: AnnoBoxProps) => { const styles = useStyles2(getStyles); const placement = 'bottom'; @@ -52,10 +56,15 @@ export const AnnotationMarker2 = ({ }); const links: LinkModel[] = []; + const actions: ActionModel[] = []; if (STATE_HOVERED) { frame.fields.forEach((field) => { links.push(...getDataLinks(field, annoIdx)); + + if (canExecuteActions) { + actions.push(...getFieldActions(frame, field, replaceVariables, annoIdx)); + } }); } @@ -67,6 +76,7 @@ export const AnnotationMarker2 = ({ timeZone={timeZone} onEdit={() => setState(STATE_EDITING)} links={links} + actions={actions} /> ) : state === STATE_EDITING ? ( void; links?: LinkModel[]; + actions?: ActionModel[]; } const retFalse = () => false; -export const AnnotationTooltip2 = ({ annoVals, annoIdx, timeZone, onEdit, links = [] }: Props) => { +export const AnnotationTooltip2 = ({ annoVals, annoIdx, timeZone, onEdit, links = [], actions = [] }: Props) => { const annoId = annoVals.id?.[annoIdx]; const styles = useStyles2(getStyles); @@ -110,7 +111,7 @@ export const AnnotationTooltip2 = ({ annoVals, annoIdx, timeZone, onEdit, links - {links.length > 0 && } + {(links.length > 0 || actions.length > 0) && } ); };