diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go
index f88d054e0a9..e8914c0e7ff 100644
--- a/pkg/expr/nodes.go
+++ b/pkg/expr/nodes.go
@@ -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)
}
diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx
new file mode 100644
index 00000000000..f584e1d80c0
--- /dev/null
+++ b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx
@@ -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(
+
+
+
+
+
+ );
+};
+
+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('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;
+ 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' },
+ ],
+ });
+ });
+});
diff --git a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx
index fb11e355d08..234dc3c3175 100644
--- a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx
+++ b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx
@@ -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 = ({ dashboard, panel, className }) => {
- const formValues = panelToRuleFormValues(panel, dashboard);
+ const { loading, value: formValues } = useAsync(() => panelToRuleFormValues(panel, dashboard), [panel, dashboard]);
const location = useLocation();
+ if (loading) {
+ return ;
+ }
if (!formValues) {
return (
@@ -29,7 +33,7 @@ export const NewRuleFromPanelButton: FC = ({ dashboard, panel, className
});
return (
-
+
Create alert rule from this panel
);
diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts
index 70dc72ad90a..bd8c0f0bc20 100644
--- a/public/app/features/alerting/unified/mocks.ts
+++ b/public/app/features/alerting/unified/mocks.ts
@@ -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 = {},
meta: Partial = {}
-): DataSourceInstanceSettings => {
+): DataSourceInstanceSettings => {
const id = partial.id ?? nextDataSourceId++;
return {
@@ -54,6 +60,40 @@ export const mockPromAlert = (partial: Partial = {}): Alert => ({
...partial,
});
+export const mockRulerGrafanaRule = (
+ partial: Partial = {},
+ partialDef: Partial = {}
+): 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 => {
return {
type: PromRuleType.Alerting,
diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts
index e5006b801ea..5ce60e559b7 100644
--- a/public/app/features/alerting/unified/utils/rule-form.ts
+++ b/public/app/features/alerting/unified/utils/rule-form.ts
@@ -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((queries, target) => {
+ scopedVars: ScopedVars | {},
+ datasourceName?: string,
+ maxDataPoints?: number,
+ minInterval?: string
+): Promise => {
+ 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 | undefined => {
+): Promise | 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);
+}
diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts
index caa8bd0de92..abb555b050e 100644
--- a/public/app/plugins/datasource/influxdb/datasource.ts
+++ b/public/app/plugins/datasource/influxdb/datasource.ts
@@ -344,7 +344,7 @@ export default class InfluxDatasource extends DataSourceWithBackend