diff --git a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx index 6742f9d90..2ecb2f6a8 100644 --- a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx +++ b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx @@ -6,6 +6,7 @@ import { Tooltip, CartesianGrid, ResponsiveContainer, + Text, } from "recharts"; import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; @@ -80,16 +81,37 @@ const CustomToolTip = ({ active, payload, label }) => { return null; }; -const MonitorDetailsAreaChart = ({ checks }) => { +const CustomTick = ({ x, y, payload, index }) => { + const theme = useTheme(); + const uiTimezone = useSelector((state) => state.ui.timezone); - const formatDate = (timestamp) => { - return formatDateWithTz(timestamp, "HH:mm:ss", uiTimezone); - }; + // Render nothing for the first tick + if (index === 0) return null; + return ( + + {formatDateWithTz(payload?.value, "HH:mm:ss", uiTimezone)} + + ); +}; - const memoizedChecks = useMemo(() => checks, [checks[0]]); +CustomTick.propTypes = { + x: PropTypes.number, + y: PropTypes.number, + payload: PropTypes.object, + index: PropTypes.number, +}; +const MonitorDetailsAreaChart = ({ checks }) => { const theme = useTheme(); + const memoizedChecks = useMemo(() => checks, [checks[0]]); return ( @@ -128,10 +150,12 @@ const MonitorDetailsAreaChart = ({ checks }) => { } + minTickGap={0} + axisLine={false} tickLine={false} - height={18} + height={20} + interval="equidistantPreserveStart" /> { - 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/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index b80fdee67..f501c6f4e 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -113,6 +113,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { {...item} /> )} + sx={{ mt: "auto" }} /> ); } diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index afaea43eb..8bd2928cb 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -58,7 +58,7 @@ const Incidents = () => { }; return ( - + {loading ? ( ) : ( diff --git a/Client/src/Pages/Monitors/Details/Charts/index.jsx b/Client/src/Pages/Monitors/Details/Charts/index.jsx index b70712340..d38335800 100644 --- a/Client/src/Pages/Monitors/Details/Charts/index.jsx +++ b/Client/src/Pages/Monitors/Details/Charts/index.jsx @@ -42,8 +42,8 @@ const CustomLabels = ({ CustomLabels.propTypes = { x: PropTypes.number.isRequired, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, + width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), firstDataPoint: PropTypes.object.isRequired, lastDataPoint: PropTypes.object.isRequired, type: PropTypes.string.isRequired, diff --git a/Client/src/Pages/Monitors/Details/index.jsx b/Client/src/Pages/Monitors/Details/index.jsx index eb18e36fc..8358897d9 100644 --- a/Client/src/Pages/Monitors/Details/index.jsx +++ b/Client/src/Pages/Monitors/Details/index.jsx @@ -218,6 +218,11 @@ const DetailsPage = ({ isAdmin }) => { onClick={openCertificate} sx={{ cursor: "pointer", + "& svg": { + width: 23, + height: 23, + top: "52%", + }, }} > @@ -444,14 +449,8 @@ const DetailsPage = ({ isAdmin }) => { data={[{ response: monitor?.periodAvgResponseTime }]} /> - - + + diff --git a/Client/src/Pages/Monitors/Details/styled.jsx b/Client/src/Pages/Monitors/Details/styled.jsx index 99112802d..728df4922 100644 --- a/Client/src/Pages/Monitors/Details/styled.jsx +++ b/Client/src/Pages/Monitors/Details/styled.jsx @@ -76,6 +76,7 @@ export const StatBox = styled(Box)(({ theme }) => ({ borderColor: theme.palette.border.light, borderRadius: 4, backgroundColor: theme.palette.background.main, + background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`, "& h2": { fontSize: 13, fontWeight: 500, diff --git a/Client/src/Pages/Monitors/utils.jsx b/Client/src/Pages/Monitors/utils.jsx index 7e6722be7..c6f0a8476 100644 --- a/Client/src/Pages/Monitors/utils.jsx +++ b/Client/src/Pages/Monitors/utils.jsx @@ -29,22 +29,26 @@ const useUtils = () => { const statusStyles = { up: { backgroundColor: theme.palette.success.bg, + background: `linear-gradient(340deg, ${theme.palette.success.light} -60%, ${theme.palette.success.bg} 35%)`, borderColor: theme.palette.success.light, "& h2": { color: theme.palette.success.main }, }, down: { backgroundColor: theme.palette.error.bg, + background: `linear-gradient(340deg, ${theme.palette.error.light} -60%, ${theme.palette.error.bg} 35%)`, borderColor: theme.palette.error.light, "& h2": { color: theme.palette.error.main }, }, paused: { backgroundColor: theme.palette.warning.light, - borderColor: theme.palette.warning.border, + background: `linear-gradient(340deg, ${theme.palette.warning.dark} -60%, ${theme.palette.warning.light} 35%)`, + borderColor: theme.palette.warning.dark, "& h2": { color: theme.palette.warning.main }, }, pending: { backgroundColor: theme.palette.warning.light, - borderColor: theme.palette.warning.border, + background: `linear-gradient(340deg, ${theme.palette.warning.dark} -60%, ${theme.palette.warning.light} 35%)`, + borderColor: theme.palette.warning.dark, "& h2": { color: theme.palette.warning.main }, }, }; diff --git a/Client/src/Pages/PageSpeed/Details/Charts/AreaChart.jsx b/Client/src/Pages/PageSpeed/Details/Charts/AreaChart.jsx new file mode 100644 index 000000000..fea80f5c4 --- /dev/null +++ b/Client/src/Pages/PageSpeed/Details/Charts/AreaChart.jsx @@ -0,0 +1,313 @@ +import PropTypes from "prop-types"; +import { + AreaChart, + Area, + XAxis, + Tooltip, + CartesianGrid, + ResponsiveContainer, + Text, +} from "recharts"; +import { useTheme } from "@emotion/react"; +import { useMemo } from "react"; +import { Box, Stack, Typography } from "@mui/material"; +import { formatDate } from "../../../../Utils/timeUtils"; + +const config = { + seo: { + id: "seo", + text: "SEO", + color: "unresolved", + }, + performance: { + id: "performance", + text: "performance", + color: "success", + }, + bestPractices: { + id: "bestPractices", + text: "best practices", + color: "warning", + }, + accessibility: { + id: "accessibility", + text: "accessibility", + color: "primary", + }, +}; + +/** + * Custom tooltip for the area chart. + * @param {Object} props + * @param {boolean} props.active - Whether the tooltip is active. + * @param {Array} props.payload - The payload data for the tooltip. + * @param {string} props.label - The label for the tooltip. + * @returns {JSX.Element|null} The tooltip element or null if not active. + */ + +const CustomToolTip = ({ active, payload, label, config }) => { + const theme = useTheme(); + console.log(payload); + if (active && payload && payload.length) { + return ( + + + {formatDate(new Date(label))} + + {Object.keys(config) + .reverse() + .map((key) => { + const { color } = config[key]; + const dotColor = theme.palette[color].main; + + return ( + + + + {config[key].text} + {" "} + + {payload[0].payload[key]} + + + ); + })} + + ); + } + return null; +}; + +CustomToolTip.propTypes = { + active: PropTypes.bool, + payload: PropTypes.array, + label: PropTypes.string, + config: PropTypes.object, +}; + +/** + * Processes data to insert gaps with null values based on the interval. + * @param {Array} data + * @param {number} interval - The interval in milliseconds for gaps. + * @returns {Array} The formatted data with gaps. + */ +const processDataWithGaps = (data, interval) => { + if (data.length === 0) return []; + let formattedData = []; + let last = new Date(data[0].createdAt).getTime(); + + // Helper function to add a null entry + const addNullEntry = (timestamp) => { + formattedData.push({ + accessibility: "N/A", + bestPractices: "N/A", + performance: "N/A", + seo: "N/A", + createdAt: timestamp, + }); + }; + + data.forEach((entry) => { + const current = new Date(entry.createdAt).getTime(); + + if (current - last > interval * 2) { + // Insert null entries for each interval + let temp = last + interval; + while (temp < current) { + addNullEntry(new Date(temp).toISOString()); + temp += interval; + } + } + + formattedData.push(entry); + last = current; + }); + + return formattedData; +}; + +/** + * Custom tick component to render ticks on the XAxis. + * + * @param {Object} props + * @param {number} props.x - The x coordinate for the tick. + * @param {number} props.y - The y coordinate for the tick. + * @param {Object} props.payload - The data object containing the tick value. + * @param {number} props.index - The index of the tick in the array of ticks. + * + * @returns {JSX.Element|null} The tick element or null if the tick should be hidden. + */ +const CustomTick = ({ x, y, payload, index }) => { + const theme = useTheme(); + + // Render nothing for the first tick + if (index === 0) return null; + + return ( + + {formatDate(new Date(payload.value), { + year: undefined, + month: undefined, + day: undefined, + })} + + ); +}; +CustomTick.propTypes = { + x: PropTypes.number, + y: PropTypes.number, + payload: PropTypes.shape({ + value: PropTypes.string.isRequired, + }), + index: PropTypes.number, +}; + +/** + * A chart displaying pagespeed details over time. + * @param {Object} props + * @param {Array} props.data - The data to display in the chart. + * @param {number} props.interval - The interval in milliseconds for processing gaps. + * @returns {JSX.Element} The area chart component. + */ + +const PagespeedDetailsAreaChart = ({ data, interval, metrics }) => { + const theme = useTheme(); + const memoizedData = useMemo( + () => processDataWithGaps(data, interval), + [data[0]] + ); + + const filteredConfig = Object.keys(config).reduce((result, key) => { + if (metrics[key]) { + result[key] = config[key]; + } + return result; + }, {}); + + return ( + + + + } + axisLine={false} + tickLine={false} + height={18} + minTickGap={0} + interval="equidistantPreserveStart" + /> + } + /> + + {Object.values(filteredConfig).map(({ id, color }) => { + const startColor = theme.palette[color].main; + const endColor = theme.palette[color].light; + + return ( + + + + + ); + })} + + {Object.keys(filteredConfig).map((key) => { + const { color } = filteredConfig[key]; + const strokeColor = theme.palette[color].main; + const bgColor = theme.palette.background.main; + + return ( + + ); + })} + + + ); +}; + +PagespeedDetailsAreaChart.propTypes = { + data: PropTypes.arrayOf( + PropTypes.shape({ + createdAt: PropTypes.string.isRequired, + accessibility: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + .isRequired, + bestPractices: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + .isRequired, + performance: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + .isRequired, + seo: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + }) + ).isRequired, + interval: PropTypes.number.isRequired, + metrics: PropTypes.object, +}; + +export default PagespeedDetailsAreaChart; diff --git a/Client/src/Pages/PageSpeed/Details/Charts/PieChart.jsx b/Client/src/Pages/PageSpeed/Details/Charts/PieChart.jsx new file mode 100644 index 000000000..37556f178 --- /dev/null +++ b/Client/src/Pages/PageSpeed/Details/Charts/PieChart.jsx @@ -0,0 +1,312 @@ +import PropTypes from "prop-types"; +import { PieChart as MuiPieChart } from "@mui/x-charts/PieChart"; +import { useDrawingArea } from "@mui/x-charts"; +import { useState } from "react"; +import { useTheme } from "@emotion/react"; +import { Box } from "@mui/material"; + +/** + * 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} + + + ); +}; + +PieCenterLabel.propTypes = { + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + color: PropTypes.string, + setExpand: PropTypes.func, +}; + +/** + * 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 / 4; // 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} + + + ); +}; + +// Validate props using PropTypes +PieValueLabel.propTypes = { + value: PropTypes.number.isRequired, + startAngle: PropTypes.number.isRequired, + endAngle: PropTypes.number.isRequired, + color: PropTypes.string.isRequired, + highlighted: PropTypes.bool.isRequired, +}; + +const PieChart = ({ audits }) => { + const theme = useTheme(); + + /** + * 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, strokeBg: string, text: string, bg: string}} The color properties for the given performance value. + */ + const getColors = (value) => { + if (value >= 90 && value <= 100) + return { + stroke: theme.palette.success.main, + strokeBg: theme.palette.success.light, + text: theme.palette.success.text, + bg: theme.palette.success.bg, + }; + else if (value >= 50 && value < 90) + return { + stroke: theme.palette.warning.main, + strokeBg: theme.palette.warning.light, + text: theme.palette.warning.text, + bg: theme.palette.warning.bg, + }; + else if (value >= 0 && value < 50) + return { + stroke: theme.palette.error.text, + strokeBg: theme.palette.error.light, + text: theme.palette.error.text, + bg: theme.palette.error.bg, + }; + return { + stroke: theme.palette.unresolved.main, + strokeBg: theme.palette.unresolved.light, + text: theme.palette.unresolved.main, + bg: theme.palette.unresolved.bg, + }; + }; + + /** + * 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 data = []; + 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); + data.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: 73, + outerRadius: 80, + additionalRadius: -20, + arcLabelRadius: 5, + }, + cx: pieSize.width / 2, + }); + + performance += Math.floor(value); + startAngle = endAngle + padding; + } + }); + + return data; + }; + + const pieSize = { width: 230, height: 230 }; + const pieData = getPieData(audits); + const colorMap = getColors(performance); + + const [highlightedItem, setHighLightedItem] = useState(null); + const [expand, setExpand] = useState(false); + return ( + setExpand(false)}> + {expand ? ( + + + {pieData?.map((pie) => ( + + ))} + + ) : ( + + + + )} + + ); +}; + +PieChart.propTypes = { + audits: PropTypes.object, +}; + +export default PieChart; diff --git a/Client/src/Pages/PageSpeed/Details/index.css b/Client/src/Pages/PageSpeed/Details/index.css index 1da62a362..2bd9bda44 100644 --- a/Client/src/Pages/PageSpeed/Details/index.css +++ b/Client/src/Pages/PageSpeed/Details/index.css @@ -1,41 +1,7 @@ -.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, -.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 h6 { - 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 { - 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; @@ -50,8 +16,3 @@ .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 index 49040a48f..06f59698e 100644 --- a/Client/src/Pages/PageSpeed/Details/index.jsx +++ b/Client/src/Pages/PageSpeed/Details/index.jsx @@ -1,156 +1,35 @@ import PropTypes from "prop-types"; -import { Box, Button, Skeleton, Stack, Typography } from "@mui/material"; -import { PieChart } from "@mui/x-charts/PieChart"; -import { useDrawingArea } from "@mui/x-charts"; +import { + Box, + Button, + Divider, + Stack, + Tooltip, + Typography, +} from "@mui/material"; import { useEffect, useState } from "react"; import { useTheme } from "@emotion/react"; import { useNavigate, useParams } from "react-router-dom"; import { useSelector } from "react-redux"; import { - formatDuration, formatDurationRounded, + formatDurationSplit, } from "../../../Utils/timeUtils"; +import { ChartBox, IconBox, StatBox } from "./styled"; import { logger } from "../../../Utils/Logger"; import { networkService } from "../../../main"; +import SkeletonLayout from "./skeleton"; 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 PageSpeedLineChart from "../../../Components/Charts/PagespeedLineChart"; +import SpedometerIcon from "../../../assets/icons/spedometer-icon.svg?react"; +import MetricsIcon from "../../../assets/icons/ruler-icon.svg?react"; +import ScoreIcon from "../../../assets/icons/monitor-graph-line.svg?react"; +import PerformanceIcon from "../../../assets/icons/performance-report.svg?react"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import PulseDot from "../../../Components/Animated/PulseDot"; +import PagespeedDetailsAreaChart from "./Charts/AreaChart"; +import Checkbox from "../../../Components/Inputs/Checkbox"; +import PieChart from "./Charts/PieChart"; import "./index.css"; -import useUtils from "../../Monitors/utils"; -import SkeletonLayout from "./skeleton"; - -const StatBox = ({ icon, title, value }) => { - const theme = useTheme(); - - return ( - - {icon} - - - {title} - - - {value} - - - - ); -}; - -StatBox.propTypes = { - icon: PropTypes.element, - title: PropTypes.string, - value: PropTypes.node, -}; - -/** - * 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} - - - ); -}; - -PieCenterLabel.propTypes = { - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - color: PropTypes.string, - setExpand: PropTypes.func, -}; - -/** - * 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} - - - ); -}; - -PieValueLabel.propTypes = { - value: PropTypes.number, - startAngle: PropTypes.number, - endAngle: PropTypes.number, - color: PropTypes.string, - highlighted: PropTypes.bool, -}; const PageSpeedDetails = () => { const theme = useTheme(); @@ -159,7 +38,6 @@ const PageSpeedDetails = () => { const [audits, setAudits] = useState({}); const { monitorId } = useParams(); const { authToken } = useSelector((state) => state.auth); - const { determineState, statusColor, pagespeedStatusMsg } = useUtils(); useEffect(() => { const fetchMonitor = async () => { @@ -184,138 +62,31 @@ const PageSpeedDetails = () => { fetchMonitor(); }, [monitorId, authToken, navigate]); - /** - * 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 { - stroke: theme.palette.success.main, - strokeBg: theme.palette.success.light, - text: theme.palette.success.text, - bg: theme.palette.success.bg, - shape: "circle", - }; - else if (value >= 50 && value < 90) - return { - stroke: theme.palette.warning.main, - strokeBg: theme.palette.warning.bg, - text: theme.palette.warning.text, - bg: theme.palette.warning.light, - shape: "square", - }; - else if (value >= 0 && value < 50) - return { - stroke: theme.palette.error.text, - strokeBg: theme.palette.error.light, - text: theme.palette.error.text, - bg: theme.palette.error.bg, - shape: "circle", - }; - return { - stroke: theme.palette.unresolved.main, - strokeBg: theme.palette.unresolved.light, - text: theme.palette.unresolved.main, - bg: theme.palette.unresolved.bg, - shape: "", - }; - }; - - /** - * 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: 210, height: 200 }; - const pieData = getPieData(audits); - const colorMap = getColors(performance); - - const [highlightedItem, setHighLightedItem] = useState(null); - const [expand, setExpand] = useState(false); - let loading = Object.keys(monitor).length === 0; const data = monitor?.checks ? [...monitor.checks].reverse() : []; - let sharedStyles = { - border: 1, - borderColor: theme.palette.border.light, - borderRadius: theme.shape.borderRadius, - backgroundColor: theme.palette.background.main, + const splitDuration = (duration) => { + const { time, format } = formatDurationSplit(duration); + return ( + <> + {time} + {format} + + ); + }; + + const [metrics, setMetrics] = useState({ + accessibility: true, + bestPractices: true, + performance: true, + seo: true, + }); + const handleMetrics = (id) => { + setMetrics((prev) => ({ ...prev, [id]: !prev[id] })); }; - const monitorState = determineState(monitor); return ( - + {loading ? ( ) : ( @@ -327,19 +98,82 @@ const PageSpeedDetails = () => { ]} /> - - {monitor?.url} - - - {pagespeedStatusMsg[monitorState] || "Pending..."} + {monitor.name} + + + + + + + + {monitor?.url} + + + Checking every {formatDurationRounded(monitor?.interval)}. + + - - } - title="Last checked" - value={ - <> - {formatDuration(monitor?.lastChecked)}{" "} - - ago - - - } - /> - } - title="Checks since" - value={ - <> - {formatDuration(monitor?.uptimeDuration)}{" "} - - ago - - - } - > - } - title="Checks every" - value={formatDurationRounded(monitor?.interval)} - > + + + checks since + + {splitDuration(monitor?.uptimeDuration)} + ago + + + + last check + + {splitDuration(monitor?.lastChecked)} + ago + + - - Score history - - - + + + Showing statistics for past 24 hours. + + + + + + + Score history + + + + + + + + + + Metrics + + + + + Shown + + + + handleMetrics("accessibility")} + /> + + handleMetrics("bestPractices")} + /> + + handleMetrics("performance")} + /> + + handleMetrics("seo")} + /> + + + + - - Performance report - - + + + + + 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 - - . - + + + + + + Performance 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 = ( - - ); + let score = audit.score * 100; + let bg = + score >= 90 + ? theme.palette.success.main + : score >= 50 + ? theme.palette.warning.main + : score >= 0 + ? theme.palette.error.text + : theme.palette.unresolved.main; // Find the position where the number ends and the unit begins const match = audit.displayValue.match( @@ -639,50 +363,55 @@ const PageSpeedDetails = () => { let unit; if (match) { value = match[1]; - unit = match[2]; + match[2] === "s" ? (unit = "seconds") : (unit = match[2]); } else { value = audit.displayValue; } return ( - {shape} - + {audit.title} {value} {unit} + ); })} - + )} diff --git a/Client/src/Pages/PageSpeed/Details/skeleton.jsx b/Client/src/Pages/PageSpeed/Details/skeleton.jsx index 24604e8a3..5f64b1e0d 100644 --- a/Client/src/Pages/PageSpeed/Details/skeleton.jsx +++ b/Client/src/Pages/PageSpeed/Details/skeleton.jsx @@ -1,5 +1,4 @@ import { Box, Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; /** * Renders a skeleton layout. @@ -7,21 +6,14 @@ import { useTheme } from "@emotion/react"; * @returns {JSX.Element} */ const SkeletonLayout = () => { - const theme = useTheme(); - return ( <> - + - + { diff --git a/Client/src/Pages/PageSpeed/Details/styled.jsx b/Client/src/Pages/PageSpeed/Details/styled.jsx new file mode 100644 index 000000000..d4feb7c8d --- /dev/null +++ b/Client/src/Pages/PageSpeed/Details/styled.jsx @@ -0,0 +1,90 @@ +import { Box, Stack, styled } from "@mui/material"; + +export const ChartBox = styled(Stack)(({ theme }) => ({ + display: "grid", + height: 300, + minWidth: 250, + border: 1, + borderStyle: "solid", + borderColor: theme.palette.border.light, + borderRadius: 4, + borderTopRightRadius: 16, + borderBottomRightRadius: 16, + backgroundColor: theme.palette.background.main, + "& h2": { + color: theme.palette.text.secondary, + fontSize: 15, + fontWeight: 500, + }, + "& p": { color: theme.palette.text.tertiary }, + "& > :nth-of-type(1)": { + gridColumn: 1, + gridRow: 1, + height: "fit-content", + paddingTop: theme.spacing(8), + paddingLeft: theme.spacing(8), + }, + "& > :nth-of-type(2)": { gridColumn: 1, gridRow: 2 }, + "& > :nth-of-type(3)": { + gridColumn: 2, + gridRow: "span 2", + padding: theme.spacing(8), + borderLeft: 1, + borderLeftStyle: "solid", + borderLeftColor: theme.palette.border.light, + borderRadius: 16, + backgroundColor: theme.palette.background.main, + background: `linear-gradient(325deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`, + }, +})); + +export const IconBox = styled(Box)(({ theme }) => ({ + height: 34, + minWidth: 34, + width: 34, + position: "relative", + border: 1, + borderStyle: "solid", + borderColor: theme.palette.border.dark, + borderRadius: 4, + backgroundColor: theme.palette.background.accent, + "& svg": { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + width: 20, + height: 20, + "& path": { + stroke: theme.palette.text.tertiary, + }, + }, +})); + +export const StatBox = styled(Box)(({ theme }) => ({ + padding: `${theme.spacing(4)} ${theme.spacing(8)}`, + minWidth: 200, + width: 225, + border: 1, + borderStyle: "solid", + borderColor: theme.palette.border.light, + borderRadius: 4, + backgroundColor: theme.palette.background.main, + background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`, + "& h2": { + fontSize: 13, + fontWeight: 500, + color: theme.palette.text.secondary, + textTransform: "uppercase", + }, + "& p": { + fontSize: 18, + color: theme.palette.text.primary, + marginTop: theme.spacing(2), + "& span": { + color: theme.palette.text.tertiary, + marginLeft: theme.spacing(2), + fontSize: 15, + }, + }, +})); diff --git a/Client/src/Utils/Theme/darkTheme.js b/Client/src/Utils/Theme/darkTheme.js index 261f8135f..2d1f80635 100644 --- a/Client/src/Utils/Theme/darkTheme.js +++ b/Client/src/Utils/Theme/darkTheme.js @@ -63,7 +63,7 @@ const darkTheme = createTheme({ uptimeGood: "#ffd600", uptimeExcellent: "#079455", }, - unresolved: { main: "#4e5ba6", light: "#e2eaf7", bg: "#f2f4f7" }, + unresolved: { main: "#664eff", light: "#3a1bff", bg: "#f2f4f7" }, divider: border.light, other: { icon: "#e6e6e6", diff --git a/Client/src/Utils/Theme/lightTheme.js b/Client/src/Utils/Theme/lightTheme.js index f00ece091..92e7f7817 100644 --- a/Client/src/Utils/Theme/lightTheme.js +++ b/Client/src/Utils/Theme/lightTheme.js @@ -51,7 +51,7 @@ const lightTheme = createTheme({ text: "#DC6803", main: "#fdb022", light: "#ffecbc", - bg: "#fffcf5", + bg: "#fef8ea", border: "#fec84b", }, percentage: { diff --git a/Client/src/assets/icons/certificate.svg b/Client/src/assets/icons/certificate.svg index a985bef9f..2bc6c7e74 100644 --- a/Client/src/assets/icons/certificate.svg +++ b/Client/src/assets/icons/certificate.svg @@ -1,3 +1,3 @@ - - + + \ No newline at end of file diff --git a/Client/src/assets/icons/monitor-graph-line.svg b/Client/src/assets/icons/monitor-graph-line.svg new file mode 100644 index 000000000..86049eb9e --- /dev/null +++ b/Client/src/assets/icons/monitor-graph-line.svg @@ -0,0 +1,3 @@ + + + diff --git a/Client/src/assets/icons/performance-report.svg b/Client/src/assets/icons/performance-report.svg new file mode 100644 index 000000000..8b7073ac3 --- /dev/null +++ b/Client/src/assets/icons/performance-report.svg @@ -0,0 +1,3 @@ + + + diff --git a/Client/src/assets/icons/ruler-icon.svg b/Client/src/assets/icons/ruler-icon.svg new file mode 100644 index 000000000..7266d2876 --- /dev/null +++ b/Client/src/assets/icons/ruler-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/Client/src/assets/icons/spedometer-icon.svg b/Client/src/assets/icons/spedometer-icon.svg new file mode 100644 index 000000000..19b13f44e --- /dev/null +++ b/Client/src/assets/icons/spedometer-icon.svg @@ -0,0 +1,3 @@ + + +