mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: interpolate variables when creating alert rule from dashboard panel (#37201)
This commit is contained in:
parent
488930dbe3
commit
3e124c854e
@ -186,7 +186,7 @@ func (s *Service) buildDSNode(dp *simple.DirectedGraph, rn *rawNode, req *Reques
|
||||
}
|
||||
|
||||
var floatIntervalMS float64
|
||||
if rawIntervalMS := rn.Query["intervalMs"]; ok {
|
||||
if rawIntervalMS, ok := rn.Query["intervalMs"]; ok {
|
||||
if floatIntervalMS, ok = rawIntervalMS.(float64); !ok {
|
||||
return nil, fmt.Errorf("expected intervalMs to be an float64, got type %T for refId %v", rawIntervalMS, rn.RefID)
|
||||
}
|
||||
@ -194,7 +194,7 @@ func (s *Service) buildDSNode(dp *simple.DirectedGraph, rn *rawNode, req *Reques
|
||||
}
|
||||
|
||||
var floatMaxDP float64
|
||||
if rawMaxDP := rn.Query["maxDataPoints"]; ok {
|
||||
if rawMaxDP, ok := rn.Query["maxDataPoints"]; ok {
|
||||
if floatMaxDP, ok = rawMaxDP.(float64); !ok {
|
||||
return nil, fmt.Errorf("expected maxDataPoints to be an float64, got type %T for refId %v", rawMaxDP, rn.RefID)
|
||||
}
|
||||
|
@ -0,0 +1,273 @@
|
||||
import React from 'react';
|
||||
import { locationService, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { render } from '@testing-library/react';
|
||||
import { PanelAlertTabContent } from './PanelAlertTabContent';
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import {
|
||||
mockDataSource,
|
||||
MockDataSourceSrv,
|
||||
mockPromAlertingRule,
|
||||
mockPromRuleGroup,
|
||||
mockPromRuleNamespace,
|
||||
mockRulerGrafanaRule,
|
||||
} from './mocks';
|
||||
import { DataSourceType } from './utils/datasource';
|
||||
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
|
||||
import { getAllDataSources } from './utils/config';
|
||||
import { fetchRules } from './api/prometheus';
|
||||
import { fetchRulerRules } from './api/ruler';
|
||||
import { Annotation } from './utils/constants';
|
||||
import { byTestId } from 'testing-library-selector';
|
||||
import { PrometheusDatasource } from 'app/plugins/datasource/prometheus/datasource';
|
||||
import { DataSourceApi } from '@grafana/data';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
|
||||
jest.mock('./api/prometheus');
|
||||
jest.mock('./api/ruler');
|
||||
jest.mock('./utils/config');
|
||||
|
||||
const dataSources = {
|
||||
prometheus: mockDataSource({
|
||||
name: 'Prometheus',
|
||||
type: DataSourceType.Prometheus,
|
||||
}),
|
||||
};
|
||||
dataSources.prometheus.meta.alerting = true;
|
||||
|
||||
const mocks = {
|
||||
getAllDataSources: typeAsJestMock(getAllDataSources),
|
||||
api: {
|
||||
fetchRules: typeAsJestMock(fetchRules),
|
||||
fetchRulerRules: typeAsJestMock(fetchRulerRules),
|
||||
},
|
||||
};
|
||||
|
||||
const renderAlertTabContent = (dashboard: DashboardModel, panel: PanelModel) => {
|
||||
const store = configureStore();
|
||||
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<Router history={locationService.getHistory()}>
|
||||
<PanelAlertTabContent dashboard={dashboard} panel={panel} />
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const rules = [
|
||||
mockPromRuleNamespace({
|
||||
name: 'default',
|
||||
groups: [
|
||||
mockPromRuleGroup({
|
||||
name: 'mygroup',
|
||||
rules: [
|
||||
mockPromAlertingRule({
|
||||
name: 'dashboardrule1',
|
||||
annotations: {
|
||||
[Annotation.dashboardUID]: '12',
|
||||
[Annotation.panelID]: '34',
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
mockPromRuleGroup({
|
||||
name: 'othergroup',
|
||||
rules: [
|
||||
mockPromAlertingRule({
|
||||
name: 'dashboardrule2',
|
||||
annotations: {
|
||||
[Annotation.dashboardUID]: '121',
|
||||
[Annotation.panelID]: '341',
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const rulerRules = {
|
||||
default: [
|
||||
{
|
||||
name: 'mygroup',
|
||||
rules: [
|
||||
mockRulerGrafanaRule(
|
||||
{
|
||||
annotations: {
|
||||
[Annotation.dashboardUID]: '12',
|
||||
[Annotation.panelID]: '34',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'dashboardrule1',
|
||||
}
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'othergroup',
|
||||
rules: [
|
||||
mockRulerGrafanaRule(
|
||||
{
|
||||
annotations: {
|
||||
[Annotation.dashboardUID]: '121',
|
||||
[Annotation.panelID]: '341',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'dashboardrule2',
|
||||
}
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const dashboard = {
|
||||
uid: '12',
|
||||
time: {
|
||||
from: 'now-6h',
|
||||
to: 'now',
|
||||
},
|
||||
meta: {
|
||||
canSave: true,
|
||||
folderId: 1,
|
||||
folderTitle: 'super folder',
|
||||
},
|
||||
} as DashboardModel;
|
||||
const panel = ({
|
||||
datasource: dataSources.prometheus.uid,
|
||||
title: 'mypanel',
|
||||
editSourceId: 34,
|
||||
targets: [
|
||||
{
|
||||
expr: 'sum(some_metric [$__interval])) by (app)',
|
||||
refId: 'A',
|
||||
},
|
||||
],
|
||||
} as any) as PanelModel;
|
||||
|
||||
const ui = {
|
||||
row: byTestId('row'),
|
||||
createButton: byTestId<HTMLAnchorElement>('create-alert-rule-button'),
|
||||
};
|
||||
|
||||
describe('PanelAlertTabContent', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
||||
const dsService = new MockDataSourceSrv(dataSources);
|
||||
dsService.datasources[dataSources.prometheus.name] = new PrometheusDatasource(
|
||||
dataSources.prometheus
|
||||
) as DataSourceApi<any, any>;
|
||||
setDataSourceSrv(dsService);
|
||||
});
|
||||
|
||||
it('Will take into account panel maxDataPoints', async () => {
|
||||
await renderAlertTabContent(dashboard, ({
|
||||
...panel,
|
||||
maxDataPoints: 100,
|
||||
interval: '10s',
|
||||
} as any) as PanelModel);
|
||||
const button = await ui.createButton.find();
|
||||
const href = button.href;
|
||||
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
|
||||
expect(match).toHaveLength(2);
|
||||
const defaults = JSON.parse(decodeURIComponent(match![1]));
|
||||
expect(defaults.queries[0].model).toEqual({
|
||||
expr: 'sum(some_metric [5m])) by (app)',
|
||||
refId: 'A',
|
||||
datasource: 'Prometheus',
|
||||
interval: '',
|
||||
intervalMs: 300000,
|
||||
maxDataPoints: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('Will take into account datasource minInterval', async () => {
|
||||
((getDatasourceSrv() as any) as MockDataSourceSrv).datasources[dataSources.prometheus.name].interval = '7m';
|
||||
|
||||
await renderAlertTabContent(dashboard, ({
|
||||
...panel,
|
||||
maxDataPoints: 100,
|
||||
} as any) as PanelModel);
|
||||
const button = await ui.createButton.find();
|
||||
const href = button.href;
|
||||
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
|
||||
expect(match).toHaveLength(2);
|
||||
const defaults = JSON.parse(decodeURIComponent(match![1]));
|
||||
expect(defaults.queries[0].model).toEqual({
|
||||
expr: 'sum(some_metric [7m])) by (app)',
|
||||
refId: 'A',
|
||||
datasource: 'Prometheus',
|
||||
interval: '',
|
||||
intervalMs: 420000,
|
||||
maxDataPoints: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('Will render alerts belonging to panel and a button to create alert from panel queries', async () => {
|
||||
mocks.api.fetchRules.mockResolvedValue(rules);
|
||||
mocks.api.fetchRulerRules.mockResolvedValue(rulerRules);
|
||||
|
||||
await renderAlertTabContent(dashboard, panel);
|
||||
|
||||
const rows = await ui.row.findAll();
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toHaveTextContent(/dashboardrule1/);
|
||||
expect(rows[0]).not.toHaveTextContent(/dashboardrule2/);
|
||||
const button = await ui.createButton.find();
|
||||
const href = button.href;
|
||||
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
|
||||
expect(match).toHaveLength(2);
|
||||
const defaults = JSON.parse(decodeURIComponent(match![1]));
|
||||
expect(defaults).toEqual({
|
||||
type: 'grafana',
|
||||
folder: { id: 1, title: 'super folder' },
|
||||
queries: [
|
||||
{
|
||||
refId: 'A',
|
||||
queryType: '',
|
||||
relativeTimeRange: { from: 21600, to: 0 },
|
||||
datasourceUid: 'mock-ds-2',
|
||||
model: {
|
||||
expr: 'sum(some_metric [15s])) by (app)',
|
||||
refId: 'A',
|
||||
datasource: 'Prometheus',
|
||||
interval: '',
|
||||
intervalMs: 15000,
|
||||
},
|
||||
},
|
||||
{
|
||||
refId: 'B',
|
||||
datasourceUid: '-100',
|
||||
queryType: '',
|
||||
model: {
|
||||
refId: 'B',
|
||||
hide: false,
|
||||
type: 'classic_conditions',
|
||||
datasource: '__expr__',
|
||||
conditions: [
|
||||
{
|
||||
type: 'query',
|
||||
evaluator: { params: [3], type: 'gt' },
|
||||
operator: { type: 'and' },
|
||||
query: { params: ['A'] },
|
||||
reducer: { params: [], type: 'last' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
name: 'mypanel',
|
||||
condition: 'B',
|
||||
annotations: [
|
||||
{ key: '__dashboardUid__', value: '12' },
|
||||
{ key: '__panelId__', value: '34' },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
@ -1,9 +1,10 @@
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import React, { FC } from 'react';
|
||||
import { Alert, LinkButton } from '@grafana/ui';
|
||||
import { Alert, LinkButton, Button } from '@grafana/ui';
|
||||
import { panelToRuleFormValues } from '../../utils/rule-form';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { urlUtil } from '@grafana/data';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
interface Props {
|
||||
panel: PanelModel;
|
||||
@ -12,8 +13,11 @@ interface Props {
|
||||
}
|
||||
|
||||
export const NewRuleFromPanelButton: FC<Props> = ({ dashboard, panel, className }) => {
|
||||
const formValues = panelToRuleFormValues(panel, dashboard);
|
||||
const { loading, value: formValues } = useAsync(() => panelToRuleFormValues(panel, dashboard), [panel, dashboard]);
|
||||
const location = useLocation();
|
||||
if (loading) {
|
||||
return <Button disabled={true}>Create alert rule from this panel</Button>;
|
||||
}
|
||||
|
||||
if (!formValues) {
|
||||
return (
|
||||
@ -29,7 +33,7 @@ export const NewRuleFromPanelButton: FC<Props> = ({ dashboard, panel, className
|
||||
});
|
||||
|
||||
return (
|
||||
<LinkButton icon="bell" href={ruleFormUrl} className={className}>
|
||||
<LinkButton icon="bell" href={ruleFormUrl} className={className} data-testid="create-alert-rule-button">
|
||||
Create alert rule from this panel
|
||||
</LinkButton>
|
||||
);
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { DataSourceApi, DataSourceInstanceSettings, DataSourcePluginMeta, ScopedVars } from '@grafana/data';
|
||||
import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto';
|
||||
import {
|
||||
GrafanaAlertStateDecision,
|
||||
GrafanaRuleDefinition,
|
||||
PromAlertingRuleState,
|
||||
PromRuleType,
|
||||
RulerGrafanaRuleDTO,
|
||||
} from 'app/types/unified-alerting-dto';
|
||||
import { AlertingRule, Alert, RecordingRule, RuleGroup, RuleNamespace } from 'app/types/unified-alerting';
|
||||
import DatasourceSrv from 'app/features/plugins/datasource_srv';
|
||||
import { DataSourceSrv, GetDataSourceListFilters } from '@grafana/runtime';
|
||||
@ -17,7 +23,7 @@ let nextDataSourceId = 1;
|
||||
export const mockDataSource = (
|
||||
partial: Partial<DataSourceInstanceSettings> = {},
|
||||
meta: Partial<DataSourcePluginMeta> = {}
|
||||
): DataSourceInstanceSettings => {
|
||||
): DataSourceInstanceSettings<any> => {
|
||||
const id = partial.id ?? nextDataSourceId++;
|
||||
|
||||
return {
|
||||
@ -54,6 +60,40 @@ export const mockPromAlert = (partial: Partial<Alert> = {}): Alert => ({
|
||||
...partial,
|
||||
});
|
||||
|
||||
export const mockRulerGrafanaRule = (
|
||||
partial: Partial<RulerGrafanaRuleDTO> = {},
|
||||
partialDef: Partial<GrafanaRuleDefinition> = {}
|
||||
): RulerGrafanaRuleDTO => {
|
||||
return {
|
||||
for: '1m',
|
||||
grafana_alert: {
|
||||
uid: '123',
|
||||
title: 'myalert',
|
||||
namespace_uid: '123',
|
||||
namespace_id: 1,
|
||||
condition: 'A',
|
||||
no_data_state: GrafanaAlertStateDecision.Alerting,
|
||||
exec_err_state: GrafanaAlertStateDecision.Alerting,
|
||||
data: [
|
||||
{
|
||||
datasourceUid: '123',
|
||||
refId: 'A',
|
||||
queryType: 'huh',
|
||||
model: {} as any,
|
||||
},
|
||||
],
|
||||
...partialDef,
|
||||
},
|
||||
annotations: {
|
||||
message: 'alert with severity "{{.warning}}}"',
|
||||
},
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
},
|
||||
...partial,
|
||||
};
|
||||
};
|
||||
|
||||
export const mockPromAlertingRule = (partial: Partial<AlertingRule> = {}): AlertingRule => {
|
||||
return {
|
||||
type: PromRuleType.Alerting,
|
||||
|
@ -1,11 +1,18 @@
|
||||
import { DataQuery, rangeUtil, RelativeTimeRange } from '@grafana/data';
|
||||
import {
|
||||
DataQuery,
|
||||
rangeUtil,
|
||||
RelativeTimeRange,
|
||||
ScopedVars,
|
||||
getDefaultRelativeTimeRange,
|
||||
TimeRange,
|
||||
IntervalValues,
|
||||
} from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import { ExpressionDatasourceID, ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource';
|
||||
import { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { RuleWithLocation } from 'app/types/unified-alerting';
|
||||
import {
|
||||
Annotations,
|
||||
@ -22,7 +29,6 @@ import { isGrafanaRulesSource } from './datasource';
|
||||
import { arrayToRecord, recordToArray } from './misc';
|
||||
import { isAlertingRulerRule, isGrafanaRulerRule } from './rules';
|
||||
import { parseInterval } from './time';
|
||||
import { getDefaultRelativeTimeRange } from '../../../../../../packages/grafana-data';
|
||||
|
||||
export const getDefaultFormValues = (): RuleFormValues =>
|
||||
Object.freeze({
|
||||
@ -193,60 +199,81 @@ const getDefaultExpression = (refId: string): AlertQuery => {
|
||||
};
|
||||
};
|
||||
|
||||
const dataQueriesToGrafanaQueries = (
|
||||
const dataQueriesToGrafanaQueries = async (
|
||||
queries: DataQuery[],
|
||||
relativeTimeRange: RelativeTimeRange,
|
||||
datasourceName?: string
|
||||
): AlertQuery[] => {
|
||||
return queries.reduce<AlertQuery[]>((queries, target) => {
|
||||
scopedVars: ScopedVars | {},
|
||||
datasourceName?: string,
|
||||
maxDataPoints?: number,
|
||||
minInterval?: string
|
||||
): Promise<AlertQuery[]> => {
|
||||
const result: AlertQuery[] = [];
|
||||
for (const target of queries) {
|
||||
const dsName = target.datasource || datasourceName;
|
||||
const datasource = await getDataSourceSrv().get(dsName);
|
||||
|
||||
const range = rangeUtil.relativeToTimeRange(relativeTimeRange);
|
||||
const { interval, intervalMs } = getIntervals(range, minInterval ?? datasource.interval, maxDataPoints);
|
||||
const queryVariables = {
|
||||
__interval: { text: interval, value: interval },
|
||||
__interval_ms: { text: intervalMs, value: intervalMs },
|
||||
...scopedVars,
|
||||
};
|
||||
const interpolatedTarget = datasource.interpolateVariablesInQueries
|
||||
? await datasource.interpolateVariablesInQueries([target], queryVariables)[0]
|
||||
: target;
|
||||
if (dsName) {
|
||||
// expressions
|
||||
if (dsName === ExpressionDatasourceID) {
|
||||
const newQuery: AlertQuery = {
|
||||
refId: target.refId,
|
||||
refId: interpolatedTarget.refId,
|
||||
queryType: '',
|
||||
relativeTimeRange,
|
||||
datasourceUid: ExpressionDatasourceUID,
|
||||
model: target,
|
||||
model: interpolatedTarget,
|
||||
};
|
||||
return [...queries, newQuery];
|
||||
result.push(newQuery);
|
||||
// queries
|
||||
} else {
|
||||
const datasource = getDataSourceSrv().getInstanceSettings(target.datasource || datasourceName);
|
||||
if (datasource && datasource.meta.alerting) {
|
||||
const datasourceSettings = getDataSourceSrv().getInstanceSettings(dsName);
|
||||
if (datasourceSettings && datasourceSettings.meta.alerting) {
|
||||
const newQuery: AlertQuery = {
|
||||
refId: target.refId,
|
||||
queryType: target.queryType ?? '',
|
||||
refId: interpolatedTarget.refId,
|
||||
queryType: interpolatedTarget.queryType ?? '',
|
||||
relativeTimeRange,
|
||||
datasourceUid: datasource.uid,
|
||||
model: target,
|
||||
datasourceUid: datasourceSettings.uid,
|
||||
model: {
|
||||
...interpolatedTarget,
|
||||
maxDataPoints,
|
||||
intervalMs,
|
||||
},
|
||||
};
|
||||
return [...queries, newQuery];
|
||||
result.push(newQuery);
|
||||
}
|
||||
}
|
||||
}
|
||||
return queries;
|
||||
}, []);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const panelToRuleFormValues = (
|
||||
export const panelToRuleFormValues = async (
|
||||
panel: PanelModel,
|
||||
dashboard: DashboardModel
|
||||
): Partial<RuleFormValues> | undefined => {
|
||||
): Promise<Partial<RuleFormValues> | undefined> => {
|
||||
const { targets } = panel;
|
||||
|
||||
// it seems if default datasource is selected, datasource=null, hah
|
||||
const datasourceName =
|
||||
panel.datasource === null ? getDatasourceSrv().getInstanceSettings('default')?.name : panel.datasource;
|
||||
|
||||
if (!panel.editSourceId || !dashboard.uid) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const relativeTimeRange = rangeUtil.timeRangeToRelative(rangeUtil.convertRawToRange(dashboard.time));
|
||||
const queries = dataQueriesToGrafanaQueries(targets, relativeTimeRange, datasourceName);
|
||||
|
||||
const queries = await dataQueriesToGrafanaQueries(
|
||||
targets,
|
||||
relativeTimeRange,
|
||||
panel.scopedVars || {},
|
||||
panel.datasource ?? undefined,
|
||||
panel.maxDataPoints ?? undefined,
|
||||
panel.interval ?? undefined
|
||||
);
|
||||
// if no alerting capable queries are found, can't create a rule
|
||||
if (!queries.length || !queries.find((query) => query.datasourceUid !== ExpressionDatasourceUID)) {
|
||||
return undefined;
|
||||
@ -269,6 +296,7 @@ export const panelToRuleFormValues = (
|
||||
: undefined,
|
||||
queries,
|
||||
name: panel.title,
|
||||
condition: queries[queries.length - 1].refId,
|
||||
annotations: [
|
||||
{
|
||||
key: Annotation.dashboardUID,
|
||||
@ -282,3 +310,17 @@ export const panelToRuleFormValues = (
|
||||
};
|
||||
return formValues;
|
||||
};
|
||||
|
||||
export function getIntervals(range: TimeRange, lowLimit?: string, resolution?: number): IntervalValues {
|
||||
if (!resolution) {
|
||||
if (lowLimit && rangeUtil.intervalToMs(lowLimit) > 1000) {
|
||||
return {
|
||||
interval: lowLimit,
|
||||
intervalMs: rangeUtil.intervalToMs(lowLimit),
|
||||
};
|
||||
}
|
||||
return { interval: '1s', intervalMs: 1000 };
|
||||
}
|
||||
|
||||
return rangeUtil.calculateInterval(range, resolution, lowLimit);
|
||||
}
|
||||
|
@ -344,7 +344,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
policy: this.templateSrv.replace(query.policy ?? '', scopedVars, 'regex'),
|
||||
};
|
||||
|
||||
if (query.rawQuery) {
|
||||
if (query.rawQuery || this.isFlux) {
|
||||
expandedQuery.query = this.templateSrv.replace(query.query ?? '', scopedVars, 'regex');
|
||||
}
|
||||
|
||||
|
@ -101,12 +101,17 @@ export enum GrafanaAlertStateDecision {
|
||||
OK = 'OK',
|
||||
}
|
||||
|
||||
interface AlertDataQuery extends DataQuery {
|
||||
maxDataPoints?: number;
|
||||
intervalMs?: number;
|
||||
}
|
||||
|
||||
export interface AlertQuery {
|
||||
refId: string;
|
||||
queryType: string;
|
||||
relativeTimeRange?: RelativeTimeRange;
|
||||
datasourceUid: string;
|
||||
model: DataQuery;
|
||||
model: AlertDataQuery;
|
||||
}
|
||||
|
||||
export interface PostableGrafanaRuleDefinition {
|
||||
|
Loading…
Reference in New Issue
Block a user