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 30587833a..8077aa3a6 100644
--- a/src/Components/NotificationIntegrationModal/NotificationIntegrationModal.jsx
+++ b/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx
@@ -1,5 +1,6 @@
import { useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
+
import {
Dialog,
DialogContent,
@@ -8,11 +9,29 @@ 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";
+
+// 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,
@@ -26,15 +45,30 @@ const NotificationIntegrationModal = ({
const theme = useTheme();
const [tabValue, setTabValue] = useState(0);
+ const [loading, _, sendTestNotification] = useNotifications();
+
+ // Helper to get the field state key with error handling
+ const getFieldKey = (typeId, fieldId) => {
+ 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
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'
@@ -42,12 +76,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'
@@ -55,18 +89,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'
@@ -74,12 +108,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'
@@ -101,7 +135,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] || "";
});
});
@@ -129,11 +163,26 @@ 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 (typeof notificationType === "undefined") {
+ return;
+ }
+
+ // Prepare config object based on notification type
+ const config = {};
+
+ // Add each field value to the config object
+ notificationType.fields.forEach(field => {
+ const fieldKey = getFieldKey(type, field.id);
+ config[field.id] = integrations[fieldKey];
+ });
+
+ await sendTestNotification(type, config);
};
-
+
const handleSave = () => {
//notifications array for selected integrations
const notifications = [...(monitor?.notifications || [])];
@@ -155,7 +204,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];
});
@@ -240,6 +289,7 @@ const NotificationIntegrationModal = ({
handleIntegrationChange={handleIntegrationChange}
handleInputChange={handleInputChange}
handleTestNotification={handleTestNotification}
+ isLoading={loading}
/>
))}
@@ -257,13 +307,14 @@ const NotificationIntegrationModal = ({
variant="contained"
color="accent"
onClick={handleSave}
+ loading={loading}
sx={{
width: 'auto',
minWidth: theme.spacing(60),
px: theme.spacing(8)
}}
>
- {t('common.save', 'Save')}
+ {t('commonSave')}
diff --git a/src/Components/NotificationIntegrationModal/TabComponent.jsx b/src/Components/NotificationIntegrationModal/Components/TabComponent.jsx
similarity index 84%
rename from src/Components/NotificationIntegrationModal/TabComponent.jsx
rename to src/Components/NotificationIntegrationModal/Components/TabComponent.jsx
index 89216a8c1..e46a8cdd5 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,14 @@ 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..08343760b
--- /dev/null
+++ b/src/Components/NotificationIntegrationModal/Hooks/useNotification.js
@@ -0,0 +1,119 @@
+import { useState } from 'react';
+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
+ */
+const useNotifications = () => {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+ 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(undefined);
+
+ // Validation based on notification type
+ let payload = { platform: type };
+ let isValid = true;
+ let errorMessage = '';
+
+ switch(type) {
+ case NOTIFICATION_TYPES.SLACK:
+ payload.webhookUrl = config.webhook;
+ if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') {
+ isValid = false;
+ errorMessage = t('notifications.slack.webhookRequired');
+ }
+ break;
+
+ case NOTIFICATION_TYPES.DISCORD:
+ payload.webhookUrl = config.webhook;
+ if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') {
+ isValid = false;
+ errorMessage = t('notifications.discord.webhookRequired');
+ }
+ break;
+
+ case NOTIFICATION_TYPES.TELEGRAM:
+ payload.botToken = config.token;
+ payload.chatId = config.chatId;
+ if (typeof payload.botToken === 'undefined' || payload.botToken === '' ||
+ typeof payload.chatId === 'undefined' || payload.chatId === '') {
+ isValid = false;
+ errorMessage = t('notifications.telegram.fieldsRequired');
+ }
+ break;
+
+ case NOTIFICATION_TYPES.WEBHOOK:
+ payload.webhookUrl = config.url;
+ payload.platform = NOTIFICATION_TYPES.SLACK;
+ if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') {
+ isValid = false;
+ errorMessage = t('notifications.webhook.urlRequired');
+ }
+ break;
+
+ default:
+ isValid = false;
+ errorMessage = t('notifications.unsupportedType');
+ }
+
+ // If validation fails, show error and return
+ if (isValid === false) {
+ toast.error(errorMessage);
+ setLoading(false);
+ return;
+ }
+
+ try {
+ const response = await networkService.testNotification({
+ platform: type,
+ payload: payload
+ });
+
+ if (response.data.success === true) {
+ toast.success(t('notifications.testSuccess'));
+ } else {
+ throw new Error(response.data.msg || t('notifications.testFailed'));
+ }
+ } catch (error) {
+ const errorMsg = error.response?.data?.msg || error.message || t('notifications.networkError');
+ toast.error(`${t('notifications.testFailed')}: ${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..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";
@@ -23,7 +24,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 = [
@@ -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" },
@@ -413,15 +415,15 @@ const CreateMonitor = () => {
onChange={(event) => handleNotifications(event, "email")}
/>
- {/*
+
- */}
+
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/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.",