From 302760652454872b19dbbf3097aa470df6f6eee1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Jun 2025 12:52:32 +0800 Subject: [PATCH 01/18] remove console.log --- client/src/Components/Common/AppBar.jsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/src/Components/Common/AppBar.jsx b/client/src/Components/Common/AppBar.jsx index df0660cf5..de5f0992e 100644 --- a/client/src/Components/Common/AppBar.jsx +++ b/client/src/Components/Common/AppBar.jsx @@ -47,9 +47,6 @@ const AppAppBar = () => { const location = useLocation(); const navigate = useNavigate(); - // Debugging: Log the current theme mode - console.log("Current theme mode:", mode); - const logoSrc = mode === "light" ? "/images/prism-black.png" : "/images/prism-white.png"; From 0225b93c72fb59f81c88f6748933c31fbba9e884 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Jun 2025 12:52:52 +0800 Subject: [PATCH 02/18] add notifications channel to sidebar --- client/src/Components/Sidebar/index.jsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/client/src/Components/Sidebar/index.jsx b/client/src/Components/Sidebar/index.jsx index 6073f428d..b0de10b5e 100644 --- a/client/src/Components/Sidebar/index.jsx +++ b/client/src/Components/Sidebar/index.jsx @@ -38,6 +38,8 @@ import ChangeLog from "../../assets/icons/changeLog.svg?react"; import Docs from "../../assets/icons/docs.svg?react"; import StatusPages from "../../assets/icons/status-pages.svg?react"; import Discussions from "../../assets/icons/discussions.svg?react"; +import NotificationAddOutlinedIcon from "@mui/icons-material/NotificationAddOutlined"; + import "./index.css"; // Utils @@ -52,13 +54,18 @@ import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMon const getMenu = (t) => [ { name: t("menu.uptime"), path: "uptime", icon: }, { name: t("menu.pagespeed"), path: "pagespeed", icon: }, - { name: t("menu.infrastructure"), path: "infrastructure", icon: }, + { name: t("menu.infrastructure"), path: "infrastructure", icon: }, + { + name: t("menu.notifications"), + path: "notifications", + icon: , + }, { name: t("menu.incidents"), path: "incidents", icon: }, { name: t("menu.statusPages"), path: "status", icon: }, { name: t("menu.maintenance"), path: "maintenance", icon: }, - // { name: t("menu.integrations"), path: "integrations", icon: }, + { name: t("menu.settings"), icon: , From 95703c15057fb6825e8d8ad8ceb13560e6b1678e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Jun 2025 12:54:41 +0800 Subject: [PATCH 03/18] remove console.log --- client/src/Pages/Settings/index.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx index dd0135edb..b0e3c476b 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -132,7 +132,6 @@ const Settings = () => { }; const handleSave = () => { - console.log(settingsData.settings); const { error } = settingsValidation.validate(settingsData.settings, { abortEarly: false, }); From 1fbfbc42049102ea11723208e1ac01adae8e9dad Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Jun 2025 12:55:01 +0800 Subject: [PATCH 04/18] add strings --- client/src/locales/en.json | 53 +++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 12442246c..16e1f5ebd 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -134,7 +134,45 @@ "distributedUptimeDetailsNoMonitorHistory": "There is no check history for this monitor yet.", "distributedUptimeDetailsStatusHeaderUptime": "Uptime:", "distributedUptimeDetailsStatusHeaderLastUpdate": "Last updated", + "createNotifications": { + "title": "Create notification channel", + "nameSettings": { + "title": "Name", + "description": "A descriptive name for your integration.", + "nameLabel": "Name", + "namePlaceholder": "e.g. Slack notifications" + }, + "typeSettings": { + "title": "Type", + "description": "Select the type of notification channel you want to create.", + "typeLabel": "Type" + }, + "emailSettings": { + "title": "Email", + "description": "Destination email addresses.", + "emailLabel": "Email address", + "emailPlaceholder": "e.g. john@example.com" + }, + "slackSettings": { + "title": "Slack", + "description": "Configure your Slack webhook here", + "webhookLabel": "Slack webhook URL", + "webhookPlaceholder": "https://hooks.slack.com/services/..." + }, + "pagerdutySettings": { + "title": "PagerDuty", + "description": "Configure your PagerDuty integration here", + "integrationKeyLabel": "Integration key", + "integrationKeyPlaceholder": "1234567890" + } + }, "notifications": { + "fallback": { + "title": "Notification", + "checks": "Alert teams about downtime or performance issues" + }, + "createButton": "Create notification channel", + "createTitle": "Notification channel", "enableNotifications": "Enable {{platform}} notifications", "testNotification": "Test notification", "addOrEditNotifications": "Add or edit notifications", @@ -173,7 +211,19 @@ "testSuccess": "Test notification sent successfully!", "testFailed": "Failed to send test notification", "unsupportedType": "Unsupported notification type", - "networkError": "Network error occurred" + "networkError": "Network error occurred", + "create": { + "success": "Notification created successfully", + "failed": "Failed to create notification" + }, + "fetch": { + "success": "Notifications fetched successfully", + "failed": "Failed to fetch notifications" + }, + "delete": { + "success": "Notification deleted successfully", + "failed": "Failed to delete notification" + } }, "testLocale": "testLocale", "add": "Add", @@ -479,6 +529,7 @@ "menu": { "uptime": "Uptime", "pagespeed": "Pagespeed", + "notifications": "Notifications", "infrastructure": "Infrastructure", "incidents": "Incidents", "statusPages": "Status pages", From 358381ddc259572db52a439bca5f071ceb3ecd9f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Jun 2025 12:55:13 +0800 Subject: [PATCH 05/18] update client side validation --- client/src/Validation/validation.js | 66 +++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index d94f7c49a..9232b481b 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -104,6 +104,9 @@ const credentials = joi.object({ }); const monitorValidation = joi.object({ + _id: joi.string(), + userId: joi.string(), + teamId: joi.string(), url: joi.when("type", { is: "docker", then: joi @@ -191,9 +194,9 @@ const monitorValidation = joi.object({ "number.base": "Frequency must be a number.", "any.required": "Frequency is required.", }), - expectedValue: joi.string().allow(""), - jsonPath: joi.string().allow(""), - matchMethod: joi.string(), + expectedValue: joi.string().allow(null, ""), + jsonPath: joi.string().allow(null, ""), + matchMethod: joi.string().allow(null, ""), }); const imageValidation = joi.object({ @@ -396,6 +399,60 @@ const infrastructureMonitorValidation = joi.object({ ), }); +const notificationEmailValidation = joi.object({ + notificationName: joi.string().required().messages({ + "string.empty": "Notification name is required", + "any.required": "Notification name is required", + }), + address: joi + .string() + .email({ tlds: { allow: false } }) + .required() + .messages({ + "string.empty": "E-mail address cannot be empty", + + "string.email": "Please enter a valid e-mail address", + "string.base": "E-mail address must be a string", + "any.required": "E-mail address is required", + }), +}); +const notificationWebhookValidation = joi.object({ + notificationName: joi.string().required().messages({ + "string.empty": "Notification name is required", + "any.required": "Notification name is required", + }), + config: joi + .object({ + webhookUrl: joi.string().uri().required().messages({ + "string.empty": "Webhook URL is required", + "string.uri": "Webhook URL must be a valid URI", + "any.required": "Webhook URL is required", + }), + platform: joi.string().required().messages({ + "string.base": "Platform must be a string", + "any.required": "Platform is required", + }), + }) + .unknown(true), +}); + +const notificationPagerDutyValidation = joi.object({ + notificationName: joi.string().required().messages({ + "string.empty": "Notification name is required", + "any.required": "Notification name is required", + }), + config: joi.object({ + platform: joi.string().required().messages({ + "string.base": "Platform must be a string", + "any.required": "Platform is required", + }), + routingKey: joi.string().required().messages({ + "string.empty": "Routing key is required", + "any.required": "Routing key is required", + }), + }), +}); + export { credentials, imageValidation, @@ -406,4 +463,7 @@ export { infrastructureMonitorValidation, statusPageValidation, logoImageValidation, + notificationEmailValidation, + notificationWebhookValidation, + notificationPagerDutyValidation, }; From e36abfa9a6a13fa361e919146da0b2bddd97ee87 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Jun 2025 12:55:45 +0800 Subject: [PATCH 06/18] add notificaitons channel to Create Uptime --- client/src/Pages/Uptime/Create/index.jsx | 302 +++++++++-------------- 1 file changed, 112 insertions(+), 190 deletions(-) diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index 6e45230f6..b9e1e8423 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -1,15 +1,12 @@ // React, Redux, Router import { useTheme } from "@emotion/react"; -import { useNavigate, useParams } from "react-router-dom"; -import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; import { useState } from "react"; import { useSelector, useDispatch } from "react-redux"; import { useTranslation } from "react-i18next"; // Utility and Network -import { checkEndpointResolution } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import { monitorValidation } from "../../../Validation/validation"; -import { getUptimeMonitorById } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; // MUI import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material"; @@ -22,11 +19,37 @@ import TextInput from "../../../Components/Inputs/TextInput"; import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; import { createToast } from "../../../Utils/toastUtils"; 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/Components/NotificationIntegrationModal"; +import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; +import NotificationsConfig from "../../../Components/NotificationConfig"; + const CreateMonitor = () => { + // Redux state + const { user } = useSelector((state) => state.auth); + const { isLoading } = useSelector((state) => state.uptimeMonitors); + const dispatch = useDispatch(); + + // Local state + const [errors, setErrors] = useState({}); + const [https, setHttps] = useState(true); + const [useAdvancedMatching, setUseAdvancedMatching] = useState(false); + const [monitor, setMonitor] = useState({ + url: "", + name: "", + type: "http", + matchMethod: "equal", + notifications: [], + interval: 1, + ignoreTlsErrors: false, + }); + + // Setup + const theme = useTheme(); + const { t } = useTranslation(); + const navigate = useNavigate(); + const [notifications, notificationsAreLoading, error] = useGetNotificationsByTeamId(); + const MS_PER_MINUTE = 60000; const SELECT_VALUES = [ { _id: 1, name: "1 minute" }, @@ -71,37 +94,19 @@ const CreateMonitor = () => { }, }; - const { user } = useSelector((state) => state.auth); - const { isLoading } = useSelector((state) => state.uptimeMonitors); - const dispatch = useDispatch(); - const navigate = useNavigate(); - const theme = useTheme(); - const { monitorId } = useParams(); - const crumbs = [ + const BREADCRUMBS = [ { name: "uptime", path: "/uptime" }, { name: "create", path: `/uptime/create` }, ]; - // State - const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false); + // Handlers - const handleOpenNotificationModal = () => { - setIsNotificationModalOpen(true); - }; - const [errors, setErrors] = useState({}); - const [https, setHttps] = useState(true); - const [monitor, setMonitor] = useState({ - url: "", - name: "", - type: "http", - ignoreTlsErrors: false, - notifications: [], - interval: 1, - }); - - const handleCreateMonitor = async (event) => { + const onSubmit = async (event) => { event.preventDefault(); + const { notifications, ...rest } = monitor; + let form = { + ...rest, url: //prepending protocol for url monitor.type === "http" @@ -110,14 +115,14 @@ const CreateMonitor = () => { port: monitor.type === "port" ? monitor.port : undefined, name: monitor.name || monitor.url.substring(0, 50), type: monitor.type, - ignoreTlsErrors: monitor.ignoreTlsErrors, interval: monitor.interval * MS_PER_MINUTE, }; - if (monitor.type === "http") { - form.expectedValue = monitor.expectedValue; - form.jsonPath = monitor.jsonPath; - form.matchMethod = monitor.matchMethod; + // If not using advanced matching, remove advanced settings + if (!useAdvancedMatching) { + form.matchMethod = undefined; + form.expectedValue = undefined; + form.jsonPath = undefined; } const { error } = monitorValidation.validate(form, { @@ -141,6 +146,7 @@ const CreateMonitor = () => { userId: user._id, notifications: monitor.notifications, }; + const action = await dispatch(createUptimeMonitor({ monitor: form })); if (action.meta.requestStatus === "fulfilled") { createToast({ body: "Monitor created successfully!" }); @@ -150,115 +156,60 @@ const CreateMonitor = () => { } }; - const handleChange = (event, formName) => { - const { type, checked, value } = event.target; - - const newVal = type === "checkbox" ? checked : value; - - const newMonitor = { - ...monitor, - [formName]: newVal, - }; - if (formName === "type") { - newMonitor.url = ""; + const onChange = (event) => { + const { name, value, checked } = event.target; + let newValue = value; + if (name === "ignoreTlsErrors") { + newValue = checked; } - setMonitor(newMonitor); + const updatedMonitor = { + ...monitor, + [name]: newValue, + }; + + setMonitor(updatedMonitor); const { error } = monitorValidation.validate( - { type: monitor.type, [formName]: newVal }, + { type: monitor.type, [name]: newValue }, { abortEarly: false } ); + setErrors((prev) => ({ ...prev, - url: undefined, - ...(error ? { [formName]: error.details[0].message } : { [formName]: undefined }), + ...(error ? { [name]: error.details[0].message } : { [name]: undefined }), })); }; - const handleNotifications = (event, type) => { - const { value } = event.target; - let notifications = [...monitor.notifications]; - const notificationExists = notifications.some((notification) => { - if (notification.type === type && notification.address === value) { - return true; - } - return false; - }); - if (notificationExists) { - notifications = notifications.filter((notification) => { - if (notification.type === type && notification.address === value) { - return false; - } - return true; - }); - } else { - notifications.push({ type, address: value }); - } - - setMonitor((prev) => ({ - ...prev, - notifications, - })); - }; - - useEffect(() => { - const fetchMonitor = async () => { - if (monitorId) { - const action = await dispatch(getUptimeMonitorById({ monitorId })); - - if (action.payload.success) { - const data = action.payload.data; - const { name, ...rest } = data; //data.name is read-only - if (rest.type === "http") { - const url = new URL(rest.url); - rest.url = url.host; - } - rest.name = `${name} (Clone)`; - rest.interval /= MS_PER_MINUTE; - setMonitor({ - ...rest, - }); - } else { - navigate("/not-found", { replace: true }); - createToast({ - body: "There was an error cloning the monitor.", - }); - } - } - }; - fetchMonitor(); - }, [monitorId, dispatch, navigate]); - - const { t } = useTranslation(); - return ( - - - + + + - - {t("createYour")}{" "} - - - {t("monitor")} - + {t("createYour")}{" "} + + {t("monitor")} + + + { handleChange(event, "type")} + onChange={onChange} /> {monitor.type === "http" ? ( @@ -304,31 +255,31 @@ const CreateMonitor = () => { )} handleChange(event, "type")} + onChange={onChange} /> handleChange(event, "type")} + onChange={onChange} /> handleChange(event, "type")} + onChange={onChange} /> {errors["type"] ? ( @@ -359,8 +310,8 @@ const CreateMonitor = () => { : null } @@ -368,29 +319,29 @@ const CreateMonitor = () => { https={https} placeholder={monitorTypeMaps[monitor.type].placeholder || ""} value={monitor.url} - onChange={(event) => handleChange(event, "url")} + onChange={onChange} error={errors["url"] ? true : false} helperText={errors["url"]} /> handleChange(event, "port")} + onChange={onChange} error={errors["port"] ? true : false} helperText={errors["port"]} hidden={monitor.type !== "port"} /> handleChange(event, "name")} + onChange={onChange} error={errors["name"] ? true : false} helperText={errors["name"]} /> @@ -398,37 +349,15 @@ const CreateMonitor = () => { - - {t("distributedUptimeCreateIncidentNotification")} - + Notifications - {t("distributedUptimeCreateIncidentDescription")} + Select the notifications you want to send out - - notification.type === "email" - )} - value={user?.email} - onChange={(event) => handleNotifications(event, "email")} - /> - - - - - + @@ -445,9 +374,9 @@ const CreateMonitor = () => { sx={{ marginLeft: 0 }} control={ handleChange(event, "ignoreTlsErrors")} + onChange={onChange} sx={{ mr: theme.spacing(2) }} /> } @@ -466,32 +395,32 @@ const CreateMonitor = () => { handleChange(event, "matchMethod")} + onChange={onChange} items={matchMethodOptions} /> handleChange(event, "expectedValue")} + onChange={onChange} error={errors["expectedValue"] ? true : false} helperText={errors["expectedValue"]} /> @@ -505,13 +434,13 @@ const CreateMonitor = () => { handleChange(event, "jsonPath")} + onChange={onChange} error={errors["jsonPath"] ? true : false} helperText={errors["jsonPath"]} /> @@ -541,9 +470,9 @@ const CreateMonitor = () => { justifyContent="flex-end" > - - setIsNotificationModalOpen(false)} - monitor={monitor} - setMonitor={setMonitor} - /> - + ); }; From 04ab4e23db941b46aae2000dd79ae4a113257b98 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Jun 2025 12:56:05 +0800 Subject: [PATCH 07/18] add notificaions channel to Uptime Configure --- client/src/Pages/Uptime/Configure/index.jsx | 909 +++++++++----------- 1 file changed, 414 insertions(+), 495 deletions(-) diff --git a/client/src/Pages/Uptime/Configure/index.jsx b/client/src/Pages/Uptime/Configure/index.jsx index f75e53e84..9667728cb 100644 --- a/client/src/Pages/Uptime/Configure/index.jsx +++ b/client/src/Pages/Uptime/Configure/index.jsx @@ -1,7 +1,7 @@ import { useNavigate, useParams } from "react-router"; import { useTheme } from "@emotion/react"; import { useDispatch, useSelector } from "react-redux"; -import { useEffect, useState } from "react"; +import { useState, useEffect } from "react"; import { Box, Stack, @@ -13,31 +13,26 @@ import { } from "@mui/material"; import { monitorValidation } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; -import { logger } from "../../../Utils/Logger"; import { useTranslation } from "react-i18next"; import ConfigBox from "../../../Components/ConfigBox"; import { updateUptimeMonitor, - pauseUptimeMonitor, - getUptimeMonitorById, deleteUptimeMonitor, } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import TextInput from "../../../Components/Inputs/TextInput"; import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import PauseIcon from "../../../assets/icons/pause-icon.svg?react"; -import ResumeIcon from "../../../assets/icons/resume-icon.svg?react"; import Select from "../../../Components/Inputs/Select"; -import Checkbox from "../../../Components/Inputs/Checkbox"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import PulseDot from "../../../Components/Animated/PulseDot"; -import SkeletonLayout from "./skeleton"; import "./index.css"; import Dialog from "../../../Components/Dialog"; -import NotificationIntegrationModal from "../../../Components/NotificationIntegrationModal/Components/NotificationIntegrationModal"; import { usePauseMonitor } from "../../../Hooks/useMonitorControls"; import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined"; import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined"; import { useMonitorUtils } from "../../../Hooks/useMonitorUtils"; +import { useFetchUptimeMonitorById } from "../../../Hooks/useFetchUptimeMonitorById"; +import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; +import NotificationsConfig from "../../../Components/NotificationConfig"; /** * Parses a URL string and returns a URL object. @@ -58,22 +53,41 @@ const parseUrl = (url) => { * @component */ const Configure = () => { + const { monitorId } = useParams(); + + // Local state + const [form, setForm] = useState({ + ignoreTlsErrors: false, + interval: 60000, + matchMethod: "equal", + expectedValue: "", + jsonPath: "", + notifications: [], + port: "", + type: "http", + }); + const [useAdvancedMatching, setUseAdvancedMatching] = useState(false); + const [updateTrigger, setUpdateTrigger] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const [errors, setErrors] = useState({}); + + const triggerUpdate = () => { + setUpdateTrigger(!updateTrigger); + }; + + // Network + const [monitor, isLoading, error] = useFetchUptimeMonitorById(monitorId, updateTrigger); + const [notifications, notificationsAreLoading, notificationsError] = + useGetNotificationsByTeamId(); + const [pauseMonitor, isPausing, pauseError] = usePauseMonitor({ + monitorId: monitor?._id, + triggerUpdate, + }); + const MS_PER_MINUTE = 60000; const navigate = useNavigate(); const theme = useTheme(); const dispatch = useDispatch(); - const { user } = useSelector((state) => state.auth); - const { isLoading } = useSelector((state) => state.uptimeMonitors); - const [monitor, setMonitor] = useState({}); - const [errors, setErrors] = useState({}); - const { monitorId } = useParams(); - const idMap = { - "monitor-url": "url", - "monitor-name": "name", - "monitor-checks-http": "type", - "monitor-checks-ping": "type", - "notify-email-default": "notification-email", - }; const matchMethodOptions = [ { _id: "equal", name: "Equal" }, @@ -81,106 +95,28 @@ const Configure = () => { { _id: "regex", name: "Regex" }, ]; + const frequencies = [ + { _id: 1, name: "1 minute" }, + { _id: 2, name: "2 minutes" }, + { _id: 3, name: "3 minutes" }, + { _id: 4, name: "4 minutes" }, + { _id: 5, name: "5 minutes" }, + ]; + const expectedValuePlaceholders = { regex: "^(success|ok)$", equal: "success", include: "ok", }; - const [trigger, setTrigger] = useState(false); - const triggerUpdate = () => { - setTrigger(!trigger); - }; - const [pauseMonitor, isPausing, error] = usePauseMonitor({ - monitorId: monitor?._id, - triggerUpdate, - }); - - useEffect(() => { - const fetchMonitor = async () => { - try { - const action = await dispatch(getUptimeMonitorById({ monitorId })); - if (getUptimeMonitorById.fulfilled.match(action)) { - const monitor = action.payload.data; - setMonitor(monitor); - } else if (getUptimeMonitorById.rejected.match(action)) { - throw new Error(action.error.message); - } - } catch (error) { - logger.error("Error fetching monitor of id: " + monitorId); - navigate("/not-found", { replace: true }); - } - }; - fetchMonitor(); - }, [monitorId, navigate, trigger]); - - const handleChange = (event, name) => { - let { checked, value, id } = event.target; - if (!name) name = idMap[id]; - - if (name.includes("notification-")) { - name = name.replace("notification-", ""); - let hasNotif = monitor.notifications.some( - (notification) => notification.type === name - ); - setMonitor((prev) => { - const notifs = [...prev.notifications]; - if (hasNotif) { - return { - ...prev, - notifications: notifs.filter((notif) => notif.type !== name), - }; - } else { - return { - ...prev, - notifications: [ - ...notifs, - name === "email" - ? { type: name, address: value } - : // TODO - phone number - { type: name, phone: value }, - ], - }; - } - }); - } else { - if (name === "interval") { - value = value * MS_PER_MINUTE; - } - if (name === "ignoreTlsErrors") { - value = checked; - } - setMonitor((prev) => ({ - ...prev, - [name]: value, - })); - - const validation = monitorValidation.validate( - { [name]: value }, - { abortEarly: false } - ); - - setErrors((prev) => { - const updatedErrors = { ...prev }; - - if (validation.error) updatedErrors[name] = validation.error.details[0].message; - else delete updatedErrors[name]; - return updatedErrors; - }); + // Handlers + const handlePause = async () => { + const res = await pauseMonitor(); + if (typeof res !== "undefined") { + triggerUpdate(); } }; - const handleSubmit = async (event) => { - event.preventDefault(); - const action = await dispatch(updateUptimeMonitor({ monitor: monitor })); - if (action.meta.requestStatus === "fulfilled") { - createToast({ body: "Monitor updated successfully!" }); - } else { - createToast({ body: "Failed to update monitor." }); - } - }; - - const [isOpen, setIsOpen] = useState(false); const handleRemove = async (event) => { event.preventDefault(); const action = await dispatch(deleteUptimeMonitor({ monitor })); @@ -191,403 +127,393 @@ const Configure = () => { } }; - const frequencies = [ - { _id: 1, name: "1 minute" }, - { _id: 2, name: "2 minutes" }, - { _id: 3, name: "3 minutes" }, - { _id: 4, name: "4 minutes" }, - { _id: 5, name: "5 minutes" }, - ]; + const onChange = (event) => { + let { name, value, checked } = event.target; + + if (name === "ignoreTlsErrors") { + value = checked; + } + + if (name === "interval") { + value = value * MS_PER_MINUTE; + } + setForm({ ...form, [name]: value }); + + const validation = monitorValidation.validate( + { [name]: value }, + { abortEarly: false } + ); + + setErrors((prev) => { + const updatedErrors = { ...prev }; + + if (validation.error) updatedErrors[name] = validation.error.details[0].message; + else delete updatedErrors[name]; + return updatedErrors; + }); + }; + + const onSubmit = async (e) => { + e.preventDefault(); + + const toSubmit = { + _id: form._id, + url: form.url, + name: form.name, + type: form.type, + matchMethod: form.matchMethod, + expectedValue: form.expectedValue, + jsonPath: form.jsonPath, + interval: form.interval, + teamId: form.teamId, + userId: form.userId, + port: form.port, + ignoreTlsErrors: form.ignoreTlsErrors, + }; + + if (!useAdvancedMatching) { + toSubmit.matchMethod = ""; + toSubmit.expectedValue = ""; + toSubmit.jsonPath = ""; + } + + const validation = monitorValidation.validate(toSubmit, { + abortEarly: false, + }); + + if (validation.error) { + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + setErrors(newErrors); + createToast({ body: "Please check the form for errors." }); + return; + } + + toSubmit.notifications = form.notifications; + const action = await dispatch(updateUptimeMonitor({ monitor: toSubmit })); + if (action.meta.requestStatus === "fulfilled") { + createToast({ body: "Monitor updated successfully!" }); + } else { + createToast({ body: "Failed to update monitor." }); + } + }; + + // Effects + useEffect(() => { + if (monitor?.matchMethod) { + setUseAdvancedMatching(true); + } + + setForm({ + ...monitor, + }); + }, [monitor, notifications]); // Parse the URL const parsedUrl = parseUrl(monitor?.url); const protocol = parsedUrl?.protocol?.replace(":", "") || ""; - // Notification modal state - const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false); - - const handleOpenNotificationModal = () => { - setIsNotificationModalOpen(true); - }; - - const handleClosenNotificationModal = () => { - setIsNotificationModalOpen(false); - }; - const { determineState, statusColor } = useMonitorUtils(); const { t } = useTranslation(); return ( - - {Object.keys(monitor).length === 0 ? ( - - ) : ( - <> - - + + + + + + + {form.name} + - - - {monitor.name} - - - - - - - - - {monitor.url?.replace(/^https?:\/\//, "") || "..."} - - - {t("editing")} - - - - - - - - - - - - {t("settingsGeneralSettings")} - - - {t("distributedUptimeCreateSelectURL")} - - - - - ) - } - id="monitor-url" - label={t("urlMonitor")} - placeholder="google.com" - value={parsedUrl?.host || monitor?.url || ""} - disabled={true} - /> - handleChange(event, "port")} - error={errors["port"] ? true : false} - helperText={errors["port"]} - hidden={monitor.type !== "port"} - /> - - - - - - - {t("distributedUptimeCreateIncidentNotification")} - - - {t("distributedUptimeCreateIncidentDescription")} - - - - {t("whenNewIncident")} - {/* {Leaving components commented for future funtionality implimentation} */} - {/* logger.warn("disabled")} - isDisabled={true} - /> */} - notification.type === "email" - ) || false - } - value={user?.email} - onChange={(event) => handleChange(event)} - /> - - + + - {/* logger.warn("disabled")} - isDisabled={true} - /> */} - {/* {monitor?.notifications?.some( - (notification) => notification.type === "emails" - ) ? ( - - logger.warn("disabled")} - /> - - You can separate multiple emails with a comma - - - ) : ( - "" - )} */} - - - - - - {t("ignoreTLSError")} - - {t("ignoreTLSErrorDescription")} - - - handleChange(event, "ignoreTlsErrors")} - sx={{ mr: theme.spacing(2) }} - /> - } - label={t("tlsErrorIgnored")} - /> - - - - - - {t("distributedUptimeCreateAdvancedSettings")} - - - - handleChange(event, "matchMethod")} - items={matchMethodOptions} - /> - - handleChange(event, "expectedValue")} - error={errors["expectedValue"] ? true : false} - helperText={errors["expectedValue"]} - /> - - {t("uptimeCreate")} - - - - handleChange(event, "jsonPath")} - error={errors["jsonPath"] ? true : false} - helperText={errors["jsonPath"]} - /> - - {t("uptimeCreateJsonPath")}  - - jmespath.org - -  {t("uptimeCreateJsonPathQuery")} - - - - )} - - - - + {form.url?.replace(/^https?:\/\//, "") || "..."} + + + {t("editing")} + + + + + + + + + + + {t("settingsGeneralSettings")} + + {t("distributedUptimeCreateSelectURL")} + + + + } + id="monitor-url" + label={t("urlMonitor")} + placeholder="google.com" + value={parsedUrl?.host || form?.url || ""} + disabled={true} + /> + - - )} - + + + + Notifications + + Select the notifications you want to send out + + + + + + + + {t("ignoreTLSError")} + + {t("ignoreTLSErrorDescription")} + + + + } + label={t("tlsErrorIgnored")} + /> + + + + + + {t("distributedUptimeCreateAdvancedSettings")} + + + + + + + + {t("uptimeCreate")} + + + + + + {t("uptimeCreateJsonPath")}  + + jmespath.org + +  {t("uptimeCreateJsonPathQuery")} + + + + )} + + + + + + { onConfirm={handleRemove} isLoading={isLoading} /> - - ); }; From e8a4496199232eae95acda0606681e96fed16093 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Jun 2025 12:56:24 +0800 Subject: [PATCH 08/18] add notification channel pages --- client/src/Routes/index.jsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 0f2079265..48343cd22 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -38,7 +38,8 @@ import CreateStatus from "../Pages/StatusPage/Create"; import StatusPages from "../Pages/StatusPage/StatusPages"; import Status from "../Pages/StatusPage/Status"; -import Integrations from "../Pages/Integrations"; +import Notifications from "../Pages/Notifications"; +import CreateNotifications from "../Pages/Notifications/create"; // Settings import Account from "../Pages/Account"; @@ -149,8 +150,12 @@ const Routes = () => { /> } + path="notifications" + element={} + /> + } /> Date: Mon, 9 Jun 2025 12:56:45 +0800 Subject: [PATCH 09/18] Add network service methods --- client/src/Utils/NetworkService.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js index 7cfa5a7f1..26c6bd460 100644 --- a/client/src/Utils/NetworkService.js +++ b/client/src/Utils/NetworkService.js @@ -1034,6 +1034,21 @@ class NetworkService { // Fallback to original behavior for backward compatibility return this.axiosInstance.post(`/settings/test-email`, { to }); } + + async createNotification(config) { + const { notification } = config; + return this.axiosInstance.post(`/notifications`, notification); + } + + async getNotificationsByTeamId(config) { + const { teamId } = config; + return this.axiosInstance.get(`/notifications/team/${teamId}`); + } + + async deleteNotificationById(config) { + const { id } = config; + return this.axiosInstance.delete(`/notifications/${id}`); + } } export default NetworkService; From ff11a911903028d7ad660087d7c55a96390906c9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Jun 2025 12:57:02 +0800 Subject: [PATCH 10/18] add notification config component --- .../Components/NotificationConfig/index.jsx | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 client/src/Components/NotificationConfig/index.jsx diff --git a/client/src/Components/NotificationConfig/index.jsx b/client/src/Components/NotificationConfig/index.jsx new file mode 100644 index 000000000..d3707eae4 --- /dev/null +++ b/client/src/Components/NotificationConfig/index.jsx @@ -0,0 +1,106 @@ +// Components +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Divider from "@mui/material/Divider"; +import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded"; +import Search from "../Inputs/Search"; + +// Utils +import { useState, useEffect } from "react"; +import { useTheme } from "@mui/material/styles"; +import PropTypes from "prop-types"; + +const NotificationConfig = ({ notifications, setMonitor, setNotifications }) => { + // Local state + const [notificationsSearch, setNotificationsSearch] = useState(""); + const [selectedNotifications, setSelectedNotifications] = useState([]); + + const handleSearch = (value) => { + setSelectedNotifications(value); + setMonitor((prev) => { + return { + ...prev, + notifications: value.map((notification) => notification._id), + }; + }); + }; + + // Handlers + const handleDelete = (id) => { + const updatedNotifications = selectedNotifications.filter( + (notification) => notification._id !== id + ); + + setSelectedNotifications(updatedNotifications); + setMonitor((prev) => { + return { + ...prev, + notifications: updatedNotifications.map((notification) => notification._id), + }; + }); + }; + + // Setup + const theme = useTheme(); + + useEffect(() => { + if (setNotifications) { + const toSet = setNotifications.map((notification) => { + return notifications.find((n) => n._id === notification); + }); + setSelectedNotifications(toSet); + } + }, [setNotifications, notifications]); + + return ( + + { + handleSearch(value); + }} + /> + + {selectedNotifications.map((notification, index) => ( + + + {notification.notificationName} + + { + handleDelete(notification._id); + }} + sx={{ cursor: "pointer" }} + /> + {index < selectedNotifications.length - 1 && } + + ))} + + + ); +}; + +NotificationConfig.propTypes = { + notifications: PropTypes.array, + setMonitor: PropTypes.func, + setNotifications: PropTypes.array, +}; + +export default NotificationConfig; From 46b2b4c3ab0db38a7f2561eb099333337ffa0f34 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Jun 2025 12:57:15 +0800 Subject: [PATCH 11/18] add hooks --- client/src/Hooks/useFetchUptimeMonitorById.js | 35 +++++++ client/src/Hooks/useNotifications.js | 91 +++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 client/src/Hooks/useFetchUptimeMonitorById.js create mode 100644 client/src/Hooks/useNotifications.js diff --git a/client/src/Hooks/useFetchUptimeMonitorById.js b/client/src/Hooks/useFetchUptimeMonitorById.js new file mode 100644 index 000000000..cb344569c --- /dev/null +++ b/client/src/Hooks/useFetchUptimeMonitorById.js @@ -0,0 +1,35 @@ +import { useState, useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { getUptimeMonitorById } from "../Features/UptimeMonitors/uptimeMonitorsSlice"; +import { useNavigate } from "react-router"; + +const useFetchUptimeMonitorById = (monitorId, updateTrigger) => { + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [monitor, setMonitor] = useState(null); + const navigate = useNavigate(); + const dispatch = useDispatch(); + useEffect(() => { + const fetchMonitor = async () => { + try { + setIsLoading(true); + const action = await dispatch(getUptimeMonitorById({ monitorId })); + + if (getUptimeMonitorById.fulfilled.match(action)) { + const monitor = action.payload.data; + setMonitor(monitor); + } else if (getUptimeMonitorById.rejected.match(action)) { + throw new Error(action.error.message); + } + } catch (error) { + navigate("/not-found", { replace: true }); + } finally { + setIsLoading(false); + } + }; + fetchMonitor(); + }, [monitorId, dispatch, navigate, updateTrigger]); + return [monitor, isLoading, error]; +}; + +export { useFetchUptimeMonitorById }; diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js new file mode 100644 index 000000000..4ece55acd --- /dev/null +++ b/client/src/Hooks/useNotifications.js @@ -0,0 +1,91 @@ +import { useState, useEffect, useCallback } from "react"; +import { createToast } from "../Utils/toastUtils"; +import { networkService } from "../main"; +import { useNavigate } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { useTranslation } from "react-i18next"; + +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([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const { user } = useSelector((state) => state.auth); + const { t } = useTranslation(); + + const getNotifications = useCallback(async () => { + try { + setIsLoading(true); + const response = await networkService.getNotificationsByTeamId({ + teamId: user.teamId, + }); + setNotifications(response?.data?.data ?? []); + } catch (error) { + setError(error); + createToast({ + body: t("notifications.fetch.failed"), + }); + } finally { + setIsLoading(false); + } + }, [user.teamId]); + + useEffect(() => { + getNotifications(); + }, [getNotifications, updateTrigger]); + + return [notifications, isLoading, error]; +}; + +const useDeleteNotification = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const { t } = useTranslation(); + + const deleteNotification = async (id, triggerUpdate) => { + try { + setIsLoading(true); + await networkService.deleteNotificationById({ id }); + createToast({ + body: t("notifications.delete.success"), + }); + triggerUpdate(); + } catch (error) { + setError(error); + createToast({ + body: t("notifications.delete.failed"), + }); + } finally { + setIsLoading(false); + } + }; + + return [deleteNotification, isLoading, error]; +}; +export { useCreateNotification, useGetNotificationsByTeamId, useDeleteNotification }; From 5b219068942a161be9feaf646954190710255a7d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Jun 2025 12:57:30 +0800 Subject: [PATCH 12/18] add notificaion pages --- .../src/Pages/Notifications/create/index.jsx | 324 ++++++++++++++++++ client/src/Pages/Notifications/index.jsx | 121 +++++++ 2 files changed, 445 insertions(+) create mode 100644 client/src/Pages/Notifications/create/index.jsx create mode 100644 client/src/Pages/Notifications/index.jsx diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx new file mode 100644 index 000000000..c188aca22 --- /dev/null +++ b/client/src/Pages/Notifications/create/index.jsx @@ -0,0 +1,324 @@ +// Components +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Breadcrumbs from "../../../Components/Breadcrumbs"; +import Button from "@mui/material/Button"; +import ConfigBox from "../../../Components/ConfigBox"; +import Box from "@mui/material/Box"; +import Select from "../../../Components/Inputs/Select"; +import TextInput from "../../../Components/Inputs/TextInput"; + +// Utils +import { useState } from "react"; +import { useSelector } from "react-redux"; +import { useTheme } from "@emotion/react"; +import { useCreateNotification } from "../../../Hooks/useNotifications"; +import { + notificationEmailValidation, + notificationWebhookValidation, + notificationPagerDutyValidation, +} from "../../../Validation/validation"; +import { createToast } from "../../../Utils/toastUtils"; +import { useTranslation } from "react-i18next"; + +// Setup + +const NOTIFICATION_TYPES = [ + { _id: 1, name: "E-mail", value: "email" }, + { _id: 2, name: "Slack", value: "webhook" }, + { _id: 3, name: "PagerDuty", value: "pager_duty" }, +]; + +const CreateNotifications = () => { + const theme = useTheme(); + const [createNotification, isLoading, error] = useCreateNotification(); + const BREADCRUMBS = [ + { name: "notifications", path: "/notifications" }, + { name: "create", path: "/notifications/create" }, + ]; + + // Redux state + const { user } = useSelector((state) => state.auth); + + // local state + const [notification, setNotification] = useState({ + userId: user._id, + teamId: user.teamId, + notificationName: "", + address: "", + type: NOTIFICATION_TYPES[0]._id, + config: { + webhookUrl: "", + platform: "", + routingKey: "", + }, + }); + const [errors, setErrors] = useState({}); + const { t } = useTranslation(); + + // handlers + const onSubmit = (e) => { + e.preventDefault(); + const form = { + ...notification, + type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value, + }; + + if (notification.type === 2) { + form.type = "webhook"; + } + + let error = null; + + if (form.type === "email") { + error = notificationEmailValidation.validate( + { notificationName: form.notificationName, address: form.address }, + { abortEarly: false } + ).error; + } else if (form.type === "webhook") { + form.config = { + platform: form.config.platform, + webhookUrl: form.config.webhookUrl, + }; + error = notificationWebhookValidation.validate( + { notificationName: form.notificationName, config: form.config }, + { abortEarly: false } + ).error; + } else if (form.type === "pager_duty") { + form.config = { + platform: form.config.platform, + routingKey: form.config.routingKey, + }; + error = notificationPagerDutyValidation.validate( + { notificationName: form.notificationName, config: form.config }, + { abortEarly: false } + ).error; + } + + if (error) { + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + createToast({ body: "Please check the form for errors." }); + setErrors(newErrors); + return; + } + + createNotification(form); + }; + + const onChange = (e) => { + const { name, value } = e.target; + + const newNotification = { ...notification, [name]: value }; + + // Handle config/platform initialization if type is webhook + + if (newNotification.type === 1) { + newNotification.config = null; + } else if (newNotification.type === 2) { + newNotification.address = ""; + newNotification.config = newNotification.config || {}; + if (name === "config") { + newNotification.config = value; + } + newNotification.config.platform = "slack"; + } else if (newNotification.type === 3) { + newNotification.config = newNotification.config || {}; + if (name === "config") { + newNotification.config = value; + } + newNotification.config.platform = "pager_duty"; + } + + // Field-level validation + let fieldError; + + if (name === "notificationName") { + const { error } = notificationEmailValidation.extract(name).validate(value); + fieldError = error?.message; + } + + if (newNotification.type === 1 && name === "address") { + const { error } = notificationEmailValidation.extract(name).validate(value); + fieldError = error?.message; + } + + if (newNotification.type === 2 && name === "config") { + // Validate only webhookUrl inside config + const { error } = notificationWebhookValidation.extract("config").validate(value); + fieldError = error?.message; + } + + // Set field-level error + setErrors((prev) => ({ + ...prev, + [name]: fieldError, + })); + + setNotification(newNotification); + }; + + return ( + + + {t("createNotifications.title")} + + + + + {t("createNotifications.nameSettings.title")} + + + {t("createNotifications.nameSettings.description")} + + + + + + + + + + {t("createNotifications.typeSettings.title")} + + + {t("createNotifications.typeSettings.description")} + + + + handleChange(event, "interval")} + onChange={onChange} /> @@ -442,7 +367,6 @@ const PageSpeedConfigure = () => { type="submit" variant="contained" color="accent" - onClick={handleSave} sx={{ px: theme.spacing(12) }} > {t("settingsSave")} diff --git a/client/src/Pages/PageSpeed/Create/index.jsx b/client/src/Pages/PageSpeed/Create/index.jsx index 1d40d4667..2b3e6e4e4 100644 --- a/client/src/Pages/PageSpeed/Create/index.jsx +++ b/client/src/Pages/PageSpeed/Create/index.jsx @@ -2,7 +2,6 @@ import { useNavigate } from "react-router-dom"; import { useState } from "react"; import { useSelector, useDispatch } from "react-redux"; - // Utility and Network import { monitorValidation } from "../../../Validation/validation"; import { @@ -11,6 +10,7 @@ import { } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; import { parseDomainName } from "../../../Utils/monitorUtils"; import { useTranslation } from "react-i18next"; +import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; // MUI import { useTheme } from "@emotion/react"; @@ -23,8 +23,8 @@ import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; import ConfigBox from "../../../Components/ConfigBox"; import { createToast } from "../../../Utils/toastUtils"; import Radio from "../../../Components/Inputs/Radio"; -import Checkbox from "../../../Components/Inputs/Checkbox"; import Select from "../../../Components/Inputs/Select"; +import NotificationsConfig from "../../../Components/NotificationConfig"; const MS_PER_MINUTE = 60000; @@ -57,6 +57,7 @@ const CreatePageSpeed = () => { const [errors, setErrors] = useState({}); const { user } = useSelector((state) => state.auth); const { isLoading } = useSelector((state) => state.pageSpeedMonitors); + const [notifications, notificationsAreLoading, error] = useGetNotificationsByTeamId(); // Setup const dispatch = useDispatch(); @@ -64,7 +65,7 @@ const CreatePageSpeed = () => { const theme = useTheme(); // Handlers - const handleCreateMonitor = async (event) => { + const onSubmit = async (event) => { event.preventDefault(); let form = { url: `http${https ? "s" : ""}://` + monitor.url, @@ -134,32 +135,6 @@ const CreatePageSpeed = () => { })); }; - const handleNotifications = (event, type) => { - const { value } = event.target; - let notifications = [...monitor.notifications]; - const notificationExists = notifications.some((notification) => { - if (notification.type === type && notification.address === value) { - return true; - } - return false; - }); - if (notificationExists) { - notifications = notifications.filter((notification) => { - if (notification.type === type && notification.address === value) { - return false; - } - return true; - }); - } else { - notifications.push({ type, address: value }); - } - - setMonitor((prev) => ({ - ...prev, - notifications, - })); - }; - const handleBlur = (event) => { const { name } = event.target; if (name === "url") { @@ -189,7 +164,7 @@ const CreatePageSpeed = () => { { component="h2" variant="h2" > - {t("distributedUptimeCreateIncidentNotification")} - - - {t("distributedUptimeCreateIncidentDescription")} + {t("notificationConfig.title")} + {t("notificationConfig.description")} - - {t("whenNewIncident")} - notification.type === "email" - )} - value={user?.email} - onChange={(event) => handleNotifications(event, "email")} - /> - + @@ -356,9 +321,9 @@ const CreatePageSpeed = () => { justifyContent="flex-end" >