Skip to content

Commit 78df27a

Browse files
authored
chore: pre-populate the alert condition based on the selected reduce to and thresholds (#8352)
1 parent b40fda0 commit 78df27a

File tree

10 files changed

+277
-11
lines changed

10 files changed

+277
-11
lines changed

frontend/src/constants/query.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,5 @@ export enum QueryParams {
4747
destination = 'destination',
4848
kindString = 'kindString',
4949
tab = 'tab',
50+
thresholds = 'thresholds',
5051
}

frontend/src/container/CreateAlertRule/AlertRuleDocumentationRedirection.test.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ROUTES from 'constants/routes';
2+
import * as usePrefillAlertConditions from 'container/FormAlertRules/usePrefillAlertConditions';
23
import CreateAlertPage from 'pages/CreateAlert';
34
import { MemoryRouter, Route } from 'react-router-dom';
45
import { act, fireEvent, render } from 'tests/test-utils';
@@ -33,6 +34,15 @@ jest.mock('hooks/useSafeNavigate', () => ({
3334
}),
3435
}));
3536

37+
jest
38+
.spyOn(usePrefillAlertConditions, 'usePrefillAlertConditions')
39+
.mockReturnValue({
40+
matchType: '3',
41+
op: '1',
42+
target: 100,
43+
targetUnit: 'rpm',
44+
});
45+
3646
let mockWindowOpen: jest.Mock;
3747

3848
window.ResizeObserver =

frontend/src/container/CreateAlertRule/AnomalyAlertDocumentationRedirection.test.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ROUTES from 'constants/routes';
2+
import * as usePrefillAlertConditions from 'container/FormAlertRules/usePrefillAlertConditions';
23
import CreateAlertPage from 'pages/CreateAlert';
34
import { MemoryRouter, Route } from 'react-router-dom';
45
import { act, fireEvent, render } from 'tests/test-utils';
@@ -41,6 +42,15 @@ jest.mock('hooks/useSafeNavigate', () => ({
4142
safeNavigate: jest.fn(),
4243
}),
4344
}));
45+
jest
46+
.spyOn(usePrefillAlertConditions, 'usePrefillAlertConditions')
47+
.mockReturnValue({
48+
matchType: '3',
49+
op: '1',
50+
target: 100,
51+
targetUnit: 'rpm',
52+
});
53+
4454
describe('Anomaly Alert Documentation Redirection', () => {
4555
let mockWindowOpen: jest.Mock;
4656

frontend/src/container/CreateAlertRule/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import logEvent from 'api/common/logEvent';
33
import { ENTITY_VERSION_V4 } from 'constants/app';
44
import { QueryParams } from 'constants/query';
55
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
6+
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
67
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
78
import history from 'lib/history';
89
import { useEffect, useState } from 'react';
@@ -32,6 +33,12 @@ function CreateRules(): JSX.Element {
3233
? AlertTypes.ANOMALY_BASED_ALERT
3334
: queryParams.get(QueryParams.alertType);
3435

36+
const { thresholds } = (location.state as {
37+
thresholds: ThresholdProps[];
38+
}) || {
39+
thresholds: null,
40+
};
41+
3542
const compositeQuery = useGetCompositeQueryParam();
3643
function getAlertTypeFromDataSource(): AlertTypes | null {
3744
if (!compositeQuery) {
@@ -96,7 +103,9 @@ function CreateRules(): JSX.Element {
96103
}
97104

98105
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
99-
history.replace(generatedUrl);
106+
history.replace(generatedUrl, {
107+
thresholds,
108+
});
100109
};
101110

102111
useEffect(() => {

frontend/src/container/FormAlertRules/RuleOptions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ function RuleOptions({
390390
<Space direction="vertical" size="large">
391391
{ruleType !== AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
392392
<Space direction="horizontal" align="center">
393-
<Form.Item noStyle name={['condition', 'target']}>
393+
<Form.Item noStyle>
394394
<InputNumber
395395
addonBefore={t('field_threshold')}
396396
value={alertDef?.condition?.target}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { renderHook } from '@testing-library/react';
2+
3+
import { usePrefillAlertConditions } from '../usePrefillAlertConditions';
4+
5+
const TEST_MAPPINGS = {
6+
op: {
7+
'>': '1',
8+
'<': '2',
9+
'=': '3',
10+
},
11+
matchType: {
12+
avg: '3',
13+
sum: '4',
14+
},
15+
};
16+
17+
jest.mock('react-router-dom-v5-compat', () => {
18+
const mockThreshold1 = {
19+
index: '0d11f426-a02e-48da-867c-b79c6ef1ff06',
20+
isEditEnabled: false,
21+
keyIndex: 1,
22+
selectedGraph: 'graph',
23+
thresholdColor: 'Orange',
24+
thresholdFormat: 'Text',
25+
thresholdLabel: 'Caution',
26+
thresholdOperator: '>',
27+
thresholdTableOptions: 'A',
28+
thresholdUnit: 'rpm',
29+
thresholdValue: 800,
30+
};
31+
const mockThreshold2 = {
32+
index: 'edbe8ef2-fa54-4cb9-b343-7afe883bb714',
33+
isEditEnabled: false,
34+
keyIndex: 0,
35+
selectedGraph: 'graph',
36+
thresholdColor: 'Red',
37+
thresholdFormat: 'Text',
38+
thresholdLabel: 'Danger',
39+
thresholdOperator: '<',
40+
thresholdTableOptions: 'A',
41+
thresholdUnit: 'rpm',
42+
thresholdValue: 900,
43+
};
44+
return {
45+
...jest.requireActual('react-router-dom-v5-compat'),
46+
useLocation: jest.fn().mockReturnValue({
47+
state: {
48+
thresholds: [mockThreshold1, mockThreshold2],
49+
},
50+
}),
51+
};
52+
});
53+
54+
const mockStagedQuery = {
55+
builder: {
56+
queryData: [
57+
{
58+
reduceTo: 'avg',
59+
},
60+
],
61+
},
62+
};
63+
64+
describe('usePrefillAlertConditions', () => {
65+
it('returns the correct matchType for a single query', () => {
66+
const { result } = renderHook(() =>
67+
usePrefillAlertConditions(mockStagedQuery as any),
68+
);
69+
expect(result.current.matchType).toBe(TEST_MAPPINGS.matchType.avg);
70+
});
71+
72+
it('returns null matchType for a single query with unsupported time aggregation', () => {
73+
const { result } = renderHook(() =>
74+
usePrefillAlertConditions({
75+
builder: { queryData: [{ reduceTo: 'p90' }] },
76+
} as any),
77+
);
78+
expect(result.current.matchType).toBe(null);
79+
});
80+
81+
it('returns the correct matchType for multiple queries with same time aggregation', () => {
82+
const { result } = renderHook(() =>
83+
usePrefillAlertConditions({
84+
builder: {
85+
queryData: [
86+
{
87+
reduceTo: 'avg',
88+
},
89+
{
90+
reduceTo: 'avg',
91+
},
92+
],
93+
},
94+
} as any),
95+
);
96+
expect(result.current.matchType).toBe(TEST_MAPPINGS.matchType.avg);
97+
});
98+
99+
it('returns null matchType for multiple queries with different time aggregation', () => {
100+
const { result } = renderHook(() =>
101+
usePrefillAlertConditions({
102+
builder: {
103+
queryData: [
104+
{
105+
reduceTo: 'avg',
106+
},
107+
{
108+
reduceTo: 'sum',
109+
},
110+
],
111+
},
112+
} as any),
113+
);
114+
expect(result.current.matchType).toBe(null);
115+
});
116+
117+
it('returns the correct op, target, targetUnit from the higher priority threshold for multiple thresholds', () => {
118+
const { result } = renderHook(() =>
119+
usePrefillAlertConditions(mockStagedQuery as any),
120+
);
121+
expect(result.current.op).toBe(TEST_MAPPINGS.op['<']);
122+
expect(result.current.target).toBe(900);
123+
expect(result.current.targetUnit).toBe('rpm');
124+
});
125+
});

frontend/src/container/FormAlertRules/index.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
StepContainer,
5858
StepHeading,
5959
} from './styles';
60+
import { usePrefillAlertConditions } from './usePrefillAlertConditions';
6061
import { getSelectedQueryOptions } from './utils';
6162

6263
export enum AlertDetectionTypes {
@@ -113,6 +114,9 @@ function FormAlertRules({
113114
handleSetConfig,
114115
redirectWithQueryBuilderData,
115116
} = useQueryBuilder();
117+
const { matchType, op, target, targetUnit } = usePrefillAlertConditions(
118+
stagedQuery,
119+
);
116120

117121
useEffect(() => {
118122
handleSetConfig(panelType || PANEL_TYPES.TIME_SERIES, dataSource);
@@ -266,6 +270,13 @@ function FormAlertRules({
266270
...initialValue,
267271
broadcastToAll: !broadcastToSpecificChannels,
268272
ruleType,
273+
condition: {
274+
...initialValue.condition,
275+
matchType: matchType || initialValue.condition.matchType,
276+
op: op || initialValue.condition.op,
277+
target: target || initialValue.condition.target,
278+
targetUnit: targetUnit || initialValue.condition.targetUnit,
279+
},
269280
});
270281

271282
setDetectionMethod(ruleType);
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
2+
import { useMemo } from 'react';
3+
import { useLocation } from 'react-router-dom-v5-compat';
4+
import { Query } from 'types/api/queryBuilder/queryBuilderData';
5+
6+
const THRESHOLD_COLORS_SORTING_ORDER = ['Red', 'Orange', 'Green', 'Blue'];
7+
8+
export const usePrefillAlertConditions = (
9+
stagedQuery: Query | null,
10+
): {
11+
matchType: string | null;
12+
op: string | null;
13+
target: number | undefined;
14+
targetUnit: string | undefined;
15+
} => {
16+
const location = useLocation();
17+
18+
// Extract and set match type
19+
const reduceTo = useMemo(() => {
20+
if (!stagedQuery) return null;
21+
const isSameTimeAggregation = stagedQuery.builder.queryData.every(
22+
(queryData) =>
23+
queryData.reduceTo === stagedQuery.builder.queryData[0].reduceTo,
24+
);
25+
return isSameTimeAggregation
26+
? stagedQuery.builder.queryData[0].reduceTo
27+
: null;
28+
}, [stagedQuery]);
29+
30+
const matchType = useMemo(() => {
31+
switch (reduceTo) {
32+
case 'avg':
33+
return '3';
34+
case 'sum':
35+
return '4';
36+
default:
37+
return null;
38+
}
39+
}, [reduceTo]);
40+
41+
// Extract and set threshold operator, value and unit
42+
const threshold = useMemo(() => {
43+
const { thresholds } = (location.state as {
44+
thresholds: ThresholdProps[];
45+
}) || {
46+
thresholds: null,
47+
};
48+
if (!thresholds || thresholds.length === 0) return null;
49+
const sortedThresholds = thresholds.sort((a, b) => {
50+
const aIndex = THRESHOLD_COLORS_SORTING_ORDER.indexOf(
51+
a.thresholdColor || '',
52+
);
53+
const bIndex = THRESHOLD_COLORS_SORTING_ORDER.indexOf(
54+
b.thresholdColor || '',
55+
);
56+
return aIndex - bIndex;
57+
});
58+
return sortedThresholds[0];
59+
}, [location.state]);
60+
61+
const thresholdOperator = useMemo(() => {
62+
const op = threshold?.thresholdOperator;
63+
switch (op) {
64+
case '>':
65+
case '>=':
66+
return '1';
67+
case '<':
68+
case '<=':
69+
return '2';
70+
case '=':
71+
return '3';
72+
default:
73+
return null;
74+
}
75+
}, [threshold]);
76+
77+
const thresholdUnit = useMemo(() => threshold?.thresholdUnit, [threshold]);
78+
79+
const thresholdValue = useMemo(() => threshold?.thresholdValue, [threshold]);
80+
81+
return {
82+
matchType,
83+
op: thresholdOperator,
84+
target: thresholdValue,
85+
targetUnit: thresholdUnit,
86+
};
87+
};

frontend/src/container/NewWidget/RightContainer/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,11 @@ function RightContainer({
130130
const selectedGraphType =
131131
GraphTypes.find((e) => e.name === selectedGraph)?.display || '';
132132

133-
const onCreateAlertsHandler = useCreateAlerts(selectedWidget, 'panelView');
133+
const onCreateAlertsHandler = useCreateAlerts(
134+
selectedWidget,
135+
'panelView',
136+
thresholds,
137+
);
134138

135139
const allowThreshold = panelTypeVsThreshold[selectedGraph];
136140
const allowSoftMinMax = panelTypeVsSoftMinMax[selectedGraph];

frontend/src/hooks/queryBuilder/useCreateAlerts.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { DEFAULT_ENTITY_VERSION } from 'constants/app';
55
import { QueryParams } from 'constants/query';
66
import ROUTES from 'constants/routes';
77
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
8+
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
89
import { useNotifications } from 'hooks/useNotifications';
910
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
1011
import { prepareQueryRangePayload } from 'lib/dashboard/prepareQueryRangePayload';
@@ -19,7 +20,11 @@ import { Widgets } from 'types/api/dashboard/getAll';
1920
import { GlobalReducer } from 'types/reducer/globalTime';
2021
import { getGraphType } from 'utils/getGraphType';
2122

22-
const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
23+
const useCreateAlerts = (
24+
widget?: Widgets,
25+
caller?: string,
26+
thresholds?: ThresholdProps[],
27+
): VoidFunction => {
2328
const queryRangeMutation = useMutation(getQueryRangeFormat);
2429

2530
const { selectedTime: globalSelectedInterval } = useSelector<
@@ -66,13 +71,17 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
6671
widget?.query,
6772
);
6873

69-
history.push(
70-
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
71-
JSON.stringify(updatedQuery),
72-
)}&${QueryParams.panelTypes}=${widget.panelTypes}&version=${
73-
selectedDashboard?.data.version || DEFAULT_ENTITY_VERSION
74-
}`,
75-
);
74+
const url = `${ROUTES.ALERTS_NEW}?${
75+
QueryParams.compositeQuery
76+
}=${encodeURIComponent(JSON.stringify(updatedQuery))}&${
77+
QueryParams.panelTypes
78+
}=${widget.panelTypes}&version=${
79+
selectedDashboard?.data.version || DEFAULT_ENTITY_VERSION
80+
}`;
81+
82+
history.push(url, {
83+
thresholds,
84+
});
7685
},
7786
onError: () => {
7887
notifications.error({

0 commit comments

Comments
 (0)