({
+ dataKey: key,
+ label: keyToLabel[key],
+ color: colors[key],
+ showMark: false,
+ }))}
+ yAxis={[{ min: 0, max: 100 }]}
+ xAxis={[
+ {
+ scaleType: "point",
+ data: xLabels,
+ valueFormatter: (val) => new Date(val).toLocaleDateString(),
+ },
+ ]}
+ dataset={pageSpeedChecks}
+ {...customize}
+ grid={{ vertical: true, horizontal: true }}
+ tooltip={{ trigger: "none" }}
+ slotProps={{
+ legend: {
+ direction: "row",
+ position: { vertical: "bottom", horizontal: "middle" },
+ padding: 2,
+ itemMarkWidth: 8,
+ itemMarkHeight: 8,
+ markGap: 5,
+ itemGap: 15,
+ labelStyle: {
+ fontSize: 13,
+ color: "#344054"
+ }
+ },
+ }}
+ />
+ );
+};
+
+PageSpeedLineChart.propTypes = {
+ pageSpeedChecks: PropTypes.array,
+};
+
+export default PageSpeedLineChart;
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) => (
|
|
);
diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx
index cde71bdf7..12f97ab1e 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
@@ -74,6 +62,10 @@ const Configure = () => {
useEffect(() => {
const data = monitors.find((monitor) => monitor._id === monitorId);
+ if (!data) {
+ console.error("Error fetching monitor of id: " + monitorId);
+ navigate("/not-found");
+ }
setMonitor({
...data,
});
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/Details/index.jsx b/Client/src/Pages/Monitors/Details/index.jsx
index 852f602e7..bbfba0b43 100644
--- a/Client/src/Pages/Monitors/Details/index.jsx
+++ b/Client/src/Pages/Monitors/Details/index.jsx
@@ -12,7 +12,7 @@ import Button from "../../../Components/Button";
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 SettingsIcon from "../../../assets/icons/settings.svg?react";
+import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
import {
formatDuration,
formatDurationRounded,
@@ -43,45 +43,53 @@ const DetailsPage = () => {
useEffect(() => {
const fetchMonitor = async () => {
- const res = await axiosInstance.get(
- `/monitors/${monitorId}?sortOrder=asc`,
- {
- headers: {
- Authorization: `Bearer ${authToken}`,
- },
- }
- );
- setMonitor(res.data.data);
- const data = {
- cols: [
- { id: 1, name: "Status" },
- { id: 2, name: "Date & Time" },
- { id: 3, name: "Message" },
- ],
- rows: res.data.data.checks.map((check, idx) => {
- const status = check.status === true ? "up" : "down";
+ try {
+ const res = await axiosInstance.get(
+ `/monitors/${monitorId}?sortOrder=asc`,
+ {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ },
+ }
+ );
+ setMonitor(res.data.data);
+ const data = {
+ cols: [
+ { id: 1, name: "Status" },
+ { id: 2, name: "Date & Time" },
+ { id: 3, name: "Message" },
+ ],
+ rows: res.data.data.checks.map((check, idx) => {
+ const status = check.status === true ? "up" : "down";
- return {
- id: check._id,
- data: [
- {
- id: idx,
- data: (
-
- ),
- },
- { id: idx + 1, data: new Date(check.createdAt).toLocaleString() },
- { id: idx + 2, data: check.statusCode },
- ],
- };
- }),
- };
+ return {
+ id: check._id,
+ data: [
+ {
+ id: idx,
+ data: (
+
+ ),
+ },
+ {
+ id: idx + 1,
+ data: new Date(check.createdAt).toLocaleString(),
+ },
+ { id: idx + 2, data: check.statusCode },
+ ],
+ };
+ }),
+ };
- setData(data);
+ setData(data);
+ } catch (error) {
+ console.error("Error fetching monitor of id: " + monitorId);
+ navigate("/not-found");
+ }
};
fetchMonitor();
}, [monitorId, authToken]);
@@ -195,7 +203,11 @@ const DetailsPage = () => {
level="tertiary"
label="Configure"
animate="rotate90"
- img={}
+ img={
+
+ }
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
sx={{
ml: "auto",
diff --git a/Client/src/Pages/Monitors/index.css b/Client/src/Pages/Monitors/index.css
index bce3bee72..180bac0b8 100644
--- a/Client/src/Pages/Monitors/index.css
+++ b/Client/src/Pages/Monitors/index.css
@@ -9,7 +9,7 @@
.monitors h2.MuiTypography-root {
font-size: var(--env-var-font-size-large);
}
-.monitors .MuiStack-root > button {
+.monitors .MuiStack-root > button:not(.MuiIconButton-root) {
font-size: var(--env-var-font-size-medium);
height: var(--env-var-height-2);
min-width: fit-content;
@@ -51,7 +51,7 @@ body:has(.monitors) .actions-menu li.MuiButtonBase-root {
body:has(.monitors) .actions-menu li.MuiButtonBase-root:last-of-type {
color: var(--env-var-color-26);
}
-body:has(.monitors) .MuiModal-root p.MuiTypography-root {
+body:has(.monitors) .actions-menu .MuiModal-root p.MuiTypography-root {
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-2);
}
diff --git a/Client/src/Pages/Monitors/index.jsx b/Client/src/Pages/Monitors/index.jsx
index 9c9f99d15..3c1d7bc64 100644
--- a/Client/src/Pages/Monitors/index.jsx
+++ b/Client/src/Pages/Monitors/index.jsx
@@ -35,17 +35,38 @@ 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 }) => {
return (
-
-
-
- {params.title}
- {params.precentage}%
+ {
+ event.stopPropagation();
+ window.open(params.url, "_blank", "noreferrer");
+ }}
+ sx={{
+ "&:focus": {
+ outline: "none",
+ },
+ mr: "3px",
+ }}
+ >
+
+
+
+ {params.title}
+
+ {params.percentage}%
+
+
);
};
@@ -60,6 +81,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 });
};
@@ -123,7 +145,7 @@ const Monitors = () => {
const params = {
url: monitor.url,
title: monitor.name,
- precentage: 100,
+ percentage: 100,
percentageColor:
monitor.status === true
? "var(--env-var-color-17)"
@@ -137,7 +159,7 @@ const Monitors = () => {
return {
id: monitor._id,
// disabled for now
- // handleClick: () => navigate(`/monitors/${monitor._id}`),
+ handleClick: () => navigate(`/monitors/${monitor._id}`),
data: [
{ id: idx, data: },
{
@@ -163,7 +185,10 @@ const Monitors = () => {
<>
openMenu(event, monitor._id, monitor.url)}
+ onClick={(event) => {
+ event.stopPropagation();
+ openMenu(event, monitor._id, monitor.url);
+ }}
sx={{
"&:focus": {
outline: "none",
@@ -227,7 +252,9 @@ const Monitors = () => {
onClose={closeMenu}
>
diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.css b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.css
index 3152d0e28..f1f10621f 100644
--- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.css
+++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.css
@@ -1,37 +1,31 @@
-.create-page-speed form {
- position: relative;
- margin: auto;
- margin-top: var(--env-var-default-2);
+.create-page-speed form > .MuiStack-root:not(:last-of-type) {
background-color: var(--env-var-color-8);
- width: 100%;
- max-width: 500px;
- box-shadow: var(--env-var-shadow-1);
+ border: solid 1px var(--env-var-color-6);
border-radius: var(--env-var-radius-1);
padding: var(--env-var-default-2);
}
-.create-page-speed h1.MuiTypography-root,
-.create-page-speed h2.MuiTypography-root,
+.create-page-speed h1.MuiTypography-root {
+ font-size: var(--env-var-font-size-large);
+ color: var(--env-var-color-1);
+}
.create-page-speed p.MuiTypography-root,
-.create-page-speed h2.MuiTypography-root > span.MuiTypography-root {
+.create-page-speed h3.MuiTypography-root {
color: var(--env-var-color-5);
}
.create-page-speed h1.MuiTypography-root,
-.create-page-speed h2.MuiTypography-root {
+.create-page-speed h3.MuiTypography-root {
font-weight: 600;
}
-.create-page-speed h2.MuiTypography-root {
- font-size: var(--env-var-font-size-medium-plus);
-}
.create-page-speed p.MuiTypography-root,
-.create-page-speed button,
-.create-page-speed h2.MuiTypography-root > span.MuiTypography-root {
+.create-page-speed h3.MuiTypography-root,
+.create-page-speed h3.MuiTypography-root > span.MuiTypography-root {
font-size: var(--env-var-font-size-medium);
}
.create-page-speed .MuiBox-root > .field + p.MuiTypography-root {
font-size: var(--env-var-font-size-small-plus);
}
.create-page-speed .MuiBox-root > .field + p.MuiTypography-root,
-.create-page-speed h2.MuiTypography-root > span.MuiTypography-root {
+.create-page-speed h3.MuiTypography-root > span.MuiTypography-root {
opacity: 0.6;
}
.create-page-speed .checkbox-wrapper {
@@ -47,17 +41,18 @@
pointer-events: none;
}
-.create-page-speed button:not(.MuiIconButton-root) {
+.create-page-speed button {
height: var(--env-var-height-2);
}
-.create-page-speed .MuiIconButton-root {
- position: absolute;
- top: 0;
- right: 0;
- margin: var(--env-var-spacing-1);
-}
body:has(.create-page-speed) .select-dropdown .MuiMenuItem-root,
.create-page-speed .select-wrapper .select-component > .MuiSelect-select {
text-transform: lowercase;
}
+
+.create-page-speed .field,
+.create-page-speed .section-disabled,
+.create-page-speed .select-wrapper,
+.create-page-speed h3.MuiTypography-root {
+ flex: 1;
+}
diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
index c85f6f77e..cb82310c4 100644
--- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
+++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
@@ -1,9 +1,8 @@
-import { Box, IconButton, Stack, Typography } from "@mui/material";
+import { Box, Stack, Typography } from "@mui/material";
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router";
-import CloseRoundedIcon from "@mui/icons-material/CloseRounded";
import Field from "../../../Components/Inputs/Field";
import Select from "../../../Components/Inputs/Select";
import Button from "../../../Components/Button";
@@ -11,6 +10,7 @@ import Checkbox from "../../../Components/Inputs/Checkbox";
import { monitorValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { createPageSpeed } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
+import WestRoundedIcon from "@mui/icons-material/WestRounded";
import "./index.css";
const CreatePageSpeed = () => {
@@ -22,17 +22,19 @@ const CreatePageSpeed = () => {
const { user, authToken } = useSelector((state) => state.auth);
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" },
+ { _id: 10, name: "10 minutes" },
+ { _id: 20, name: "20 minutes" },
+ { _id: 60, name: "1 hour" },
+ { _id: 1440, name: "1 day" },
+ { _id: 10080, name: "1 week" },
];
const [form, setForm] = useState({
name: "",
url: "",
- interval: 1,
+ interval: 3,
});
const [errors, setErrors] = useState({});
@@ -81,7 +83,7 @@ const CreatePageSpeed = () => {
try {
const action = await dispatch(createPageSpeed({ authToken, monitor }));
if (action.meta.requestStatus === "fulfilled") {
- navigate("/page-speed");
+ navigate("/pagespeed");
}
} catch (error) {
createToast({
@@ -96,102 +98,114 @@ const CreatePageSpeed = () => {
return (
+ }
+ onClick={() => navigate("/pagespeed")}
+ sx={{
+ backgroundColor: theme.palette.otherColors.fillGray,
+ mb: theme.gap.large,
+ px: theme.gap.ml,
+ "& svg.MuiSvgIcon-root": {
+ mr: theme.gap.small,
+ fill: theme.palette.otherColors.slateGray,
+ },
+ }}
+ />
diff --git a/Client/src/Pages/PageSpeed/Details/index.css b/Client/src/Pages/PageSpeed/Details/index.css
new file mode 100644
index 000000000..5d4d037b7
--- /dev/null
+++ b/Client/src/Pages/PageSpeed/Details/index.css
@@ -0,0 +1,72 @@
+.page-speed-details h1 {
+ font-size: var(--env-var-font-size-large-plus);
+ color: var(--env-var-color-1);
+}
+.page-speed-details h2,
+.page-speed-details h6 {
+ font-size: var(--env-var-font-size-large);
+}
+.page-speed-details h4,
+.page-speed-details p,
+.page-speed-details p > span {
+ font-size: var(--env-var-font-size-medium);
+}
+.page-speed-details h1 + span,
+.page-speed-details h4 > span {
+ font-size: var(--env-var-font-size-small-plus);
+}
+.page-speed-details h2,
+.page-speed-details h4,
+.page-speed-details p {
+ 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 button.MuiButtonBase-root {
+ height: 34px;
+}
+
+.page-speed-details .stat-box,
+.page-speed-details > .MuiStack-root:last-child,
+.page-speed-details > .MuiBox-root:nth-last-child(3) {
+ 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;
+}
+.page-speed-details .MuiPieArcLabel-root {
+ font-size: var(--env-var-font-size-small-plus);
+ transition: fill 300ms ease;
+}
+.page-speed-details .MuiPieArcLabel-faded {
+ fill: rgba(0, 0, 0, 0.3);
+}
+.page-speed-details .pie-label,
+.page-speed-details .pie-value-label {
+ transition: all 400ms ease;
+}
+.page-speed-details .MuiPieArc-root {
+ stroke: inherit;
+}
+
+.page-speed-details .metric {
+ min-width: 200px;
+ flex: 1;
+}
diff --git a/Client/src/Pages/PageSpeed/Details/index.jsx b/Client/src/Pages/PageSpeed/Details/index.jsx
new file mode 100644
index 000000000..ad3b66abc
--- /dev/null
+++ b/Client/src/Pages/PageSpeed/Details/index.jsx
@@ -0,0 +1,605 @@
+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";
+import PageSpeedLineChart from "../../../Components/Charts/PagespeedLineChart";
+
+/**
+ * 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, setExpand }) => {
+ const { width, height } = useDrawingArea();
+ return (
+ setExpand(true)}
+ >
+
+
+ {value}
+
+
+ );
+};
+
+/**
+ * A component that renders a label on a pie chart slice.
+ * The label is positioned relative to the center of the pie chart and is optionally highlighted.
+ *
+ * @param {Object} props
+ * @param {number} props.value - The value to display inside the pie slice.
+ * @param {number} props.startAngle - The starting angle of the pie slice in degrees.
+ * @param {number} props.endAngle - The ending angle of the pie slice in degrees.
+ * @param {string} props.color - The color of the label text when highlighted.
+ * @param {boolean} props.highlighted - Determines if the label should be highlighted or not.
+ * @returns {JSX.Element}
+ */
+const PieValueLabel = ({ value, startAngle, endAngle, color, highlighted }) => {
+ const { width, height } = useDrawingArea();
+
+ // Compute the midpoint angle in radians
+ const angle = (((startAngle + endAngle) / 2) * Math.PI) / 180;
+ const radius = height / 3.5; // length from center of the circle to where the text is positioned
+
+ // Calculate x and y positions
+ const x = Math.sin(angle) * radius;
+ const y = -Math.cos(angle) * radius;
+
+ return (
+
+
+ +{value}
+
+
+ );
+};
+
+const PageSpeedDetails = () => {
+ const theme = useTheme();
+ const navigate = useNavigate();
+ const [monitor, setMonitor] = useState({});
+ const [audits, setAudits] = 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);
+ setAudits(res.data.data.checks[0].audits);
+ };
+
+ fetchMonitor();
+ }, [monitorId]);
+
+ /**
+ * Weight constants for different performance metrics.
+ * @type {Object}
+ */
+ const weights = {
+ fcp: 10,
+ si: 10,
+ lcp: 25,
+ tbt: 30,
+ cls: 25,
+ };
+
+ /**
+ * 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;
+ };
+
+ /**
+ * Calculates and formats the data needed for rendering a pie chart based on audit scores and weights.
+ * This function generates properties for each pie slice, including angles, radii, and colors.
+ * It also calculates performance based on the weighted values.
+ *
+ * @returns {Array