(feature): UI for Test alert channels (#994)

* (feature): Implemented test channel function for webhook and slack
This commit is contained in:
Amol Umbark 2022-04-22 16:56:18 +05:30 committed by GitHub
parent 508c6ced80
commit 2b5b79e34a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 389 additions and 88 deletions

View File

@ -0,0 +1,30 @@
{
"page_title_create": "New Notification Channels",
"page_title_edit": "Edit Notification Channels",
"button_save_channel": "Save",
"button_test_channel": "Test",
"button_return": "Back",
"field_channel_name": "Name",
"field_channel_type": "Type",
"field_webhook_url": "Webhook URL",
"field_slack_recipient": "Recipient",
"field_slack_title": "Title",
"field_slack_description": "Description",
"field_webhook_username": "User Name (optional)",
"field_webhook_password": "Password (optional)",
"placeholder_slack_description": "Description",
"help_webhook_username": "Leave empty for bearer auth or when authentication is not necessary.",
"help_webhook_password": "Specify a password or bearer token",
"channel_creation_done": "Successfully created the channel",
"channel_creation_failed": "An unexpected error occurred while creating this channel",
"channel_edit_done": "Channels Edited Successfully",
"channel_edit_failed": "An unexpected error occurred while updating this channel",
"selected_channel_invalid": "Channel type selected is invalid",
"username_no_password": "A Password must be provided with user name",
"test_unsupported": "Sorry, this channel type does not support test yet",
"channel_test_done": "An alert has been sent to this channel",
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
"webhook_url_required": "Webhook URL is mandatory",
"slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)"
}

View File

@ -0,0 +1,30 @@
{
"page_title_create": "New Notification Channels",
"page_title_edit": "Edit Notification Channels",
"button_save_channel": "Save",
"button_test_channel": "Test",
"button_return": "Back",
"field_channel_name": "Name",
"field_channel_type": "Type",
"field_webhook_url": "Webhook URL",
"field_slack_recipient": "Recipient",
"field_slack_title": "Title",
"field_slack_description": "Description",
"field_webhook_username": "User Name (optional)",
"field_webhook_password": "Password (optional)",
"placeholder_slack_description": "Description",
"help_webhook_username": "Leave empty for bearer auth or when authentication is not necessary.",
"help_webhook_password": "Specify a password or bearer token",
"channel_creation_done": "Successfully created the channel",
"channel_creation_failed": "An unexpected error occurred while creating this channel",
"channel_edit_done": "Channels Edited Successfully",
"channel_edit_failed": "An unexpected error occurred while updating this channel",
"selected_channel_invalid": "Channel type selected is invalid",
"username_no_password": "A Password must be provided with user name",
"test_unsupported": "Sorry, this channel type does not support test yet",
"channel_test_done": "An alert has been sent to this channel",
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
"webhook_url_required": "Webhook URL is mandatory",
"slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)"
}

View File

@ -0,0 +1,35 @@
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/channels/createSlack';
const testSlack = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/testChannel', {
name: props.name,
slack_configs: [
{
send_resolved: true,
api_url: props.api_url,
channel: props.channel,
title: props.title,
text: props.text,
},
],
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default testSlack;

View File

@ -0,0 +1,51 @@
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/channels/createWebhook';
const testWebhook = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
let httpConfig = {};
if (props.username !== '' && props.password !== '') {
httpConfig = {
basic_auth: {
username: props.username,
password: props.password,
},
};
} else if (props.username === '' && props.password !== '') {
httpConfig = {
authorization: {
type: 'bearer',
credentials: props.password,
},
};
}
const response = await axios.post('/testChannel', {
name: props.name,
webhook_configs: [
{
send_resolved: true,
url: props.api_url,
http_config: httpConfig,
},
],
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default testWebhook;

View File

@ -1,10 +1,13 @@
import { Form, notification } from 'antd';
import createSlackApi from 'api/channels/createSlack';
import createWebhookApi from 'api/channels/createWebhook';
import testSlackApi from 'api/channels/testSlack';
import testWebhookApi from 'api/channels/testWebhook';
import ROUTES from 'constants/routes';
import FormAlertChannels from 'container/FormAlertChannels';
import history from 'lib/history';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ChannelType,
@ -17,6 +20,9 @@ import {
function CreateAlertChannels({
preType = 'slack',
}: CreateAlertChannelsProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('channels');
const [formInstance] = Form.useForm();
const [selectedConfig, setSelectedConfig] = useState<
@ -45,6 +51,7 @@ function CreateAlertChannels({
{{- end }}`,
});
const [savingState, setSavingState] = useState<boolean>(false);
const [testingState, setTestingState] = useState<boolean>(false);
const [notifications, NotificationElement] = notification.useNotification();
const [type, setType] = useState<ChannelType>(preType);
@ -52,26 +59,26 @@ function CreateAlertChannels({
setType(value as ChannelType);
}, []);
const onTestHandler = useCallback(() => {
console.log('test');
}, []);
const prepareSlackRequest = useCallback(() => {
return {
api_url: selectedConfig?.api_url || '',
channel: selectedConfig?.channel || '',
name: selectedConfig?.name || '',
send_resolved: true,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
};
}, [selectedConfig]);
const onSlackHandler = useCallback(async () => {
try {
setSavingState(true);
const response = await createSlackApi({
api_url: selectedConfig?.api_url || '',
channel: selectedConfig?.channel || '',
name: selectedConfig?.name || '',
send_resolved: true,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
});
const response = await createSlackApi(prepareSlackRequest());
if (response.statusCode === 200) {
notifications.success({
message: 'Success',
description: 'Successfully created the channel',
description: t('channel_creation_done'),
});
setTimeout(() => {
history.replace(ROUTES.SETTINGS);
@ -79,21 +86,20 @@ function CreateAlertChannels({
} else {
notifications.error({
message: 'Error',
description: response.error || 'Error while creating the channel',
description: response.error || t('channel_creation_failed'),
});
}
setSavingState(false);
} catch (error) {
notifications.error({
message: 'Error',
description:
'An unexpected error occurred while creating this channel, please try again',
description: t('channel_creation_failed'),
});
setSavingState(false);
}
}, [notifications, selectedConfig]);
}, [prepareSlackRequest, t, notifications]);
const onWebhookHandler = useCallback(async () => {
const prepareWebhookRequest = useCallback(() => {
// initial api request without auth params
let request: WebhookChannel = {
api_url: selectedConfig?.api_url || '',
@ -101,39 +107,42 @@ function CreateAlertChannels({
send_resolved: true,
};
setSavingState(true);
try {
if (selectedConfig?.username !== '' || selectedConfig?.password !== '') {
if (selectedConfig?.username !== '') {
// if username is not null then password must be passed
if (selectedConfig?.password !== '') {
request = {
...request,
username: selectedConfig.username,
password: selectedConfig.password,
};
} else {
notifications.error({
message: 'Error',
description: 'A Password must be provided with user name',
});
}
} else if (selectedConfig?.password !== '') {
// only password entered, set bearer token
if (selectedConfig?.username !== '' || selectedConfig?.password !== '') {
if (selectedConfig?.username !== '') {
// if username is not null then password must be passed
if (selectedConfig?.password !== '') {
request = {
...request,
username: '',
username: selectedConfig.username,
password: selectedConfig.password,
};
} else {
notifications.error({
message: 'Error',
description: t('username_no_password'),
});
}
} else if (selectedConfig?.password !== '') {
// only password entered, set bearer token
request = {
...request,
username: '',
password: selectedConfig.password,
};
}
}
return request;
}, [notifications, t, selectedConfig]);
const onWebhookHandler = useCallback(async () => {
setSavingState(true);
try {
const request = prepareWebhookRequest();
const response = await createWebhookApi(request);
if (response.statusCode === 200) {
notifications.success({
message: 'Success',
description: 'Successfully created the channel',
description: t('channel_creation_done'),
});
setTimeout(() => {
history.replace(ROUTES.SETTINGS);
@ -141,19 +150,17 @@ function CreateAlertChannels({
} else {
notifications.error({
message: 'Error',
description: response.error || 'Error while creating the channel',
description: response.error || t('channel_creation_failed'),
});
}
} catch (error) {
notifications.error({
message: 'Error',
description:
'An unexpected error occurred while creating this channel, please try again',
description: t('channel_creation_failed'),
});
}
setSavingState(false);
}, [notifications, selectedConfig]);
}, [prepareWebhookRequest, t, notifications]);
const onSaveHandler = useCallback(
async (value: ChannelType) => {
switch (value) {
@ -166,11 +173,64 @@ function CreateAlertChannels({
default:
notifications.error({
message: 'Error',
description: 'channel type selected is invalid',
description: t('selected_channel_invalid'),
});
}
},
[onSlackHandler, onWebhookHandler, notifications],
[onSlackHandler, t, onWebhookHandler, notifications],
);
const performChannelTest = useCallback(
async (channelType: ChannelType) => {
setTestingState(true);
try {
let request;
let response;
switch (channelType) {
case WebhookType:
request = prepareWebhookRequest();
response = await testWebhookApi(request);
break;
case SlackType:
request = prepareSlackRequest();
response = await testSlackApi(request);
break;
default:
notifications.error({
message: 'Error',
description: t('test_unsupported'),
});
setTestingState(false);
return;
}
if (response.statusCode === 200) {
notifications.success({
message: 'Success',
description: t('channel_test_done'),
});
} else {
notifications.error({
message: 'Error',
description: t('channel_test_failed'),
});
}
} catch (error) {
notifications.error({
message: 'Error',
description: t('channel_test_unexpected'),
});
}
setTestingState(false);
},
[prepareWebhookRequest, t, prepareSlackRequest, notifications],
);
const onTestHandler = useCallback(
async (value: ChannelType) => {
performChannelTest(value);
},
[performChannelTest],
);
return (
@ -183,8 +243,9 @@ function CreateAlertChannels({
onTestHandler,
onSaveHandler,
savingState,
testingState,
NotificationElement,
title: 'New Notification Channels',
title: t('page_title_create'),
initialValue: {
type,
...selectedConfig,

View File

@ -1,6 +1,8 @@
import { Form, notification } from 'antd';
import editSlackApi from 'api/channels/editSlack';
import editWebhookApi from 'api/channels/editWebhook';
import testSlackApi from 'api/channels/testSlack';
import testWebhookApi from 'api/channels/testWebhook';
import ROUTES from 'constants/routes';
import {
ChannelType,
@ -12,11 +14,15 @@ import {
import FormAlertChannels from 'container/FormAlertChannels';
import history from 'lib/history';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
function EditAlertChannels({
initialValue,
}: EditAlertChannelsProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('channels');
const [formInstance] = Form.useForm();
const [selectedConfig, setSelectedConfig] = useState<
Partial<SlackChannel & WebhookChannel>
@ -24,6 +30,7 @@ function EditAlertChannels({
...initialValue,
});
const [savingState, setSavingState] = useState<boolean>(false);
const [testingState, setTestingState] = useState<boolean>(false);
const [notifications, NotificationElement] = notification.useNotification();
const { id } = useParams<{ id: string }>();
@ -35,9 +42,8 @@ function EditAlertChannels({
setType(value as ChannelType);
}, []);
const onSlackEditHandler = useCallback(async () => {
setSavingState(true);
const response = await editSlackApi({
const prepareSlackRequest = useCallback(() => {
return {
api_url: selectedConfig?.api_url || '',
channel: selectedConfig?.channel || '',
name: selectedConfig?.name || '',
@ -45,12 +51,27 @@ function EditAlertChannels({
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
id,
});
};
}, [id, selectedConfig]);
const onSlackEditHandler = useCallback(async () => {
setSavingState(true);
if (selectedConfig?.api_url === '') {
notifications.error({
message: 'Error',
description: t('webhook_url_required'),
});
setSavingState(false);
return;
}
const response = await editSlackApi(prepareSlackRequest());
if (response.statusCode === 200) {
notifications.success({
message: 'Success',
description: 'Channels Edited Successfully',
description: t('channel_edit_done'),
});
setTimeout(() => {
@ -59,15 +80,27 @@ function EditAlertChannels({
} else {
notifications.error({
message: 'Error',
description: response.error || 'error while updating the Channels',
description: response.error || t('channel_edit_failed'),
});
}
setSavingState(false);
}, [selectedConfig, notifications, id]);
}, [prepareSlackRequest, t, notifications, selectedConfig]);
const prepareWebhookRequest = useCallback(() => {
const { name, username, password } = selectedConfig;
return {
api_url: selectedConfig?.api_url || '',
name: name || '',
send_resolved: true,
username,
password,
id,
};
}, [id, selectedConfig]);
const onWebhookEditHandler = useCallback(async () => {
setSavingState(true);
const { name, username, password } = selectedConfig;
const { username, password } = selectedConfig;
const showError = (msg: string): void => {
notifications.error({
@ -77,40 +110,33 @@ function EditAlertChannels({
};
if (selectedConfig?.api_url === '') {
showError('Webhook URL is mandatory');
showError(t('webhook_url_required'));
setSavingState(false);
return;
}
if (username && (!password || password === '')) {
showError('Please enter a password');
showError(t('username_no_password'));
setSavingState(false);
return;
}
const response = await editWebhookApi({
api_url: selectedConfig?.api_url || '',
name: name || '',
send_resolved: true,
username,
password,
id,
});
const response = await editWebhookApi(prepareWebhookRequest());
if (response.statusCode === 200) {
notifications.success({
message: 'Success',
description: 'Channels Edited Successfully',
description: t('channel_edit_done'),
});
setTimeout(() => {
history.replace(ROUTES.SETTINGS);
}, 2000);
} else {
showError(response.error || 'error while updating the Channels');
showError(response.error || t('channel_edit_failed'));
}
setSavingState(false);
}, [selectedConfig, notifications, id]);
}, [prepareWebhookRequest, t, notifications, selectedConfig]);
const onSaveHandler = useCallback(
(value: ChannelType) => {
@ -123,9 +149,58 @@ function EditAlertChannels({
[onSlackEditHandler, onWebhookEditHandler],
);
const onTestHandler = useCallback(() => {
console.log('test');
}, []);
const performChannelTest = useCallback(
async (channelType: ChannelType) => {
setTestingState(true);
try {
let request;
let response;
switch (channelType) {
case WebhookType:
request = prepareWebhookRequest();
response = await testWebhookApi(request);
break;
case SlackType:
request = prepareSlackRequest();
response = await testSlackApi(request);
break;
default:
notifications.error({
message: 'Error',
description: t('test_unsupported'),
});
setTestingState(false);
return;
}
if (response.statusCode === 200) {
notifications.success({
message: 'Success',
description: t('channel_test_done'),
});
} else {
notifications.error({
message: 'Error',
description: t('channel_test_failed'),
});
}
} catch (error) {
notifications.error({
message: 'Error',
description: t('channel_test_failed'),
});
}
setTestingState(false);
},
[prepareWebhookRequest, t, prepareSlackRequest, notifications],
);
const onTestHandler = useCallback(
async (value: ChannelType) => {
performChannelTest(value);
},
[performChannelTest],
);
return (
<FormAlertChannels
@ -136,9 +211,10 @@ function EditAlertChannels({
type,
onTestHandler,
onSaveHandler,
testingState,
savingState,
NotificationElement,
title: 'Edit Notification Channels',
title: t('page_title_edit'),
initialValue,
nameDisable: true,
}}

View File

@ -1,15 +1,18 @@
import { Input } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SlackChannel } from '../../CreateAlertChannels/config';
const { TextArea } = Input;
function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
const { t } = useTranslation('channels');
return (
<>
<FormItem name="api_url" label="Webhook URL">
<FormItem name="api_url" label={t('field_webhook_url')}>
<Input
onChange={(event): void => {
setSelectedConfig((value) => ({
@ -22,8 +25,8 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
<FormItem
name="channel"
help="Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace),"
label="Recipient"
help={t('slack_channel_help')}
label={t('field_slack_recipient')}
>
<Input
onChange={(event): void =>
@ -35,7 +38,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
/>
</FormItem>
<FormItem name="title" label="Title">
<FormItem name="title" label={t('field_slack_title')}>
<TextArea
rows={4}
// value={`[{{ .Status | toUpper }}{{ if eq .Status \"firing\" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}\n{{- if gt (len .CommonLabels) (len .GroupLabels) -}}\n{{\" \"}}(\n{{- with .CommonLabels.Remove .GroupLabels.Names }}\n {{- range $index, $label := .SortedPairs -}}\n {{ if $index }}, {{ end }}\n {{- $label.Name }}=\"{{ $label.Value -}}\"\n {{- end }}\n{{- end -}}\n)\n{{- end }}`}
@ -48,7 +51,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
/>
</FormItem>
<FormItem name="text" label="Description">
<FormItem name="text" label={t('field_slack_description')}>
<TextArea
onChange={(event): void =>
setSelectedConfig((value) => ({
@ -56,7 +59,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
text: event.target.value,
}))
}
placeholder="description"
placeholder={t('placeholder_slack_description')}
/>
</FormItem>
</>

View File

@ -1,13 +1,16 @@
import { Input } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { WebhookChannel } from '../../CreateAlertChannels/config';
function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
const { t } = useTranslation('channels');
return (
<>
<FormItem name="api_url" label="Webhook URL">
<FormItem name="api_url" label={t('field_webhook_url')}>
<Input
onChange={(event): void => {
setSelectedConfig((value) => ({
@ -19,8 +22,8 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
</FormItem>
<FormItem
name="username"
label="User Name (optional)"
help="Leave empty for bearer auth or when authentication is not necessary."
label={t('field_webhook_username')}
help={t('help_webhook_username')}
>
<Input
onChange={(event): void => {
@ -34,7 +37,7 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
<FormItem
name="password"
label="Password (optional)"
help="Specify a password or bearer token"
help={t('help_webhook_password')}
>
<Input
type="password"

View File

@ -10,6 +10,7 @@ import {
} from 'container/CreateAlertChannels/config';
import history from 'lib/history';
import React from 'react';
import { useTranslation } from 'react-i18next';
import SlackSettings from './Settings/Slack';
import WebhookSettings from './Settings/Webhook';
@ -23,14 +24,17 @@ function FormAlertChannels({
type,
setSelectedConfig,
onTypeChangeHandler,
// onTestHandler,
onTestHandler,
onSaveHandler,
savingState,
testingState,
NotificationElement,
title,
initialValue,
nameDisable = false,
}: FormAlertChannelsProps): JSX.Element {
const { t } = useTranslation('channels');
const renderSettings = (): React.ReactElement | null => {
switch (type) {
case SlackType:
@ -48,7 +52,7 @@ function FormAlertChannels({
<Title level={3}>{title}</Title>
<Form initialValues={initialValue} layout="vertical" form={formInstance}>
<FormItem label="Name" labelAlign="left" name="name">
<FormItem label={t('field_channel_name')} labelAlign="left" name="name">
<Input
disabled={nameDisable}
onChange={(event): void => {
@ -60,7 +64,7 @@ function FormAlertChannels({
/>
</FormItem>
<FormItem label="Type" labelAlign="left" name="type">
<FormItem label={t('field_channel_type')} labelAlign="left" name="type">
<Select onChange={onTypeChangeHandler} value={type}>
<Option value="slack" key="slack">
Slack
@ -80,15 +84,21 @@ function FormAlertChannels({
type="primary"
onClick={(): void => onSaveHandler(type)}
>
Save
{t('button_save_channel')}
</Button>
<Button
disabled={testingState}
loading={testingState}
onClick={(): void => onTestHandler(type)}
>
{t('button_test_channel')}
</Button>
{/* <Button onClick={onTestHandler}>Test</Button> */}
<Button
onClick={(): void => {
history.replace(ROUTES.SETTINGS);
}}
>
Back
{t('button_return')}
</Button>
</FormItem>
</Form>
@ -102,6 +112,8 @@ interface FormAlertChannelsProps {
setSelectedConfig: React.Dispatch<React.SetStateAction<Partial<SlackChannel>>>;
onTypeChangeHandler: (value: ChannelType) => void;
onSaveHandler: (props: ChannelType) => void;
onTestHandler: (props: ChannelType) => void;
testingState: boolean;
savingState: boolean;
NotificationElement: React.ReactElement<
unknown,