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", "label_channel_select": "Notification Channels",
"placeholder_channel_select": "select one or more channels", "placeholder_channel_select": "select one or more channels",
"channel_select_tooltip": "Leave empty to send this alert on all the configured 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", "label_channel_select": "Notification Channels",
"placeholder_channel_select": "select one or more channels", "placeholder_channel_select": "select one or more channels",
"channel_select_tooltip": "Leave empty to send this alert on all the configured 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; selectedTime?: timePreferenceType;
selectedInterval?: Time; selectedInterval?: Time;
headline?: JSX.Element; headline?: JSX.Element;
threshold?: number; threshold?: number | undefined;
} }
function ChartPreview({ function ChartPreview({
@ -35,7 +35,7 @@ function ChartPreview({
}: ChartPreviewProps): JSX.Element | null { }: ChartPreviewProps): JSX.Element | null {
const { t } = useTranslation('alerts'); const { t } = useTranslation('alerts');
const staticLine: StaticLineProps | undefined = const staticLine: StaticLineProps | undefined =
threshold && threshold > 0 threshold !== undefined
? { ? {
yMin: threshold, yMin: threshold,
yMax: threshold, yMax: threshold,
@ -117,7 +117,7 @@ ChartPreview.defaultProps = {
selectedTime: 'GLOBAL_TIME', selectedTime: 'GLOBAL_TIME',
selectedInterval: '5min', selectedInterval: '5min',
headline: undefined, headline: undefined,
threshold: 0, threshold: undefined,
}; };
export default ChartPreview; export default ChartPreview;

View File

@ -156,7 +156,7 @@ function RuleOptions({
...alertDef, ...alertDef,
condition: { condition: {
...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 { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
import { FormInstance, Modal, notification, Typography } from 'antd'; import { FormInstance, Modal, notification, Typography } from 'antd';
import saveAlertApi from 'api/alerts/save'; import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag'; import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag'; 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 => { const isFormValid = useCallback((): boolean => {
let retval = true;
if (!alertDef.alert || alertDef.alert === '') { if (!alertDef.alert || alertDef.alert === '') {
notification.error({ notification.error({
message: 'Error', message: 'Error',
@ -155,57 +220,14 @@ function FormAlertRules({
return false; return false;
} }
if ( if (!validatePromParams()) {
queryCategory === EQueryType.PROM &&
(!promQueries || Object.keys(promQueries).length === 0)
) {
notification.error({
message: 'Error',
description: t('promql_required'),
});
return false; return false;
} }
if ( return validateQBParams();
(queryCategory === EQueryType.QUERY_BUILDER && !metricQueries) || }, [t, validateQBParams, alertDef, validatePromParams]);
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;
}
const preparePostData = (): AlertDef => {
const postableAlert: AlertDef = { const postableAlert: AlertDef = {
...alertDef, ...alertDef,
source: window?.location.toString(), 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); setLoading(true);
try { try {
@ -250,24 +288,13 @@ function FormAlertRules({
}); });
} }
} catch (e) { } catch (e) {
console.log('save alert api failed:', e);
notification.error({ notification.error({
message: 'Error', message: 'Error',
description: t('unexpected_error'), description: t('unexpected_error'),
}); });
} }
setLoading(false); setLoading(false);
}, [ }, [t, isFormValid, ruleId, ruleCache, memoizedPreparePostData]);
t,
isFormValid,
queryCategory,
ruleId,
alertDef,
metricQueries,
formulaQueries,
promQueries,
ruleCache,
]);
const onSaveHandler = useCallback(async () => { const onSaveHandler = useCallback(async () => {
const content = ( const content = (
@ -288,6 +315,44 @@ function FormAlertRules({
}); });
}, [t, saveRule, queryCategory]); }, [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 => ( const renderBasicInfo = (): JSX.Element => (
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} /> <BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
); );
@ -354,6 +419,14 @@ function FormAlertRules({
> >
{ruleId > 0 ? t('button_savechanges') : t('button_createrule')} {ruleId > 0 ? t('button_savechanges') : t('button_createrule')}
</ActionButton> </ActionButton>
<ActionButton
loading={loading || false}
type="default"
onClick={onTestRuleHandler}
>
{' '}
{t('button_testrule')}
</ActionButton>
<ActionButton <ActionButton
disabled={loading || false} disabled={loading || false}
type="default" 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;
}