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,
|
||||
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<TestDataDataQuery>
|
|||
req: DataQueryRequest<TestDataDataQuery>
|
||||
): Observable<DataQueryResponse> {
|
||||
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));
|
||||
|
|
|
|||
|
|
@ -321,6 +321,7 @@ export const CandlestickPanel = ({
|
|||
/>
|
||||
)}
|
||||
<AnnotationsPlugin2
|
||||
replaceVariables={replaceVariables}
|
||||
annotations={data.annotations ?? []}
|
||||
config={uplotConfig}
|
||||
timeZone={timeZone}
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ export const HeatmapPanel = ({
|
|||
/>
|
||||
)}
|
||||
<AnnotationsPlugin2
|
||||
replaceVariables={replaceVariables}
|
||||
annotations={data.annotations ?? []}
|
||||
config={builder}
|
||||
timeZone={timeZone}
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ export const StateTimelinePanel = ({
|
|||
)}
|
||||
{alignedFrame.fields[0].config.custom?.axisPlacement !== AxisPlacement.Hidden && (
|
||||
<AnnotationsPlugin2
|
||||
replaceVariables={replaceVariables}
|
||||
annotations={data.annotations ?? []}
|
||||
config={builder}
|
||||
timeZone={timeZone}
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ export const StatusHistoryPanel = ({
|
|||
)}
|
||||
{alignedFrame.fields[0].config.custom?.axisPlacement !== AxisPlacement.Hidden && (
|
||||
<AnnotationsPlugin2
|
||||
replaceVariables={replaceVariables}
|
||||
annotations={data.annotations ?? []}
|
||||
config={builder}
|
||||
timeZone={timeZone}
|
||||
|
|
|
|||
|
|
@ -33,17 +33,28 @@ export const getFieldActions = (
|
|||
const actions: Array<ActionModel<Field>> = [];
|
||||
const actionLookup = new Set<string>();
|
||||
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -191,6 +191,7 @@ export const TimeSeriesPanel = ({
|
|||
{!isVerticallyOriented && (
|
||||
<>
|
||||
<AnnotationsPlugin2
|
||||
replaceVariables={replaceVariables}
|
||||
annotations={data.annotations ?? []}
|
||||
config={uplotConfig}
|
||||
timeZone={timeZone}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,31 @@
|
|||
import { css } from '@emotion/css';
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useReducer } from 'react';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import tinycolor from 'tinycolor2';
|
||||
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 { 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';
|
||||
|
||||
|
|
@ -26,6 +42,7 @@ interface AnnotationsPluginProps {
|
|||
newRange: TimeRange2 | null;
|
||||
setNewRange: (newRage: TimeRange2 | null) => 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<uPlot>();
|
||||
|
|
@ -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<HTMLDivElement>();
|
||||
|
||||
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<ActionModel<Field>> = [];
|
||||
|
||||
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(
|
||||
<AnnotationMarker2
|
||||
actions={actions}
|
||||
links={links}
|
||||
annoIdx={i}
|
||||
annoVals={vals}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useState } from 'react';
|
|||
import * as React from 'react';
|
||||
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 { TimeZone } from '@grafana/schema';
|
||||
import { floatingUtils, useStyles2 } from '@grafana/ui';
|
||||
|
|
@ -22,6 +22,7 @@ interface AnnoBoxProps {
|
|||
exitWipEdit?: null | (() => void);
|
||||
portalRoot: HTMLElement;
|
||||
links: LinkModel[];
|
||||
actions: Array<ActionModel<Field>>;
|
||||
}
|
||||
|
||||
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 ? (
|
||||
<AnnotationEditor2
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
import { css } from '@emotion/css';
|
||||
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 { Stack, IconButton, Tag, usePanelContext, useStyles2 } from '@grafana/ui';
|
||||
import { VizTooltipFooter } from '@grafana/ui/internal';
|
||||
|
|
@ -13,11 +21,12 @@ interface Props {
|
|||
timeZone: string;
|
||||
onEdit: () => void;
|
||||
links: LinkModel[];
|
||||
actions: Array<ActionModel<Field>>;
|
||||
}
|
||||
|
||||
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
|
|||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
<VizTooltipFooter dataLinks={links} />
|
||||
<VizTooltipFooter dataLinks={links} actions={actions} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue