Alerting: Fix expression queries when coming from a panel (#114095)

* Add getDefaultExpressions tests

* Fix expression queries when building form values from a panel

* make query params empty and add some better types

* update snapshots

* fix tests

* Revert devenv changes

* update shapshot

---------

Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
Konrad Lalik
2025-11-20 08:38:49 +01:00
committed by GitHub
co-authored by Gilles De Mey
parent b0f3e531b1
commit 4e24b2977c
7 changed files with 246 additions and 139 deletions
@@ -52,9 +52,7 @@ exports[`PanelAlertTabContent Will render alerts belonging to panel and a button
"type": "and",
},
"query": {
"params": [
"B",
],
"params": [],
},
"reducer": {
"params": [],
@@ -90,9 +88,7 @@ exports[`PanelAlertTabContent Will render alerts belonging to panel and a button
"type": "and",
},
"query": {
"params": [
"C",
],
"params": [],
},
"reducer": {
"params": [],
@@ -78,9 +78,7 @@ exports[`RuleEditor grafana recording rules can create new grafana recording rul
"type": "and",
},
"query": {
"params": [
"B",
],
"params": [],
},
"reducer": {
"params": [],
@@ -208,9 +206,7 @@ exports[`RuleEditor grafana recording rules can create new grafana recording rul
"type": "and",
},
"query": {
"params": [
"B",
],
"params": [],
},
"reducer": {
"params": [],
@@ -79,9 +79,7 @@ exports[`Can create a new grafana managed alert using simplified routing can cre
"type": "and",
},
"query": {
"params": [
"B",
],
"params": [],
},
"reducer": {
"params": [],
@@ -117,9 +115,7 @@ exports[`Can create a new grafana managed alert using simplified routing can cre
"type": "and",
},
"query": {
"params": [
"C",
],
"params": [],
},
"reducer": {
"params": [],
@@ -249,9 +245,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch
"type": "and",
},
"query": {
"params": [
"B",
],
"params": [],
},
"reducer": {
"params": [],
@@ -287,9 +281,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch
"type": "and",
},
"query": {
"params": [
"C",
],
"params": [],
},
"reducer": {
"params": [],
@@ -422,9 +414,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch
"type": "and",
},
"query": {
"params": [
"B",
],
"params": [],
},
"reducer": {
"params": [],
@@ -460,9 +450,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch
"type": "and",
},
"query": {
"params": [
"C",
],
"params": [],
},
"reducer": {
"params": [],
@@ -598,9 +586,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch
"type": "and",
},
"query": {
"params": [
"B",
],
"params": [],
},
"reducer": {
"params": [],
@@ -636,9 +622,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch
"type": "and",
},
"query": {
"params": [
"C",
],
"params": [],
},
"reducer": {
"params": [],
@@ -771,9 +755,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch
"type": "and",
},
"query": {
"params": [
"B",
],
"params": [],
},
"reducer": {
"params": [],
@@ -809,9 +791,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch
"type": "and",
},
"query": {
"params": [
"C",
],
"params": [],
},
"reducer": {
"params": [],
@@ -81,9 +81,7 @@ exports[`RuleEditor grafana managed rules can create new grafana managed alert 1
"type": "and",
},
"query": {
"params": [
"B",
],
"params": [],
},
"reducer": {
"params": [],
@@ -119,9 +117,7 @@ exports[`RuleEditor grafana managed rules can create new grafana managed alert 1
"type": "and",
},
"query": {
"params": [
"C",
],
"params": [],
},
"reducer": {
"params": [],
@@ -1,4 +1,5 @@
import { PromQuery } from '@grafana/prometheus';
import { ExpressionDatasourceUID, ExpressionQueryType } from 'app/features/expressions/types';
import { RuleWithLocation } from 'app/types/unified-alerting';
import {
AlertDataQuery,
@@ -8,6 +9,7 @@ import {
RulerAlertingRuleDTO,
} from 'app/types/unified-alerting-dto';
import { EvalFunction } from '../../state/alertDef';
import { mockDataSource, mockRuleWithLocation, mockRulerGrafanaRecordingRule } from '../mocks';
import { getDefaultFormValues } from '../rule-editor/formDefaults';
import { setupDataSources } from '../testSetup/datasources';
@@ -21,6 +23,7 @@ import {
formValuesToRulerGrafanaRuleDTO,
formValuesToRulerRuleDTO,
getContactPointsFromDTO,
getDefaultExpressions,
getInstantFromDataQuery,
getNotificationSettingsForDTO,
rulerRuleToFormValues,
@@ -417,3 +420,103 @@ describe('getInstantFromDataQuery', () => {
expect(result).toBe(false);
});
});
describe('getDefaultExpressions', () => {
it('should create a reduce expression as the first query', () => {
const result = getDefaultExpressions('B', 'C');
const reduceQuery = result[0];
const model = reduceQuery.model;
expect(reduceQuery.refId).toBe('B');
expect(reduceQuery.datasourceUid).toBe(ExpressionDatasourceUID);
expect(reduceQuery.queryType).toBe('');
expect(model.type).toBe(ExpressionQueryType.reduce);
expect(model.datasource?.uid).toBe(ExpressionDatasourceUID);
expect(model.reducer).toBe('last');
});
it('should create reduce expression with proper conditions structure', () => {
const result = getDefaultExpressions('B', 'C');
const reduceQuery = result[0];
const model = reduceQuery.model;
expect(model.conditions).toHaveLength(1);
expect(model.expression).toBe('A');
expect(model.conditions?.[0]).toEqual({
type: 'query',
evaluator: {
params: [],
type: EvalFunction.IsAbove,
},
operator: {
type: 'and',
},
query: {
params: [],
},
reducer: {
params: [],
type: 'last',
},
});
});
it('should create a threshold expression as the second query', () => {
const result = getDefaultExpressions('B', 'C');
const thresholdQuery = result[1];
const model = thresholdQuery.model;
expect(thresholdQuery.refId).toBe('C');
expect(thresholdQuery.datasourceUid).toBe(ExpressionDatasourceUID);
expect(thresholdQuery.queryType).toBe('');
expect(model.type).toBe(ExpressionQueryType.threshold);
expect(model.datasource?.uid).toBe(ExpressionDatasourceUID);
});
it('should create threshold expression with proper conditions structure', () => {
const result = getDefaultExpressions('B', 'C');
const thresholdQuery = result[1];
const model = thresholdQuery.model;
expect(model.conditions).toHaveLength(1);
expect(model.conditions?.[0]).toEqual({
type: 'query',
evaluator: {
params: [0],
type: EvalFunction.IsAbove,
},
operator: {
type: 'and',
},
query: {
params: [],
},
reducer: {
params: [],
type: 'last',
},
});
});
it('should reference the reduce expression in the threshold expression', () => {
const result = getDefaultExpressions('B', 'C');
const thresholdQuery = result[1];
const model = thresholdQuery.model;
expect(model.expression).toBe('B');
});
it('should properly use different refIds throughout the structure', () => {
const result = getDefaultExpressions('X', 'Y');
const reduceModel = result[0].model;
const thresholdModel = result[1].model;
expect(result[0].refId).toBe('X');
expect(reduceModel.refId).toBe('X');
expect(reduceModel.conditions?.[0].query.params).toEqual([]);
expect(result[1].refId).toBe('Y');
expect(thresholdModel.refId).toBe('Y');
expect(thresholdModel.expression).toBe('X');
});
});
@@ -536,85 +536,18 @@ export const getDefaultRecordingRulesQueries = (
},
];
};
const getDefaultExpressions = (...refIds: [string, string]): AlertQuery[] => {
export const getDefaultExpressions = (...refIds: [string, string]) => {
const refOne = refIds[0];
const refTwo = refIds[1];
const reduceExpression: ExpressionQuery = {
refId: refIds[0],
type: ExpressionQueryType.reduce,
datasource: {
uid: ExpressionDatasourceUID,
type: ExpressionDatasourceRef.type,
},
conditions: [
{
type: 'query',
evaluator: {
params: [],
type: EvalFunction.IsAbove,
},
operator: {
type: 'and',
},
query: {
params: [refOne],
},
reducer: {
params: [],
type: 'last',
},
},
],
reducer: 'last',
expression: 'A',
};
const reduceQuery = getDefaultReduceExpression({ inputRefId: 'A', reduceRefId: refOne });
const thresholdQuery = getDefaultThresholdExpression({ inputRefId: refOne, thresholdRefId: refTwo });
const thresholdExpression: ExpressionQuery = {
refId: refTwo,
type: ExpressionQueryType.threshold,
datasource: {
uid: ExpressionDatasourceUID,
type: ExpressionDatasourceRef.type,
},
conditions: [
{
type: 'query',
evaluator: {
params: [0],
type: EvalFunction.IsAbove,
},
operator: {
type: 'and',
},
query: {
params: [refTwo],
},
reducer: {
params: [],
type: 'last',
},
},
],
expression: refOne,
};
return [
{
refId: refOne,
datasourceUid: ExpressionDatasourceUID,
queryType: '',
model: reduceExpression,
},
{
refId: refTwo,
datasourceUid: ExpressionDatasourceUID,
queryType: '',
model: thresholdExpression,
},
];
return [reduceQuery, thresholdQuery] as const;
};
const getDefaultExpressionsForRecording = (refOne: string): AlertQuery[] => {
const getDefaultExpressionsForRecording = (refOne: string): Array<AlertQuery<ExpressionQuery>> => {
const reduceExpression: ExpressionQuery = {
refId: refOne,
type: ExpressionQueryType.reduce,
@@ -633,7 +566,7 @@ const getDefaultExpressionsForRecording = (refOne: string): AlertQuery[] => {
type: 'and',
},
query: {
params: [refOne],
params: [],
},
reducer: {
params: [],
@@ -655,6 +588,95 @@ const getDefaultExpressionsForRecording = (refOne: string): AlertQuery[] => {
];
};
function getDefaultReduceExpression({
inputRefId,
reduceRefId,
}: {
inputRefId: string;
reduceRefId: string;
}): AlertQuery<ExpressionQuery> {
const reduceExpression: ExpressionQuery = {
refId: reduceRefId,
type: ExpressionQueryType.reduce,
datasource: {
uid: ExpressionDatasourceUID,
type: ExpressionDatasourceRef.type,
},
conditions: [
{
type: 'query',
evaluator: {
params: [],
type: EvalFunction.IsAbove,
},
operator: {
type: 'and',
},
query: {
params: [],
},
reducer: {
params: [],
type: 'last',
},
},
],
reducer: 'last',
expression: inputRefId,
};
return {
refId: reduceRefId,
datasourceUid: ExpressionDatasourceUID,
queryType: '',
model: reduceExpression,
};
}
function getDefaultThresholdExpression({
inputRefId,
thresholdRefId,
}: {
inputRefId: string;
thresholdRefId: string;
}): AlertQuery<ExpressionQuery> {
const thresholdExpression: ExpressionQuery = {
refId: thresholdRefId,
type: ExpressionQueryType.threshold,
datasource: {
uid: ExpressionDatasourceUID,
type: ExpressionDatasourceRef.type,
},
conditions: [
{
type: 'query',
evaluator: {
params: [0],
type: EvalFunction.IsAbove,
},
operator: {
type: 'and',
},
query: {
params: [],
},
reducer: {
params: [],
type: 'last',
},
},
],
expression: inputRefId,
};
return {
refId: thresholdRefId,
datasourceUid: ExpressionDatasourceUID,
queryType: '',
model: thresholdExpression,
};
}
const dataQueriesToGrafanaQueries = async (
queries: DataQuery[],
relativeTimeRange: RelativeTimeRange,
@@ -736,14 +758,23 @@ export const panelToRuleFormValues = async (
return undefined;
}
const lastQuery = queries.at(-1);
if (!lastQuery) {
return undefined;
}
if (!queries.find((query) => query.datasourceUid === ExpressionDatasourceUID)) {
const [reduceExpression, _thresholdExpression] = getDefaultExpressions(getNextRefId(queries), '-');
const reduceExpression = getDefaultReduceExpression({
inputRefId: lastQuery.refId,
reduceRefId: getNextRefId(queries),
});
queries.push(reduceExpression);
const [_reduceExpression, thresholdExpression] = getDefaultExpressions(
reduceExpression.refId,
getNextRefId(queries)
);
const thresholdExpression = getDefaultThresholdExpression({
inputRefId: reduceExpression.refId,
thresholdRefId: getNextRefId(queries),
});
queries.push(thresholdExpression);
}
@@ -808,14 +839,23 @@ export const scenesPanelToRuleFormValues = async (vizPanel: VizPanel): Promise<P
return undefined;
}
const lastQuery = grafanaQueries.at(-1);
if (!lastQuery) {
return undefined;
}
if (!grafanaQueries.find((query) => query.datasourceUid === ExpressionDatasourceUID)) {
const [reduceExpression, _thresholdExpression] = getDefaultExpressions(getNextRefId(grafanaQueries), '-');
const reduceExpression = getDefaultReduceExpression({
inputRefId: lastQuery.refId,
reduceRefId: getNextRefId(grafanaQueries),
});
grafanaQueries.push(reduceExpression);
const [_reduceExpression, thresholdExpression] = getDefaultExpressions(
reduceExpression.refId,
getNextRefId(grafanaQueries)
);
const thresholdExpression = getDefaultThresholdExpression({
inputRefId: reduceExpression.refId,
thresholdRefId: getNextRefId(grafanaQueries),
});
grafanaQueries.push(thresholdExpression);
}
@@ -47,9 +47,7 @@ exports[`PanelAlertTabContent Will render alerts belonging to panel and a button
"type": "and",
},
"query": {
"params": [
"B",
],
"params": [],
},
"reducer": {
"params": [],
@@ -85,9 +83,7 @@ exports[`PanelAlertTabContent Will render alerts belonging to panel and a button
"type": "and",
},
"query": {
"params": [
"C",
],
"params": [],
},
"reducer": {
"params": [],