Merge pull request #538 from bluewave-labs/feat/pagespeed-details

Pagespeed details
This commit is contained in:
Alexander Holliday authored and GitHub committed 2024-08-03 10:04:35 -07:00
commit 84fa9cc7c6
14 files changed
+503 -47

No files matched your search

+7 -2
View File
@@ -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={<ProtectedRoute Component={Account} open="team" />}
/>
<Route
path="page-speed"
path="pagespeed"
element={<ProtectedRoute Component={PageSpeed} />}
/>
<Route
path="page-speed/create"
path="pagespeed/create"
element={<ProtectedRoute Component={CreatePageSpeed} />}
/>
<Route
path="pagespeed/:monitorId"
element={<ProtectedRoute Component={PageSpeedDetails} />}
/>
</Route>
<Route exact path="/login" element={<Login />} />
+10 -2
View File
@@ -26,7 +26,7 @@ const menu = [
{ name: "Incidents", path: "incidents", icon: <Incidents /> },
{ name: "Status pages", path: "status", icon: <StatusPages /> },
{ name: "Maintenance", path: "maintenance", icon: <Maintenance /> },
{ name: "Page speed", path: "page-speed", icon: <PageSpeed /> },
{ name: "Page speed", path: "pagespeed", icon: <PageSpeed /> },
{ name: "Integrations", path: "integrations", icon: <Integrations /> },
{ name: "Support", path: "support", icon: <Support /> },
{ name: "Settings", path: "settings", icon: <Settings /> },
@@ -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}
+1 -13
View File
@@ -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
@@ -150,7 +150,7 @@ const CreateMonitor = () => {
img={<WestRoundedIcon />}
onClick={() => navigate("/monitors")}
sx={{
backgroundColor: "#f4f4f4",
backgroundColor: theme.palette.otherColors.fillGray,
mb: theme.gap.medium,
px: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
+3 -3
View File
@@ -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 }) => {
<Box>
{params.title}
<Typography component="span" sx={{ color: params.percentageColor }}>
{params.precentage}%
{params.percentage}%
</Typography>
</Box>
</Stack>
@@ -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)"
@@ -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={<WestRoundedIcon />}
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": {
@@ -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;
}
@@ -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 (
<Stack
className="stat-box"
direction="row"
gap={theme.gap.small}
p={theme.gap.ml}
pb={theme.gap.mlplus}
>
{icon}
<Box>
<Typography variant="h6" mb={theme.gap.medium}>
{title}
</Typography>
<Typography variant="h4">{value}</Typography>
</Box>
</Stack>
);
};
/**
* 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 (
<text
x={left + width - 1}
y={top + height / 2 + 2}
style={{
fill: color,
fontSize: "55px",
textAnchor: "middle",
dominantBaseline: "central",
userSelect: "none",
}}
>
{value}
</text>
);
};
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.<metric> - An object for each performance metric.
* @param {number} audits.<metric>.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 (
<Stack className="page-speed-details" gap={theme.gap.large}>
<Button
level="tertiary"
label="Back"
animate="slideLeft"
img={<WestRoundedIcon />}
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,
},
}}
/>
<Stack
direction="row"
gap={theme.gap.small}
justifyContent="space-between"
>
<GreenCheck />
<Box>
<Typography component="h1" sx={{ lineHeight: 1 }}>
google.com
</Typography>
<Typography
mt={theme.gap.small}
sx={{ color: "var(--env-var-color-17)" }}
>
Your pagespeed monitor is live.
</Typography>
</Box>
<Button
level="tertiary"
label="Configure"
animate="rotate90"
img={
<SettingsIcon
style={{ width: theme.gap.mlplus, height: theme.gap.mlplus }}
/>
}
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
sx={{
ml: "auto",
alignSelf: "flex-end",
backgroundColor: theme.palette.otherColors.fillGray,
px: theme.gap.medium,
"& svg": {
mr: "6px",
},
}}
/>
</Stack>
<Stack
direction="row"
justifyContent="space-between"
gap={theme.gap.xl}
flexWrap="wrap"
>
<StatBox
icon={<LastCheckedIcon />}
title="Last checked"
value={
<>
{formatDate(getLastChecked(monitor?.checks, false))}{" "}
<Typography
component="span"
fontStyle="italic"
sx={{ opacity: 0.8 }}
>
({formatDurationRounded(getLastChecked(monitor?.checks))} ago)
</Typography>
</>
}
/>
<StatBox
icon={<ClockIcon />}
title="Checks since"
value={
<>
{formatDate(new Date(monitor?.createdAt))}{" "}
<Typography
component="span"
fontStyle="italic"
sx={{ opacity: 0.8 }}
>
(
{formatDurationRounded(
new Date() - new Date(monitor?.createdAt)
)}{" "}
ago)
</Typography>
</>
}
></StatBox>
<StatBox
icon={<IntervalCheckIcon />}
title="Checks every"
value={formatDurationRounded(monitor?.interval)}
></StatBox>
</Stack>
<Typography component="h2">Score history</Typography>
<Typography component="h2">Performance report</Typography>
<Box p={theme.gap.ml}>
<Stack mx="auto" width="fit-content" alignItems="center">
<PieChart
series={[
{
data: [
{
value: 100,
color: colorMap.bg,
},
],
color: "red",
outerRadius: 65,
cx: "100%",
cy: "50%",
},
{
data: [
{
value: performance,
color: colorMap.stroke,
},
],
color: "red",
innerRadius: 60,
outerRadius: 70,
paddingAngle: 5,
cornerRadius: 5,
startAngle: 0,
endAngle: (performance / 100) * 360,
cx: "100%",
cy: "50%",
},
]}
width={200}
height={160}
tooltip={{ trigger: "none" }}
>
<PieCenterLabel value={performance} color={colorMap.text} />
</PieChart>
<Typography component="h2">Performance</Typography>
<Typography>Values are estimated and may vary.</Typography>
</Stack>
</Box>
</Stack>
);
};
export default PageSpeedDetails;
+17 -22
View File
@@ -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 (
<Grid item lg={6} flexGrow={1}>
<Stack direction="row" gap={theme.gap.medium} p={theme.gap.ml}>
<Grid
item
lg={6}
flexGrow={1}
sx={{ "&:hover > .MuiStack-root": { backgroundColor: "#f9fafb" } }}
>
<Stack
direction="row"
gap={theme.gap.medium}
p={theme.gap.ml}
onClick={() => navigate(`/pagespeed/${data._id}`)}
sx={{ cursor: "pointer" }}
>
<PageSpeedIcon style={{ width: theme.gap.ml, height: theme.gap.ml }} />
<Box flex={1}>
<Stack direction="row" justifyContent="space-between">
@@ -91,7 +86,7 @@ const PageSpeed = () => {
<Button
level="primary"
label="Create new"
onClick={() => navigate("/page-speed/create")}
onClick={() => navigate("/pagespeed/create")}
/>
</Stack>
<Grid container spacing={theme.gap.large}>
@@ -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"
/>
)}
</Box>
+7
View File
@@ -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;
+17
View File
@@ -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);
};
-1
View File
@@ -50,7 +50,6 @@ export const formatDate = (date) => {
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: true,
};
@@ -0,0 +1,3 @@
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 10.5V7.8C19 6.11984 19 5.27976 18.673 4.63803C18.3854 4.07354 17.9265 3.6146 17.362 3.32698C16.7202 3 15.8802 3 14.2 3H5.8C4.11984 3 3.27976 3 2.63803 3.32698C2.07354 3.6146 1.6146 4.07354 1.32698 4.63803C1 5.27976 1 6.11984 1 7.8V16.2C1 17.8802 1 18.7202 1.32698 19.362C1.6146 19.9265 2.07354 20.3854 2.63803 20.673C3.27976 21 4.11984 21 5.8 21H10.5M19 9H1M14 1V5M6 1V5M16 20V14M13 17H19" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 591 B

@@ -0,0 +1,3 @@
<svg width="17" height="18" viewBox="0 0 17 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.9118 14.2882C14.7835 13.0993 16 11.1735 16 9C16 5.38891 12.6421 2.46154 8.5 2.46154H8.05882M8.5 15.5385C4.35786 15.5385 1 12.6111 1 9C1 6.82651 2.21647 4.90072 4.08824 3.71185M7.61765 17L9.38235 15.4615L7.61765 13.9231M9.38235 4.07692L7.61765 2.53846L9.38235 1" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 463 B