mirror of https://github.com/grafana/grafana.git
chore: adding action support
This commit is contained in:
parent
0b84ae5b54
commit
0e522362af
|
|
@ -18,9 +18,13 @@ import {
|
||||||
AnnotationQuery,
|
AnnotationQuery,
|
||||||
getSearchFilterScopedVar,
|
getSearchFilterScopedVar,
|
||||||
FieldType,
|
FieldType,
|
||||||
|
ActionType,
|
||||||
|
HttpRequestMethod,
|
||||||
|
ActionVariableType,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { DataSourceWithBackend, getBackendSrv, getGrafanaLiveSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
import { DataSourceWithBackend, getBackendSrv, getGrafanaLiveSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-restricted-paths
|
||||||
import { Scenario, TestDataDataQuery, TestDataQueryType } from './dataquery';
|
import { Scenario, TestDataDataQuery, TestDataQueryType } from './dataquery';
|
||||||
import { queryMetricTree } from './metricTree';
|
import { queryMetricTree } from './metricTree';
|
||||||
import { generateRandomEdges, generateRandomNodes, generateShowcaseData, savedNodesResponse } from './nodeGraphUtils';
|
import { generateRandomEdges, generateRandomNodes, generateShowcaseData, savedNodesResponse } from './nodeGraphUtils';
|
||||||
|
|
@ -179,17 +183,70 @@ export class TestDataDataSource extends DataSourceWithBackend<TestDataDataQuery>
|
||||||
req: DataQueryRequest<TestDataDataQuery>
|
req: DataQueryRequest<TestDataDataQuery>
|
||||||
): Observable<DataQueryResponse> {
|
): Observable<DataQueryResponse> {
|
||||||
const events = this.buildFakeAnnotationEvents(req.range, target.lines ?? 10);
|
const events = this.buildFakeAnnotationEvents(req.range, target.lines ?? 10);
|
||||||
|
|
||||||
const dataFrame = new ArrayDataFrame(events);
|
const dataFrame = new ArrayDataFrame(events);
|
||||||
dataFrame.meta = { dataTopic: DataTopic.Annotations };
|
dataFrame.meta = { dataTopic: DataTopic.Annotations };
|
||||||
if (dataFrame.fields?.[1]) {
|
if (dataFrame.fields?.[1]) {
|
||||||
dataFrame.fields[1].config = {
|
dataFrame.fields[1].config = {
|
||||||
...dataFrame.fields[1].config,
|
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
url: 'https://grafana.com',
|
url: 'https://grafana.com',
|
||||||
title: 'Annotation Data link',
|
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));
|
return of({ key: target.refId, data: [dataFrame] }).pipe(delay(100));
|
||||||
|
|
|
||||||
|
|
@ -321,6 +321,7 @@ export const CandlestickPanel = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AnnotationsPlugin2
|
<AnnotationsPlugin2
|
||||||
|
replaceVariables={replaceVariables}
|
||||||
annotations={data.annotations ?? []}
|
annotations={data.annotations ?? []}
|
||||||
config={uplotConfig}
|
config={uplotConfig}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,7 @@ export const HeatmapPanel = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AnnotationsPlugin2
|
<AnnotationsPlugin2
|
||||||
|
replaceVariables={replaceVariables}
|
||||||
annotations={data.annotations ?? []}
|
annotations={data.annotations ?? []}
|
||||||
config={builder}
|
config={builder}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,7 @@ export const StateTimelinePanel = ({
|
||||||
)}
|
)}
|
||||||
{alignedFrame.fields[0].config.custom?.axisPlacement !== AxisPlacement.Hidden && (
|
{alignedFrame.fields[0].config.custom?.axisPlacement !== AxisPlacement.Hidden && (
|
||||||
<AnnotationsPlugin2
|
<AnnotationsPlugin2
|
||||||
|
replaceVariables={replaceVariables}
|
||||||
annotations={data.annotations ?? []}
|
annotations={data.annotations ?? []}
|
||||||
config={builder}
|
config={builder}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,7 @@ export const StatusHistoryPanel = ({
|
||||||
)}
|
)}
|
||||||
{alignedFrame.fields[0].config.custom?.axisPlacement !== AxisPlacement.Hidden && (
|
{alignedFrame.fields[0].config.custom?.axisPlacement !== AxisPlacement.Hidden && (
|
||||||
<AnnotationsPlugin2
|
<AnnotationsPlugin2
|
||||||
|
replaceVariables={replaceVariables}
|
||||||
annotations={data.annotations ?? []}
|
annotations={data.annotations ?? []}
|
||||||
config={builder}
|
config={builder}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
|
|
|
||||||
|
|
@ -33,17 +33,28 @@ export const getFieldActions = (
|
||||||
const actions: Array<ActionModel<Field>> = [];
|
const actions: Array<ActionModel<Field>> = [];
|
||||||
const actionLookup = new Set<string>();
|
const actionLookup = new Set<string>();
|
||||||
|
|
||||||
const actionsModel = getActions(dataFrame, field, field.state!.scopedVars!, replaceVars, field.config.actions ?? [], {
|
if (field.state?.scopedVars) {
|
||||||
valueRowIndex: rowIndex,
|
const actionsModel = getActions(
|
||||||
});
|
dataFrame,
|
||||||
|
field,
|
||||||
|
field.state!.scopedVars!,
|
||||||
|
replaceVars,
|
||||||
|
field.config.actions ?? [],
|
||||||
|
{
|
||||||
|
valueRowIndex: rowIndex,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
actionsModel.forEach((action) => {
|
actionsModel.forEach((action) => {
|
||||||
const key = `${action.title}`;
|
const key = `${action.title}`;
|
||||||
if (!actionLookup.has(key)) {
|
if (!actionLookup.has(key)) {
|
||||||
actions.push(action);
|
actions.push(action);
|
||||||
actionLookup.add(key);
|
actionLookup.add(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('no scoped vars!', field);
|
||||||
|
}
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@ export const TimeSeriesPanel = ({
|
||||||
{!isVerticallyOriented && (
|
{!isVerticallyOriented && (
|
||||||
<>
|
<>
|
||||||
<AnnotationsPlugin2
|
<AnnotationsPlugin2
|
||||||
|
replaceVariables={replaceVariables}
|
||||||
annotations={data.annotations ?? []}
|
annotations={data.annotations ?? []}
|
||||||
config={uplotConfig}
|
config={uplotConfig}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,31 @@
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useReducer } from 'react';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
import { arrayToDataFrame, colorManipulator, DataFrame, DataTopic, Field, LinkModel } from '@grafana/data';
|
import {
|
||||||
|
ActionModel,
|
||||||
|
arrayToDataFrame,
|
||||||
|
colorManipulator,
|
||||||
|
DataFrame,
|
||||||
|
DataTopic,
|
||||||
|
Field,
|
||||||
|
InterpolateFunction,
|
||||||
|
LinkModel,
|
||||||
|
} from '@grafana/data';
|
||||||
import { TimeZone } from '@grafana/schema';
|
import { TimeZone } from '@grafana/schema';
|
||||||
import { DEFAULT_ANNOTATION_COLOR, getPortalContainer, UPlotConfigBuilder, useStyles2, useTheme2 } from '@grafana/ui';
|
import {
|
||||||
|
DEFAULT_ANNOTATION_COLOR,
|
||||||
|
getPortalContainer,
|
||||||
|
UPlotConfigBuilder,
|
||||||
|
usePanelContext,
|
||||||
|
useStyles2,
|
||||||
|
useTheme2,
|
||||||
|
} from '@grafana/ui';
|
||||||
|
|
||||||
import { getDataLinks } from '../../status-history/utils';
|
import { getDataLinks, getFieldActions } from '../../status-history/utils';
|
||||||
|
|
||||||
import { AnnotationMarker2 } from './annotations2/AnnotationMarker2';
|
import { AnnotationMarker2 } from './annotations2/AnnotationMarker2';
|
||||||
|
|
||||||
|
|
@ -26,6 +42,7 @@ interface AnnotationsPluginProps {
|
||||||
newRange: TimeRange2 | null;
|
newRange: TimeRange2 | null;
|
||||||
setNewRange: (newRage: TimeRange2 | null) => void;
|
setNewRange: (newRage: TimeRange2 | null) => void;
|
||||||
canvasRegionRendering?: boolean;
|
canvasRegionRendering?: boolean;
|
||||||
|
replaceVariables: InterpolateFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: batch by color, use Path2D objects
|
// TODO: batch by color, use Path2D objects
|
||||||
|
|
@ -64,6 +81,7 @@ export const AnnotationsPlugin2 = ({
|
||||||
config,
|
config,
|
||||||
newRange,
|
newRange,
|
||||||
setNewRange,
|
setNewRange,
|
||||||
|
replaceVariables,
|
||||||
canvasRegionRendering = true,
|
canvasRegionRendering = true,
|
||||||
}: AnnotationsPluginProps) => {
|
}: AnnotationsPluginProps) => {
|
||||||
const [plot, setPlot] = useState<uPlot>();
|
const [plot, setPlot] = useState<uPlot>();
|
||||||
|
|
@ -75,6 +93,9 @@ export const AnnotationsPlugin2 = ({
|
||||||
|
|
||||||
const [_, forceUpdate] = useReducer((x) => x + 1, 0);
|
const [_, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
|
const { canExecuteActions } = usePanelContext();
|
||||||
|
const userCanExecuteActions = useMemo(() => canExecuteActions?.() ?? false, [canExecuteActions]);
|
||||||
|
|
||||||
const annos = useMemo(() => {
|
const annos = useMemo(() => {
|
||||||
let annos = annotations.filter(
|
let annos = annotations.filter(
|
||||||
(frame) => frame.name !== 'exemplar' && frame.length > 0 && frame.fields.some((f) => f.name === 'time')
|
(frame) => frame.name !== 'exemplar' && frame.length > 0 && frame.fields.some((f) => f.name === 'time')
|
||||||
|
|
@ -113,7 +134,6 @@ export const AnnotationsPlugin2 = ({
|
||||||
annoRef.current = annos;
|
annoRef.current = annos;
|
||||||
const newRangeRef = useRef(newRange);
|
const newRangeRef = useRef(newRange);
|
||||||
newRangeRef.current = newRange;
|
newRangeRef.current = newRange;
|
||||||
|
|
||||||
const xAxisRef = useRef<HTMLDivElement>();
|
const xAxisRef = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
|
@ -248,14 +268,25 @@ export const AnnotationsPlugin2 = ({
|
||||||
// @TODO: Reset newRange after annotation is saved
|
// @TODO: Reset newRange after annotation is saved
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
let isWip = frame.meta?.custom?.isWip;
|
let isWip = frame.meta?.custom?.isWip;
|
||||||
|
const links: LinkModel[] = [];
|
||||||
|
const actions: Array<ActionModel<Field>> = [];
|
||||||
|
|
||||||
let links: LinkModel[] = [];
|
// @todo only grab links/actions from y-axis field, or from all fields?
|
||||||
frame.fields.forEach((field: Field) => {
|
frame.fields.forEach((field: Field) => {
|
||||||
|
// Get data links
|
||||||
links.push(...getDataLinks(field, i));
|
links.push(...getDataLinks(field, i));
|
||||||
|
|
||||||
|
// Get actions
|
||||||
|
if (userCanExecuteActions) {
|
||||||
|
actions.push(...getFieldActions(frame, field, replaceVariables, i));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('actions', actions, frame);
|
||||||
|
|
||||||
markers.push(
|
markers.push(
|
||||||
<AnnotationMarker2
|
<AnnotationMarker2
|
||||||
|
actions={actions}
|
||||||
links={links}
|
links={links}
|
||||||
annoIdx={i}
|
annoIdx={i}
|
||||||
annoVals={vals}
|
annoVals={vals}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { useState } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { GrafanaTheme2, LinkModel } from '@grafana/data';
|
import { ActionModel, Field, GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { TimeZone } from '@grafana/schema';
|
import { TimeZone } from '@grafana/schema';
|
||||||
import { floatingUtils, useStyles2 } from '@grafana/ui';
|
import { floatingUtils, useStyles2 } from '@grafana/ui';
|
||||||
|
|
@ -22,6 +22,7 @@ interface AnnoBoxProps {
|
||||||
exitWipEdit?: null | (() => void);
|
exitWipEdit?: null | (() => void);
|
||||||
portalRoot: HTMLElement;
|
portalRoot: HTMLElement;
|
||||||
links: LinkModel[];
|
links: LinkModel[];
|
||||||
|
actions: Array<ActionModel<Field>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATE_DEFAULT = 0;
|
const STATE_DEFAULT = 0;
|
||||||
|
|
@ -37,6 +38,7 @@ export const AnnotationMarker2 = ({
|
||||||
timeZone,
|
timeZone,
|
||||||
portalRoot,
|
portalRoot,
|
||||||
links,
|
links,
|
||||||
|
actions,
|
||||||
}: AnnoBoxProps) => {
|
}: AnnoBoxProps) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const placement = 'bottom';
|
const placement = 'bottom';
|
||||||
|
|
@ -58,6 +60,7 @@ export const AnnotationMarker2 = ({
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
onEdit={() => setState(STATE_EDITING)}
|
onEdit={() => setState(STATE_EDITING)}
|
||||||
links={links}
|
links={links}
|
||||||
|
actions={actions}
|
||||||
/>
|
/>
|
||||||
) : state === STATE_EDITING ? (
|
) : state === STATE_EDITING ? (
|
||||||
<AnnotationEditor2
|
<AnnotationEditor2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,15 @@
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, dateTimeFormat, systemDateFormats, textUtil, LinkModel } from '@grafana/data';
|
import {
|
||||||
|
GrafanaTheme2,
|
||||||
|
dateTimeFormat,
|
||||||
|
systemDateFormats,
|
||||||
|
textUtil,
|
||||||
|
LinkModel,
|
||||||
|
ActionModel,
|
||||||
|
Field,
|
||||||
|
} from '@grafana/data';
|
||||||
import { t } from '@grafana/i18n';
|
import { t } from '@grafana/i18n';
|
||||||
import { Stack, IconButton, Tag, usePanelContext, useStyles2 } from '@grafana/ui';
|
import { Stack, IconButton, Tag, usePanelContext, useStyles2 } from '@grafana/ui';
|
||||||
import { VizTooltipFooter } from '@grafana/ui/internal';
|
import { VizTooltipFooter } from '@grafana/ui/internal';
|
||||||
|
|
@ -13,11 +21,12 @@ interface Props {
|
||||||
timeZone: string;
|
timeZone: string;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
links: LinkModel[];
|
links: LinkModel[];
|
||||||
|
actions: Array<ActionModel<Field>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const retFalse = () => false;
|
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 annoId = annoVals.id?.[annoIdx];
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
@ -109,7 +118,7 @@ export const AnnotationTooltip2 = ({ annoVals, annoIdx, timeZone, onEdit, links
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<VizTooltipFooter dataLinks={links} />
|
<VizTooltipFooter dataLinks={links} actions={actions} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue