Alerts/607 test notifications UI (#1469)

* feat: added test alert feature

* fix: solved the lint issues

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
This commit is contained in:
Amol Umbark 2022-08-03 19:40:20 +05:30 committed by GitHub
parent cca4db602c
commit a6ed6c03c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 182 additions and 65 deletions

View File

@ -1,4 +1,8 @@
{
"target_missing": "Please enter a threshold to proceed",
"rule_test_fired": "Test notification sent successfully",
"no_alerts_found": "No alerts found during the evaluation. This happens when rule condition is unsatisfied. You may adjust the rule threshold and retry.",
"button_testrule": "Test Notification",
"label_channel_select": "Notification Channels",
"placeholder_channel_select": "select one or more channels",
"channel_select_tooltip": "Leave empty to send this alert on all the configured channels",

View File

@ -1,4 +1,8 @@
{
"target_missing": "Please enter a threshold to proceed",
"rule_test_fired": "Test notification sent successfully",
"no_alerts_found": "No alerts found during the evaluation. This happens when rule condition is unsatisfied. You may adjust the rule threshold and retry.",
"button_testrule": "Test Notification",
"label_channel_select": "Notification Channels",
"placeholder_channel_select": "select one or more channels",
"channel_select_tooltip": "Leave empty to send this alert on all the configured channels",

View File

@ -0,0 +1,26 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/testAlert';
const testAlert = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/testRule', {
...props.data,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default testAlert;

View File

@ -21,7 +21,7 @@ export interface ChartPreviewProps {
selectedTime?: timePreferenceType;
selectedInterval?: Time;
headline?: JSX.Element;
threshold?: number;
threshold?: number | undefined;
}
function ChartPreview({
@ -35,7 +35,7 @@ function ChartPreview({
}: ChartPreviewProps): JSX.Element | null {
const { t } = useTranslation('alerts');
const staticLine: StaticLineProps | undefined =
threshold && threshold > 0
threshold !== undefined
? {
yMin: threshold,
yMax: threshold,
@ -117,7 +117,7 @@ ChartPreview.defaultProps = {
selectedTime: 'GLOBAL_TIME',
selectedInterval: '5min',
headline: undefined,
threshold: 0,
threshold: undefined,
};
export default ChartPreview;

View File

@ -156,7 +156,7 @@ function RuleOptions({
...alertDef,
condition: {
...alertDef.condition,
target: (value as number) || undefined,
target: value as number,
},
});
}}

View File

@ -1,6 +1,7 @@
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
import { FormInstance, Modal, notification, Typography } from 'antd';
import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import ROUTES from 'constants/routes';
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
@ -143,10 +144,74 @@ function FormAlertRules({
});
}
};
const validatePromParams = useCallback((): boolean => {
let retval = true;
if (queryCategory !== EQueryType.PROM) return retval;
if (!promQueries || Object.keys(promQueries).length === 0) {
notification.error({
message: 'Error',
description: t('promql_required'),
});
return false;
}
Object.keys(promQueries).forEach((key) => {
if (promQueries[key].query === '') {
notification.error({
message: 'Error',
description: t('promql_required'),
});
retval = false;
}
});
return retval;
}, [t, promQueries, queryCategory]);
const validateQBParams = useCallback((): boolean => {
let retval = true;
if (queryCategory !== EQueryType.QUERY_BUILDER) return true;
if (!metricQueries || Object.keys(metricQueries).length === 0) {
notification.error({
message: 'Error',
description: t('condition_required'),
});
return false;
}
if (!alertDef.condition?.target) {
notification.error({
message: 'Error',
description: t('target_missing'),
});
return false;
}
Object.keys(metricQueries).forEach((key) => {
if (metricQueries[key].metricName === '') {
notification.error({
message: 'Error',
description: t('metricname_missing', { where: metricQueries[key].name }),
});
retval = false;
}
});
Object.keys(formulaQueries).forEach((key) => {
if (formulaQueries[key].expression === '') {
notification.error({
message: 'Error',
description: t('expression_missing', formulaQueries[key].name),
});
retval = false;
}
});
return retval;
}, [t, alertDef, queryCategory, metricQueries, formulaQueries]);
const isFormValid = useCallback((): boolean => {
let retval = true;
if (!alertDef.alert || alertDef.alert === '') {
notification.error({
message: 'Error',
@ -155,57 +220,14 @@ function FormAlertRules({
return false;
}
if (
queryCategory === EQueryType.PROM &&
(!promQueries || Object.keys(promQueries).length === 0)
) {
notification.error({
message: 'Error',
description: t('promql_required'),
});
if (!validatePromParams()) {
return false;
}
if (
(queryCategory === EQueryType.QUERY_BUILDER && !metricQueries) ||
Object.keys(metricQueries).length === 0
) {
notification.error({
message: 'Error',
description: t('condition_required'),
});
return false;
}
if (queryCategory === EQueryType.QUERY_BUILDER) {
Object.keys(metricQueries).forEach((key) => {
if (metricQueries[key].metricName === '') {
retval = false;
notification.error({
message: 'Error',
description: t('metricname_missing', { where: metricQueries[key].name }),
});
}
});
Object.keys(formulaQueries).forEach((key) => {
if (formulaQueries[key].expression === '') {
retval = false;
notification.error({
message: 'Error',
description: t('expression_missing', formulaQueries[key].name),
});
}
});
}
return retval;
}, [t, alertDef, queryCategory, metricQueries, formulaQueries, promQueries]);
const saveRule = useCallback(async () => {
if (!isFormValid()) {
return;
}
return validateQBParams();
}, [t, validateQBParams, alertDef, validatePromParams]);
const preparePostData = (): AlertDef => {
const postableAlert: AlertDef = {
...alertDef,
source: window?.location.toString(),
@ -220,6 +242,22 @@ function FormAlertRules({
},
},
};
return postableAlert;
};
const memoizedPreparePostData = useCallback(preparePostData, [
queryCategory,
alertDef,
metricQueries,
formulaQueries,
promQueries,
]);
const saveRule = useCallback(async () => {
if (!isFormValid()) {
return;
}
const postableAlert = memoizedPreparePostData();
setLoading(true);
try {
@ -250,24 +288,13 @@ function FormAlertRules({
});
}
} catch (e) {
console.log('save alert api failed:', e);
notification.error({
message: 'Error',
description: t('unexpected_error'),
});
}
setLoading(false);
}, [
t,
isFormValid,
queryCategory,
ruleId,
alertDef,
metricQueries,
formulaQueries,
promQueries,
ruleCache,
]);
}, [t, isFormValid, ruleId, ruleCache, memoizedPreparePostData]);
const onSaveHandler = useCallback(async () => {
const content = (
@ -288,6 +315,44 @@ function FormAlertRules({
});
}, [t, saveRule, queryCategory]);
const onTestRuleHandler = useCallback(async () => {
if (!isFormValid()) {
return;
}
const postableAlert = memoizedPreparePostData();
setLoading(true);
try {
const response = await testAlertApi({ data: postableAlert });
if (response.statusCode === 200) {
const { payload } = response;
if (payload?.alertCount === 0) {
notification.error({
message: 'Error',
description: t('no_alerts_found'),
});
} else {
notification.success({
message: 'Success',
description: t('rule_test_fired'),
});
}
} else {
notification.error({
message: 'Error',
description: response.error || t('unexpected_error'),
});
}
} catch (e) {
notification.error({
message: 'Error',
description: t('unexpected_error'),
});
}
setLoading(false);
}, [t, isFormValid, memoizedPreparePostData]);
const renderBasicInfo = (): JSX.Element => (
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
);
@ -354,6 +419,14 @@ function FormAlertRules({
>
{ruleId > 0 ? t('button_savechanges') : t('button_createrule')}
</ActionButton>
<ActionButton
loading={loading || false}
type="default"
onClick={onTestRuleHandler}
>
{' '}
{t('button_testrule')}
</ActionButton>
<ActionButton
disabled={loading || false}
type="default"

View File

@ -0,0 +1,10 @@
import { AlertDef } from 'types/api/alerts/def';
export interface Props {
data: AlertDef;
}
export interface PayloadProps {
alertCount: number;
message: string;
}