diff --git a/client/src/Pages/Uptime/Configure/index.css b/client/src/Pages/Uptime/Configure/index.css deleted file mode 100644 index 3f24bd011..000000000 --- a/client/src/Pages/Uptime/Configure/index.css +++ /dev/null @@ -1,11 +0,0 @@ -.configure-monitor button.MuiButtonBase-root { - height: var(--env-var-height-2); -} - -.configure-monitor .MuiStack-root:has(span.MuiTypography-root.input-error) { - position: relative; -} -.configure-monitor span.MuiTypography-root.input-error { - position: absolute; - top: 100%; -} diff --git a/client/src/Pages/Uptime/Configure/index.jsx b/client/src/Pages/Uptime/Configure/index.jsx deleted file mode 100644 index 061db6ac0..000000000 --- a/client/src/Pages/Uptime/Configure/index.jsx +++ /dev/null @@ -1,526 +0,0 @@ -// Components -import Box from "@mui/material/Box"; -import Stack from "@mui/material/Stack"; -import Tooltip from "@mui/material/Tooltip"; -import Typography from "@mui/material/Typography"; -import Button from "@mui/material/Button"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import Switch from "@mui/material/Switch"; -import ConfigBox from "../../../Components/ConfigBox"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import TextInput from "../../../Components/Inputs/TextInput"; -import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import Select from "../../../Components/Inputs/Select"; -import Dialog from "../../../Components/Dialog"; -import PulseDot from "../../../Components/Animated/PulseDot"; -import Checkbox from "../../../Components/Inputs/Checkbox"; - -// Utils -import { useParams } from "react-router"; -import { useTheme } from "@emotion/react"; -import { useState } from "react"; -import { monitorValidation } from "../../../Validation/validation"; -import { createToast } from "../../../Utils/toastUtils"; -import { useTranslation } from "react-i18next"; -import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined"; -import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined"; -import { useMonitorUtils } from "../../../Hooks/useMonitorUtils"; -import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; -import { - useDeleteMonitor, - useUpdateMonitor, - usePauseMonitor, - useFetchMonitorById, -} from "../../../Hooks/monitorHooks"; -import NotificationsConfig from "../../../Components/NotificationConfig"; - -/** - * Parses a URL string and returns a URL object. - * - * @param {string} url - The URL string to parse. - * @returns {URL} - The parsed URL object if valid, otherwise an empty string. - */ -const parseUrl = (url) => { - try { - return new URL(url); - } catch (error) { - return null; - } -}; - -/** - * Configure page displays monitor configurations and allows for editing actions. - * @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 [notifications, notificationsAreLoading, notificationsError] = - useGetNotificationsByTeamId(); - const [pauseMonitor, isPausing, pauseError] = usePauseMonitor({}); - const [deleteMonitor, isDeleting] = useDeleteMonitor(); - const [updateMonitor, isUpdating] = useUpdateMonitor(); - const [isLoading] = useFetchMonitorById({ - monitorId, - setMonitor: setForm, - updateTrigger, - }); - - const MS_PER_MINUTE = 60000; - const theme = useTheme(); - - const matchMethodOptions = [ - { _id: "equal", name: "Equal" }, - { _id: "include", name: "Include" }, - { _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", - }; - - // Handlers - const handlePause = async () => { - const res = await pauseMonitor({ monitorId: form?._id, triggerUpdate }); - if (typeof res !== "undefined") { - triggerUpdate(); - } - }; - - const handleRemove = async (event) => { - event.preventDefault(); - await deleteMonitor({ monitor: form, redirect: "/uptime" }); - }; - - const onChange = (event) => { - let { name, value, checked } = event.target; - - if (name === "ignoreTlsErrors") { - value = checked; - } - - if (name === "useAdvancedMatching") { - setForm((prevForm) => { - return { - ...prevForm, - matchMethod: "equal", - }; - }); - setUseAdvancedMatching(!useAdvancedMatching); - return; - } - - 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 = {}; - validation.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; - await updateMonitor({ monitor: toSubmit, redirect: "/uptime" }); - }; - - // Parse the URL - const parsedUrl = parseUrl(form?.url); - const protocol = parsedUrl?.protocol?.replace(":", "") || ""; - - const { determineState, statusColor } = useMonitorUtils(); - - const { t } = useTranslation(); - - return ( - - - - - - - {form?.name} - - - - - - - - - {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")} - - - - )} - - - - - - - setIsOpen(false)} - confirmationButtonLabel="Delete" - onConfirm={handleRemove} - isLoading={isLoading} - /> - - ); -}; - -export default Configure; diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index 2416d2120..2d918cb9f 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -1,4 +1,14 @@ //Components +import { + Box, + Button, + ButtonGroup, + FormControlLabel, + Stack, + Switch, + Tooltip, + Typography, +} from "@mui/material"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import TextInput from "../../../Components/Inputs/TextInput"; import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; @@ -6,132 +16,186 @@ import Radio from "../../../Components/Inputs/Radio"; import Select from "../../../Components/Inputs/Select"; import ConfigBox from "../../../Components/ConfigBox"; import NotificationsConfig from "../../../Components/NotificationConfig"; -import Button from "@mui/material/Button"; -import ButtonGroup from "@mui/material/ButtonGroup"; -import Box from "@mui/material/Box"; -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import Switch from "@mui/material/Switch"; -import FormControlLabel from "@mui/material/FormControlLabel"; import Checkbox from "../../../Components/Inputs/Checkbox"; +import Dialog from "../../../Components/Dialog"; +import PulseDot from "../../../Components/Animated/PulseDot"; +import SkeletonLayout from "./skeleton"; // Utils +import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { monitorValidation } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; +import { + PauseOutlined as PauseOutlinedIcon, + PlayArrowOutlined as PlayArrowOutlinedIcon, +} from "@mui/icons-material"; +import { useMonitorUtils } from "../../../Hooks/useMonitorUtils"; import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; -import { useCreateMonitor, useFetchMonitorById } from "../../../Hooks/monitorHooks"; import { useParams } from "react-router-dom"; +import { + useCreateMonitor, + useDeleteMonitor, + useUpdateMonitor, + usePauseMonitor, + useFetchMonitorById, +} from "../../../Hooks/monitorHooks"; -const CreateMonitor = () => { - // Local state - const [errors, setErrors] = useState({}); - const [https, setHttps] = useState(true); - const [useAdvancedMatching, setUseAdvancedMatching] = useState(false); +/** + * Parses a URL string and returns a URL object. + * + * @param {string} url - The URL string to parse. + * @returns {URL} - The parsed URL object if valid, otherwise an empty string. + */ +const parseUrl = (url) => { + try { + return new URL(url); + } catch (error) { + return null; + } +}; + +/** + * Create page renders monitor creation or configuration views. + * @component + */ +const UptimeCreate = ({ isClone = false }) => { + const { monitorId } = useParams(); + const isCreate = typeof monitorId === "undefined" || isClone; + + // States const [monitor, setMonitor] = useState({ - url: "", - name: "", type: "http", matchMethod: "equal", expectedValue: "", jsonPath: "", notifications: [], - interval: 1, + interval: 60000, ignoreTlsErrors: false, + ...(isCreate ? { url: "", name: "" } : { port: undefined }), }); - - // Setup - const MS_PER_MINUTE = 60000; - const theme = useTheme(); - const { t } = useTranslation(); - const [notifications, notificationsAreLoading, error] = useGetNotificationsByTeamId(); - const [createMonitor, isCreating] = useCreateMonitor(); - const { monitorId } = useParams(); - - const formatAndSet = (monitor) => { - monitor.interval = monitor.interval / MS_PER_MINUTE; - setMonitor(monitor); + const [errors, setErrors] = useState({}); + const [https, setHttps] = useState(true); + const [isOpen, setIsOpen] = useState(false); + const [useAdvancedMatching, setUseAdvancedMatching] = useState(false); + const [updateTrigger, setUpdateTrigger] = useState(false); + const triggerUpdate = () => { + setUpdateTrigger(!updateTrigger); }; + + // Hooks + const [notifications, notificationsAreLoading, notificationsError] = + useGetNotificationsByTeamId(); + const { determineState, statusColor } = useMonitorUtils(); + // Network const [isLoading] = useFetchMonitorById({ monitorId, - setMonitor: formatAndSet, + setMonitor, updateTrigger: true, }); + const [createMonitor, isCreating] = useCreateMonitor(); + const [pauseMonitor, isPausing] = usePauseMonitor({}); + const [deleteMonitor, isDeleting] = useDeleteMonitor(); + const [updateMonitor, isUpdating] = useUpdateMonitor(); - const SELECT_VALUES = [ - { _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" }, + // Setup + const theme = useTheme(); + const { t } = useTranslation(); + + // Constants + const MS_PER_MINUTE = 60000; + const FREQUENCIES = [ + { _id: 1, name: t("time.oneMinute") }, + { _id: 2, name: t("time.twoMinutes") }, + { _id: 3, name: t("time.threeMinutes") }, + { _id: 4, name: t("time.fourMinutes") }, + { _id: 5, name: t("time.fiveMinutes") }, + ]; + const CRUMBS = [ + { name: "uptime", path: "/uptime" }, + ...(isCreate + ? [{ name: "create", path: `/uptime/create` }] + : [ + { name: "details", path: `/uptime/${monitorId}` }, + { name: "configure", path: `/uptime/configure/${monitorId}` }, + ]), ]; - const matchMethodOptions = [ - { _id: "equal", name: "Equal" }, - { _id: "include", name: "Include" }, - { _id: "regex", name: "Regex" }, + { _id: "equal", name: t("matchMethodOptions.equal") }, + { _id: "include", name: t("matchMethodOptions.include") }, + { _id: "regex", name: t("matchMethodOptions.regex") }, ]; - const expectedValuePlaceholders = { - regex: "^(success|ok)$", - equal: "success", - include: "ok", + regex: t("matchMethodOptions.regexPlaceholder"), + equal: t("matchMethodOptions.equalPlaceholder"), + include: t("matchMethodOptions.includePlaceholder"), }; - const monitorTypeMaps = { http: { - label: "URL to monitor", - placeholder: "google.com", - namePlaceholder: "Google", + label: t("monitorType.http.label"), + placeholder: t("monitorType.http.placeholder"), + namePlaceholder: t("monitorType.http.namePlaceholder"), }, ping: { - label: "IP address to monitor", - placeholder: "1.1.1.1", - namePlaceholder: "Google", + label: t("monitorType.ping.label"), + placeholder: t("monitorType.ping.placeholder"), + namePlaceholder: t("monitorType.ping.namePlaceholder"), }, docker: { - label: "Container ID", - placeholder: "abc123", - namePlaceholder: "My Container", + label: t("monitorType.docker.label"), + placeholder: t("monitorType.docker.placeholder"), + namePlaceholder: t("monitorType.docker.namePlaceholder"), }, port: { - label: "URL to monitor", - placeholder: "localhost", - namePlaceholder: "Localhost:5173", + label: t("monitorType.port.label"), + placeholder: t("monitorType.port.placeholder"), + namePlaceholder: t("monitorType.port.namePlaceholder"), }, }; - const BREADCRUMBS = [ - { name: "uptime", path: "/uptime" }, - { name: "create", path: `/uptime/create` }, - ]; - // Handlers - const onSubmit = async (event) => { event.preventDefault(); const { notifications, ...rest } = monitor; - let form = { - ...rest, - url: - //prepending protocol for url - monitor.type === "http" - ? `http${https ? "s" : ""}://` + monitor.url - : monitor.url, - port: monitor.type === "port" ? monitor.port : undefined, - name: monitor.name || monitor.url.substring(0, 50), - type: monitor.type, - interval: monitor.interval * MS_PER_MINUTE, - }; - - // If not using advanced matching, remove advanced settings + let form = {}; + if (isCreate) { + form = { + url: + monitor.type === "http" && !isClone + ? `http${https ? "s" : ""}://` + monitor.url + : monitor.url, + name: monitor.name || monitor.url.substring(0, 50), + type: monitor.type, + port: monitor.type === "port" ? monitor.port : undefined, + interval: monitor.interval, + matchMethod: monitor.matchMethod, + expectedValue: monitor.expectedValue, + jsonPath: monitor.jsonPath, + ignoreTlsErrors: monitor.ignoreTlsErrors, + }; + } else { + form = { + _id: monitor._id, + url: monitor.url, + name: monitor.name || monitor.url.substring(0, 50), + type: monitor.type, + matchMethod: monitor.matchMethod, + expectedValue: monitor.expectedValue, + jsonPath: monitor.jsonPath, + interval: monitor.interval, + teamId: monitor.teamId, + userId: monitor.userId, + port: monitor.type === "port" ? monitor.port : undefined, + ignoreTlsErrors: monitor.ignoreTlsErrors, + }; + } if (!useAdvancedMatching) { - form.matchMethod = undefined; - form.expectedValue = undefined; - form.jsonPath = undefined; + form.matchMethod = isCreate ? undefined : ""; + form.expectedValue = isCreate ? undefined : ""; + form.jsonPath = isCreate ? undefined : ""; } const { error } = monitorValidation.validate(form, { @@ -144,7 +208,7 @@ const CreateMonitor = () => { newErrors[err.path[0]] = err.message; }); setErrors(newErrors); - createToast({ body: "Please check the form for errors." }); + createToast({ body: t("checkFormError") }); return; } @@ -154,15 +218,18 @@ const CreateMonitor = () => { notifications: monitor.notifications, }; - await createMonitor({ monitor: form, redirect: "/uptime" }); + if (isCreate) { + await createMonitor({ monitor: form, redirect: "/uptime" }); + } else { + await updateMonitor({ monitor: form, redirect: "/uptime" }); + } }; const onChange = (event) => { - const { name, value, checked } = event.target; + let { name, value, checked } = event.target; - let newValue = value; if (name === "ignoreTlsErrors") { - newValue = checked; + value = checked; } if (name === "useAdvancedMatching") { @@ -170,15 +237,14 @@ const CreateMonitor = () => { return; } - const updatedMonitor = { - ...monitor, - [name]: newValue, - }; + if (name === "interval") { + value = value * MS_PER_MINUTE; + } - setMonitor(updatedMonitor); + setMonitor((prev) => ({ ...prev, [name]: value })); const { error } = monitorValidation.validate( - { type: monitor.type, [name]: newValue }, + { type: monitor.type, [name]: value }, { abortEarly: false } ); @@ -188,122 +254,254 @@ const CreateMonitor = () => { })); }; + const handlePause = async () => { + await pauseMonitor({ monitorId, triggerUpdate }); + }; + + const handleRemove = async (event) => { + event.preventDefault(); + await deleteMonitor({ monitor, redirect: "/uptime" }); + }; + + const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing; + const displayInterval = monitor?.interval / MS_PER_MINUTE || 1; + const parsedUrl = parseUrl(monitor?.url); + const protocol = parsedUrl?.protocol?.replace(":", "") || ""; + + useEffect(() => { + if (!isCreate || isClone) { + if (monitor.matchMethod) { + setUseAdvancedMatching(true); + } else { + setUseAdvancedMatching(false); + } + } + }, [monitor, isCreate]); + + if (Object.keys(monitor).length === 0) { + return ; + } + return ( - + - - - {t("createYour")}{" "} - - - {t("monitor")} - - - + - {t("distributedUptimeCreateChecks")} - - - {t("distributedUptimeCreateChecksDescription")} + + {!isCreate ? monitor.name : t("createYour") + " "} + + {isCreate && ( + + {t("monitor")} + + )} + {!isCreate && ( + + + + + + + + {monitor.url?.replace(/^https?:\/\//, "") || "..."} + + + {t("editing")} + + + )} - - + {!isCreate && ( + + + + + )} + + {isCreate && ( + + + + {t("distributedUptimeCreateChecks")} + + + {t("distributedUptimeCreateChecksDescription")} + + + + + + {monitor.type === "http" ? ( + + + + + ) : ( + "" + )} + - {monitor.type === "http" ? ( - - - - + {errors["type"]} + + ) : ( "" )} - - - - {errors["type"] ? ( - - - {errors["type"]} - - - ) : ( - "" - )} - - + + )} { {t("settingsGeneralSettings")} - {t(`uptimeGeneralInstructions.${monitor.type}`)} + {isCreate + ? t(`uptimeGeneralInstructions.${monitor.type}`) + : t("distributedUptimeCreateSelectURL")} - + : null + type={monitor?.type === "http" ? "url" : "text"} + label={ + (monitor.type === "http" || monitor.type === "port") && !isCreate + ? t("url") + : monitorTypeMaps[monitor.type].label || t("urlMonitor") } - label={monitorTypeMaps[monitor.type].label || "URL to monitor"} - https={https} placeholder={monitorTypeMaps[monitor.type].placeholder || ""} - value={monitor.url} - onChange={onChange} - error={errors["url"] ? true : false} + value={parsedUrl?.host + parsedUrl?.pathname || monitor?.url || ""} + https={isCreate ? https : protocol === "https"} + startAdornment={ + monitor?.type === "http" && ( + + ) + } helperText={errors["url"]} + onChange={onChange} + disabled={!isCreate} /> { type="text" label={t("displayName")} isOptional={true} - placeholder={monitorTypeMaps[monitor.type].namePlaceholder || ""} - value={monitor.name} + placeholder={monitorTypeMaps[monitor.type].namePlaceholder} + value={monitor.name || ""} onChange={onChange} error={errors["name"] ? true : false} helperText={errors["name"]} @@ -368,6 +575,7 @@ const CreateMonitor = () => { @@ -382,7 +590,7 @@ const CreateMonitor = () => { { {t("distributedUptimeCreateAdvancedSettings")} - + { { color={theme.palette.primary.contrastTextTertiary} opacity={0.8} > - {t("uptimeCreateJsonPath")}  + {t("uptimeCreateJsonPath") + " "} { > jmespath.org -  {t("uptimeCreateJsonPathQuery")} + {" " + t("uptimeCreateJsonPathQuery")} @@ -491,14 +701,32 @@ const CreateMonitor = () => { variant="contained" color="accent" disabled={!Object.values(errors).every((value) => value === undefined)} - loading={isCreating} + loading={isBusy} + sx={{ px: theme.spacing(12) }} > - {t("createMonitor")} + {t("settingsSave")} + + {!isCreate && ( + setIsOpen(false)} + confirmationButtonLabel={t("delete")} + onConfirm={handleRemove} + isLoading={isLoading} + /> + )} ); }; -export default CreateMonitor; +UptimeCreate.propTypes = { + isClone: PropTypes.bool, +}; + +export default UptimeCreate; diff --git a/client/src/Pages/Uptime/Configure/skeleton.jsx b/client/src/Pages/Uptime/Create/skeleton.jsx similarity index 100% rename from client/src/Pages/Uptime/Configure/skeleton.jsx rename to client/src/Pages/Uptime/Create/skeleton.jsx diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 6de07a2de..350d2f82d 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -14,7 +14,6 @@ import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed"; import Uptime from "../Pages/Uptime/Monitors"; import UptimeDetails from "../Pages/Uptime/Details"; import UptimeCreate from "../Pages/Uptime/Create"; -import UptimeConfigure from "../Pages/Uptime/Configure"; // PageSpeed import PageSpeed from "../Pages/PageSpeed/Monitors"; @@ -81,16 +80,20 @@ const Routes = () => { /> } /> + } + /> } /> } + element={} />