From 9199a8b8003d54b4b2ba35ca4eda7f46e37477aa Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Wed, 17 Nov 2021 12:56:37 +0100 Subject: [PATCH] Alerting: rewire expression references when queries are updated (#41478) --- .../components/rule-editor/QueryRows.tsx | 8 +- .../components/rule-editor/util.test.ts | 169 ++++++++++++++++++ .../unified/components/rule-editor/util.ts | 64 +++++++ 3 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 public/app/features/alerting/unified/components/rule-editor/util.test.ts create mode 100644 public/app/features/alerting/unified/components/rule-editor/util.ts diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx index cda10ba55b9..4bde536aa5e 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx @@ -13,6 +13,7 @@ import { getDataSourceSrv } from '@grafana/runtime'; import { QueryWrapper } from './QueryWrapper'; import { AlertQuery } from 'app/types/unified-alerting-dto'; import { isExpressionQuery } from 'app/features/expressions/guards'; +import { queriesWithUpdatedReferences } from './util'; interface Props { // The query configuration @@ -128,11 +129,16 @@ export class QueryRows extends PureComponent { onChangeQuery = (query: DataQuery, index: number) => { const { queries, onQueriesChange } = this.props; + // find what queries still have a reference to the old name + const previousRefId = queries[index].refId; + const newRefId = query.refId; + onQueriesChange( - queries.map((item, itemIndex) => { + queriesWithUpdatedReferences(queries, previousRefId, newRefId).map((item, itemIndex) => { if (itemIndex !== index) { return item; } + return { ...item, refId: query.refId, diff --git a/public/app/features/alerting/unified/components/rule-editor/util.test.ts b/public/app/features/alerting/unified/components/rule-editor/util.test.ts new file mode 100644 index 00000000000..2c23db035fd --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/util.test.ts @@ -0,0 +1,169 @@ +import { ClassicCondition, ExpressionQuery } from 'app/features/expressions/types'; +import { AlertQuery } from 'app/types/unified-alerting-dto'; +import { queriesWithUpdatedReferences, updateMathExpressionRefs } from './util'; + +describe('rule-editor', () => { + const dataSource: AlertQuery = { + refId: 'A', + datasourceUid: 'abc123', + queryType: '', + relativeTimeRange: { + from: 600, + to: 0, + }, + model: { + refId: 'A', + }, + }; + + const classicCondition = { + refId: 'B', + datasourceUid: '-100', + queryType: '', + model: { + refId: 'B', + type: 'classic_conditions', + datasource: { + uid: '-100', + type: 'grafana-expression', + }, + conditions: [ + { + type: 'query', + evaluator: { + params: [3], + type: 'gt', + }, + operator: { + type: 'and', + }, + query: { + params: ['A'], + }, + reducer: { + params: [], + type: 'last', + }, + }, + ], + }, + }; + + const mathExpression = { + refId: 'B', + datasourceUid: '-100', + queryType: '', + model: { + refId: 'B', + type: 'math', + datasource: { + uid: '-100', + type: 'grafana-expression', + }, + conditions: [], + expression: 'abs($A) + $A', + }, + }; + + const reduceExpression = { + refId: 'B', + datasourceUid: '-100', + queryType: '', + model: { + refId: 'B', + type: 'reduce', + datasource: { + uid: '-100', + type: 'grafana-expression', + }, + conditions: [], + reducer: 'mean', + expression: 'A', + }, + }; + + const resampleExpression = { + refId: 'A', + datasourceUid: '-100', + model: { + refId: 'A', + type: 'resample', + datasource: { + type: '__expr__', + uid: '__expr__', + }, + conditions: [], + downsampler: 'mean', + upsampler: 'fillna', + expression: 'A', + window: '30m', + }, + queryType: '', + }; + + describe('rewires query names', () => { + it('should rewire classic expressions', () => { + const queries: AlertQuery[] = [dataSource, classicCondition]; + const rewiredQueries = queriesWithUpdatedReferences(queries, 'A', 'C'); + + const queryModel = rewiredQueries[1].model as ExpressionQuery; + + const checkConditionParams = (condition: ClassicCondition) => { + return expect(condition.query.params).toEqual(['C']); + }; + + expect(queryModel.conditions?.every(checkConditionParams)); + }); + + it('should rewire math expressions', () => { + const queries: AlertQuery[] = [dataSource, mathExpression]; + const rewiredQueries = queriesWithUpdatedReferences(queries, 'A', 'Query A'); + + const queryModel = rewiredQueries[1].model as ExpressionQuery; + + expect(queryModel.expression).toBe('abs(${Query A}) + ${Query A}'); + }); + + it('should rewire reduce expressions', () => { + const queries: AlertQuery[] = [dataSource, reduceExpression]; + const rewiredQueries = queriesWithUpdatedReferences(queries, 'A', 'C'); + + const queryModel = rewiredQueries[1].model as ExpressionQuery; + expect(queryModel.expression).toBe('C'); + }); + + it('should rewire resample expressions', () => { + const queries: AlertQuery[] = [dataSource, resampleExpression]; + const rewiredQueries = queriesWithUpdatedReferences(queries, 'A', 'C'); + + const queryModel = rewiredQueries[1].model as ExpressionQuery; + expect(queryModel.expression).toBe('C'); + }); + + it('should rewire multiple expressions', () => { + const queries: AlertQuery[] = [dataSource, mathExpression, resampleExpression]; + const rewiredQueries = queriesWithUpdatedReferences(queries, 'A', 'C'); + + expect(rewiredQueries[1].model as ExpressionQuery).toHaveProperty('expression', 'abs(${C}) + ${C}'); + expect(rewiredQueries[2].model as ExpressionQuery).toHaveProperty('expression', 'C'); + }); + + it('should skip if refs are identical', () => { + const queries: AlertQuery[] = [dataSource, reduceExpression, mathExpression]; + const rewiredQueries = queriesWithUpdatedReferences(queries, 'A', 'A'); + + expect(rewiredQueries[0]).toEqual(queries[0]); + expect(rewiredQueries[1]).toEqual(queries[1]); + expect(rewiredQueries[2]).toEqual(queries[2]); + }); + }); + + describe('updateMathExpressionRefs', () => { + it('should rewire refs without brackets', () => { + expect(updateMathExpressionRefs('abs($Foo) + $Foo', 'Foo', 'Bar')).toBe('abs(${Bar}) + ${Bar}'); + }); + it('should rewire refs with brackets', () => { + expect(updateMathExpressionRefs('abs(${Foo}) + $Foo', 'Foo', 'Bar')).toBe('abs(${Bar}) + ${Bar}'); + }); + }); +}); diff --git a/public/app/features/alerting/unified/components/rule-editor/util.ts b/public/app/features/alerting/unified/components/rule-editor/util.ts new file mode 100644 index 00000000000..e49bf28c191 --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/util.ts @@ -0,0 +1,64 @@ +import { isExpressionQuery } from 'app/features/expressions/guards'; +import { AlertQuery } from 'app/types/unified-alerting-dto'; + +export function queriesWithUpdatedReferences( + queries: AlertQuery[], + previousRefId: string, + newRefId: string +): AlertQuery[] { + return queries.map((query) => { + if (previousRefId === newRefId) { + return query; + } + + if (!isExpressionQuery(query.model)) { + return query; + } + + const isMathExpression = query.model.type === 'math'; + const isReduceExpression = query.model.type === 'reduce'; + const isResampleExpression = query.model.type === 'resample'; + const isClassicExpression = query.model.type === 'classic_conditions'; + + if (isMathExpression) { + return { + ...query, + model: { + ...query.model, + expression: updateMathExpressionRefs(query.model.expression ?? '', previousRefId, newRefId), + }, + }; + } + + if (isResampleExpression || isReduceExpression) { + return { + ...query, + model: { + ...query.model, + expression: newRefId, + }, + }; + } + + if (isClassicExpression) { + const conditions = query.model.conditions?.map((condition) => ({ + ...condition, + query: { + ...condition.query, + params: condition.query.params.map((param: string) => (param === previousRefId ? newRefId : param)), + }, + })); + + return { ...query, model: { ...query.model, conditions } }; + } + + return query; + }); +} + +export function updateMathExpressionRefs(expression: string, previousRefId: string, newRefId: string): string { + const oldExpression = new RegExp('\\${?' + previousRefId + '}?', 'gm'); + const newExpression = '${' + newRefId + '}'; + + return expression.replace(oldExpression, newExpression); +}