diff --git a/public/app/features/alerting/state/actions.ts b/public/app/features/alerting/state/actions.ts
index cc94c6005e6..44770313c35 100644
--- a/public/app/features/alerting/state/actions.ts
+++ b/public/app/features/alerting/state/actions.ts
@@ -1,29 +1,38 @@
-import { AppEvents, dateMath } from '@grafana/data';
+import {
+ AppEvents,
+ applyFieldOverrides,
+ arrowTableToDataFrame,
+ base64StringToArrowTable,
+ DataSourceApi,
+ dateMath,
+} from '@grafana/data';
import { config, getBackendSrv, getDataSourceSrv } from '@grafana/runtime';
import { appEvents } from 'app/core/core';
import { updateLocation } from 'app/core/actions';
import store from 'app/core/store';
import {
- notificationChannelLoaded,
+ ALERT_DEFINITION_UI_STATE_STORAGE_KEY,
+ cleanUpState,
loadAlertRules,
loadedAlertRules,
- setNotificationChannels,
- setUiState,
- ALERT_DEFINITION_UI_STATE_STORAGE_KEY,
- updateAlertDefinitionOptions,
- setQueryOptions,
- setAlertDefinitions,
+ notificationChannelLoaded,
setAlertDefinition,
+ setAlertDefinitions,
+ setInstanceData,
+ setNotificationChannels,
+ setQueryOptions,
+ setUiState,
+ updateAlertDefinitionOptions,
} from './reducers';
import {
AlertDefinition,
+ AlertDefinitionState,
AlertDefinitionUiState,
AlertRuleDTO,
NotifierDTO,
- ThunkResult,
- QueryGroupOptions,
QueryGroupDataSource,
- AlertDefinitionState,
+ QueryGroupOptions,
+ ThunkResult,
} from 'app/types';
import { ExpressionDatasourceID } from '../../expressions/ExpressionDatasource';
import { ExpressionQuery } from '../../expressions/types';
@@ -161,10 +170,12 @@ export function queryOptionsChange(queryOptions: QueryGroupOptions): ThunkResult
export function onRunQueries(): ThunkResult
{
return (dispatch, getStore) => {
- const { queryRunner, queryOptions } = getStore().alertDefinition;
+ const { queryRunner, getQueryOptions } = getStore().alertDefinition;
const timeRange = { from: 'now-1h', to: 'now' };
+ const queryOptions = getQueryOptions();
- queryRunner.run({
+ queryRunner!.run({
+ // if the queryRunner is undefined here somethings very wrong so it's ok to throw an unhandled error
timezone: 'browser',
timeRange: { from: dateMath.parse(timeRange.from)!, to: dateMath.parse(timeRange.to)!, raw: timeRange },
maxDataPoints: queryOptions.maxDataPoints ?? 100,
@@ -175,41 +186,99 @@ export function onRunQueries(): ThunkResult {
};
}
+export function evaluateAlertDefinition(): ThunkResult {
+ return async (dispatch, getStore) => {
+ const { alertDefinition } = getStore().alertDefinition;
+
+ const response: { instances: string[] } = await getBackendSrv().get(
+ `/api/alert-definitions/eval/${alertDefinition.uid}`
+ );
+
+ const handledResponse = handleBase64Response(response.instances);
+
+ dispatch(setInstanceData(handledResponse));
+ appEvents.emit(AppEvents.alertSuccess, ['Alert definition tested successfully']);
+ };
+}
+
+export function evaluateNotSavedAlertDefinition(): ThunkResult {
+ return async (dispatch, getStore) => {
+ const { alertDefinition, getQueryOptions } = getStore().alertDefinition;
+ const defaultDataSource = await getDataSourceSrv().get(null);
+
+ const response: { instances: string[] } = await getBackendSrv().post('/api/alert-definitions/eval', {
+ condition: alertDefinition.condition,
+ data: buildDataQueryModel(getQueryOptions(), defaultDataSource),
+ });
+
+ const handledResponse = handleBase64Response(response.instances);
+ dispatch(setInstanceData(handledResponse));
+ appEvents.emit(AppEvents.alertSuccess, ['Alert definition tested successfully']);
+ };
+}
+
+export function cleanUpDefinitionState(): ThunkResult {
+ return (dispatch) => {
+ dispatch(cleanUpState(undefined));
+ };
+}
+
async function buildAlertDefinition(state: AlertDefinitionState) {
- const queryOptions = state.queryOptions;
+ const queryOptions = state.getQueryOptions();
const currentAlertDefinition = state.alertDefinition;
const defaultDataSource = await getDataSourceSrv().get(null);
return {
...currentAlertDefinition,
- data: queryOptions.queries.map((query) => {
- let dataSource: QueryGroupDataSource;
- const isExpression = query.datasource === ExpressionDatasourceID;
-
- if (isExpression) {
- dataSource = { name: ExpressionDatasourceID, uid: ExpressionDatasourceID };
- } else {
- const dataSourceSetting = getDataSourceSrv().getInstanceSettings(query.datasource);
-
- dataSource = {
- name: dataSourceSetting?.name ?? defaultDataSource.name,
- uid: dataSourceSetting?.uid ?? defaultDataSource.uid,
- };
- }
-
- return {
- model: {
- ...query,
- type: isExpression ? (query as ExpressionQuery).type : query.queryType,
- datasource: dataSource.name,
- datasourceUid: dataSource.uid,
- },
- refId: query.refId,
- relativeTimeRange: {
- From: 500,
- To: 0,
- },
- };
- }),
+ data: buildDataQueryModel(queryOptions, defaultDataSource),
};
}
+
+function handleBase64Response(frames: string[]) {
+ const dataFrames = frames.map((instance) => {
+ const table = base64StringToArrowTable(instance);
+ return arrowTableToDataFrame(table);
+ });
+
+ return applyFieldOverrides({
+ data: dataFrames,
+ fieldConfig: {
+ defaults: {},
+ overrides: [],
+ },
+ replaceVariables: (value: any) => value,
+ theme: config.theme,
+ });
+}
+
+function buildDataQueryModel(queryOptions: QueryGroupOptions, defaultDataSource: DataSourceApi) {
+ return queryOptions.queries.map((query) => {
+ let dataSource: QueryGroupDataSource;
+ const isExpression = query.datasource === ExpressionDatasourceID;
+
+ if (isExpression) {
+ dataSource = { name: ExpressionDatasourceID, uid: ExpressionDatasourceID };
+ } else {
+ const dataSourceSetting = getDataSourceSrv().getInstanceSettings(query.datasource);
+
+ dataSource = {
+ name: dataSourceSetting?.name ?? defaultDataSource.name,
+ uid: dataSourceSetting?.uid ?? defaultDataSource.uid,
+ };
+ }
+
+ return {
+ model: {
+ ...query,
+ type: isExpression ? (query as ExpressionQuery).type : query.queryType,
+ datasource: dataSource.name,
+ datasourceUid: dataSource.uid,
+ },
+ refId: query.refId,
+ relativeTimeRange: {
+ From: 500,
+ To: 0,
+ },
+ };
+ });
+}
diff --git a/public/app/features/alerting/state/reducers.ts b/public/app/features/alerting/state/reducers.ts
index 79407f55ce9..36ca8e90324 100644
--- a/public/app/features/alerting/state/reducers.ts
+++ b/public/app/features/alerting/state/reducers.ts
@@ -1,5 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
-import { ApplyFieldOverrideOptions, DataTransformerConfig, dateTime, FieldColorModeId } from '@grafana/data';
+import { ApplyFieldOverrideOptions, DataFrame, DataTransformerConfig, dateTime, FieldColorModeId } from '@grafana/data';
import alertDef from './alertDef';
import {
AlertDefinition,
@@ -62,11 +62,14 @@ export const initialAlertDefinitionState: AlertDefinitionState = {
data: [],
intervalSeconds: 60,
},
- queryOptions: { maxDataPoints: 100, dataSource: { name: '-- Mixed --' }, queries: [] },
queryRunner: new PanelQueryRunner(dataConfig),
uiState: { ...store.getObject(ALERT_DEFINITION_UI_STATE_STORAGE_KEY, DEFAULT_ALERT_DEFINITION_UI_STATE) },
data: [],
alertDefinitions: [] as AlertDefinition[],
+ /* These are functions as they are mutated later on and redux toolkit will Object.freeze state so
+ * we need to store these using functions instead */
+ getInstances: () => [] as DataFrame[],
+ getQueryOptions: () => ({ maxDataPoints: 100, dataSource: { name: '-- Mixed --' }, queries: [] }),
};
function convertToAlertRule(dto: AlertRuleDTO, state: string): AlertRule {
@@ -160,37 +163,49 @@ const alertDefinitionSlice = createSlice({
initialState: initialAlertDefinitionState,
reducers: {
setAlertDefinition: (state: AlertDefinitionState, action: PayloadAction) => {
- return {
- ...state,
- alertDefinition: {
- title: action.payload.title,
- id: action.payload.id,
- uid: action.payload.uid,
- condition: action.payload.condition,
- intervalSeconds: action.payload.intervalSeconds,
- data: action.payload.data,
- description: '',
- },
- queryOptions: {
- ...state.queryOptions,
- queries: action.payload.data.map((q: AlertDefinitionQueryModel) => ({ ...q.model })),
- },
- };
+ const currentOptions = state.getQueryOptions();
+
+ state.alertDefinition.title = action.payload.title;
+ state.alertDefinition.id = action.payload.id;
+ state.alertDefinition.uid = action.payload.uid;
+ state.alertDefinition.condition = action.payload.condition;
+ state.alertDefinition.intervalSeconds = action.payload.intervalSeconds;
+ state.alertDefinition.data = action.payload.data;
+ state.alertDefinition.description = action.payload.description;
+ state.getQueryOptions = () => ({
+ ...currentOptions,
+ queries: action.payload.data.map((q: AlertDefinitionQueryModel) => ({ ...q.model })),
+ });
},
updateAlertDefinitionOptions: (state: AlertDefinitionState, action: PayloadAction>) => {
- return { ...state, alertDefinition: { ...state.alertDefinition, ...action.payload } };
+ state.alertDefinition = { ...state.alertDefinition, ...action.payload };
},
setUiState: (state: AlertDefinitionState, action: PayloadAction) => {
- return { ...state, uiState: { ...state.uiState, ...action.payload } };
+ state.uiState = { ...state.uiState, ...action.payload };
},
setQueryOptions: (state: AlertDefinitionState, action: PayloadAction) => {
- return {
- ...state,
- queryOptions: action.payload,
- };
+ state.getQueryOptions = () => action.payload;
},
setAlertDefinitions: (state: AlertDefinitionState, action: PayloadAction) => {
- return { ...state, alertDefinitions: action.payload };
+ state.alertDefinitions = action.payload;
+ },
+ setInstanceData: (state: AlertDefinitionState, action: PayloadAction) => {
+ state.getInstances = () => action.payload;
+ },
+ cleanUpState: (state: AlertDefinitionState, action: PayloadAction) => {
+ if (state.queryRunner) {
+ state.queryRunner.destroy();
+ state.queryRunner = undefined;
+ delete state.queryRunner;
+ state.queryRunner = new PanelQueryRunner(dataConfig);
+ }
+
+ state.alertDefinitions = initialAlertDefinitionState.alertDefinitions;
+ state.alertDefinition = initialAlertDefinitionState.alertDefinition;
+ state.data = initialAlertDefinitionState.data;
+ state.getInstances = initialAlertDefinitionState.getInstances;
+ state.getQueryOptions = initialAlertDefinitionState.getQueryOptions;
+ state.uiState = initialAlertDefinitionState.uiState;
},
},
});
@@ -209,6 +224,8 @@ export const {
setQueryOptions,
setAlertDefinitions,
setAlertDefinition,
+ setInstanceData,
+ cleanUpState,
} = alertDefinitionSlice.actions;
export const alertRulesReducer = alertRulesSlice.reducer;
diff --git a/public/app/types/alerting.ts b/public/app/types/alerting.ts
index 71c04156f03..f34490f2f2f 100644
--- a/public/app/types/alerting.ts
+++ b/public/app/types/alerting.ts
@@ -1,4 +1,4 @@
-import { DataQuery, PanelData, SelectableValue, TimeRange } from '@grafana/data';
+import { DataFrame, DataQuery, PanelData, SelectableValue, TimeRange } from '@grafana/data';
import { PanelQueryRunner } from '../features/query/state/PanelQueryRunner';
import { QueryGroupOptions } from './query';
import { ExpressionQuery } from '../features/expressions/types';
@@ -140,10 +140,11 @@ export interface AlertNotification {
export interface AlertDefinitionState {
uiState: AlertDefinitionUiState;
alertDefinition: AlertDefinition;
- queryOptions: QueryGroupOptions;
- queryRunner: PanelQueryRunner;
+ queryRunner?: PanelQueryRunner;
data: PanelData[];
alertDefinitions: AlertDefinition[];
+ getInstances: () => DataFrame[];
+ getQueryOptions: () => QueryGroupOptions;
}
export interface AlertDefinition {