diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 53bcb2c27..cad8a4ebd 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -24,6 +24,7 @@ import withAdminCheck from "./HOC/withAdminCheck"; import Configure from "./Pages/Monitors/Configure"; import PageSpeed from "./Pages/PageSpeed"; import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed"; +import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenanceWindow"; import PageSpeedDetails from "./Pages/PageSpeed/Details"; function App() { @@ -70,6 +71,10 @@ function App() { path="maintenance" element={} /> + } + /> } diff --git a/Client/src/Components/BasicTable/index.jsx b/Client/src/Components/BasicTable/index.jsx index 2a580d512..3f89de700 100644 --- a/Client/src/Components/BasicTable/index.jsx +++ b/Client/src/Components/BasicTable/index.jsx @@ -135,7 +135,9 @@ const BasicTable = ({ data, paginated, reversed, rowsPerPage = 5 }) => { {displayData.map((row) => { return ( diff --git a/Client/src/Components/Charts/ResponseTimeChart/index.jsx b/Client/src/Components/Charts/ResponseTimeChart/index.jsx index d76abc196..063ce3b16 100644 --- a/Client/src/Components/Charts/ResponseTimeChart/index.jsx +++ b/Client/src/Components/Charts/ResponseTimeChart/index.jsx @@ -8,7 +8,12 @@ const ResponseTimeChart = ({ checks = [] }) => { return ( - + {normalizedChecks.map((check, index) => ( { const handleNewPasswordRejected = (state, action) => { state.isLoading = false; state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to reset password."; + state.msg = action.payload ? action.payload.msg : "Failed to reset password."; }; const authSlice = createSlice({ diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js index 05c182f5f..52a02cd55 100644 --- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js @@ -77,9 +77,9 @@ export const updateUptimeMonitor = createAsyncThunk( name: monitor.name, description: monitor.description, interval: monitor.interval, - notifications: monitor.notifications + notifications: monitor.notifications, }; - const res = await axiosInstance.post( + const res = await axiosInstance.put( `/monitors/edit/${monitor._id}`, updatedFields, { @@ -104,16 +104,12 @@ export const deleteUptimeMonitor = createAsyncThunk( async (data, thunkApi) => { try { const { authToken, monitor } = data; - const res = await axiosInstance.post( - `/monitors/delete/${monitor._id}`, - {}, - { - headers: { - Authorization: `Bearer ${authToken}`, - "Content-Type": "application/json", - }, - } - ); + const res = await axiosInstance.delete(`/monitors/${monitor._id}`, { + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + }, + }); return res.data; } catch (error) { if (error.response && error.response.data) { diff --git a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css new file mode 100644 index 000000000..a799bf0af --- /dev/null +++ b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css @@ -0,0 +1,21 @@ +.maintenance-options > .MuiStack-root > .MuiStack-root.select-wrapper { + width: 380px; +} + +.date-picker .MuiOutlinedInput-root, +.time-picker .MuiOutlinedInput-root { + height: 34px; + font-size: var(--env-var-font-size-medium); +} + +.date-picker .MuiOutlinedInput-root { + width: 140px; +} + +.time-picker .MuiOutlinedInput-root { + width: 90px; +} + +.MuiInputAdornment-outlined .MuiIconButton-root svg { + width: 20px; +} diff --git a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx new file mode 100644 index 000000000..aea012e2c --- /dev/null +++ b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx @@ -0,0 +1,151 @@ +import { Box, Stack, Typography } from "@mui/material"; +import "./index.css"; +import React, { useState } from "react"; +import Button from "../../../Components/Button"; +import Back from "../../../assets/icons/left-arrow.svg?react"; +import Select from "../../../Components/Inputs/Select"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import dayjs from "dayjs"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker"; + +const directory = { + title: "Create a maintenance window", + description: "Your pings won’t be sent in this time frame.", +}; + +const repeatOptions = [ + { _id: 1, name: "Don't repeat" }, + { _id: 2, name: "Repeat daily" }, + { _id: 3, name: "Repeat weekly" }, +]; + +const configOptionTitle = (title, description) => { + return ( + + + {title} + + {description && ( + + {description} + + )} + + ); +}; + +const CreateNewMaintenanceWindow = () => { + const [values, setValues] = useState({ + repeat: 1, + }); + + const handleChange = (event, name) => { + const { value } = event.target; + setValues((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const configOptions = [ + { + title: "Repeat", + component: ( + handleChange(e, "repeat")} + id="repeat-mode" + items={repeatOptions} + value={values.repeat} + /> + ), + }, + { + title: "Date", + component: ( + + + + ), + }, + { + title: "Start time", + description: "Your pings won’t be sent in this time frame.", + component: ( + + + + ), + }, + ]; + + return ( + + + } + /> + + + {directory.title} + + + + {directory.description} + + + + {configOptions.map((item, index) => ( + + {configOptionTitle(item.title, item.description)} + {item.component && item.component} + + ))} + + + + ); +}; + +export default CreateNewMaintenanceWindow; diff --git a/Client/src/Pages/Maintenance/index.jsx b/Client/src/Pages/Maintenance/index.jsx index 92cd351ef..c589552f7 100644 --- a/Client/src/Pages/Maintenance/index.jsx +++ b/Client/src/Pages/Maintenance/index.jsx @@ -14,6 +14,7 @@ const Maintenance = () => { "Eliminate any misunderstandings", "Stop sending alerts in maintenance windows", ]} + link="/maintenance/create" /> ); diff --git a/Client/src/Pages/Monitors/index.jsx b/Client/src/Pages/Monitors/index.jsx index 654fd91dc..145faaec3 100644 --- a/Client/src/Pages/Monitors/index.jsx +++ b/Client/src/Pages/Monitors/index.jsx @@ -41,7 +41,14 @@ import { const Host = ({ params }) => { return ( - + { + event.stopPropagation(); + }} + > @@ -64,6 +71,7 @@ const Monitors = () => { const [anchorEl, setAnchorEl] = useState(null); const [actions, setActions] = useState({}); const openMenu = (event, id, url) => { + event.preventDefault(); setAnchorEl(event.currentTarget); setActions({ id: id, url: url }); }; @@ -141,7 +149,7 @@ const Monitors = () => { return { id: monitor._id, // disabled for now - // handleClick: () => navigate(`/monitors/${monitor._id}`), + handleClick: () => navigate(`/monitors/${monitor._id}`), data: [ { id: idx, data: }, { @@ -167,7 +175,10 @@ const Monitors = () => { <> openMenu(event, monitor._id, monitor.url)} + onClick={(event) => { + event.stopPropagation(); + openMenu(event, monitor._id, monitor.url); + }} sx={{ "&:focus": { outline: "none", @@ -231,7 +242,9 @@ const Monitors = () => { onClose={closeMenu} > window.open(actions.url, "_blank", "noreferrer")} + onClick={() => { + window.open(actions.url, "_blank", "noreferrer"); + }} > Open site diff --git a/Client/src/assets/icons/left-arrow.svg b/Client/src/assets/icons/left-arrow.svg new file mode 100644 index 000000000..db9b827ed --- /dev/null +++ b/Client/src/assets/icons/left-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/Client/src/index.css b/Client/src/index.css index 40f7dcd5d..7f66b6ebd 100644 --- a/Client/src/index.css +++ b/Client/src/index.css @@ -50,6 +50,8 @@ --env-var-color-28: #f79009; --env-var-color-29: #d0d5dd; --env-var-color-30: #fcfcfd; + --env-var-color-31: #1a1919; + --env-var-color-32: #f5f5f5; --env-var-radius-1: 4px; --env-var-radius-2: 8px; diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 6f34f71ce..02eb1bea4 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -439,7 +439,9 @@ const deleteUserController = async (req, res, next) => { const decodedToken = jwt.decode(token); const { _id, email } = decodedToken; + // TODO fix this hack const decodedTokenCastedAsRequest = { + query: {}, params: { userId: _id, }, @@ -480,6 +482,7 @@ const deleteUserController = async (req, res, next) => { await req.jobQueue.deleteJob(monitor); await req.db.deleteChecks(monitor._id); await req.db.deleteAlertByMonitorId(monitor._id); + await req.db.deletePageSpeedChecksByMonitorId(monitor._id); await req.db.deleteNotificationsByMonitorId(monitor._id); }) ); diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index e05ed0389..d45b300f8 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -182,6 +182,7 @@ const deleteMonitor = async (req, res, next) => { await req.jobQueue.deleteJob(monitor); await req.db.deleteChecks(monitor._id); await req.db.deleteAlertByMonitorId(monitor._id); + await req.db.deletePageSpeedChecksByMonitorId(monitor._id); await req.db.deleteNotificationsByMonitorId(monitor._id); /** diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js index 2813a3c00..68597a794 100644 --- a/Server/db/mongo/MongoDB.js +++ b/Server/db/mongo/MongoDB.js @@ -79,7 +79,7 @@ const { const { createPageSpeedCheck, getPageSpeedChecks, - deletePageSpeedChecks, + deletePageSpeedChecksByMonitorId, } = require("./modules/pageSpeedCheckModule"); //**************************************** @@ -157,7 +157,7 @@ module.exports = { deleteMonitorsByUserId, createPageSpeedCheck, getPageSpeedChecks, - deletePageSpeedChecks, + deletePageSpeedChecksByMonitorId, createMaintenanceWindow, getMaintenanceWindowsByUserId, getMaintenanceWindowsByMonitorId, diff --git a/Server/db/mongo/modules/pageSpeedCheckModule.js b/Server/db/mongo/modules/pageSpeedCheckModule.js index 65376a324..92f87d060 100644 --- a/Server/db/mongo/modules/pageSpeedCheckModule.js +++ b/Server/db/mongo/modules/pageSpeedCheckModule.js @@ -48,7 +48,7 @@ const getPageSpeedChecks = async (monitorId) => { * @throws {Error} */ -const deletePageSpeedChecks = async (monitorId) => { +const deletePageSpeedChecksByMonitorId = async (monitorId) => { try { const result = await PageSpeedCheck.deleteMany({ monitorId }); return result.deletedCount; @@ -60,5 +60,5 @@ const deletePageSpeedChecks = async (monitorId) => { module.exports = { createPageSpeedCheck, getPageSpeedChecks, - deletePageSpeedChecks, + deletePageSpeedChecksByMonitorId, }; diff --git a/Server/models/PageSpeedCheck.js b/Server/models/PageSpeedCheck.js index 8ff630197..3ae0bada9 100644 --- a/Server/models/PageSpeedCheck.js +++ b/Server/models/PageSpeedCheck.js @@ -1,5 +1,39 @@ const mongoose = require("mongoose"); +const AuditSchema = mongoose.Schema({ + id: { type: String, required: true }, + title: { type: String, required: true }, + description: { type: String, required: true }, + score: { type: Number, required: true }, + scoreDisplayMode: { type: String, required: true }, + displayValue: { type: String, required: true }, + numericValue: { type: Number, required: true }, + numericUnit: { type: String, required: true }, +}); + +const AuditsSchema = mongoose.Schema({ + cls: { + type: AuditSchema, + required: true, + }, + si: { + type: AuditSchema, + required: true, + }, + fcp: { + type: AuditSchema, + required: true, + }, + lcp: { + type: AuditSchema, + required: true, + }, + tbt: { + type: AuditSchema, + required: true, + }, +}); + /** * Mongoose schema for storing metrics from Google Lighthouse. * @typedef {Object} PageSpeedCheck @@ -37,6 +71,10 @@ const PageSpeedCheck = mongoose.Schema( type: Number, required: true, }, + audits: { + type: AuditsSchema, + required: true, + }, }, { timestamps: true, diff --git a/Server/routes/authRoute.js b/Server/routes/authRoute.js index 137434b7a..b01e90294 100644 --- a/Server/routes/authRoute.js +++ b/Server/routes/authRoute.js @@ -23,7 +23,7 @@ const { //Auth routes router.post("/register", upload.single("profileImage"), registerController); router.post("/login", loginController); -router.post( +router.put( "/user/:userId", upload.single("profileImage"), verifyJWT, diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index ae7a5852e..1f50c9e70 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -8,12 +8,12 @@ router.get("/:monitorId", monitorController.getMonitorById); router.get("/user/:userId", monitorController.getMonitorsByUserId); router.post("/", monitorController.createMonitor); -router.post( - "/delete/:monitorId", +router.delete( + "/:monitorId", verifyOwnership(Monitor, "monitorId"), monitorController.deleteMonitor ); -router.post( +router.put( "/edit/:monitorId", verifyOwnership(Monitor, "monitorId"), monitorController.editMonitor diff --git a/Server/service/networkService.js b/Server/service/networkService.js index 0ee65c424..dc8994945 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -129,6 +129,22 @@ class NetworkService { ); const pageSpeedResults = response.data; const categories = pageSpeedResults.lighthouseResult?.categories; + const audits = pageSpeedResults.lighthouseResult?.audits; + const { + "cumulative-layout-shift": cls, + "speed-index": si, + "first-contentful-paint": fcp, + "largest-contentful-paint": lcp, + "total-blocking-time": tbt, + } = audits; + + // Weights + // First Contentful Paint 10% + // Speed Index 10% + // Largest Contentful Paint 25% + // Total Blocking Time 30% + // Cumulative Layout Shift 25% + const checkData = { monitorId: job.data._id, status: true, @@ -136,7 +152,15 @@ class NetworkService { bestPractices: (categories["best-practices"]?.score || 0) * 100, seo: (categories.seo?.score || 0) * 100, performance: (categories.performance?.score || 0) * 100, + audits: { + cls, + si, + fcp, + lcp, + tbt, + }, }; + this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck); } catch (error) { const checkData = {