diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 69edf9a6d..cad8a4ebd 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -25,6 +25,7 @@ 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() { const AdminCheckedRegister = withAdminCheck(Register); @@ -91,13 +92,17 @@ function App() { element={} /> } /> } /> + } + /> } /> diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index 020831313..3b2a32394 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -26,7 +26,7 @@ const menu = [ { name: "Incidents", path: "incidents", icon: }, { name: "Status pages", path: "status", icon: }, { name: "Maintenance", path: "maintenance", icon: }, - { name: "Page speed", path: "page-speed", icon: }, + { name: "Page speed", path: "pagespeed", icon: }, { name: "Integrations", path: "integrations", icon: }, { name: "Support", path: "support", icon: }, { name: "Settings", path: "settings", icon: }, @@ -50,7 +50,15 @@ function Sidebar() { alignItems="center" gap={theme.gap.small} borderRadius={`${theme.shape.borderRadius}px`} - onClick={() => navigate(`/${item.path}`)} + onClick={() => + item.path === "support" + ? window.open( + "https://github.com/bluewave-labs/bluewave-uptime", + "_blank", + "noreferrer" + ) + : navigate(`/${item.path}`) + } sx={{ p: `${theme.gap.small} ${theme.gap.medium}` }} > {item.icon} diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx index cde71bdf7..c34be3a2f 100644 --- a/Client/src/Pages/Monitors/Configure/index.jsx +++ b/Client/src/Pages/Monitors/Configure/index.jsx @@ -9,7 +9,7 @@ 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 { getLastChecked } from "../../../Utils/monitorUtils"; import "./index.css"; import { monitorValidation } from "../../../Validation/validation"; import Select from "../../../Components/Inputs/Select"; @@ -36,18 +36,6 @@ const parseUrl = (url) => { } }; -/** - * 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 diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index 42ebb7cf4..e4e3f4d1c 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -150,7 +150,7 @@ const CreateMonitor = () => { img={} onClick={() => navigate("/monitors")} sx={{ - backgroundColor: "#f4f4f4", + backgroundColor: theme.palette.otherColors.fillGray, mb: theme.gap.medium, px: theme.gap.ml, "& svg.MuiSvgIcon-root": { diff --git a/Client/src/Pages/Monitors/index.jsx b/Client/src/Pages/Monitors/index.jsx index d1c3b9009..145faaec3 100644 --- a/Client/src/Pages/Monitors/index.jsx +++ b/Client/src/Pages/Monitors/index.jsx @@ -35,7 +35,7 @@ import { * @param {string} params.url - The URL of the host. * @param {string} params.title - The name of the host. * @param {string} params.percentageColor - The color of the percentage text. - * @param {number} params.precentage - The percentage to display. + * @param {number} params.percentage - The percentage to display. * @returns {React.ElementType} Returns a div element with the host details. */ const Host = ({ params }) => { @@ -54,7 +54,7 @@ const Host = ({ params }) => { {params.title} - {params.precentage}% + {params.percentage}% @@ -135,7 +135,7 @@ const Monitors = () => { const params = { url: monitor.url, title: monitor.name, - precentage: 100, + percentage: 100, percentageColor: monitor.status === true ? "var(--env-var-color-17)" diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx index 03d9a5414..f3b10e174 100644 --- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx @@ -82,7 +82,7 @@ const CreatePageSpeed = () => { try { const action = await dispatch(createPageSpeed({ authToken, monitor })); if (action.meta.requestStatus === "fulfilled") { - navigate("/page-speed"); + navigate("/pagespeed"); } } catch (error) { createToast({ @@ -102,9 +102,9 @@ const CreatePageSpeed = () => { label="Back" animate="slideLeft" img={} - onClick={() => navigate("/page-speed")} + onClick={() => navigate("/pagespeed")} sx={{ - backgroundColor: "#f4f4f4", + backgroundColor: theme.palette.otherColors.fillGray, mb: theme.gap.large, px: theme.gap.ml, "& svg.MuiSvgIcon-root": { diff --git a/Client/src/Pages/PageSpeed/Details/index.css b/Client/src/Pages/PageSpeed/Details/index.css new file mode 100644 index 000000000..39f0c60a5 --- /dev/null +++ b/Client/src/Pages/PageSpeed/Details/index.css @@ -0,0 +1,53 @@ +.page-speed-details h1 { + font-size: var(--env-var-font-size-large-plus); +} +.page-speed-details h2, +.page-speed-details h6 { + font-size: var(--env-var-font-size-large); +} +.page-speed-details h4, +.page-speed-details p { + font-size: var(--env-var-font-size-medium); +} +.page-speed-details svg + .MuiBox-root > p, +.page-speed-details h4 > span { + font-size: var(--env-var-font-size-small-plus); +} +.page-speed-details h2, +.page-speed-details h4 { + color: var(--env-var-color-5); +} +.page-speed-details h6 { + color: var(--env-var-color-3); + line-height: 22px; +} +.page-speed-details h1, +.page-speed-details h2, +.page-speed-details h6 { + font-weight: 600; +} +.page-speed-details .MuiBox-root > h2 { + font-size: 18px; + color: var(--env-var-color-1); +} +.page-speed-details button.MuiButtonBase-root { + height: 34px; +} + +.page-speed-details .stat-box, +.page-speed-details > .MuiBox-root { + border: solid 1px var(--env-var-color-6); + border-radius: var(--env-var-radius-2); + background-color: var(--env-var-color-8); + min-width: 200px; +} +.page-speed-details .stat-box { + flex: 1; +} +.page-speed-details .stat-box:last-of-type { + flex-shrink: 1; +} +.page-speed-details .stat-box svg { + width: 22px; + height: 22px; +} diff --git a/Client/src/Pages/PageSpeed/Details/index.jsx b/Client/src/Pages/PageSpeed/Details/index.jsx new file mode 100644 index 000000000..8dcf240b6 --- /dev/null +++ b/Client/src/Pages/PageSpeed/Details/index.jsx @@ -0,0 +1,378 @@ +import { Box, Stack, Typography } from "@mui/material"; +import { PieChart } from "@mui/x-charts/PieChart"; +import { useDrawingArea } from "@mui/x-charts"; +import { useEffect, useState } from "react"; +import { useTheme } from "@emotion/react"; +import { useNavigate, useParams } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { formatDate, formatDurationRounded } from "../../../Utils/timeUtils"; +import { getLastChecked } from "../../../Utils/monitorUtils"; +import axiosInstance from "../../../Utils/axiosConfig"; +import Button from "../../../Components/Button"; +import WestRoundedIcon from "@mui/icons-material/WestRounded"; +import SettingsIcon from "../../../assets/icons/settings-bold.svg?react"; +import LastCheckedIcon from "../../../assets/icons/calendar-check.svg?react"; +import ClockIcon from "../../../assets/icons/maintenance.svg?react"; +import IntervalCheckIcon from "../../../assets/icons/interval-check.svg?react"; +import GreenCheck from "../../../assets/icons/checkbox-green.svg?react"; +import RedCheck from "../../../assets/icons/checkbox-red.svg?react"; + +import "./index.css"; + +/** + * Displays a box with an icon, title, and value. + * + * @param {Object} props + * @param {ReactNode} props.icon - The icon to display in the box. + * @param {string} props.title - The title text to display above the value. + * @param {string | number} props.value - The value text to display in the box. + * @returns {JSX.Element} + */ +const StatBox = ({ icon, title, value }) => { + const theme = useTheme(); + + return ( + + {icon} + + + {title} + + {value} + + + ); +}; + +/** + * Renders a centered label within a pie chart. + * + * @param {Object} props + * @param {string | number} props.value - The value to display in the label. + * @param {string} props.color - The color of the text. + * @returns {JSX.Element} + */ +const PieCenterLabel = ({ value, color }) => { + const { width, height, left, top } = useDrawingArea(); + return ( + + {value} + + ); +}; + +const PageSpeedDetails = () => { + const theme = useTheme(); + const navigate = useNavigate(); + const [monitor, setMonitor] = useState({}); + const { monitorId } = useParams(); + const { authToken } = useSelector((state) => state.auth); + + useEffect(() => { + const fetchMonitor = async () => { + const res = await axiosInstance.get( + `/monitors/${monitorId}?sortOrder=desc`, + { + headers: { + Authorization: `Bearer ${authToken}`, + }, + } + ); + setMonitor(res.data.data); + }; + + fetchMonitor(); + }, []); + + const data = { + _id: "66ad34001d483d284550e8cb", + monitorId: "66ad2f5b4dfcd19cdbfc7205", + status: true, + accessibility: 100, + bestPractices: 79, + seo: 100, + performance: 100, + audits: { + cls: { + id: "cumulative-layout-shift", + title: "Cumulative Layout Shift", + description: + "Cumulative Layout Shift measures the movement of visible elements within the viewport.", + score: 1, + scoreDisplayMode: "numeric", + displayValue: "0", + numericValue: 0, + numericUnit: "unitless", + _id: "66ad34001d483d284550e8cd", + }, + si: { + id: "speed-index", + title: "Speed Index", + description: + "Speed Index shows how quickly the contents of a page are visibly populated.", + score: 1, + scoreDisplayMode: "numeric", + displayValue: "0.6s", + numericValue: 567.8934352052013, + numericUnit: "millisecond", + _id: "66ad34001d483d284550e8ce", + }, + fcp: { + id: "first-contentful-paint", + title: "First Contentful Paint", + description: + "First Contentful Paint marks the time at which the first text or image is painted.", + score: 0.1, + scoreDisplayMode: "numeric", + displayValue: "0.4s", + numericValue: 419, + numericUnit: "millisecond", + _id: "66ad34001d483d284550e8cf", + }, + lcp: { + id: "largest-contentful-paint", + title: "Largest Contentful Paint", + description: + "Largest Contentful Paint marks the time at which the largest text or image is painted.", + score: 1, + scoreDisplayMode: "numeric", + displayValue: "0.4s", + numericValue: 422.5, + numericUnit: "millisecond", + _id: "66ad34001d483d284550e8d0", + }, + tbt: { + id: "total-blocking-time", + title: "Total Blocking Time", + description: + "Sum of all time periods between FCP and Time to Interactive", + score: 1, + scoreDisplayMode: "numeric", + displayValue: "20ms", + numericValue: 16, + numericUnit: "millisecond", + _id: "66ad34001d483d284550e8d1", + }, + _id: "66ad34001d483d284550e8cc", + }, + createdAt: "2024-08-02T19:31:12.732Z", + updatedAt: "2024-08-02T19:31:12.732Z", + __v: 0, + }; + + /** + * Weight constants for different performance metrics. + * @type {Object} + */ + const weights = { + fcp: 10, + si: 10, + lcp: 25, + tbt: 30, + cls: 25, + }; + + /** + * Calculates the performance score based on the provided audit data and weights. + * + * @param {Object} audits - An object containing audit data. + * @param {Object} audits. - An object for each performance metric. + * @param {number} audits..score - The score for the specific metric. + * @returns {number} The calculated performance score, rounded to the nearest integer. + */ + const calculatePerformance = (audits) => { + let sum = 0; + Object.keys(audits).forEach((key) => { + if (audits[key].score) sum += audits[key].score * weights[key]; + }); + return Math.round(sum); + }; + + var performance = calculatePerformance(data.audits); + + /** + * Retrieves color properties based on the performance value. + * + * @param {number} value - The performance score used to determine the color properties. + * @returns {{stroke: string, text: string, bg: string}} The color properties for the given performance value. + */ + const getColors = (value) => { + if (value >= 90 && value <= 100) return theme.pie.green; + else if (value >= 50 && value < 90) return theme.pie.yellow; + else if (value >= 0 && value < 50) return theme.pie.red; + return theme.pie.default; + }; + + const colorMap = getColors(performance); + + return ( + +