diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts b/public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts index 52b151d590d..22e00bc6a96 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts @@ -18,9 +18,13 @@ import { AnnotationQuery, getSearchFilterScopedVar, FieldType, + ActionType, + HttpRequestMethod, + ActionVariableType, } from '@grafana/data'; import { DataSourceWithBackend, getBackendSrv, getGrafanaLiveSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; +// eslint-disable-next-line import/no-restricted-paths import { Scenario, TestDataDataQuery, TestDataQueryType } from './dataquery'; import { queryMetricTree } from './metricTree'; import { generateRandomEdges, generateRandomNodes, generateShowcaseData, savedNodesResponse } from './nodeGraphUtils'; @@ -179,17 +183,70 @@ export class TestDataDataSource extends DataSourceWithBackend req: DataQueryRequest ): Observable { const events = this.buildFakeAnnotationEvents(req.range, target.lines ?? 10); + const dataFrame = new ArrayDataFrame(events); dataFrame.meta = { dataTopic: DataTopic.Annotations }; if (dataFrame.fields?.[1]) { dataFrame.fields[1].config = { - ...dataFrame.fields[1].config, links: [ { url: 'https://grafana.com', title: 'Annotation Data link', }, ], + // The API call doesn't actually succeed, it appears to be impossible to call an internal Grafana API from an action at this time + actions: [ + { + // type: ActionType.Infinity, + // infinity: { + // headers: [['Content-Type', 'application/json'], ['Accept', 'application/json, text/plain, */*'], ['Authorization', 'Bearer $serviceToken']], + // method: HttpRequestMethod.POST, + // url: '/api/annotations', + // body: `{"dashboardUID":"$dashboardUID","isRegion":true,"panelId":$panelID,"time":$__from,"timeEnd":$__to,"tags":["tag1","tag2"],"text":"Annotation Description"}`, + // // DatasourceUid does not interpolate variables so there's also no way to use the infinity action type with a grafana api either + // datasourceUid: '$infinityDatasourceUID', + // }, + + type: ActionType.Fetch, + fetch: { + // For some reason x-grafana-action is defined on all fetch requests but it causes all internal API calls to fail. Removing it would work, except defining it here doesn't remove the value, it appends to the existing value, so there's no way to get a fetch request to call a Grafana API within an action. + // If you copy this request as cURL from the browser and then drop into insomnia/postman and remove the `x-grafana-action` header then this call succeeds + headers: [ + ['Content-Type', 'application/json'], + ['Accept', 'application/json, text/plain, */*'], + ['Authorization', 'Bearer $serviceToken'], + ['x-grafana-action', ''], + ], + method: HttpRequestMethod.POST, + url: '/api/annotations', + body: `{"dashboardUID":"$dashboardUID","isRegion":true,"panelId":$panelID,"time":$__from,"timeEnd":$__to,"tags":["tag1","tag2"],"text":"Annotation Description"}`, + }, + variables: [ + { + key: 'dashboardUID', + name: 'dashboardUID', + type: ActionVariableType.String, + }, + { + key: 'panelID', + name: 'panelID', + type: ActionVariableType.String, + }, + { + key: 'serviceToken', + name: 'serviceToken', + type: ActionVariableType.String, + }, + // { + // key: 'infinityDatasourceUID', + // name: 'infinityDatasourceUID', + // type: ActionVariableType.String, + // } + ], + title: 'Add annotation to panel for this time range', + }, + ], + ...dataFrame.fields[1].config, }; } return of({ key: target.refId, data: [dataFrame] }).pipe(delay(100)); 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 = ({ /> )} )} > = []; const actionLookup = new Set(); - const actionsModel = getActions(dataFrame, field, field.state!.scopedVars!, replaceVars, field.config.actions ?? [], { - valueRowIndex: rowIndex, - }); + if (field.state?.scopedVars) { + 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); - } - }); + actionsModel.forEach((action) => { + const key = `${action.title}`; + if (!actionLookup.has(key)) { + actions.push(action); + actionLookup.add(key); + } + }); + } else { + console.warn('no scoped vars!', field); + } 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 @@ -64,6 +81,7 @@ export const AnnotationsPlugin2 = ({ config, newRange, setNewRange, + replaceVariables, canvasRegionRendering = true, }: AnnotationsPluginProps) => { const [plot, setPlot] = useState(); @@ -75,6 +93,9 @@ export const AnnotationsPlugin2 = ({ const [_, forceUpdate] = useReducer((x) => x + 1, 0); + const { canExecuteActions } = usePanelContext(); + const userCanExecuteActions = useMemo(() => canExecuteActions?.() ?? false, [canExecuteActions]); + const annos = useMemo(() => { let annos = annotations.filter( (frame) => frame.name !== 'exemplar' && frame.length > 0 && frame.fields.some((f) => f.name === 'time') @@ -113,7 +134,6 @@ export const AnnotationsPlugin2 = ({ annoRef.current = annos; const newRangeRef = useRef(newRange); newRangeRef.current = newRange; - const xAxisRef = useRef(); useLayoutEffect(() => { @@ -248,14 +268,25 @@ export const AnnotationsPlugin2 = ({ // @TODO: Reset newRange after annotation is saved if (isVisible) { let isWip = frame.meta?.custom?.isWip; + const links: LinkModel[] = []; + const actions: Array> = []; - let links: LinkModel[] = []; + // @todo only grab links/actions from y-axis field, or from all fields? frame.fields.forEach((field: Field) => { + // Get data links links.push(...getDataLinks(field, i)); + + // Get actions + if (userCanExecuteActions) { + actions.push(...getFieldActions(frame, field, replaceVariables, i)); + } }); + console.log('actions', actions, frame); + markers.push( void); portalRoot: HTMLElement; links: LinkModel[]; + actions: Array>; } const STATE_DEFAULT = 0; @@ -37,6 +38,7 @@ export const AnnotationMarker2 = ({ timeZone, portalRoot, links, + actions, }: AnnoBoxProps) => { const styles = useStyles2(getStyles); const placement = 'bottom'; @@ -58,6 +60,7 @@ export const AnnotationMarker2 = ({ timeZone={timeZone} onEdit={() => setState(STATE_EDITING)} links={links} + actions={actions} /> ) : state === STATE_EDITING ? ( void; links: LinkModel[]; + actions: Array>; } 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); @@ -109,7 +118,7 @@ export const AnnotationTooltip2 = ({ annoVals, annoIdx, timeZone, onEdit, links - + ); };