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 (
+
+ }
+ onClick={() => navigate("/pagespeed")}
+ sx={{
+ width: "fit-content",
+ backgroundColor: theme.palette.otherColors.fillGray,
+ px: theme.gap.ml,
+ "& svg.MuiSvgIcon-root": {
+ mr: theme.gap.small,
+ fill: theme.palette.otherColors.slateGray,
+ },
+ }}
+ />
+
+
+
+
+ google.com
+
+
+ Your pagespeed monitor is live.
+
+
+
+ }
+ onClick={() => navigate(`/monitors/configure/${monitorId}`)}
+ sx={{
+ ml: "auto",
+ alignSelf: "flex-end",
+ backgroundColor: theme.palette.otherColors.fillGray,
+ px: theme.gap.medium,
+ "& svg": {
+ mr: "6px",
+ },
+ }}
+ />
+
+
+ }
+ title="Last checked"
+ value={
+ <>
+ {formatDate(getLastChecked(monitor?.checks, false))}{" "}
+
+ ({formatDurationRounded(getLastChecked(monitor?.checks))} ago)
+
+ >
+ }
+ />
+ }
+ title="Checks since"
+ value={
+ <>
+ {formatDate(new Date(monitor?.createdAt))}{" "}
+
+ (
+ {formatDurationRounded(
+ new Date() - new Date(monitor?.createdAt)
+ )}{" "}
+ ago)
+
+ >
+ }
+ >
+ }
+ title="Checks every"
+ value={formatDurationRounded(monitor?.interval)}
+ >
+
+ Score history
+ Performance report
+
+
+
+
+
+ Performance
+ Values are estimated and may vary.
+
+
+
+ );
+};
+
+export default PageSpeedDetails;
diff --git a/Client/src/Pages/PageSpeed/index.jsx b/Client/src/Pages/PageSpeed/index.jsx
index aec775ead..14e976dc4 100644
--- a/Client/src/Pages/PageSpeed/index.jsx
+++ b/Client/src/Pages/PageSpeed/index.jsx
@@ -10,31 +10,26 @@ import Fallback from "../../Components/Fallback";
import "./index.css";
import Button from "../../Components/Button";
import { useNavigate } from "react-router";
+import { getLastChecked } from "../../Utils/monitorUtils";
const Card = ({ data }) => {
const theme = useTheme();
-
- /**
- * Helper function to get duration since last check or the last date checked
- * @param {Array} checks Array of check objects.
- * @param {boolean} duration Whether the function should return the duration since last checked or the date itself
- * @returns {number} Timestamp of the most recent check.
- */
- const getLastChecked = (checks, duration = true) => {
- if (!checks || checks.length === 0) {
- return 0; // Handle case when no checks are available
- }
-
- // Data is sorted newest -> oldest, so newest check is the most recent
- if (!duration) {
- return new Date(checks[0].createdAt);
- }
- return new Date() - new Date(checks[0].createdAt);
- };
+ const navigate = useNavigate();
return (
-
-
+ .MuiStack-root": { backgroundColor: "#f9fafb" } }}
+ >
+ navigate(`/pagespeed/${data._id}`)}
+ sx={{ cursor: "pointer" }}
+ >
@@ -91,7 +86,7 @@ const PageSpeed = () => {
@@ -108,7 +103,7 @@ const PageSpeed = () => {
"Help analyze webpage speed",
"Give suggestions on how the page can be improved",
]}
- link="/page-speed/create"
+ link="/pagespeed/create"
/>
)}
diff --git a/Client/src/Utils/Theme.js b/Client/src/Utils/Theme.js
index f7dad9279..ba456654a 100644
--- a/Client/src/Utils/Theme.js
+++ b/Client/src/Utils/Theme.js
@@ -94,6 +94,7 @@ const theme = createTheme({
small: "8px",
medium: "12px",
ml: "16px",
+ mlplus: "20px",
large: "24px",
xl: "40px",
xxl: "60px",
@@ -133,6 +134,12 @@ const theme = createTheme({
dotColor: "#4e5ba6",
},
},
+ pie: {
+ green: { stroke: "#17b26a", text: "#079455", bg: "#ecfdf3" },
+ yellow: { stroke: "#fdb022", text: "#dc6803", bg: "#fffcf5" },
+ red: { stroke: "#f04438", text: "#f04438", bg: "#ffeaea" },
+ default: { stroke: "#4e5ba6", text: "#4e5ba6", bg: "#f2f4f7" },
+ },
});
export default theme;
diff --git a/Client/src/Utils/monitorUtils.js b/Client/src/Utils/monitorUtils.js
new file mode 100644
index 000000000..5ea5462e2
--- /dev/null
+++ b/Client/src/Utils/monitorUtils.js
@@ -0,0 +1,17 @@
+/**
+ * Helper function to get duration since last check or the last date checked
+ * @param {Array} checks Array of check objects.
+ * @param {boolean} duration Whether the function should return the duration since last checked or the date itself
+ * @returns {number} Timestamp of the most recent check.
+ */
+export const getLastChecked = (checks, duration = true) => {
+ if (!checks || checks.length === 0) {
+ return 0; // Handle case when no checks are available
+ }
+
+ // Data is sorted newest -> oldest, so newest check is the most recent
+ if (!duration) {
+ return new Date(checks[0].createdAt);
+ }
+ return new Date() - new Date(checks[0].createdAt);
+};
diff --git a/Client/src/Utils/timeUtils.js b/Client/src/Utils/timeUtils.js
index 37a821605..f7742dbaf 100644
--- a/Client/src/Utils/timeUtils.js
+++ b/Client/src/Utils/timeUtils.js
@@ -50,7 +50,6 @@ export const formatDate = (date) => {
day: "numeric",
hour: "numeric",
minute: "numeric",
- second: "numeric",
hour12: true,
};
diff --git a/Client/src/assets/icons/calendar-check.svg b/Client/src/assets/icons/calendar-check.svg
new file mode 100644
index 000000000..07d96dad9
--- /dev/null
+++ b/Client/src/assets/icons/calendar-check.svg
@@ -0,0 +1,3 @@
+
diff --git a/Client/src/assets/icons/interval-check.svg b/Client/src/assets/icons/interval-check.svg
new file mode 100644
index 000000000..923475c8b
--- /dev/null
+++ b/Client/src/assets/icons/interval-check.svg
@@ -0,0 +1,3 @@
+