From 2b5b79e34a2beda439feac7d2ad234187db50a48 Mon Sep 17 00:00:00 2001 From: Amol Umbark Date: Fri, 22 Apr 2022 16:56:18 +0530 Subject: [PATCH] (feature): UI for Test alert channels (#994) * (feature): Implemented test channel function for webhook and slack --- frontend/public/locales/en-GB/channels.json | 30 ++++ frontend/public/locales/en/channels.json | 30 ++++ frontend/src/api/channels/testSlack.ts | 35 ++++ frontend/src/api/channels/testWebhook.ts | 51 ++++++ .../container/CreateAlertChannels/index.tsx | 155 ++++++++++++------ .../src/container/EditAlertChannels/index.tsx | 126 +++++++++++--- .../FormAlertChannels/Settings/Slack.tsx | 15 +- .../FormAlertChannels/Settings/Webhook.tsx | 11 +- .../src/container/FormAlertChannels/index.tsx | 24 ++- 9 files changed, 389 insertions(+), 88 deletions(-) create mode 100644 frontend/public/locales/en-GB/channels.json create mode 100644 frontend/public/locales/en/channels.json create mode 100644 frontend/src/api/channels/testSlack.ts create mode 100644 frontend/src/api/channels/testWebhook.ts diff --git a/frontend/public/locales/en-GB/channels.json b/frontend/public/locales/en-GB/channels.json new file mode 100644 index 0000000000..1378ba73b8 --- /dev/null +++ b/frontend/public/locales/en-GB/channels.json @@ -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)" +} \ No newline at end of file diff --git a/frontend/public/locales/en/channels.json b/frontend/public/locales/en/channels.json new file mode 100644 index 0000000000..1378ba73b8 --- /dev/null +++ b/frontend/public/locales/en/channels.json @@ -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)" +} \ No newline at end of file diff --git a/frontend/src/api/channels/testSlack.ts b/frontend/src/api/channels/testSlack.ts new file mode 100644 index 0000000000..a2b4b1f40a --- /dev/null +++ b/frontend/src/api/channels/testSlack.ts @@ -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 | 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; diff --git a/frontend/src/api/channels/testWebhook.ts b/frontend/src/api/channels/testWebhook.ts new file mode 100644 index 0000000000..4b915e9a3a --- /dev/null +++ b/frontend/src/api/channels/testWebhook.ts @@ -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 | 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; diff --git a/frontend/src/container/CreateAlertChannels/index.tsx b/frontend/src/container/CreateAlertChannels/index.tsx index 02cd7b274a..dc3eee86a6 100644 --- a/frontend/src/container/CreateAlertChannels/index.tsx +++ b/frontend/src/container/CreateAlertChannels/index.tsx @@ -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(false); + const [testingState, setTestingState] = useState(false); const [notifications, NotificationElement] = notification.useNotification(); const [type, setType] = useState(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, diff --git a/frontend/src/container/EditAlertChannels/index.tsx b/frontend/src/container/EditAlertChannels/index.tsx index e4aab19d31..b8db5a0e9b 100644 --- a/frontend/src/container/EditAlertChannels/index.tsx +++ b/frontend/src/container/EditAlertChannels/index.tsx @@ -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 @@ -24,6 +30,7 @@ function EditAlertChannels({ ...initialValue, }); const [savingState, setSavingState] = useState(false); + const [testingState, setTestingState] = useState(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 ( - + { setSelectedConfig((value) => ({ @@ -22,8 +25,8 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element { @@ -35,7 +38,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element { /> - +