diff --git a/Client/src/App.jsx b/Client/src/App.jsx index a10228795..1443be30c 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -21,6 +21,7 @@ import ProtectedRoute from "./Components/ProtectedRoute"; import Details from "./Pages/Monitors/Details"; import Maintenance from "./Pages/Maintenance"; import withAdminCheck from "./HOC/withAdminCheck"; +import Configure from "./Pages/Monitors/Configure"; function App() { const AdminCheckedRegister = withAdminCheck(Register); @@ -45,6 +46,10 @@ function App() { path="/monitors/:monitorId/" element={} /> + } + /> } diff --git a/Client/src/Components/RadioButton/index.css b/Client/src/Components/RadioButton/index.css index dab37dda0..353bbdf55 100644 --- a/Client/src/Components/RadioButton/index.css +++ b/Client/src/Components/RadioButton/index.css @@ -1,13 +1,12 @@ -.create-monitor-form .custom-radio-button.MuiFormControlLabel-root { +.create-monitor-form .custom-radio-button.MuiFormControlLabel-root, +.configure-monitor .custom-radio-button.MuiFormControlLabel-root { padding: 5px; margin: -5px; border-radius: var(--env-var-radius-1); } -.create-monitor-form .custom-radio-button.MuiFormControlLabel-root:hover { - background-color: var(--env-var-color-13); -} -.create-monitor-form .custom-radio-button.MuiFormControlLabel-root svg{ - stroke-width: 0; +.create-monitor-form .custom-radio-button.MuiFormControlLabel-root:hover, +.configure-monitor .custom-radio-button.MuiFormControlLabel-root:hover { + background-color: var(--env-var-color-15); } .custom-radio-button.MuiFormControlLabel-root .MuiButtonBase-root { diff --git a/Client/src/Components/TabPanels/Account/TeamPanel.jsx b/Client/src/Components/TabPanels/Account/TeamPanel.jsx index 5ef356f9c..9312c3228 100644 --- a/Client/src/Components/TabPanels/Account/TeamPanel.jsx +++ b/Client/src/Components/TabPanels/Account/TeamPanel.jsx @@ -23,6 +23,7 @@ import { useState } from "react"; import EditSvg from "../../../assets/icons/edit.svg?react"; import Field from "../../Inputs/Field"; import { credentials } from "../../../Validation/validation"; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; /** * TeamPanel component manages the organization and team members, @@ -504,6 +505,7 @@ const TeamPanel = () => { style: { padding: 0 }, }, }} + IconComponent={KeyboardArrowDownIcon} sx={{ mt: theme.gap.xs }} > diff --git a/Client/src/Pages/Monitors/Configure/index.css b/Client/src/Pages/Monitors/Configure/index.css new file mode 100644 index 000000000..b9b8792b4 --- /dev/null +++ b/Client/src/Pages/Monitors/Configure/index.css @@ -0,0 +1,66 @@ +.configure-monitor h1.MuiTypography-root { + font-size: var(--env-var-font-size-large-plus); + color: var(--env-var-color-1); + font-weight: 600; +} +.configure-monitor h2.MuiTypography-root { + font-size: var(--env-var-font-size-large); + font-weight: 600; +} +.configure-monitor h2.MuiTypography-root, +.configure-monitor .config-box p.MuiTypography-root, +.configure-monitor .MuiSelect-select { + color: var(--env-var-color-5); +} +.configure-monitor p.MuiTypography-root, +.configure-monitor .MuiSelect-select { + font-size: var(--env-var-font-size-medium); +} +.configure-monitor h6.MuiTypography-root { + opacity: 0.6; +} +.configure-monitor h6.MuiTypography-root, +.configure-monitor p.MuiTypography-root:has(span), +.configure-monitor p.MuiTypography-root span.MuiTypography-root { + font-size: var(--env-var-font-size-small-plus); +} +.configure-monitor button.MuiButtonBase-root { + font-size: var(--env-var-font-size-medium); +} +.configure-monitor button.MuiButtonBase-root { + height: var(--env-var-height-2); +} +.configure-monitor button.MuiButtonBase-root:has(svg) { + line-height: 1; +} +.configure-monitor .config-box { + padding: var(--env-var-spacing-4) 50px; + padding-bottom: 60px; + border: 1px solid var(--env-var-color-16); + border-radius: var(--env-var-radius-1); +} +.configure-monitor .config-box .MuiBox-root, +.configure-monitor .config-box .MuiStack-root { + flex: 1; +} +.configure-monitor .MuiStack-root:has(span.MuiTypography-root.input-error) { + position: relative; +} +.configure-monitor span.MuiTypography-root.input-error { + position: absolute; + top: 100%; +} + +.MuiInputBase-root:has(#monitor-interval) { + height: 34px; + width: 100%; +} +#monitor-interval { + padding: 0 10px; + height: 100%; + display: flex; + align-items: center; +} +.MuiInputBase-root:not(.Mui-focused):has(#monitor-interval):hover fieldset { + border-color: var(--env-var-color-29); +} diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx new file mode 100644 index 000000000..8f0047206 --- /dev/null +++ b/Client/src/Pages/Monitors/Configure/index.jsx @@ -0,0 +1,333 @@ +import { useNavigate, useParams } from "react-router"; +import { useTheme } from "@emotion/react"; +import { useDispatch, useSelector } from "react-redux"; +import { useEffect, useState } from "react"; +import Button from "../../../Components/Button"; +import Field from "../../../Components/Inputs/Field"; +import RadioButton from "../../../Components/RadioButton"; +import { Box, MenuItem, Select, Stack, Typography } from "@mui/material"; +import WestRoundedIcon from "@mui/icons-material/WestRounded"; +import GreenCheck from "../../../assets/icons/checkbox-green.svg?react"; +import RedCheck from "../../../assets/icons/checkbox-red.svg?react"; +import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; + +import "./index.css"; +import { monitorValidation } from "../../../Validation/validation"; + +const formatDurationRounded = (ms) => { + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + let time = ""; + if (days > 0) { + time += `${days} day${days !== 1 ? "s" : ""}`; + return time; + } + if (hours > 0) { + time += `${hours} hour${hours !== 1 ? "s" : ""}`; + return time; + } + if (minutes > 0) { + time += `${minutes} minute${minutes !== 1 ? "s" : ""}`; + return time; + } + if (seconds > 0) { + time += `${seconds} second${seconds !== 1 ? "s" : ""}`; + return time; + } + + return time; +}; + +/** + * Helper function to get duration since last check + * @param {Array} checks Array of check objects. + * @returns {number} Timestamp of the most recent check. + */ +const getLastChecked = (checks) => { + if (!checks || checks.length === 0) { + return 0; // Handle case when no checks are available + } + return new Date() - new Date(checks[0].createdAt); +}; + +/** + * Configure page displays monitor configurations and allows for editing actions. + * @component + */ +const Configure = () => { + const MS_PER_MINUTE = 60000; + const navigate = useNavigate(); + const theme = useTheme(); + const dispatch = useDispatch(); + const { authToken } = useSelector((state) => state.auth); + const { monitors } = useSelector((state) => state.monitors); + const { monitorId } = useParams(); + + const idMap = { + "monitor-url": "url", + "monitor-name": "name", + "monitor-checks-http": "type", + "monitor-checks-ping": "type", + }; + + const [config, setConfig] = useState(); + const [monitor, setMonitor] = useState(); + const [errors, setErrors] = useState({}); + useEffect(() => { + const fetchMonitor = () => { + const data = monitors.find((monitor) => monitor._id === monitorId); + setConfig(data); + setMonitor({ + name: data.name, + url: data.url.replace(/^https?:\/\//, ""), + type: data.type, + interval: data.interval / MS_PER_MINUTE, + }); + }; + fetchMonitor(); + }, [monitorId, authToken]); + + const handleChange = (event, name) => { + const { value, id } = event.target; + if (!name) name = idMap[id]; + 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; + }); + }; + + const handleSubmit = (event) => { + event.preventDefault(); + // TODO + }; + + const frequencies = [1, 2, 3, 4, 5]; + + return ( + + } + onClick={() => navigate(-1)} + sx={{ + backgroundColor: "#f4f4f4", + mb: theme.gap.medium, + px: theme.gap.ml, + "& svg.MuiSvgIcon-root": { + pr: theme.gap.small, + }, + }} + /> + + + + {config?.status ? : } + + + {config?.url.replace(/^https?:\/\//, "") || "..."} + + + + Your site is {config?.status ? "up" : "down"}. + {" "} + Checking every {formatDurationRounded(config?.interval)}. Last + time checked{" "} + {formatDurationRounded(getLastChecked(config?.checks))} ago. + + + + } + sx={{ + backgroundColor: "#f4f4f4", + pl: theme.gap.small, + pr: theme.gap.medium, + "& svg": { + pr: theme.gap.xs, + }, + }} + /> + + + + + + General settings + + Here you can select the URL of the host, together with the type + of monitor. + + + + + + + + + + Checks to perform + + You can always add or remove checks after adding your site. + + + + + + + {errors["type"] ? ( + + {errors["type"]} + + ) : ( + "" + )} + + + + + + Advanced settings + + + + + Check frequency + + handleChange(event, "interval")} + > + {frequencies.map((freq) => ( + + {freq} {freq === 1 ? "minute" : "minutes"} + + ))} + + + + + + + + + + + ); +}; + +export default Configure; diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index e460ccaa2..04cae3fb6 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -5,7 +5,7 @@ import RadioButton from "../../../Components/RadioButton"; import Button from "../../../Components/Button"; import { Box, MenuItem, Select, Stack, Typography } from "@mui/material"; import { useSelector, useDispatch } from "react-redux"; -import { createMonitorValidation } from "../../../Validation/validation"; +import { monitorValidation } from "../../../Validation/validation"; import { createMonitor } from "../../../Features/Monitors/monitorsSlice"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; @@ -37,7 +37,7 @@ const CreateMonitor = () => { // }); //Advanced Settings Form const [advancedSettings, setAdvancedSettings] = useState({ - frequency: 1, + interval: 1, // retries: "", // codes: "", // redirects: "", @@ -57,7 +57,7 @@ const CreateMonitor = () => { [id]: checkbox ? true : value, })); - const validation = createMonitorValidation.validate( + const validation = monitorValidation.validate( { [id]: value }, { abortEarly: false } ); @@ -92,7 +92,7 @@ const CreateMonitor = () => { ...checks, }; - const { error } = createMonitorValidation.validate(monitor, { + const { error } = monitorValidation.validate(monitor, { abortEarly: false, }); @@ -107,8 +107,8 @@ const CreateMonitor = () => { ...monitor, description: monitor.name, userId: user._id, - // ...advancedSettings, // TODO frequency should be interval, then we can use spread - interval: advancedSettings.frequency * MS_PER_MINUTE, + // ...advancedSettings + interval: advancedSettings.interval * MS_PER_MINUTE, }; try { const action = await dispatch(createMonitor({ authToken, monitor })); @@ -333,10 +333,10 @@ const CreateMonitor = () => { - handleChange(event, "frequency", setAdvancedSettings) + handleChange(event, "interval", setAdvancedSettings) } MenuProps={{ PaperProps: { diff --git a/Client/src/Pages/Monitors/Details/index.jsx b/Client/src/Pages/Monitors/Details/index.jsx index 8ccf15d01..7e9214f18 100644 --- a/Client/src/Pages/Monitors/Details/index.jsx +++ b/Client/src/Pages/Monitors/Details/index.jsx @@ -237,6 +237,7 @@ const DetailsPage = () => { level="tertiary" label="Configure" img={} + onClick={() => navigate(`/monitors/configure/${monitorId}`)} sx={{ ml: "auto", alignSelf: "flex-end", diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 86753e5f9..a5bffb80e 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -73,7 +73,7 @@ const credentials = joi.object({ }), }); -const createMonitorValidation = joi.object({ +const monitorValidation = joi.object({ url: joi .string() .trim() @@ -85,7 +85,7 @@ const createMonitorValidation = joi.object({ .string() .trim() .messages({ "string.empty": "*This field is required." }), - frequency: joi.number().messages({ + interval: joi.number().messages({ "number.base": "*Frequency must be a number.", "any.required": "*Frequency is required.", }), @@ -106,4 +106,4 @@ const imageValidation = joi.object({ }), }); -export { credentials, imageValidation, createMonitorValidation }; +export { credentials, imageValidation, monitorValidation };