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"; diff --git a/client/src/Components/MonitorStatusHeader/ConfigButton/index.jsx b/client/src/Components/MonitorStatusHeader/ConfigButton/index.jsx index c966abf33..7ad6ae9af 100644 --- a/client/src/Components/MonitorStatusHeader/ConfigButton/index.jsx +++ b/client/src/Components/MonitorStatusHeader/ConfigButton/index.jsx @@ -34,8 +34,8 @@ const ConfigButton = ({ shouldRender = true, monitorId, path }) => { ConfigButton.propTypes = { shouldRender: PropTypes.bool, - monitorId: PropTypes.string.isRequired, - path: PropTypes.string.isRequired, + monitorId: PropTypes.string, + path: PropTypes.string, }; export default ConfigButton; 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; diff --git a/client/src/Components/Sidebar/index.jsx b/client/src/Components/Sidebar/index.jsx index f1c512771..831782b53 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: , 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 }; diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx index 3e439a6e0..9425b7429 100644 --- a/client/src/Pages/Infrastructure/Create/index.jsx +++ b/client/src/Pages/Infrastructure/Create/index.jsx @@ -13,6 +13,8 @@ import { import { useHardwareMonitorsFetch } from "../Details/Hooks/useHardwareMonitorsFetch"; import { capitalizeFirstLetter } from "../../../Utils/stringUtils"; import { useTranslation } from "react-i18next"; +import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; +import NotificationsConfig from "../../../Components/NotificationConfig"; // MUI import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material"; @@ -24,7 +26,6 @@ import ConfigBox from "../../../Components/ConfigBox"; import TextInput from "../../../Components/Inputs/TextInput"; import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; import { createToast } from "../../../Utils/toastUtils"; -import Checkbox from "../../../Components/Inputs/Checkbox"; import Select from "../../../Components/Inputs/Select"; import { CustomThreshold } from "./Components/CustomThreshold"; @@ -65,6 +66,8 @@ const CreateInfrastructureMonitor = () => { // Fetch monitor details if editing const { monitor, isLoading, networkError } = useHardwareMonitorsFetch({ monitorId }); + const [notifications, notificationsAreLoading, notificationsError] = + useGetNotificationsByTeamId(); // State const [errors, setErrors] = useState({}); @@ -93,8 +96,7 @@ const CreateInfrastructureMonitor = () => { setInfrastructureMonitor({ url: monitor.url.replace(/^https?:\/\//, ""), name: monitor.name || "", - notifications: monitor.notifications?.filter((n) => typeof n === "object") || [], - notify_email: (monitor.notifications?.length ?? 0) > 0, + notifications: monitor.notifications, interval: monitor.interval / MS_PER_MINUTE, cpu: monitor.thresholds?.usage_cpu !== undefined, usage_cpu: monitor.thresholds?.usage_cpu ? monitor.thresholds.usage_cpu * 100 : "", @@ -119,17 +121,9 @@ const CreateInfrastructureMonitor = () => { }, [isCreate, monitor]); // Handlers - const handleCreateInfrastructureMonitor = async (event) => { + const onSubmit = async (event) => { event.preventDefault(); - const formattedNotifications = infrastructureMonitor.notifications.map((n) => - typeof n === "string" ? { type: "email", address: n } : n - ); - - if (infrastructureMonitor.notify_email) { - formattedNotifications.push({ type: "email", address: user.email }); - } - // Build the form let form = { url: `http${https ? "s" : ""}://` + infrastructureMonitor.url, @@ -155,7 +149,6 @@ const CreateInfrastructureMonitor = () => { ? { usage_temperature: infrastructureMonitor.usage_temperature } : {}), secret: infrastructureMonitor.secret, - notifications: formattedNotifications, }; const { error } = infrastructureMonitorValidation.validate(form, { @@ -167,6 +160,7 @@ const CreateInfrastructureMonitor = () => { error.details.forEach((err) => { newErrors[err.path[0]] = err.message; }); + console.log(newErrors); setErrors(newErrors); createToast({ body: "Please check the form for errors." }); return; @@ -218,7 +212,7 @@ const CreateInfrastructureMonitor = () => { } }; - const handleChange = (event) => { + const onChange = (event) => { const { value, name } = event.target; setInfrastructureMonitor({ ...infrastructureMonitor, @@ -244,27 +238,6 @@ const CreateInfrastructureMonitor = () => { }); }; - const handleNotifications = (event, type) => { - const { value, checked } = event.target; - let notifications = [...infrastructureMonitor.notifications]; - - if (checked) { - if (!notifications.some((n) => n.type === type && n.address === value)) { - notifications.push({ type, address: value }); - } - } else { - notifications = notifications.filter( - (n) => !(n.type === type && n.address === value) - ); - } - - setInfrastructureMonitor((prev) => ({ - ...prev, - notifications, - ...(type === "email" ? { notify_email: checked } : {}), - })); - }; - return ( { /> { label={t("infrastructureServerUrlLabel")} https={https} value={infrastructureMonitor.url} - onChange={handleChange} + onChange={onChange} error={errors["url"] ? true : false} helperText={errors["url"]} disabled={!isCreate} @@ -370,7 +342,7 @@ const CreateInfrastructureMonitor = () => { placeholder="Google" isOptional={true} value={infrastructureMonitor.name} - onChange={handleChange} + onChange={onChange} error={errors["name"]} /> { name="secret" label={t("infrastructureAuthorizationSecretLabel")} value={infrastructureMonitor.secret} - onChange={handleChange} + onChange={onChange} error={errors["secret"] ? true : false} helperText={errors["secret"]} /> @@ -387,25 +359,14 @@ const CreateInfrastructureMonitor = () => { - - {t("distributedUptimeCreateIncidentNotification")} - - - {t("distributedUptimeCreateIncidentDescription")} - + {t("notificationConfig.title")} + {t("notificationConfig.description")} - - handleNotifications(event, "email")} - /> - + @@ -438,7 +399,7 @@ const CreateInfrastructureMonitor = () => { fieldId={METRIC_PREFIX + metric} fieldName={METRIC_PREFIX + metric} fieldValue={String(infrastructureMonitor[METRIC_PREFIX + metric])} - onFieldChange={handleChange} + onFieldChange={onChange} alertUnit={metric == "temperature" ? "°C" : "%"} /> ); @@ -474,7 +435,7 @@ const CreateInfrastructureMonitor = () => { name="interval" label="Check frequency" value={infrastructureMonitor.interval || 15} - onChange={handleChange} + onChange={onChange} items={SELECT_VALUES} /> @@ -484,9 +445,9 @@ const CreateInfrastructureMonitor = () => { justifyContent="flex-end" > - - - - - - - {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} + /> + - - )} - + + + + {t("notificationConfig.title")} + {t("notificationConfig.description")} + + + + + + + {t("ignoreTLSError")} + + {t("ignoreTLSErrorDescription")} + + + + } + label={t("tlsErrorIgnored")} + /> + + + + + + {t("distributedUptimeCreateAdvancedSettings")} + + + + + + + + {t("uptimeCreate")} + + + + + + {t("uptimeCreateJsonPath")}  + + jmespath.org + +  {t("uptimeCreateJsonPathQuery")} + + + + )} + + + + + + { onConfirm={handleRemove} isLoading={isLoading} /> - - ); }; diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index 6e45230f6..c233b2a36 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,13 @@ const CreateMonitor = () => { - - {t("distributedUptimeCreateIncidentNotification")} - - - {t("distributedUptimeCreateIncidentDescription")} - + {t("notificationConfig.title")} + {t("notificationConfig.description")} - - notification.type === "email" - )} - value={user?.email} - onChange={(event) => handleNotifications(event, "email")} - /> - - - - - + @@ -445,9 +372,9 @@ const CreateMonitor = () => { sx={{ marginLeft: 0 }} control={ handleChange(event, "ignoreTlsErrors")} + onChange={onChange} sx={{ mr: theme.spacing(2) }} /> } @@ -466,32 +393,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 +432,13 @@ const CreateMonitor = () => { handleChange(event, "jsonPath")} + onChange={onChange} error={errors["jsonPath"] ? true : false} helperText={errors["jsonPath"]} /> @@ -541,9 +468,9 @@ const CreateMonitor = () => { justifyContent="flex-end" > - - setIsNotificationModalOpen(false)} - monitor={monitor} - setMonitor={setMonitor} - /> - + ); }; 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={} + /> + } /> 0) { - monitor.notifications = await Promise.all( - notifications.map(async (notification) => { - notification.monitorId = monitor._id; - return await this.db.createNotification(notification); - }) - ); - } - - await monitor.save(); // Add monitor to job queue this.jobQueue.addJob(monitor._id, monitor); return res.success({ @@ -325,18 +314,6 @@ class MonitorController { await Promise.all( monitors.map(async (monitor, index) => { - const notifications = enrichedData[index].notifications; - - if (notifications?.length) { - monitor.notifications = await Promise.all( - notifications.map(async (notification) => { - notification.monitorId = monitor._id; - return await this.db.createNotification(notification); - }) - ); - await monitor.save(); - } - this.jobQueue.addJob(monitor._id, monitor); }) ); @@ -422,10 +399,7 @@ class MonitorController { name: "deletePageSpeedChecks", fn: () => this.db.deletePageSpeedChecksByMonitorId(monitor._id), }, - { - name: "deleteNotifications", - fn: () => this.db.deleteNotificationsByMonitorId(monitor._id), - }, + { name: "deleteHardwareChecks", fn: () => this.db.deleteHardwareChecksByMonitorId(monitor._id), @@ -530,21 +504,9 @@ class MonitorController { try { const { monitorId } = req.params; - const monitorBeforeEdit = await this.db.getMonitorById(monitorId); - // Get notifications from the request body - const notifications = req.body.notifications ?? []; const editedMonitor = await this.db.editMonitor(monitorId, req.body); - await this.db.deleteNotificationsByMonitorId(editedMonitor._id); - - await Promise.all( - notifications.map(async (notification) => { - notification.monitorId = editedMonitor._id; - await this.db.createNotification(notification); - }) - ); - await this.jobQueue.updateJob(editedMonitor); return res.success({ diff --git a/server/db/models/Monitor.js b/server/db/models/Monitor.js index 5fa2834fa..3079abfba 100755 --- a/server/db/models/Monitor.js +++ b/server/db/models/Monitor.js @@ -42,7 +42,7 @@ const MonitorSchema = mongoose.Schema( }, matchMethod: { type: String, - enum: ["equal", "include", "regex"], + enum: ["equal", "include", "regex", ""], }, url: { type: String, diff --git a/server/db/models/Notification.js b/server/db/models/Notification.js index 27f2f932a..34afa1f31 100755 --- a/server/db/models/Notification.js +++ b/server/db/models/Notification.js @@ -7,7 +7,7 @@ const configSchema = mongoose.Schema( chatId: { type: String }, platform: { type: String, - enum: ["slack", "pager_duty"], + enum: ["slack", "pager_duty", "webhook"], }, routingKey: { type: String }, }, diff --git a/server/db/mongo/modules/monitorModule.js b/server/db/mongo/modules/monitorModule.js index 8e44daea7..3d9adab39 100755 --- a/server/db/mongo/modules/monitorModule.js +++ b/server/db/mongo/modules/monitorModule.js @@ -500,14 +500,6 @@ const getMonitorById = async (monitorId) => { error.status = 404; throw error; } - // Get notifications - const notifications = await Notification.find({ - monitorId: monitorId, - }); - - // Update monitor with notifications and save - monitor.notifications = notifications; - await monitor.save(); return monitor; } catch (error) { @@ -664,10 +656,8 @@ const getMonitorsWithChecksByTeamId = async (req) => { const createMonitor = async (req, res) => { try { const monitor = new Monitor({ ...req.body }); - // Remove notifications fom monitor as they aren't needed here - monitor.notifications = undefined; - await monitor.save(); - return monitor; + const saved = await monitor.save(); + return saved; } catch (error) { error.service = SERVICE_NAME; error.method = "createMonitor"; @@ -764,8 +754,6 @@ const deleteMonitorsByUserId = async (userId) => { * @throws {Error} */ const editMonitor = async (candidateId, candidateMonitor) => { - candidateMonitor.notifications = undefined; - try { const editedMonitor = await Monitor.findByIdAndUpdate(candidateId, candidateMonitor, { new: true, diff --git a/server/db/mongo/modules/monitorModuleQueries.js b/server/db/mongo/modules/monitorModuleQueries.js index abf91b325..f17eff2c2 100755 --- a/server/db/mongo/modules/monitorModuleQueries.js +++ b/server/db/mongo/modules/monitorModuleQueries.js @@ -148,6 +148,7 @@ const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => { type: 1, url: 1, isActive: 1, + notifications: 1, }, }, ], diff --git a/server/service/bufferService.js b/server/service/bufferService.js index 888b3ed08..84f3efd30 100755 --- a/server/service/bufferService.js +++ b/server/service/bufferService.js @@ -1,5 +1,5 @@ const SERVICE_NAME = "BufferService"; -const BUFFER_TIMEOUT = 1000 * 60 * 1; // 1 minute +const BUFFER_TIMEOUT = process.env.NODE_ENV === "development" ? 5000 : 1000 * 60 * 1; // 1 minute const TYPE_MAP = { http: "checks", ping: "checks", @@ -89,7 +89,7 @@ class BufferService { } this.buffers[bufferName] = []; } - this.logger.info({ + this.logger.debug({ message: `Flushed ${items} items`, service: this.SERVICE_NAME, method: "flushBuffers", diff --git a/server/validation/joi.js b/server/validation/joi.js index 9d01922e1..271cd2d21 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -183,7 +183,7 @@ const createMonitorBodyValidation = joi.object({ usage_disk: joi.number(), usage_temperature: joi.number(), }), - notifications: joi.array().items(joi.object()), + notifications: joi.array().items(joi.string()), secret: joi.string(), jsonPath: joi.string().allow(""), expectedValue: joi.string().allow(""), @@ -196,12 +196,12 @@ const editMonitorBodyValidation = joi.object({ name: joi.string(), description: joi.string(), interval: joi.number(), - notifications: joi.array().items(joi.object()), + notifications: joi.array().items(joi.string()), secret: joi.string(), ignoreTlsErrors: joi.boolean(), jsonPath: joi.string().allow(""), expectedValue: joi.string().allow(""), - matchMethod: joi.string(), + matchMethod: joi.string().allow(null, ""), port: joi.number().min(1).max(65535), thresholds: joi.object().keys({ usage_cpu: joi.number(),