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")} /> - {/* + - */} +