Merge pull request #1904 from bluewave-labs/feat/handleTestNotification

Implemented the handleTestNotification function.
This commit is contained in:
Alexander Holliday authored and GitHub committed 2025-03-19 08:59:22 -07:00
commit 97e4eb557d
7 files changed
+233 -28

No files matched your search

@@ -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}
/>
</TabPanel>
))}
@@ -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')}
</Button>
</DialogActions>
</Dialog>
@@ -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}
/>
</Box>
@@ -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}
/>
</Box>
);
@@ -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 ? (
<CircularProgress
size={theme.spacing(8)}
sx={{ mr: theme.spacing(1), color: theme.palette.accent.main}}
/>
) : null}
{t('notifications.testNotification')}
</Button>
</Box>
@@ -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;
+7 -5
View File
@@ -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" },
@@ -415,15 +417,15 @@ const CreateMonitor = () => {
onChange={(event) => handleNotifications(event, "email")}
/>
{/* <Box mt={theme.spacing(2)}>
<Box mt={theme.spacing(2)}>
<Button
variant="contained"
color="accent"
onClick={handleOpenNotificationModal}
>
Notification Integration
>
{t('notifications.integrationButton')}
</Button>
</Box> */}
</Box>
</Stack>
</ConfigBox>
+22
View File
@@ -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<AxiosResponse>} 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
+2
View File
@@ -11,6 +11,7 @@
"authLoginTitle": "Log In",
"authLoginEnterPassword": "Enter your password",
"commonPassword": "Password",
"commonSave": "Save",
"commonBack": "Back",
"authForgotPasswordTitle": "Forgot password?",
"authForgotPasswordResetPassword": "Reset password",
@@ -245,6 +246,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.",