Date: Thu, 29 Jan 2026 21:15:14 +0000
Subject: [PATCH 060/392] remove unused
---
.../NotificationIntegrationModal.jsx | 419 ------------------
.../Components/TabComponent.jsx | 115 -----
.../Components/TabPanel.jsx | 41 --
.../Hooks/useNotification.js | 129 ------
client/src/Hooks/useNotifications.js | 152 +------
client/src/Pages/Notifications/create/old.jsx | 320 -------------
client/src/Pages/Notifications/utils.js | 44 --
7 files changed, 1 insertion(+), 1219 deletions(-)
delete mode 100644 client/src/Components/v1/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx
delete mode 100644 client/src/Components/v1/NotificationIntegrationModal/Components/TabComponent.jsx
delete mode 100644 client/src/Components/v1/NotificationIntegrationModal/Components/TabPanel.jsx
delete mode 100644 client/src/Components/v1/NotificationIntegrationModal/Hooks/useNotification.js
delete mode 100644 client/src/Pages/Notifications/create/old.jsx
delete mode 100644 client/src/Pages/Notifications/utils.js
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 (
-
- );
-};
-
-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 (
-
- {value === index && {children}}
-
- );
-}
-
-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")}
-
-
-
-
-
-
-
-
-
- {t(TITLE_MAP[type])}
-
- {t(DESCRIPTION_MAP[type])}
-
-
- {type === "matrix" ? (
- <>
-
-
-
- >
- ) : (
-
- )}
-
- {" "}
-
-
- {notificationId && (
-
- )}
-
-
-
-
- );
-};
-
-export default CreateNotifications;
diff --git a/client/src/Pages/Notifications/utils.js b/client/src/Pages/Notifications/utils.js
deleted file mode 100644
index 9ad422101..000000000
--- a/client/src/Pages/Notifications/utils.js
+++ /dev/null
@@ -1,44 +0,0 @@
-export const NOTIFICATION_TYPES = [
- { id: 1, name: "E-mail", value: "email" },
- { id: 2, name: "Slack", value: "slack" },
- { id: 3, name: "PagerDuty", value: "pager_duty" },
- { id: 4, name: "Webhook", value: "webhook" },
- { id: 5, name: "Discord", value: "discord" },
- { id: 6, name: "Matrix", value: "matrix" },
-];
-
-export const TITLE_MAP = {
- email: "createNotifications.emailSettings.title",
- slack: "createNotifications.slackSettings.title",
- pager_duty: "createNotifications.pagerdutySettings.title",
- webhook: "createNotifications.webhookSettings.title",
- discord: "createNotifications.discordSettings.title",
- matrix: "createNotifications.matrixSettings.title",
-};
-
-export const DESCRIPTION_MAP = {
- email: "createNotifications.emailSettings.description",
- slack: "createNotifications.slackSettings.description",
- pager_duty: "createNotifications.pagerdutySettings.description",
- webhook: "createNotifications.webhookSettings.description",
- discord: "createNotifications.discordSettings.description",
- matrix: "createNotifications.matrixSettings.description",
-};
-
-export const LABEL_MAP = {
- email: "createNotifications.emailSettings.emailLabel",
- slack: "createNotifications.slackSettings.webhookLabel",
- pager_duty: "createNotifications.pagerdutySettings.integrationKeyLabel",
- webhook: "createNotifications.webhookSettings.webhookLabel",
- discord: "createNotifications.discordSettings.webhookLabel",
- matrix: "createNotifications.matrixSettings.homeserverLabel",
-};
-
-export const PLACEHOLDER_MAP = {
- email: "createNotifications.emailSettings.emailPlaceholder",
- slack: "createNotifications.slackSettings.webhookPlaceholder",
- pager_duty: "createNotifications.pagerdutySettings.integrationKeyPlaceholder",
- webhook: "createNotifications.webhookSettings.webhookPlaceholder",
- discord: "createNotifications.discordSettings.webhookPlaceholder",
- matrix: "createNotifications.matrixSettings.homeserverPlaceholder",
-};
From 4b92dbd19b763d36a1a6a80461e34cfc441ca52f Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Thu, 29 Jan 2026 22:17:56 +0000
Subject: [PATCH 061/392] initial commit
---
client/src/Components/v2/inputs/Radio.tsx | 88 +++++++
client/src/Components/v2/inputs/index.tsx | 1 +
client/src/Hooks/useMonitorForm.ts | 105 +++++++++
client/src/Pages/CreateMonitor/index.tsx | 266 ++++++++++++++++++++++
client/src/Routes/index.jsx | 12 +
client/src/Validation/monitor.ts | 122 ++++++++++
client/src/locales/en.json | 31 +++
7 files changed, 625 insertions(+)
create mode 100644 client/src/Components/v2/inputs/Radio.tsx
create mode 100644 client/src/Hooks/useMonitorForm.ts
create mode 100644 client/src/Pages/CreateMonitor/index.tsx
create mode 100644 client/src/Validation/monitor.ts
diff --git a/client/src/Components/v2/inputs/Radio.tsx b/client/src/Components/v2/inputs/Radio.tsx
new file mode 100644
index 000000000..ad8dc3374
--- /dev/null
+++ b/client/src/Components/v2/inputs/Radio.tsx
@@ -0,0 +1,88 @@
+import Radio from "@mui/material/Radio";
+import type { RadioProps } from "@mui/material/Radio";
+import { useTheme } from "@mui/material/styles";
+import { Circle, CircleDot } from "lucide-react";
+import FormControlLabel from "@mui/material/FormControlLabel";
+import Typography from "@mui/material/Typography";
+
+interface RadioInputProps extends RadioProps {}
+
+export const RadioInput = ({ ...props }: RadioInputProps) => {
+ const theme = useTheme();
+ return (
+
+ }
+ checkedIcon={
+
+ }
+ sx={{
+ padding: 0,
+ mt: theme.spacing(0.5),
+ color: theme.palette.text.secondary,
+ "&.Mui-checked": {
+ color: theme.palette.primary.main,
+ "& svg circle": {
+ fill: theme.palette.primary.main,
+ },
+ },
+ "& .MuiSvgIcon-root": {
+ fontSize: 16,
+ },
+ "& svg": {
+ stroke: "currentColor",
+ },
+ "& svg path, & svg line, & svg polyline, & svg rect, & svg circle": {
+ stroke: "currentColor",
+ fill: "none",
+ },
+ }}
+ />
+ );
+};
+
+export const RadioWithDescription = ({
+ label,
+ description,
+ ...props
+}: RadioInputProps & { label: string; description: string }) => {
+ const theme = useTheme();
+ return (
+ }
+ label={
+ <>
+ {label}
+
+ {description}
+
+ >
+ }
+ sx={{
+ alignItems: "flex-start",
+ p: theme.spacing(2.5),
+ m: theme.spacing(-2.5),
+ borderRadius: theme.shape.borderRadius,
+
+ "&:hover": {
+ backgroundColor: theme.palette.background.paper,
+ },
+ "& .MuiButtonBase-root": {
+ p: 0,
+ mr: theme.spacing(6),
+ },
+ }}
+ />
+ );
+};
diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx
index 70e740dbd..a0646d394 100644
--- a/client/src/Components/v2/inputs/index.tsx
+++ b/client/src/Components/v2/inputs/index.tsx
@@ -9,3 +9,4 @@ export {
ToggleButtonGroupInput as ToggleButtonGroup,
} from "./ToggleButton";
export { DialogInput as Dialog } from "./Dialog";
+export * from "./Radio";
diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts
new file mode 100644
index 000000000..2d7df97ab
--- /dev/null
+++ b/client/src/Hooks/useMonitorForm.ts
@@ -0,0 +1,105 @@
+import { useMemo } from "react";
+import { monitorSchema, type MonitorFormData } from "@/Validation/monitor";
+import type { Monitor, MonitorType } from "@/Types/Monitor";
+
+interface UseMonitorFormOptions {
+ data?: Monitor | null;
+ defaultType?: MonitorType;
+}
+
+const getBaseDefaults = (data?: Monitor | null) => ({
+ name: data?.name || "",
+ interval: data?.interval || 60000,
+ notifications: data?.notifications || [],
+ statusWindowSize: data?.statusWindowSize || 5,
+ statusWindowThreshold: data?.statusWindowThreshold || 60,
+});
+
+export const useMonitorForm = ({
+ data = null,
+ defaultType = "http",
+}: UseMonitorFormOptions = {}) => {
+ return useMemo(() => {
+ const type = data?.type || defaultType;
+ const base = getBaseDefaults(data);
+
+ let defaults: MonitorFormData;
+
+ switch (type) {
+ case "http":
+ defaults = {
+ ...base,
+ type: "http",
+ url: data?.url || "",
+ ignoreTlsErrors: data?.ignoreTlsErrors || false,
+ matchMethod: data?.matchMethod || "",
+ expectedValue: data?.expectedValue || "",
+ jsonPath: data?.jsonPath || "",
+ };
+ break;
+ case "ping":
+ defaults = {
+ ...base,
+ type: "ping",
+ url: data?.url || "",
+ };
+ break;
+ case "port":
+ defaults = {
+ ...base,
+ type: "port",
+ url: data?.url || "",
+ port: data?.port || 80,
+ };
+ break;
+ case "docker":
+ defaults = {
+ ...base,
+ type: "docker",
+ url: data?.url || "",
+ };
+ break;
+ case "game":
+ defaults = {
+ ...base,
+ type: "game",
+ url: data?.url || "",
+ port: data?.port || 27015,
+ gameId: data?.gameId || "",
+ };
+ break;
+ case "pagespeed":
+ defaults = {
+ ...base,
+ type: "pagespeed",
+ url: data?.url || "",
+ };
+ break;
+ case "hardware":
+ defaults = {
+ ...base,
+ type: "hardware",
+ url: data?.url || "",
+ secret: data?.secret || "",
+ cpuAlertThreshold: data?.cpuAlertThreshold ?? 80,
+ memoryAlertThreshold: data?.memoryAlertThreshold ?? 80,
+ diskAlertThreshold: data?.diskAlertThreshold ?? 80,
+ tempAlertThreshold: data?.tempAlertThreshold ?? 80,
+ selectedDisks: data?.selectedDisks || [],
+ };
+ break;
+ default:
+ defaults = {
+ ...base,
+ type: "http",
+ url: "",
+ ignoreTlsErrors: false,
+ matchMethod: "",
+ expectedValue: "",
+ jsonPath: "",
+ };
+ }
+
+ return { schema: monitorSchema, defaults };
+ }, [data, defaultType]);
+};
diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx
new file mode 100644
index 000000000..80f818407
--- /dev/null
+++ b/client/src/Pages/CreateMonitor/index.tsx
@@ -0,0 +1,266 @@
+import { useMemo } from "react";
+import { useEffect } from "react";
+import { useParams } from "react-router";
+import { useForm, Controller } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useTheme } from "@mui/material";
+import Stack from "@mui/material/Stack";
+import RadioGroup from "@mui/material/RadioGroup";
+import FormControl from "@mui/material/FormControl";
+import { useTranslation } from "react-i18next";
+
+import { BasePage, ConfigBox } from "@/Components/v2/design-elements";
+import { RadioWithDescription, Button, TextField, Select } from "@/Components/v2/inputs";
+import { useGet } from "@/Hooks/UseApi";
+import { useMonitorForm } from "@/Hooks/useMonitorForm";
+import type { Monitor, MonitorType } from "@/Types/Monitor";
+import type { MonitorFormData } from "@/Validation/monitor";
+import MenuItem from "@mui/material/MenuItem";
+
+interface GeneralSettingsConfig {
+ urlLabel: string;
+ urlPlaceholder: string;
+ namePlaceholder: string;
+ showPort: boolean;
+ showGameSelect: boolean;
+}
+
+const getGeneralSettingsConfig = (
+ type: MonitorType,
+ t: (key: string) => string
+): GeneralSettingsConfig => {
+ const configs: Record = {
+ http: {
+ urlLabel: t("monitorType.http.label"),
+ urlPlaceholder: t("monitorType.http.placeholder"),
+ namePlaceholder: t("monitorType.http.namePlaceholder"),
+ showPort: false,
+ showGameSelect: false,
+ },
+ ping: {
+ urlLabel: t("monitorType.ping.label"),
+ urlPlaceholder: t("monitorType.ping.placeholder"),
+ namePlaceholder: t("monitorType.ping.namePlaceholder"),
+ showPort: false,
+ showGameSelect: false,
+ },
+ docker: {
+ urlLabel: t("monitorType.docker.label"),
+ urlPlaceholder: t("monitorType.docker.placeholder"),
+ namePlaceholder: t("monitorType.docker.namePlaceholder"),
+ showPort: false,
+ showGameSelect: false,
+ },
+ port: {
+ urlLabel: t("monitorType.port.label"),
+ urlPlaceholder: t("monitorType.port.placeholder"),
+ namePlaceholder: t("monitorType.port.namePlaceholder"),
+ showPort: true,
+ showGameSelect: false,
+ },
+ game: {
+ urlLabel: t("monitorType.game.label"),
+ urlPlaceholder: t("monitorType.game.placeholder"),
+ namePlaceholder: t("monitorType.game.namePlaceholder"),
+ showPort: true,
+ showGameSelect: true,
+ },
+ };
+ return configs[type] || configs.http;
+};
+
+const CreateMonitorPage = () => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const { monitorId } = useParams();
+ const isEditMode = Boolean(monitorId);
+
+ const { data: existingMonitor } = useGet(
+ isEditMode ? `/monitors/${monitorId}` : null
+ );
+
+ const { schema, defaults } = useMonitorForm({ data: existingMonitor ?? null });
+
+ const form = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: defaults,
+ });
+ const { control, watch, handleSubmit, clearErrors } = form;
+
+ useEffect(() => {
+ form.reset(defaults);
+ }, [defaults, form]);
+
+ const watchedType = watch("type") as MonitorType;
+
+ useEffect(() => {
+ clearErrors();
+ }, [watchedType, clearErrors]);
+
+ const generalSettingsConfig = useMemo(
+ () => getGeneralSettingsConfig(watchedType, t),
+ [watchedType, t]
+ );
+
+ const onSubmit = async (data: MonitorFormData) => {
+ console.log(data);
+ };
+
+ const onError = (errors: unknown) => {
+ console.log(errors);
+ };
+
+ return (
+
+ {/* Monitor Type Selection */}
+ (
+
+
+
+
+
+
+
+
+
+ )}
+ />
+ }
+ />
+
+ {/* General Settings - Dynamic based on type */}
+
+ {/* URL/Host/Container field */}
+ (
+
+ )}
+ />
+
+ {/* Port field - only for port and game types */}
+ {generalSettingsConfig.showPort && (
+ (
+ field.onChange(Number(e.target.value) || 0)}
+ type="number"
+ fieldLabel={t("portToMonitor")}
+ placeholder="5173"
+ fullWidth
+ error={!!fieldState.error}
+ helperText={fieldState.error?.message ?? ""}
+ />
+ )}
+ />
+ )}
+
+ {/* Game select - only for game type */}
+ {generalSettingsConfig.showGameSelect && (
+ (
+
+ )}
+ />
+ )}
+
+ {/* Display name field - common to all types */}
+ (
+
+ )}
+ />
+
+ }
+ />
+
+ {/* Submit Button */}
+
+
+
+
+ );
+};
+
+export default CreateMonitorPage;
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index 498be4ba7..c2613a16c 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -60,6 +60,8 @@ import withAdminCheck from "@/Components/v1/HOC/withAdminCheck";
import BulkImport from "../Pages/Uptime/BulkImport/index.jsx";
import Logs from "../Pages/Logs/index.jsx";
+import CreateMonitor from "@/Pages/CreateMonitor";
+
const Routes = () => {
const mode = useSelector((state) => state.ui.mode);
const AdminCheckedRegister = withAdminCheck(AuthRegister);
@@ -78,6 +80,16 @@ const Routes = () => {
path="/"
element={}
/>
+
+
+
+
+ >
+ }
+ />
;
+
+// Type-specific schemas exported for individual use
+export {
+ httpSchema,
+ pingSchema,
+ portSchema,
+ dockerSchema,
+ gameSchema,
+ pagespeedSchema,
+ hardwareSchema,
+};
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 80da1835c..d9f2e7ac6 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -220,6 +220,37 @@
}
}
},
+ "createMonitor": {
+ "form": {
+ "type": {
+ "description": "Select the type of check to perform",
+ "optionType": "Type",
+ "title": "Type",
+ "optionHttp": "HTTP(S)",
+ "optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
+ "optionPing": "Ping",
+ "optionPingDescription": "Use ICMP Ping to monitor if a server is online.",
+ "optionDocker": "Docker",
+ "optionDockerDescription": "Use Docker to monitor if a container is running.",
+ "optionPort": "Port",
+ "optionPortDescription": "Monitor if a specific port on a server is open.",
+ "optionGame": "Game",
+ "optionGameDescription": "Monitor if a specific game server is online."
+ },
+ "general": {
+ "title": "General settings",
+ "description": {
+ "http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.",
+ "ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.",
+ "docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).",
+ "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.",
+ "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.",
+ "pagespeed": "Here you can select the URL of the host, together with the type of monitor.",
+ "infrastructure": "Here you can select the URL of the host, together with the friendly name and authorization secret to connect to the server agent."
+ }
+ }
+ }
+ },
"uptime": {
"table": {
"headers": {
From 84e4ea7b649802dbbef7738ba472c79d4aab82a2 Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Thu, 29 Jan 2026 22:20:50 +0000
Subject: [PATCH 062/392] forward ref
---
client/src/Components/v2/inputs/Select.tsx | 32 ++++++++++++++--------
1 file changed, 20 insertions(+), 12 deletions(-)
diff --git a/client/src/Components/v2/inputs/Select.tsx b/client/src/Components/v2/inputs/Select.tsx
index 057b0e72c..8be20897d 100644
--- a/client/src/Components/v2/inputs/Select.tsx
+++ b/client/src/Components/v2/inputs/Select.tsx
@@ -1,7 +1,7 @@
import Select from "@mui/material/Select";
-import React from "react";
-import { useTheme } from "@mui/material/styles";
import type { SelectProps } from "@mui/material/Select";
+import React, { forwardRef } from "react";
+import { useTheme } from "@mui/material/styles";
import Stack from "@mui/material/Stack";
import { FieldLabel } from "./FieldLabel";
import { ChevronDown } from "lucide-react";
@@ -14,18 +14,21 @@ interface SelectInputProps extends Omit, "label"> {
placeholderColor?: string;
}
-export const SelectInput = ({
- fieldLabel,
- required,
- placeholder,
- placeholderColor,
- ...props
-}: SelectInputProps) => {
+const SelectInputInner = (
+ {
+ fieldLabel,
+ required,
+ placeholder,
+ placeholderColor,
+ ...props
+ }: SelectInputProps,
+ ref: React.ForwardedRef
+) => {
const theme = useTheme();
const emptyPlaceholderColor = placeholderColor || theme.palette.text.disabled;
- const renderValue = (selected: any) => {
- const isMultiple = Boolean((props as any).multiple);
+ const renderValue = (selected: unknown) => {
+ const isMultiple = Boolean((props as { multiple?: boolean }).multiple);
const isEmpty = isMultiple
? !Array.isArray(selected) || selected.length === 0
: selected === undefined || selected === null || selected === "";
@@ -39,7 +42,7 @@ export const SelectInput = ({
const capitalized = items.map(
(item) => item.charAt(0).toUpperCase() + item.slice(1)
);
- return ({capitalized.join(" | ")}) as any;
+ return {capitalized.join(" | ")};
}
const nodes = React.Children.toArray(props.children as React.ReactNode);
@@ -59,6 +62,7 @@ export const SelectInput = ({
const select = (