diff --git a/Client/src/App.jsx b/Client/src/App.jsx index a5b649e8b..cad8a4ebd 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -24,6 +24,8 @@ 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() { const AdminCheckedRegister = withAdminCheck(Register); @@ -69,6 +71,10 @@ function App() { path="maintenance" element={} /> + } + /> } @@ -86,13 +92,17 @@ function App() { element={} /> } /> } /> + } + /> } /> diff --git a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx index 02729a709..985b5b846 100644 --- a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx +++ b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx @@ -60,8 +60,6 @@ const MonitorDetailsAreaChart = ({ checks, filter }) => { const checkTime = new Date(checks[i].createdAt).getTime(); if (now - checkTime < limits[filter]) { result.push(checks[i]); - } else { - break; } } return result; diff --git a/Client/src/Components/Charts/PagespeedLineChart/index.css b/Client/src/Components/Charts/PagespeedLineChart/index.css new file mode 100644 index 000000000..e69de29bb diff --git a/Client/src/Components/Charts/PagespeedLineChart/index.jsx b/Client/src/Components/Charts/PagespeedLineChart/index.jsx new file mode 100644 index 000000000..270cbaca0 --- /dev/null +++ b/Client/src/Components/Charts/PagespeedLineChart/index.jsx @@ -0,0 +1,71 @@ +import { LineChart } from "@mui/x-charts/LineChart"; +import PropTypes from "prop-types"; + +const PageSpeedLineChart = ({ pageSpeedChecks = [] }) => { + const keyToLabel = { + performance: "Performance", + seo: "SEO", + bestPractices: "Best practices", + accessibility: "Accessibility", + }; + + const colors = { + performance: "#2aa02b", + seo: "#9467bd", + bestPractices: "#ff7f0e", + accessibility: "#1f76b3", + }; + + const customize = { + legend: { position: { vertical: "bottom", horizontal: "middle" } }, + margin: { bottom: 75 }, + }; + + const xLabels = pageSpeedChecks.map((check) => { + return check.createdAt; + }); + + return ( + ({ + 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/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/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/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..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..1af0fbba1 --- /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} An array of objects, each representing the properties for a slice of the pie chart. + * @returns {number} performance - A variable updated with the rounded sum of weighted values. + */ + let performance = 0; + const getPieData = (audits) => { + let props = []; + let startAngle = 0; + const padding = 3; // padding between arcs + const max = 360 - padding * (Object.keys(audits).length - 1); // _id is a child of audits + + Object.keys(audits).forEach((key) => { + if (audits[key].score) { + let value = audits[key].score * weights[key]; + let endAngle = startAngle + (weights[key] * max) / 100; + + let theme = getColors(audits[key].score * 100); + props.push({ + id: key, + data: [ + { + value: value, + color: theme.stroke, + label: key.toUpperCase(), + }, + { + value: weights[key] - value, + color: theme.strokeBg, + label: "", + }, + ], + arcLabel: (item) => `${item.label}`, + arcLabelRadius: 95, + startAngle: startAngle, + endAngle: endAngle, + innerRadius: 73, + outerRadius: 80, + cornerRadius: 2, + highlightScope: { faded: "global", highlighted: "series" }, + faded: { + innerRadius: 63, + outerRadius: 70, + additionalRadius: -20, + arcLabelRadius: 5, + }, + cx: pieSize.width / 2, + }); + + performance += Math.round(value); + startAngle = endAngle + padding; + } + }); + + return props; + }; + + const pieSize = { width: 200, height: 200 }; + const pieData = getPieData(audits); + const colorMap = getColors(performance); + + const [highlightedItem, setHighLightedItem] = useState(null); + const [expand, setExpand] = useState(false); + + 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, + }, + }} + /> + + + + + {monitor?.url} + + + Your pagespeed monitor is live. + + + + } + onClick={() => navigate(`/pagespeed/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 + + + setExpand(false)}> + {expand ? ( + + + {pieData?.map((pie) => ( + + ))} + + ) : ( + + + + )} + + + Values are estimated and may vary.{" "} + + See calculator + + + + + + The{" "} + + performance score is calculated + {" "} + directly from these{" "} + + metrics + + . + + + {Object.keys(audits).map((key) => { + if (key === "_id") return; + + let audit = audits[key]; + let metricParams = getColors(audit.score * 100); + + let shape = ( + + ); + if (metricParams.shape === "square") + shape = ( + + ); + else if (metricParams.shape === "triangle") + shape = ( + + ); + + // Find the position where the number ends and the unit begins + const match = audit.displayValue.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/); + let value; + let unit; + if (match) { + value = match[1]; + unit = match[2]; + } else { + value = audit.displayValue; + } + + return ( + + {shape} + + + {audit.title} + + + {value} + + {unit} + + + + + ); + })} + + + + + ); +}; + +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 = () => { navigate("/page-speed/create")} + onClick={() => navigate("/pagespeed/create")} /> @@ -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..72250bebb 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,36 @@ const theme = createTheme({ dotColor: "#4e5ba6", }, }, + pie: { + green: { + stroke: "#17b26a", + strokeBg: "#d4f4e1", + text: "#079455", + bg: "#ecfdf3", + shape: "circle", + }, + yellow: { + stroke: "#fdb022", + strokeBg: "rgba(255, 192, 34, 0.3)", + text: "#dc6803", + bg: "#fffcf5", + shape: "square", + }, + red: { + stroke: "#f04438", + strokeBg: "#ffecea", + text: "#f04438", + bg: "#ffeaea", + shape: "triangle", + }, + default: { + stroke: "#4e5ba6", + strokeBg: "#f2f4f7", + text: "#4e5ba6", + bg: "#f2f4f7", + shape: "", + }, + }, }); 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 @@ + + + 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/docs/CNAME b/docs/CNAME deleted file mode 100644 index 05329a3ec..000000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -uptime.bluewavelabs.ca \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 000000000..0e0aff729 --- /dev/null +++ b/index.html @@ -0,0 +1 @@ +BlueWave Uptime web page