diff --git a/frontend/public/locales/en-GB/alerts.json b/frontend/public/locales/en-GB/alerts.json index f421c78bc1..cae309fd45 100644 --- a/frontend/public/locales/en-GB/alerts.json +++ b/frontend/public/locales/en-GB/alerts.json @@ -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", diff --git a/frontend/public/locales/en/alerts.json b/frontend/public/locales/en/alerts.json index f421c78bc1..cae309fd45 100644 --- a/frontend/public/locales/en/alerts.json +++ b/frontend/public/locales/en/alerts.json @@ -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", diff --git a/frontend/src/api/alerts/testAlert.ts b/frontend/src/api/alerts/testAlert.ts new file mode 100644 index 0000000000..a30e977a10 --- /dev/null +++ b/frontend/src/api/alerts/testAlert.ts @@ -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 | 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; diff --git a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx index 88364fad1b..6243c1d4d4 100644 --- a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx +++ b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx @@ -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; diff --git a/frontend/src/container/FormAlertRules/RuleOptions.tsx b/frontend/src/container/FormAlertRules/RuleOptions.tsx index d9aff8bfb1..8794f87b2c 100644 --- a/frontend/src/container/FormAlertRules/RuleOptions.tsx +++ b/frontend/src/container/FormAlertRules/RuleOptions.tsx @@ -156,7 +156,7 @@ function RuleOptions({ ...alertDef, condition: { ...alertDef.condition, - target: (value as number) || undefined, + target: value as number, }, }); }} diff --git a/frontend/src/container/FormAlertRules/index.tsx b/frontend/src/container/FormAlertRules/index.tsx index 8643eb4060..022e913f8e 100644 --- a/frontend/src/container/FormAlertRules/index.tsx +++ b/frontend/src/container/FormAlertRules/index.tsx @@ -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 => ( ); @@ -354,6 +419,14 @@ function FormAlertRules({ > {ruleId > 0 ? t('button_savechanges') : t('button_createrule')} + + {' '} + {t('button_testrule')} +