mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 03:59:04 +08:00
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:
parent
cca4db602c
commit
a6ed6c03c1
@ -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",
|
||||||
|
@ -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",
|
||||||
|
26
frontend/src/api/alerts/testAlert.ts
Normal file
26
frontend/src/api/alerts/testAlert.ts
Normal 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;
|
@ -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;
|
||||||
|
@ -156,7 +156,7 @@ function RuleOptions({
|
|||||||
...alertDef,
|
...alertDef,
|
||||||
condition: {
|
condition: {
|
||||||
...alertDef.condition,
|
...alertDef.condition,
|
||||||
target: (value as number) || undefined,
|
target: value as number,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@ -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"
|
||||||
|
10
frontend/src/types/api/alerts/testAlert.ts
Normal file
10
frontend/src/types/api/alerts/testAlert.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { AlertDef } from 'types/api/alerts/def';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
data: AlertDef;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PayloadProps {
|
||||||
|
alertCount: number;
|
||||||
|
message: string;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user