Alerting: Clean up dangling references when queries are removed
When a data query or expression is deleted, other expressions that
reference it would contain invalid/dangling references, potentially
causing errors or unexpected behavior.
This change introduces automatic cleanup of dangling references across
all expression types:
- Math expressions: Removes variable references ($A, ${A}) from the
expression string
- Reduce/Resample/Threshold: Clears the expression field if it
references the removed query
- Classic conditions: Filters out removed refIds from params array
- Handles cascading removals when expressions reference other
expressions
The cleanup is triggered in two scenarios:
1. When explicitly removing an expression via removeExpression action
2. When updating data queries via setDataQueries (e.g., user deletes
a query in the query editor)
Also migrates the Reduce component from Select to Combobox for better
UX consistency, with improved layout spacing.
This commit is contained in:
+2
-2
@@ -471,7 +471,7 @@ describe('Query and expressions reducer', () => {
|
||||
// The reduce expression should still exist but its reference should be cleared
|
||||
expect(newState.queries).toHaveLength(1);
|
||||
expect(newState.queries[0].refId).toBe('B');
|
||||
expect(newState.queries[0].model.expression).toBeNull();
|
||||
expect(newState.queries[0].model.expression).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should clear expression reference when removing a data query via setDataQueries', () => {
|
||||
@@ -521,7 +521,7 @@ describe('Query and expressions reducer', () => {
|
||||
expect(newState.queries).toHaveLength(2);
|
||||
expect(newState.queries.map((q) => q.refId)).toEqual(['A', 'C']);
|
||||
const thresholdQuery = newState.queries.find((q) => q.refId === 'C');
|
||||
expect(thresholdQuery?.model.expression).toBeNull();
|
||||
expect(thresholdQuery?.model.expression).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -237,7 +237,7 @@ describe('rule-editor', () => {
|
||||
const updatedQueries = queriesWithRemovedReferences(queries, 'A');
|
||||
|
||||
expect(updatedQueries[0]).toEqual(dataSource);
|
||||
expect(updatedQueries[1].model.expression).toBeNull();
|
||||
expect(updatedQueries[1].model.expression).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should clear reference in threshold expression when expression is removed', () => {
|
||||
@@ -246,7 +246,7 @@ describe('rule-editor', () => {
|
||||
|
||||
expect(updatedQueries[0]).toEqual(dataSource);
|
||||
expect(updatedQueries[1]).toEqual(reduceExpression);
|
||||
expect(updatedQueries[2].model.expression).toBeNull();
|
||||
expect(updatedQueries[2].model.expression).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should remove reference from math expression', () => {
|
||||
@@ -282,7 +282,7 @@ describe('rule-editor', () => {
|
||||
const queries: AlertQuery[] = [dataSource, resampleExpression];
|
||||
const updatedQueries = queriesWithRemovedReferences(queries, 'A');
|
||||
|
||||
expect(updatedQueries[1].model.expression).toBeNull();
|
||||
expect(updatedQueries[1].model.expression).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -105,8 +105,8 @@ export function queriesWithRemovedReferences(queries: AlertQuery[], removedRefId
|
||||
...query,
|
||||
model: {
|
||||
...query.model,
|
||||
// Set to null so Select component shows no selection
|
||||
expression: isReferencing ? null : query.model.expression,
|
||||
// Set to undefined to clear the dangling reference
|
||||
expression: isReferencing ? undefined : query.model.expression,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { CoreApp, SelectableValue } from '@grafana/data';
|
||||
import { CoreApp } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Alert, InlineField, InlineFieldRow, Input, Select, TextLink } from '@grafana/ui';
|
||||
import { Alert, Combobox, ComboboxOption, InlineField, InlineFieldRow, Input, TextLink } from '@grafana/ui';
|
||||
|
||||
import { ExpressionQuery, ExpressionQuerySettings, ReducerMode, reducerModes, reducerTypes } from '../types';
|
||||
|
||||
interface Props {
|
||||
app?: CoreApp;
|
||||
labelWidth?: number | 'auto';
|
||||
refIds: Array<SelectableValue<string>>;
|
||||
refIds: Array<ComboboxOption<string>>;
|
||||
query: ExpressionQuery;
|
||||
onChange: (query: ExpressionQuery) => void;
|
||||
}
|
||||
|
||||
export const Reduce = ({ labelWidth = 'auto', onChange, app, refIds, query }: Props) => {
|
||||
const reducer = reducerTypes.find((o) => o.value === query.reducer);
|
||||
|
||||
const onRefIdChange = (value: SelectableValue<string>) => {
|
||||
onChange({ ...query, expression: value.value });
|
||||
const onRefIdChange = (option: ComboboxOption<string> | null) => {
|
||||
onChange({ ...query, expression: option?.value });
|
||||
};
|
||||
|
||||
const onSelectReducer = (value: SelectableValue<string>) => {
|
||||
onChange({ ...query, reducer: value.value });
|
||||
const onSelectReducer = (option: ComboboxOption<string> | null) => {
|
||||
onChange({ ...query, reducer: option?.value });
|
||||
};
|
||||
|
||||
const onSettingsChanged = (settings: ExpressionQuerySettings) => {
|
||||
onChange({ ...query, settings: settings });
|
||||
};
|
||||
|
||||
const onModeChanged = (value: SelectableValue<ReducerMode>) => {
|
||||
const onModeChanged = (option: ComboboxOption<ReducerMode> | null) => {
|
||||
if (!option || option.value === null || option.value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newSettings: ExpressionQuerySettings;
|
||||
switch (value.value) {
|
||||
switch (option.value) {
|
||||
case ReducerMode.Strict:
|
||||
newSettings = { mode: ReducerMode.Strict };
|
||||
break;
|
||||
@@ -49,7 +51,7 @@ export const Reduce = ({ labelWidth = 'auto', onChange, app, refIds, query }: Pr
|
||||
|
||||
default:
|
||||
newSettings = {
|
||||
mode: value.value,
|
||||
mode: option.value,
|
||||
};
|
||||
}
|
||||
onSettingsChanged(newSettings);
|
||||
@@ -101,15 +103,17 @@ export const Reduce = ({ labelWidth = 'auto', onChange, app, refIds, query }: Pr
|
||||
{strictModeNotification()}
|
||||
<InlineFieldRow>
|
||||
<InlineField label={t('expressions.reduce.label-input', 'Input')} labelWidth={labelWidth}>
|
||||
<Select onChange={onRefIdChange} options={refIds} value={query.expression} width={'auto'} />
|
||||
<Combobox onChange={onRefIdChange} options={refIds} value={query.expression} width={50} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label={t('expressions.reduce.label-function', 'Function')} labelWidth={labelWidth}>
|
||||
<Select options={reducerTypes} value={reducer} onChange={onSelectReducer} width={20} />
|
||||
<Combobox options={reducerTypes} value={query.reducer} onChange={onSelectReducer} width={50} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label={t('expressions.reduce.label-mode', 'Mode')} labelWidth={labelWidth}>
|
||||
<Select onChange={onModeChanged} options={reducerModes} value={mode} width={25} />
|
||||
<Combobox onChange={onModeChanged} options={reducerModes} value={mode} width={50} />
|
||||
</InlineField>
|
||||
{replaceWithNumber()}
|
||||
</InlineFieldRow>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DataQuery, ReducerID, SelectableValue } from '@grafana/data';
|
||||
import { ComboboxOption } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
import { EvalFunction } from '../alerting/state/alertDef';
|
||||
@@ -75,7 +76,7 @@ export const expressionTypes: Array<SelectableValue<ExpressionQueryType>> = [
|
||||
return true;
|
||||
});
|
||||
|
||||
export const reducerTypes: Array<SelectableValue<string>> = [
|
||||
export const reducerTypes: Array<ComboboxOption<string>> = [
|
||||
{ value: ReducerID.min, label: 'Min', description: 'Get the minimum value' },
|
||||
{ value: ReducerID.max, label: 'Max', description: 'Get the maximum value' },
|
||||
{ value: ReducerID.mean, label: 'Mean', description: 'Get the average value' },
|
||||
@@ -91,7 +92,7 @@ export enum ReducerMode {
|
||||
DropNonNumbers = 'dropNN',
|
||||
}
|
||||
|
||||
export const reducerModes: Array<SelectableValue<ReducerMode>> = [
|
||||
export const reducerModes: Array<ComboboxOption<ReducerMode>> = [
|
||||
{
|
||||
value: ReducerMode.Strict,
|
||||
label: 'Strict',
|
||||
|
||||
Reference in New Issue
Block a user