From 7b1a60d6f967a1ea969e525ce8d6ed269de92f89 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Mon, 10 Mar 2025 16:32:56 -0700 Subject: [PATCH 01/18] Implemented the handleTestNotification function. --- .../NotificationIntegrationModal.jsx | 86 ++++++++++++++++++- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/src/Components/NotificationIntegrationModal/NotificationIntegrationModal.jsx b/src/Components/NotificationIntegrationModal/NotificationIntegrationModal.jsx index 30587833a..6608b7c65 100644 --- a/src/Components/NotificationIntegrationModal/NotificationIntegrationModal.jsx +++ b/src/Components/NotificationIntegrationModal/NotificationIntegrationModal.jsx @@ -1,5 +1,7 @@ import { useState, useMemo } from "react"; import { useTranslation } from "react-i18next"; +import { toast } from 'react-toastify'; + import { Dialog, DialogContent, @@ -129,11 +131,87 @@ const NotificationIntegrationModal = ({ })); }; - const handleTestNotification = (type) => { - console.log(`Testing ${type} notification`); - //implement the test notification functionality - }; + const handleTestNotification = async (type) => { + // Get the notification type details + const notificationType = activeNotificationTypes.find(t => t.id === type); + + if (notificationType === undefined) { + return; + } + + // Helper to get the field state key + const getFieldKey = (typeId, fieldId) => { + return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`; + }; + + // Prepare request payload based on notification type + let payload = { platform: type }; + + switch(type) { + case 'slack': + payload.webhookUrl = integrations[getFieldKey('slack', 'webhook')]; + if (!payload.webhookUrl) { + toast.error('Please enter a Slack webhook URL first.'); + return; + } + break; + + case 'discord': + payload.webhookUrl = integrations[getFieldKey('discord', 'webhook')]; + if (!payload.webhookUrl) { + toast.error('Please enter a Discord webhook URL first.'); + return; + } + break; + + case 'telegram': + payload.botToken = integrations[getFieldKey('telegram', 'token')]; + payload.chatId = integrations[getFieldKey('telegram', 'chatId')]; + if (!payload.botToken || !payload.chatId) { + toast.error('Please enter both Telegram bot token and chat ID.'); + return; + } + break; + + case 'webhook': + payload.webhookUrl = integrations[getFieldKey('webhook', 'url')]; + payload.platform = 'slack'; // Use slack as platform for webhooks + if (!payload.webhookUrl) { + toast.error('Please enter a webhook URL first.'); + return; + } + break; + + default: + toast.error('This notification type cannot be tested.'); + return; + } + + try { + const apiUrl = `${import.meta.env.VITE_APP_API_BASE_URL || 'http://localhost:5000'}/api/v1/notifications/test-webhook`; + + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload), + }); + + const data = await response.json(); + + if (response.ok && data.success) { + toast.success('Test notification sent successfully!'); + } else { + throw new Error(data.msg || 'Failed to send test notification'); + } + } catch (error) { + toast.error(`Failed to send test notification: ${error.message}`); + } + }; + + const handleSave = () => { //notifications array for selected integrations const notifications = [...(monitor?.notifications || [])]; From 9aa00410221013b78d18dee41a1b95da88ce2955 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Tue, 11 Mar 2025 18:37:14 -0700 Subject: [PATCH 02/18] Used the network service class for network requests. --- .../NotificationIntegrationModal.jsx | 83 +++------------ .../{ => Components}/TabComponent.jsx | 18 ++-- .../{ => Components}/TabPanel.jsx | 0 .../Hooks/useNotification.js | 100 ++++++++++++++++++ src/Pages/Uptime/Create/index.jsx | 6 +- 5 files changed, 131 insertions(+), 76 deletions(-) rename src/Components/NotificationIntegrationModal/{ => Components}/NotificationIntegrationModal.jsx (78%) rename src/Components/NotificationIntegrationModal/{ => Components}/TabComponent.jsx (86%) rename src/Components/NotificationIntegrationModal/{ => Components}/TabPanel.jsx (100%) create mode 100644 src/Components/NotificationIntegrationModal/Hooks/useNotification.js diff --git a/src/Components/NotificationIntegrationModal/NotificationIntegrationModal.jsx b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx similarity index 78% rename from src/Components/NotificationIntegrationModal/NotificationIntegrationModal.jsx rename to src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx index 6608b7c65..85b5232f3 100644 --- a/src/Components/NotificationIntegrationModal/NotificationIntegrationModal.jsx +++ b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx @@ -1,6 +1,5 @@ import { useState, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { toast } from 'react-toastify'; import { Dialog, @@ -10,11 +9,13 @@ import { Typography, Box, Tabs, - Tab + Tab, + CircularProgress } from "@mui/material"; import { useTheme } from "@emotion/react"; import TabPanel from "./TabPanel"; import TabComponent from "./TabComponent"; +import useNotifications from "../Hooks/useNotification"; const NotificationIntegrationModal = ({ open, @@ -28,6 +29,8 @@ const NotificationIntegrationModal = ({ const theme = useTheme(); const [tabValue, setTabValue] = useState(0); + const { loading, sendTestNotification } = useNotifications(); + // Define notification types const DEFAULT_NOTIFICATION_TYPES = [ { @@ -144,74 +147,18 @@ const NotificationIntegrationModal = ({ return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`; }; - // Prepare request payload based on notification type - let payload = { platform: type }; + // Prepare config object based on notification type + const config = {}; - switch(type) { - case 'slack': - payload.webhookUrl = integrations[getFieldKey('slack', 'webhook')]; - if (!payload.webhookUrl) { - toast.error('Please enter a Slack webhook URL first.'); - return; - } - break; - - case 'discord': - payload.webhookUrl = integrations[getFieldKey('discord', 'webhook')]; - if (!payload.webhookUrl) { - toast.error('Please enter a Discord webhook URL first.'); - return; - } - break; - - case 'telegram': - payload.botToken = integrations[getFieldKey('telegram', 'token')]; - payload.chatId = integrations[getFieldKey('telegram', 'chatId')]; - if (!payload.botToken || !payload.chatId) { - toast.error('Please enter both Telegram bot token and chat ID.'); - return; - } - break; - - case 'webhook': - payload.webhookUrl = integrations[getFieldKey('webhook', 'url')]; - payload.platform = 'slack'; // Use slack as platform for webhooks - if (!payload.webhookUrl) { - toast.error('Please enter a webhook URL first.'); - return; - } - break; - - default: - toast.error('This notification type cannot be tested.'); - return; - } + // Add each field value to the config object + notificationType.fields.forEach(field => { + const fieldKey = getFieldKey(type, field.id); + config[field.id] = integrations[fieldKey]; + }); - try { - const apiUrl = `${import.meta.env.VITE_APP_API_BASE_URL || 'http://localhost:5000'}/api/v1/notifications/test-webhook`; - - - const response = await fetch(apiUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload), - }); - - const data = await response.json(); - - if (response.ok && data.success) { - toast.success('Test notification sent successfully!'); - } else { - throw new Error(data.msg || 'Failed to send test notification'); - } - } catch (error) { - toast.error(`Failed to send test notification: ${error.message}`); - } + await sendTestNotification(type, config); }; - const handleSave = () => { //notifications array for selected integrations const notifications = [...(monitor?.notifications || [])]; @@ -318,6 +265,7 @@ const NotificationIntegrationModal = ({ handleIntegrationChange={handleIntegrationChange} handleInputChange={handleInputChange} handleTestNotification={handleTestNotification} + isLoading={loading} /> ))} @@ -335,13 +283,14 @@ const NotificationIntegrationModal = ({ variant="contained" color="accent" onClick={handleSave} + disabled={loading} sx={{ width: 'auto', minWidth: theme.spacing(60), px: theme.spacing(8) }} > - {t('common.save', 'Save')} + {loading ? : t('common.save', 'Save')} diff --git a/src/Components/NotificationIntegrationModal/TabComponent.jsx b/src/Components/NotificationIntegrationModal/Components/TabComponent.jsx similarity index 86% rename from src/Components/NotificationIntegrationModal/TabComponent.jsx rename to src/Components/NotificationIntegrationModal/Components/TabComponent.jsx index 89216a8c1..df13ca6f6 100644 --- a/src/Components/NotificationIntegrationModal/TabComponent.jsx +++ b/src/Components/NotificationIntegrationModal/Components/TabComponent.jsx @@ -2,19 +2,21 @@ import React from "react"; import { Typography, Box, - Button + Button, + CircularProgress } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; -import TextInput from "../../../src/Components/Inputs/TextInput"; -import Checkbox from "../../../src/Components/Inputs/Checkbox"; +import TextInput from "../../../Components/Inputs/TextInput"; +import Checkbox from "../../../Components/Inputs/Checkbox"; const TabComponent = ({ type, integrations, handleIntegrationChange, handleInputChange, - handleTestNotification + handleTestNotification, + isLoading }) => { const theme = useTheme(); const { t } = useTranslation(); @@ -55,6 +57,7 @@ const TabComponent = ({ label={t('notifications.enableNotifications', { platform: type.label })} isChecked={integrations[type.id]} onChange={(e) => handleIntegrationChange(type.id, e.target.checked)} + disabled={isLoading} /> @@ -77,7 +80,7 @@ const TabComponent = ({ placeholder={field.placeholder} value={integrations[fieldKey]} onChange={(e) => handleInputChange(fieldKey, e.target.value)} - disabled={!integrations[type.id]} + disabled={!integrations[type.id] || isLoading} /> ); @@ -88,8 +91,11 @@ const TabComponent = ({ variant="text" color="info" onClick={() => handleTestNotification(type.id)} - disabled={!integrations[type.id] || !areAllFieldsFilled()} + disabled={!integrations[type.id] || !areAllFieldsFilled() || isLoading} > + {isLoading ? ( + + ) : null} {t('notifications.testNotification')} diff --git a/src/Components/NotificationIntegrationModal/TabPanel.jsx b/src/Components/NotificationIntegrationModal/Components/TabPanel.jsx similarity index 100% rename from src/Components/NotificationIntegrationModal/TabPanel.jsx rename to src/Components/NotificationIntegrationModal/Components/TabPanel.jsx diff --git a/src/Components/NotificationIntegrationModal/Hooks/useNotification.js b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js new file mode 100644 index 000000000..03ef99163 --- /dev/null +++ b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js @@ -0,0 +1,100 @@ +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { useTranslation } from "react-i18next"; +import { networkService } from '../../../Utils/NetworkService'; + +/** + * Custom hook for notification-related operations + */ +const useNotifications = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { t } = useTranslation(); + + /** + * Send a test notification + * @param {string} type - The notification type (slack, discord, telegram, webhook) + * @param {object} config - Configuration object with necessary params + */ + const sendTestNotification = async (type, config) => { + setLoading(true); + setError(null); + + // Validation based on notification type + let payload = { platform: type }; + let isValid = true; + let errorMessage = ''; + + switch(type) { + case 'slack': + payload.webhookUrl = config.webhook; + if (!payload.webhookUrl) { + isValid = false; + errorMessage = t('notifications.slack.webhookRequired', 'Please enter a Slack webhook URL first.'); + } + break; + + case 'discord': + payload.webhookUrl = config.webhook; + if (!payload.webhookUrl) { + isValid = false; + errorMessage = t('notifications.discord.webhookRequired', 'Please enter a Discord webhook URL first.'); + } + break; + + case 'telegram': + payload.botToken = config.token; + payload.chatId = config.chatId; + if (!payload.botToken || !payload.chatId) { + isValid = false; + errorMessage = t('notifications.telegram.fieldsRequired', 'Please enter both Telegram bot token and chat ID.'); + } + break; + + case 'webhook': + payload.webhookUrl = config.url; + payload.platform = 'slack'; // Use slack as platform for webhooks + if (!payload.webhookUrl) { + isValid = false; + errorMessage = t('notifications.webhook.urlRequired', 'Please enter a webhook URL first.'); + } + break; + + default: + isValid = false; + errorMessage = t('notifications.unsupportedType', 'This notification type cannot be tested.'); + } + + // If validation fails, show error and return + if (!isValid) { + toast.error(errorMessage); + setLoading(false); + return; + } + + try { + // Use your existing NetworkService to make the API call + const response = await networkService.axiosInstance.post('/notifications/test-webhook', payload); + + if (response.data.success) { + toast.success(t('notifications.testSuccess', 'Test notification sent successfully!')); + } else { + throw new Error(response.data.msg || t('notifications.testFailed', 'Failed to send test notification')); + } + } catch (error) { + const errorMsg = error.response?.data?.msg || error.message || t('notifications.networkError', 'Network error occurred'); + toast.error(`${t('notifications.testFailed', 'Failed to send test notification')}: ${errorMsg}`); + setError(errorMsg); + } finally { + setLoading(false); + } + }; + + return { + loading, + error, + sendTestNotification + }; +}; + +export default useNotifications; \ No newline at end of file diff --git a/src/Pages/Uptime/Create/index.jsx b/src/Pages/Uptime/Create/index.jsx index 3b295f586..05b42766d 100644 --- a/src/Pages/Uptime/Create/index.jsx +++ b/src/Pages/Uptime/Create/index.jsx @@ -23,7 +23,7 @@ import Radio from "../../../Components/Inputs/Radio"; import Checkbox from "../../../Components/Inputs/Checkbox"; import Select from "../../../Components/Inputs/Select"; import ConfigBox from "../../../Components/ConfigBox"; -import NotificationIntegrationModal from "../../../Components/NotificationIntegrationModal/NotificationIntegrationModal"; +import NotificationIntegrationModal from "../../../Components/NotificationIntegrationModal/Components/NotificationIntegrationModal"; const CreateMonitor = () => { const MS_PER_MINUTE = 60000; const SELECT_VALUES = [ @@ -413,7 +413,7 @@ const CreateMonitor = () => { onChange={(event) => handleNotifications(event, "email")} /> - {/* + - */} + From b811fb91ca0b921560f4d1d80aee1f32a1099a6e Mon Sep 17 00:00:00 2001 From: Skorpios Date: Tue, 11 Mar 2025 18:40:53 -0700 Subject: [PATCH 03/18] Use explicit type checking. --- .../Components/NotificationIntegrationModal.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx index 85b5232f3..b9e16f5d4 100644 --- a/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx +++ b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx @@ -138,7 +138,7 @@ const NotificationIntegrationModal = ({ // Get the notification type details const notificationType = activeNotificationTypes.find(t => t.id === type); - if (notificationType === undefined) { + if (typeof notificationType === undefined) { return; } From f7b247fa9d123c70e2d92085f5965530815463b0 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Tue, 11 Mar 2025 18:53:35 -0700 Subject: [PATCH 04/18] Error handling and a more safe approach for the getFieldKey function. --- .../NotificationIntegrationModal.jsx | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx index b9e16f5d4..d0b199c5b 100644 --- a/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx +++ b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx @@ -31,6 +31,20 @@ const NotificationIntegrationModal = ({ const { loading, sendTestNotification } = useNotifications(); + // Helper to get the field state key with error handling + const getFieldKey = (typeId, fieldId) => { + try { + if (typeof typeId !== 'string' || typeId === '' || + typeof fieldId !== 'string' || fieldId === '') { + return ''; + } + return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`; + } catch (error) { + console.error('Error generating field key:', error); + return ''; + } + }; + // Define notification types const DEFAULT_NOTIFICATION_TYPES = [ { @@ -106,7 +120,7 @@ const NotificationIntegrationModal = ({ // Add state for each field in the notification type type.fields.forEach(field => { - const fieldKey = `${type.id}${field.id.charAt(0).toUpperCase() + field.id.slice(1)}`; + const fieldKey = getFieldKey(type.id, field.id); state[fieldKey] = monitor?.notifications?.find(n => n.type === type.id)?.[field.id] || ""; }); }); @@ -138,15 +152,10 @@ const NotificationIntegrationModal = ({ // Get the notification type details const notificationType = activeNotificationTypes.find(t => t.id === type); - if (typeof notificationType === undefined) { + if (typeof notificationType === "undefined") { return; } - // Helper to get the field state key - const getFieldKey = (typeId, fieldId) => { - return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`; - }; - // Prepare config object based on notification type const config = {}; @@ -180,7 +189,7 @@ const NotificationIntegrationModal = ({ // Add each field value to the notification object type.fields.forEach(field => { - const fieldKey = `${type.id}${field.id.charAt(0).toUpperCase() + field.id.slice(1)}`; + const fieldKey = getFieldKey(type.id, field.id); notificationObject[field.id] = integrations[fieldKey]; }); From 798a680fc6eb711960b5e07200cb9140c120fb78 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Tue, 11 Mar 2025 19:06:57 -0700 Subject: [PATCH 05/18] Got rid of magic numbers in the modal. --- .../NotificationIntegrationModal.jsx | 34 ++++++++++++++----- .../Hooks/useNotification.js | 2 +- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx index d0b199c5b..658ac839c 100644 --- a/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx +++ b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx @@ -17,6 +17,22 @@ import TabPanel from "./TabPanel"; import TabComponent from "./TabComponent"; import useNotifications from "../Hooks/useNotification"; +// Define constants for notification types to avoid magic values +const NOTIFICATION_TYPES = { + SLACK: 'slack', + DISCORD: 'discord', + TELEGRAM: 'telegram', + WEBHOOK: 'webhook' +}; + +// Define constants for field IDs +const FIELD_IDS = { + WEBHOOK: 'webhook', + TOKEN: 'token', + CHAT_ID: 'chatId', + URL: 'url' +}; + const NotificationIntegrationModal = ({ open, onClose, @@ -48,12 +64,12 @@ const NotificationIntegrationModal = ({ // Define notification types const DEFAULT_NOTIFICATION_TYPES = [ { - id: 'slack', + id: NOTIFICATION_TYPES.SLACK, label: t('notifications.slack.label'), description: t('notifications.slack.description'), fields: [ { - id: 'webhook', + id: FIELD_IDS.WEBHOOK, label: t('notifications.slack.webhookLabel'), placeholder: t('notifications.slack.webhookPlaceholder'), type: 'text' @@ -61,12 +77,12 @@ const NotificationIntegrationModal = ({ ] }, { - id: 'discord', + id: NOTIFICATION_TYPES.DISCORD, label: t('notifications.discord.label'), description: t('notifications.discord.description'), fields: [ { - id: 'webhook', + id: FIELD_IDS.WEBHOOK, label: t('notifications.discord.webhookLabel'), placeholder: t('notifications.discord.webhookPlaceholder'), type: 'text' @@ -74,18 +90,18 @@ const NotificationIntegrationModal = ({ ] }, { - id: 'telegram', + id: NOTIFICATION_TYPES.TELEGRAM, label: t('notifications.telegram.label'), description: t('notifications.telegram.description'), fields: [ { - id: 'token', + id: FIELD_IDS.TOKEN, label: t('notifications.telegram.tokenLabel'), placeholder: t('notifications.telegram.tokenPlaceholder'), type: 'text' }, { - id: 'chatId', + id: FIELD_IDS.CHAT_ID, label: t('notifications.telegram.chatIdLabel'), placeholder: t('notifications.telegram.chatIdPlaceholder'), type: 'text' @@ -93,12 +109,12 @@ const NotificationIntegrationModal = ({ ] }, { - id: 'webhook', + id: NOTIFICATION_TYPES.WEBHOOK, label: t('notifications.webhook.label'), description: t('notifications.webhook.description'), fields: [ { - id: 'url', + id: FIELD_IDS.URL, label: t('notifications.webhook.urlLabel'), placeholder: t('notifications.webhook.urlPlaceholder'), type: 'text' diff --git a/src/Components/NotificationIntegrationModal/Hooks/useNotification.js b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js index 03ef99163..aceba2172 100644 --- a/src/Components/NotificationIntegrationModal/Hooks/useNotification.js +++ b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js @@ -53,7 +53,7 @@ const useNotifications = () => { case 'webhook': payload.webhookUrl = config.url; - payload.platform = 'slack'; // Use slack as platform for webhooks + payload.platform = 'slack'; if (!payload.webhookUrl) { isValid = false; errorMessage = t('notifications.webhook.urlRequired', 'Please enter a webhook URL first.'); From 8e026ede1b184861baaf2ce08f4c02b8efe8e99a Mon Sep 17 00:00:00 2001 From: Skorpios Date: Tue, 11 Mar 2025 19:12:00 -0700 Subject: [PATCH 06/18] Refactored out constant keys. --- .../Hooks/useNotification.js | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/Components/NotificationIntegrationModal/Hooks/useNotification.js b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js index aceba2172..45e273e12 100644 --- a/src/Components/NotificationIntegrationModal/Hooks/useNotification.js +++ b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js @@ -3,6 +3,22 @@ import { toast } from 'react-toastify'; import { useTranslation } from "react-i18next"; import { networkService } from '../../../Utils/NetworkService'; +// Define constants for notification types to avoid magic values +const NOTIFICATION_TYPES = { + SLACK: 'slack', + DISCORD: 'discord', + TELEGRAM: 'telegram', + WEBHOOK: 'webhook' +}; + +// Define constants for field IDs +const FIELD_IDS = { + WEBHOOK: 'webhook', + TOKEN: 'token', + CHAT_ID: 'chatId', + URL: 'url' +}; + /** * Custom hook for notification-related operations */ @@ -26,35 +42,36 @@ const useNotifications = () => { let errorMessage = ''; switch(type) { - case 'slack': + case NOTIFICATION_TYPES.SLACK: payload.webhookUrl = config.webhook; - if (!payload.webhookUrl) { + if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') { isValid = false; errorMessage = t('notifications.slack.webhookRequired', 'Please enter a Slack webhook URL first.'); } break; - case 'discord': + case NOTIFICATION_TYPES.DISCORD: payload.webhookUrl = config.webhook; - if (!payload.webhookUrl) { + if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') { isValid = false; errorMessage = t('notifications.discord.webhookRequired', 'Please enter a Discord webhook URL first.'); } break; - case 'telegram': + case NOTIFICATION_TYPES.TELEGRAM: payload.botToken = config.token; payload.chatId = config.chatId; - if (!payload.botToken || !payload.chatId) { + if (typeof payload.botToken === 'undefined' || payload.botToken === '' || + typeof payload.chatId === 'undefined' || payload.chatId === '') { isValid = false; errorMessage = t('notifications.telegram.fieldsRequired', 'Please enter both Telegram bot token and chat ID.'); } break; - case 'webhook': + case NOTIFICATION_TYPES.WEBHOOK: payload.webhookUrl = config.url; - payload.platform = 'slack'; - if (!payload.webhookUrl) { + payload.platform = NOTIFICATION_TYPES.SLACK; // Use slack as platform for webhooks + if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') { isValid = false; errorMessage = t('notifications.webhook.urlRequired', 'Please enter a webhook URL first.'); } @@ -66,17 +83,16 @@ const useNotifications = () => { } // If validation fails, show error and return - if (!isValid) { + if (isValid === false) { toast.error(errorMessage); setLoading(false); return; } try { - // Use your existing NetworkService to make the API call const response = await networkService.axiosInstance.post('/notifications/test-webhook', payload); - if (response.data.success) { + if (response.data.success === true) { toast.success(t('notifications.testSuccess', 'Test notification sent successfully!')); } else { throw new Error(response.data.msg || t('notifications.testFailed', 'Failed to send test notification')); From f735b082a370e6de5abab5b4865d146d80bb47d5 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Tue, 11 Mar 2025 19:14:42 -0700 Subject: [PATCH 07/18] Removed comment --- .../NotificationIntegrationModal/Hooks/useNotification.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/NotificationIntegrationModal/Hooks/useNotification.js b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js index 45e273e12..476128c6a 100644 --- a/src/Components/NotificationIntegrationModal/Hooks/useNotification.js +++ b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js @@ -70,7 +70,7 @@ const useNotifications = () => { case NOTIFICATION_TYPES.WEBHOOK: payload.webhookUrl = config.url; - payload.platform = NOTIFICATION_TYPES.SLACK; // Use slack as platform for webhooks + payload.platform = NOTIFICATION_TYPES.SLACK; if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') { isValid = false; errorMessage = t('notifications.webhook.urlRequired', 'Please enter a webhook URL first.'); From 00700c8651856af0d3a165af1570094ecf5738c7 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 16 Mar 2025 13:12:13 -0700 Subject: [PATCH 08/18] Return an array rather than an object. --- .../Components/NotificationIntegrationModal.jsx | 2 +- .../NotificationIntegrationModal/Hooks/useNotification.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx index 658ac839c..dcd194048 100644 --- a/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx +++ b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx @@ -45,7 +45,7 @@ const NotificationIntegrationModal = ({ const theme = useTheme(); const [tabValue, setTabValue] = useState(0); - const { loading, sendTestNotification } = useNotifications(); + const [loading, _, sendTestNotification] = useNotifications(); // Helper to get the field state key with error handling const getFieldKey = (typeId, fieldId) => { diff --git a/src/Components/NotificationIntegrationModal/Hooks/useNotification.js b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js index 476128c6a..b4f38dc80 100644 --- a/src/Components/NotificationIntegrationModal/Hooks/useNotification.js +++ b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js @@ -106,11 +106,11 @@ const useNotifications = () => { } }; - return { + return [ loading, error, sendTestNotification - }; + ]; }; export default useNotifications; \ No newline at end of file From 8994b4b39bb52d7e3bf3e4fbfb3198a0d9c7eccc Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 16 Mar 2025 13:15:25 -0700 Subject: [PATCH 09/18] Use undefined instead of null for conventional purposes. --- .../NotificationIntegrationModal/Hooks/useNotification.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Components/NotificationIntegrationModal/Hooks/useNotification.js b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js index b4f38dc80..20b6149b6 100644 --- a/src/Components/NotificationIntegrationModal/Hooks/useNotification.js +++ b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js @@ -24,7 +24,7 @@ const FIELD_IDS = { */ const useNotifications = () => { const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState(undefined); const { t } = useTranslation(); /** @@ -34,7 +34,7 @@ const useNotifications = () => { */ const sendTestNotification = async (type, config) => { setLoading(true); - setError(null); + setError(undefined); // Validation based on notification type let payload = { platform: type }; From 182c51387f71d60d7518f5a4a53707854ea183fb Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 16 Mar 2025 13:28:32 -0700 Subject: [PATCH 10/18] Abort early if the typeId or fieldId are not present. --- .../Components/NotificationIntegrationModal.jsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx index dcd194048..0bcb8cf37 100644 --- a/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx +++ b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx @@ -49,16 +49,15 @@ const NotificationIntegrationModal = ({ // Helper to get the field state key with error handling const getFieldKey = (typeId, fieldId) => { - try { - if (typeof typeId !== 'string' || typeId === '' || - typeof fieldId !== 'string' || fieldId === '') { - return ''; - } - return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`; - } catch (error) { - console.error('Error generating field key:', error); - return ''; + if (typeof typeId !== 'string' || typeId === '') { + throw new Error('Invalid typeId provided to getFieldKey'); + } + + if (typeof fieldId !== 'string' || fieldId === '') { + throw new Error('Invalid fieldId provided to getFieldKey'); } + + return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`; }; // Define notification types From 910defbc47a582a2754539eca6b52544631beee6 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 16 Mar 2025 13:43:19 -0700 Subject: [PATCH 11/18] Used the theme for the circular progress. --- .../Components/NotificationIntegrationModal.jsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx index 0bcb8cf37..abca40a65 100644 --- a/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx +++ b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx @@ -314,7 +314,15 @@ const NotificationIntegrationModal = ({ px: theme.spacing(8) }} > - {loading ? : t('common.save', 'Save')} + {loading ? + + : t('common.save', 'Save') + } From 4cbcebb445497a62502d98a7c8369546cf6293af Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 16 Mar 2025 13:49:34 -0700 Subject: [PATCH 12/18] Used theme for sizes and margins. --- .../Components/TabComponent.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Components/NotificationIntegrationModal/Components/TabComponent.jsx b/src/Components/NotificationIntegrationModal/Components/TabComponent.jsx index df13ca6f6..b0e87884d 100644 --- a/src/Components/NotificationIntegrationModal/Components/TabComponent.jsx +++ b/src/Components/NotificationIntegrationModal/Components/TabComponent.jsx @@ -94,7 +94,11 @@ const TabComponent = ({ disabled={!integrations[type.id] || !areAllFieldsFilled() || isLoading} > {isLoading ? ( - + ) : null} {t('notifications.testNotification')} From 13b8bb6656c98a1ae92b121fff058e54e56acae1 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 16 Mar 2025 13:58:53 -0700 Subject: [PATCH 13/18] Used theme for colors. --- .../NotificationIntegrationModal/Components/TabComponent.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Components/NotificationIntegrationModal/Components/TabComponent.jsx b/src/Components/NotificationIntegrationModal/Components/TabComponent.jsx index b0e87884d..e46a8cdd5 100644 --- a/src/Components/NotificationIntegrationModal/Components/TabComponent.jsx +++ b/src/Components/NotificationIntegrationModal/Components/TabComponent.jsx @@ -96,8 +96,7 @@ const TabComponent = ({ {isLoading ? ( ) : null} {t('notifications.testNotification')} From caab2c36b5fced3fa9ecad31cf72403e372e10cc Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 16 Mar 2025 14:04:56 -0700 Subject: [PATCH 14/18] Removed fallback strings. --- .../Hooks/useNotification.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Components/NotificationIntegrationModal/Hooks/useNotification.js b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js index 20b6149b6..60b9783a4 100644 --- a/src/Components/NotificationIntegrationModal/Hooks/useNotification.js +++ b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js @@ -46,7 +46,7 @@ const useNotifications = () => { payload.webhookUrl = config.webhook; if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') { isValid = false; - errorMessage = t('notifications.slack.webhookRequired', 'Please enter a Slack webhook URL first.'); + errorMessage = t('notifications.slack.webhookRequired'); } break; @@ -54,7 +54,7 @@ const useNotifications = () => { payload.webhookUrl = config.webhook; if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') { isValid = false; - errorMessage = t('notifications.discord.webhookRequired', 'Please enter a Discord webhook URL first.'); + errorMessage = t('notifications.discord.webhookRequired'); } break; @@ -64,7 +64,7 @@ const useNotifications = () => { if (typeof payload.botToken === 'undefined' || payload.botToken === '' || typeof payload.chatId === 'undefined' || payload.chatId === '') { isValid = false; - errorMessage = t('notifications.telegram.fieldsRequired', 'Please enter both Telegram bot token and chat ID.'); + errorMessage = t('notifications.telegram.fieldsRequired'); } break; @@ -73,13 +73,13 @@ const useNotifications = () => { payload.platform = NOTIFICATION_TYPES.SLACK; if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') { isValid = false; - errorMessage = t('notifications.webhook.urlRequired', 'Please enter a webhook URL first.'); + errorMessage = t('notifications.webhook.urlRequired'); } break; default: isValid = false; - errorMessage = t('notifications.unsupportedType', 'This notification type cannot be tested.'); + errorMessage = t('notifications.unsupportedType'); } // If validation fails, show error and return @@ -93,13 +93,13 @@ const useNotifications = () => { const response = await networkService.axiosInstance.post('/notifications/test-webhook', payload); if (response.data.success === true) { - toast.success(t('notifications.testSuccess', 'Test notification sent successfully!')); + toast.success(t('notifications.testSuccess')); } else { - throw new Error(response.data.msg || t('notifications.testFailed', 'Failed to send test notification')); + throw new Error(response.data.msg || t('notifications.testFailed')); } } catch (error) { - const errorMsg = error.response?.data?.msg || error.message || t('notifications.networkError', 'Network error occurred'); - toast.error(`${t('notifications.testFailed', 'Failed to send test notification')}: ${errorMsg}`); + const errorMsg = error.response?.data?.msg || error.message || t('notifications.networkError'); + toast.error(`${t('notifications.testFailed')}: ${errorMsg}`); setError(errorMsg); } finally { setLoading(false); From c84a547f35c4c7d0352a3c75a3b42fac1f8b5208 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 16 Mar 2025 14:46:17 -0700 Subject: [PATCH 15/18] Extracted string to use i18n implementation. --- .../Hooks/useNotification.js | 5 ++++- src/Pages/Uptime/Create/index.jsx | 6 +++-- src/Utils/NetworkService.js | 22 +++++++++++++++++++ src/locales/en.json | 1 + 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/Components/NotificationIntegrationModal/Hooks/useNotification.js b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js index 60b9783a4..08343760b 100644 --- a/src/Components/NotificationIntegrationModal/Hooks/useNotification.js +++ b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js @@ -90,7 +90,10 @@ const useNotifications = () => { } try { - const response = await networkService.axiosInstance.post('/notifications/test-webhook', payload); + const response = await networkService.testNotification({ + platform: type, + payload: payload + }); if (response.data.success === true) { toast.success(t('notifications.testSuccess')); diff --git a/src/Pages/Uptime/Create/index.jsx b/src/Pages/Uptime/Create/index.jsx index 05b42766d..2f0908c24 100644 --- a/src/Pages/Uptime/Create/index.jsx +++ b/src/Pages/Uptime/Create/index.jsx @@ -4,6 +4,7 @@ import { useNavigate, useParams } from "react-router-dom"; import { useEffect } from "react"; import { useState } from "react"; import { useSelector, useDispatch } from "react-redux"; +import { useTranslation } from "react-i18next"; // Utility and Network import { checkEndpointResolution } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; @@ -74,6 +75,7 @@ const CreateMonitor = () => { const dispatch = useDispatch(); const navigate = useNavigate(); const theme = useTheme(); + const { t } = useTranslation(); const { monitorId } = useParams(); const crumbs = [ { name: "uptime", path: "/uptime" }, @@ -418,8 +420,8 @@ const CreateMonitor = () => { variant="contained" color="accent" onClick={handleOpenNotificationModal} - > - Notification Integration + > + {t('notifications.integrationButton')} diff --git a/src/Utils/NetworkService.js b/src/Utils/NetworkService.js index ec8a10abf..8924d6466 100644 --- a/src/Utils/NetworkService.js +++ b/src/Utils/NetworkService.js @@ -669,6 +669,28 @@ class NetworkService { }); } + /** + * ************************************ + * Test a notification integration + * ************************************ + * + * @async + * @param {Object} config - The configuration object. + * @param {string} config.platform - The notification platform (slack, discord, telegram, webhook). + * @param {Object} config.payload - The payload with configuration for the notification. + * @returns {Promise} The response from the axios POST request. + */ + async testNotification(config) { + return this.axiosInstance.post('/notifications/test-webhook', { + platform: config.platform, + ...config.payload + }, { + headers: { + "Content-Type": "application/json", + }, + }); + } + /** * ************************************ * Creates a maintenance window diff --git a/src/locales/en.json b/src/locales/en.json index 7b99214d2..bf2cf3316 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -147,6 +147,7 @@ "enableNotifications": "Enable {{platform}} notifications", "testNotification": "Test notification", "addOrEditNotifications": "Add or edit notifications", + "integrationButton": "Notification Integration", "slack": { "label": "Slack", "description": "To enable Slack notifications, create a Slack app and enable incoming webhooks. After that, simply provide the webhook URL here.", From 100c5d3f21615e3c0e2f29f68f966febfab5ce30 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Mon, 17 Mar 2025 16:26:43 -0700 Subject: [PATCH 16/18] Use built-in loading prop for save button. --- .../Components/NotificationIntegrationModal.jsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx index abca40a65..5839a93a5 100644 --- a/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx +++ b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx @@ -307,22 +307,14 @@ const NotificationIntegrationModal = ({ variant="contained" color="accent" onClick={handleSave} - disabled={loading} + loading={loading} sx={{ width: 'auto', minWidth: theme.spacing(60), px: theme.spacing(8) }} > - {loading ? - - : t('common.save', 'Save') - } + {t('common.save', 'Save')} From 946263291f0522e78ff35c76524414a06fbfdb03 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Mon, 17 Mar 2025 16:33:13 -0700 Subject: [PATCH 17/18] Remove fallback string from save button. --- .../Components/NotificationIntegrationModal.jsx | 2 +- src/locales/en.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx index 5839a93a5..8077aa3a6 100644 --- a/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx +++ b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx @@ -314,7 +314,7 @@ const NotificationIntegrationModal = ({ px: theme.spacing(8) }} > - {t('common.save', 'Save')} + {t('commonSave')} diff --git a/src/locales/en.json b/src/locales/en.json index bf2cf3316..5bd53e08d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -11,6 +11,7 @@ "authLoginTitle": "Log In", "authLoginEnterPassword": "Enter your password", "commonPassword": "Password", + "commonSave": "Save", "commonBack": "Back", "authForgotPasswordTitle": "Forgot password?", "authForgotPasswordResetPassword": "Reset password", From 51da6b1fe9e7ace2e4be11bb6856f744b1acfdc4 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Tue, 18 Mar 2025 16:05:41 -0700 Subject: [PATCH 18/18] Updated the new locales file with the proper translation keys. --- src/locales/gb.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/locales/gb.json b/src/locales/gb.json index 662478270..91e664194 100644 --- a/src/locales/gb.json +++ b/src/locales/gb.json @@ -11,6 +11,7 @@ "authLoginTitle": "Log In", "authLoginEnterPassword": "Enter your password", "commonPassword": "Password", + "commonSave": "Save", "commonBack": "Back", "authForgotPasswordTitle": "Forgot password?", "authForgotPasswordResetPassword": "Reset password", @@ -147,6 +148,7 @@ "enableNotifications": "Enable {{platform}} notifications", "testNotification": "Test notification", "addOrEditNotifications": "Add or edit notifications", + "integrationButton": "Notification Integration", "slack": { "label": "Slack", "description": "To enable Slack notifications, create a Slack app and enable incoming webhooks. After that, simply provide the webhook URL here.",