diff --git a/client/src/Components/v1/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx b/client/src/Components/v1/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx deleted file mode 100644 index 2aae996f6..000000000 --- a/client/src/Components/v1/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx +++ /dev/null @@ -1,419 +0,0 @@ -import { useState, useMemo, useEffect, useCallback } from "react"; -import { useTranslation } from "react-i18next"; -import PropTypes from "prop-types"; - -import { - Dialog, - DialogContent, - DialogActions, - Button, - Typography, - Box, - Tabs, - Tab, - Stack, -} from "@mui/material"; -import { useTheme } from "@emotion/react"; -import TabPanel from "./TabPanel.jsx"; -import TabComponent from "./TabComponent.jsx"; -import useNotifications from "../Hooks/useNotification.js"; - -// 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, - monitor, - setMonitor, - // Optional prop to configure available notification types - notificationTypes = null, -}) => { - const { t } = useTranslation(); - const theme = useTheme(); - const [tabValue, setTabValue] = useState(0); - - const [loading, _, sendTestNotification] = useNotifications(); - - // Helper to get the field state key with error handling - const getFieldKey = useCallback( - (typeId, fieldId) => { - if (typeof typeId !== "string" || typeId === "") { - throw new Error(t("errorInvalidTypeId")); - } - - if (typeof fieldId !== "string" || fieldId === "") { - throw new Error(t("errorInvalidFieldId")); - } - - return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`; - }, - [t] - ); - - // Define notification types - const DEFAULT_NOTIFICATION_TYPES = [ - { - id: NOTIFICATION_TYPES.SLACK, - label: t("notifications.slack.label"), - description: t("notifications.slack.description"), - fields: [ - { - id: FIELD_IDS.WEBHOOK, - label: t("notifications.slack.webhookLabel"), - placeholder: t("notifications.slack.webhookPlaceholder"), - type: "text", - }, - ], - }, - { - id: NOTIFICATION_TYPES.DISCORD, - label: t("notifications.discord.label"), - description: t("notifications.discord.description"), - fields: [ - { - id: FIELD_IDS.WEBHOOK, - label: t("notifications.discord.webhookLabel"), - placeholder: t("notifications.discord.webhookPlaceholder"), - type: "text", - }, - ], - }, - { - id: NOTIFICATION_TYPES.TELEGRAM, - label: t("notifications.telegram.label"), - description: t("notifications.telegram.description"), - fields: [ - { - id: FIELD_IDS.TOKEN, - label: t("notifications.telegram.tokenLabel"), - placeholder: t("notifications.telegram.tokenPlaceholder"), - type: "text", - }, - { - id: FIELD_IDS.CHAT_ID, - label: t("notifications.telegram.chatIdLabel"), - placeholder: t("notifications.telegram.chatIdPlaceholder"), - type: "text", - }, - ], - }, - { - id: NOTIFICATION_TYPES.WEBHOOK, - label: t("notifications.webhook.label"), - description: t("notifications.webhook.description"), - fields: [ - { - id: FIELD_IDS.URL, - label: t("notifications.webhook.urlLabel"), - placeholder: t("notifications.webhook.urlPlaceholder"), - type: "text", - }, - ], - }, - ]; - - // Use provided notification types or default to our translated ones - const activeNotificationTypes = notificationTypes || DEFAULT_NOTIFICATION_TYPES; - - // Memoized function to initialize integrations state - const initialIntegrationsState = useMemo(() => { - const state = {}; - - activeNotificationTypes.forEach((type) => { - // Add enabled flag for each notification type - state[type.id] = false; - - // Add state for each field in the notification type - type.fields.forEach((field) => { - const fieldKey = getFieldKey(type.id, field.id); - state[fieldKey] = ""; - }); - }); - - return state; - }, [activeNotificationTypes, getFieldKey]); // Only recompute when these dependencies change - - const [integrations, setIntegrations] = useState(initialIntegrationsState); - - useEffect(() => { - if (open) { - const extractNotificationValues = () => { - const values = {}; - - if (!monitor?.notifications || !Array.isArray(monitor.notifications)) { - return values; - } - - monitor.notifications.forEach((notification) => { - // Handle notification based on its structure - if (notification.type === "webhook" && notification.platform) { - if (typeof notification.config === "undefined") return; - const platform = notification.platform; - values[platform] = true; // Set platform as enabled - - // Extract configuration based on platform - switch (platform) { - case NOTIFICATION_TYPES.SLACK: - case NOTIFICATION_TYPES.DISCORD: - if (notification.config.webhookUrl) { - values[getFieldKey(platform, FIELD_IDS.WEBHOOK)] = - notification.config.webhookUrl; - } - break; - case NOTIFICATION_TYPES.TELEGRAM: - if (notification.config.botToken) { - values[getFieldKey(platform, FIELD_IDS.TOKEN)] = - notification.config.botToken; - } - if (notification.config.chatId) { - values[getFieldKey(platform, FIELD_IDS.CHAT_ID)] = - notification.config.chatId; - } - break; - case NOTIFICATION_TYPES.WEBHOOK: - if (notification.config.webhookUrl) { - values[getFieldKey(platform, FIELD_IDS.URL)] = - notification.config.webhookUrl; - } - break; - } - } - }); - - return values; - }; - - const extractedValues = extractNotificationValues(); - setIntegrations((prev) => ({ - ...prev, - ...extractedValues, - })); - } - }, [open, monitor, getFieldKey]); - - const handleChangeTab = (event, newValue) => { - setTabValue(newValue); - }; - - const handleIntegrationChange = (type, checked) => { - setIntegrations((prev) => ({ - ...prev, - [type]: checked, - })); - }; - - const handleInputChange = (type, value) => { - setIntegrations((prev) => ({ - ...prev, - [type]: value, - })); - }; - - 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); - }; - - // In NotificationIntegrationModal.jsx, update the handleSave function: - - const handleSave = () => { - // Get existing notifications - const notifications = [...(monitor?.notifications || [])]; - - // Get all notification types IDs - const existingTypes = activeNotificationTypes.map((type) => type.id); - - // Filter out notifications that are configurable in this modal - const filteredNotifications = notifications.filter((notification) => { - if (notification.platform) { - return !existingTypes.includes(notification.platform); - } - - return !existingTypes.includes(notification.type); - }); - - // Add each enabled notification with its configured fields - activeNotificationTypes.forEach((type) => { - if (integrations[type.id]) { - let notificationObject = { - type: "webhook", - platform: type.id, // Set platform to identify the specific service - config: {}, - }; - - // Configure based on notification type - switch (type.id) { - case "slack": - case "discord": - notificationObject.config.webhookUrl = - integrations[getFieldKey(type.id, "webhook")]; - break; - case "telegram": - notificationObject.config.botToken = - integrations[getFieldKey(type.id, "token")]; - notificationObject.config.chatId = - integrations[getFieldKey(type.id, "chatId")]; - break; - case "webhook": - notificationObject.config.webhookUrl = - integrations[getFieldKey(type.id, "url")]; - break; - } - - filteredNotifications.push(notificationObject); - } - }); - - // Update monitor with new notifications - setMonitor((prev) => ({ - ...prev, - notifications: filteredNotifications, - })); - - onClose(); - }; - - return ( - - - - {/* Left sidebar with tabs */} - - - {t("notifications.addOrEditNotifications")} - - - - {activeNotificationTypes.map((type) => ( - - ))} - - - - {/* Right side content */} - - {activeNotificationTypes.map((type, index) => ( - - - - ))} - - - - - - - - ); -}; - -NotificationIntegrationModal.propTypes = { - open: PropTypes.bool.isRequired, - onClose: PropTypes.func.isRequired, - monitor: PropTypes.object.isRequired, - setMonitor: PropTypes.func.isRequired, - notificationTypes: PropTypes.array, -}; - -export default NotificationIntegrationModal; diff --git a/client/src/Components/v1/NotificationIntegrationModal/Components/TabComponent.jsx b/client/src/Components/v1/NotificationIntegrationModal/Components/TabComponent.jsx deleted file mode 100644 index 87fb2c301..000000000 --- a/client/src/Components/v1/NotificationIntegrationModal/Components/TabComponent.jsx +++ /dev/null @@ -1,115 +0,0 @@ -import React from "react"; -import { Typography, Box, Button, CircularProgress } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { useTheme } from "@emotion/react"; -import TextInput from "../../Inputs/TextInput/index.jsx"; -import Checkbox from "../../Inputs/Checkbox/index.jsx"; - -const TabComponent = ({ - type, - integrations, - handleIntegrationChange, - handleInputChange, - handleTestNotification, - isLoading, -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - - // Helper to get the field state key (e.g., slackWebhook, telegramToken) - const getFieldKey = (typeId, fieldId) => { - return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`; - }; - - // Check if all fields have values to enable test button - const areAllFieldsFilled = () => { - return type.fields.every((field) => { - const fieldKey = getFieldKey(type.id, field.id); - return integrations[fieldKey]; - }); - }; - - return ( - <> - - {type.label} - - - - {type.description} - - - - handleIntegrationChange(type.id, e.target.checked)} - disabled={isLoading} - /> - - - {type.fields.map((field) => { - const fieldKey = getFieldKey(type.id, field.id); - - return ( - - - {field.label} - - - handleInputChange(fieldKey, e.target.value)} - disabled={!integrations[type.id] || isLoading} - /> - - ); - })} - - - - - - ); -}; - -export default TabComponent; diff --git a/client/src/Components/v1/NotificationIntegrationModal/Components/TabPanel.jsx b/client/src/Components/v1/NotificationIntegrationModal/Components/TabPanel.jsx deleted file mode 100644 index 93546e7bc..000000000 --- a/client/src/Components/v1/NotificationIntegrationModal/Components/TabPanel.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; -import { Box } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -/** - * TabPanel component that displays content for the selected tab. - * - * @component - * @param {Object} props - The component props. - * @param {React.ReactNode} props.children - The content to be displayed when this tab panel is selected. - * @param {number} props.value - The currently selected tab value. - * @param {number} props.index - The index of this specific tab panel. - * @param {Object} props.other - Any additional props to be spread to the root element. - * @returns {React.ReactElement|null} The rendered tab panel or null if not selected. - */ -function TabPanel({ children, value, index, ...other }) { - const theme = useTheme(); - - return ( - - ); -} - -TabPanel.propTypes = { - children: PropTypes.node, - - index: PropTypes.number.isRequired, - - value: PropTypes.number.isRequired, -}; - -export default TabPanel; diff --git a/client/src/Components/v1/NotificationIntegrationModal/Hooks/useNotification.js b/client/src/Components/v1/NotificationIntegrationModal/Hooks/useNotification.js deleted file mode 100644 index c0712fa5f..000000000 --- a/client/src/Components/v1/NotificationIntegrationModal/Hooks/useNotification.js +++ /dev/null @@ -1,129 +0,0 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { networkService } from "../../../../Utils/NetworkService.js"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; - -// 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) { - createToast({ - body: errorMessage, - variant: "error", - }); - setLoading(false); - return; - } - - try { - const response = await networkService.testNotification({ - platform: type, - payload: payload, - }); - - if (response.data.success === true) { - createToast({ - body: t("notifications.testSuccess"), - variant: "info", - }); - } else { - throw new Error(response.data.msg || t("notifications.testFailed")); - } - } catch (error) { - const errorMsg = - error.response?.data?.msg || error.message || t("notifications.networkError"); - createToast({ - body: `${t("notifications.testFailed")}: ${errorMsg}`, - variant: "error", - }); - setError(errorMsg); - } finally { - setLoading(false); - } - }; - - return [loading, error, sendTestNotification]; -}; - -export default useNotifications; diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js index 6c5919fac..6cf84e45f 100644 --- a/client/src/Hooks/useNotifications.js +++ b/client/src/Hooks/useNotifications.js @@ -1,36 +1,7 @@ import { useState, useEffect, useCallback } from "react"; import { createToast } from "../Utils/toastUtils.jsx"; import { networkService } from "../main.jsx"; -import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { NOTIFICATION_TYPES } from "../Pages/Notifications/utils.js"; - -const useCreateNotification = () => { - const navigate = useNavigate(); - const { t } = useTranslation(); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const createNotification = async (notification) => { - try { - setIsLoading(true); - await networkService.createNotification({ notification }); - createToast({ - body: t("notifications.create.success"), - }); - navigate("/notifications"); - } catch (error) { - setError(error); - createToast({ - body: t("notifications.create.failed"), - }); - } finally { - setIsLoading(false); - } - }; - - return [createNotification, isLoading, error]; -}; const useGetNotificationsByTeamId = (updateTrigger) => { const [notifications, setNotifications] = useState([]); @@ -60,119 +31,6 @@ const useGetNotificationsByTeamId = (updateTrigger) => { return [notifications, isLoading, error]; }; -const useDeleteNotification = () => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const { t } = useTranslation(); - - const deleteNotification = async (id, callback) => { - try { - setIsLoading(true); - await networkService.deleteNotificationById({ id }); - createToast({ - body: t("notifications.delete.success"), - }); - if (callback) { - callback(); - } - } catch (error) { - setError(error); - createToast({ - body: t("notifications.delete.failed"), - }); - } finally { - setIsLoading(false); - } - }; - - return [deleteNotification, isLoading, error]; -}; - -const useGetNotificationById = (id, setNotification) => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const getNotificationById = useCallback(async () => { - try { - setIsLoading(true); - const response = await networkService.getNotificationById({ id }); - - const notification = response?.data?.data ?? null; - - const notificationData = { - address: notification?.address, - notificationName: notification?.notificationName, - type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?.id, - }; - - setNotification(notificationData); - } catch (error) { - setError(error); - } finally { - setIsLoading(false); - } - }, [id, setNotification]); - - useEffect(() => { - if (id) { - getNotificationById(); - } - }, [getNotificationById, id]); - - return [isLoading, error]; -}; - -const useEditNotification = () => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const { t } = useTranslation(); - const navigate = useNavigate(); - const editNotification = async (id, notification) => { - try { - setIsLoading(true); - await networkService.editNotification({ id, notification }); - createToast({ - body: t("notifications.edit.success"), - }); - navigate(`/notifications`); - } catch (error) { - setError(error); - createToast({ - body: t("notifications.edit.failed"), - }); - } finally { - setIsLoading(false); - } - }; - - return [editNotification, isLoading, error]; -}; - -const useTestNotification = () => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const { t } = useTranslation(); - - const testNotification = async (notification) => { - try { - setIsLoading(true); - await networkService.testNotification({ notification }); - createToast({ - body: t("notifications.test.success"), - }); - } catch (error) { - setError(error); - createToast({ - body: error?.response?.data?.msg || t("notifications.test.failed"), - }); - } finally { - setIsLoading(false); - } - }; - - return [testNotification, isLoading, error]; -}; - const useTestAllNotifications = () => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(undefined); @@ -197,12 +55,4 @@ const useTestAllNotifications = () => { return [testAllNotifications, isLoading, error]; }; -export { - useCreateNotification, - useGetNotificationsByTeamId, - useDeleteNotification, - useGetNotificationById, - useEditNotification, - useTestNotification, - useTestAllNotifications, -}; +export { useGetNotificationsByTeamId, useTestAllNotifications }; diff --git a/client/src/Pages/Notifications/create/old.jsx b/client/src/Pages/Notifications/create/old.jsx deleted file mode 100644 index 14b72ab61..000000000 --- a/client/src/Pages/Notifications/create/old.jsx +++ /dev/null @@ -1,320 +0,0 @@ -// Components -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import Button from "@mui/material/Button"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import Box from "@mui/material/Box"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; - -// Utils -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { - useCreateNotification, - useGetNotificationById, - useEditNotification, - useTestNotification, - useDeleteNotification, -} from "../../../Hooks/useNotifications.js"; -import { notificationValidation } from "../../../Validation/validation.js"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import { useTranslation } from "react-i18next"; -import { useParams, useNavigate } from "react-router-dom"; -import { - NOTIFICATION_TYPES, - TITLE_MAP, - DESCRIPTION_MAP, - LABEL_MAP, - PLACEHOLDER_MAP, -} from "../utils.js"; - -// Setup - -const CreateNotifications = () => { - const { notificationId } = useParams(); - const navigate = useNavigate(); - const theme = useTheme(); - const [createNotification, isCreating] = useCreateNotification(); - const [editNotification, isEditing] = useEditNotification(); - const [testNotification, isTesting] = useTestNotification(); - const [deleteNotification, isDeleting] = useDeleteNotification(); - - const BREADCRUMBS = [ - { name: "notifications", path: "/notifications" }, - { - name: notificationId ? "edit" : "create", - path: notificationId ? `/notifications/${notificationId}` : "/notifications/create", - }, - ]; - - // Redux state - - // local state - const [notification, setNotification] = useState({ - notificationName: "", - address: "", - type: NOTIFICATION_TYPES[0].id, - }); - const [errors, setErrors] = useState({}); - const { t } = useTranslation(); - - const [notificationIsLoading] = useGetNotificationById(notificationId, setNotification); - - const [isOpen, setIsOpen] = useState(false); - - const getNotificationTypeValue = (typeId) => { - return NOTIFICATION_TYPES.find((type) => type.id === typeId)?.value || "email"; - }; - - const extractError = (error, field) => - error?.details.find((d) => d.path.includes(field))?.message; - - // handlers - const onSubmit = (e) => { - e.preventDefault(); - const form = { - ...notification, - type: getNotificationTypeValue(notification.type), - }; - - let error = null; - - error = notificationValidation.validate(form, { abortEarly: false }).error; - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - createToast({ body: Object.values(newErrors)[0] }); - setErrors(newErrors); - return; - } - - if (notificationId) { - editNotification(notificationId, form); - } else { - createNotification(form); - } - }; - - const onChange = (e) => { - const { name, value } = e.target; - let rawNotification = { ...notification, [name]: value }; - let newNotification = { - ...rawNotification, - type: getNotificationTypeValue(rawNotification.type), - }; - - const { error } = notificationValidation.validate(newNotification, { - abortEarly: false, - }); - let validationError = { ...errors }; - - if (name === "type") { - validationError["type"] = extractError(error, "type"); - validationError["address"] = extractError(error, "address"); - } else { - validationError[name] = extractError(error, name); - } - - setNotification(rawNotification); - setErrors(validationError); - }; - - const onTestNotification = () => { - const form = { - ...notification, - type: getNotificationTypeValue(notification.type), - }; - - let error = null; - - error = notificationValidation.validate(form, { abortEarly: false }).error; - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - createToast({ body: Object.values(newErrors)[0] }); - setErrors(newErrors); - return; - } - - testNotification(form); - }; - - const onDelete = () => { - if (notificationId) { - deleteNotification(notificationId, () => navigate("/notifications")); - } - }; - - const type = getNotificationTypeValue(notification.type); - return ( - - - {t("createNotifications.title")} - - - - - {t("createNotifications.nameSettings.title")} - - - {t("createNotifications.nameSettings.description")} - - - - - - - - - - {t("createNotifications.typeSettings.title")} - - - {t("createNotifications.typeSettings.description")} - - - -