Merge pull request #1904 from bluewave-labs/feat/handleTestNotification
Implemented the handleTestNotification function.
This commit is contained in:
7 files changed
+233
-28
No files matched your search
+68
-17
@@ -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>
|
||||
+15
-6
@@ -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>
|
||||
File renamed without changes.
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in new issue
Block a user