pagespeed v2 refactor

This commit is contained in:
Alex Holliday
2026-01-26 22:28:02 +00:00
parent b61077bd3f
commit 1bcb2ddf91
12 changed files with 434 additions and 51 deletions
+1 -29
View File
@@ -26,7 +26,6 @@
"joi": "17.13.3",
"lucide-react": "^0.562.0",
"mui-color-input": "^6.0.0",
"pretty-ms": "9.3.0",
"react": "18.3.1",
"react-dom": "^18.2.0",
"react-hook-form": "7.63.0",
@@ -38,7 +37,7 @@
"react-toastify": "^10.0.5",
"recharts": "2.15.2",
"redux-persist": "6.0.0",
"swr": "2.3.6",
"swr": "^2.3.6",
"vite-plugin-svgr": "^4.2.0",
"zod": "4.1.11"
},
@@ -5780,18 +5779,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parse-ms": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -5919,21 +5906,6 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-ms": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
"integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==",
"license": "MIT",
"dependencies": {
"parse-ms": "^4.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
-1
View File
@@ -31,7 +31,6 @@
"joi": "17.13.3",
"lucide-react": "^0.562.0",
"mui-color-input": "^6.0.0",
"pretty-ms": "9.3.0",
"react": "18.3.1",
"react-dom": "^18.2.0",
"react-hook-form": "7.63.0",
@@ -34,7 +34,6 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
>
<PulseDot color={getStatusColor(monitor.status, theme)} />
<Typography
color={theme.palette.text.secondary}
fontSize={typographyLevels.l}
fontWeight={"bolder"}
fontFamily={"monospace"}
@@ -0,0 +1,144 @@
import { BaseChart } from "@/Components/v2/design-elements";
import { TrendingUp } from "lucide-react";
import { XTick } from "@/Components/v2/monitors/";
import {
XAxis,
AreaChart,
Area,
Tooltip,
CartesianGrid,
ResponsiveContainer,
} from "recharts";
import { HistogramPageSpeedScoresTooltip } from "@/Components/v2/monitors";
import { useTheme } from "@mui/material/styles";
import type { Check } from "@/Types/Check";
import type { Palette } from "@mui/material/styles";
type PaletteColorKey = Extract<
keyof Palette,
"primary" | "error" | "success" | "warning"
>;
export interface ConfigItem {
id: string;
text: string;
palette: PaletteColorKey;
}
const config: Record<string, ConfigItem> = {
seo: {
id: "seo",
text: "SEO",
palette: "primary",
},
performance: {
id: "performance",
text: "performance",
palette: "success",
},
bestPractices: {
id: "bestPractices",
text: "best practices",
palette: "warning",
},
accessibility: {
id: "accessibility",
text: "accessibility",
palette: "error",
},
};
export const HistogramPageSpeedDetails = ({
checks,
range,
}: {
checks: Check[];
range: string;
}) => {
const theme = useTheme();
return (
<BaseChart
icon={
<TrendingUp
size={20}
strokeWidth={1.5}
/>
}
title="Score history"
>
<ResponsiveContainer
width="100%"
minWidth={25}
height={215}
>
<AreaChart data={checks}>
<XAxis
dataKey={"createdAt"}
tick={(props) => (
<XTick
{...props}
range={range}
/>
)}
/>
<CartesianGrid
stroke={theme.palette.divider}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<Tooltip
cursor={{ stroke: theme.palette.divider }}
content={<HistogramPageSpeedScoresTooltip config={config} />}
/>
<defs>
{Object.values(config).map(({ id, palette }) => {
const startColor = theme.palette[palette].main;
const endColor = theme.palette[palette].light;
return (
<linearGradient
id={id}
x1="0"
y1="0"
x2="0"
y2="1"
key={id}
>
<stop
offset="0%"
stopColor={startColor}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={endColor}
stopOpacity={0}
/>
</linearGradient>
);
})}
</defs>
{Object.keys(config).map((key) => {
const { palette } = config[key];
const strokeColor = theme.palette[palette].main;
const bgColor = theme.palette.primary.main;
return (
<Area
connectNulls
key={key}
dataKey={key}
stackId={1}
stroke={strokeColor}
fill={`url(#${config[key].id})`}
activeDot={{ stroke: bgColor, fill: strokeColor, r: 4.5 }}
/>
);
})}
</AreaChart>
</ResponsiveContainer>
</BaseChart>
);
};
@@ -0,0 +1,78 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { formatDateWithTz } from "@/Utils/TimeUtils";
import type { ConfigItem } from "@/Components/v2/monitors";
import type { TooltipProps } from "recharts";
import { useSelector } from "react-redux";
import type { RootState } from "@/Types/state";
interface HistogramPageSpeedScoresTooltipProps
extends Partial<TooltipProps<number, string>> {
config: Record<string, ConfigItem>;
}
export const HistogramPageSpeedScoresTooltip = ({
active,
payload,
label,
config,
}: HistogramPageSpeedScoresTooltipProps) => {
const theme = useTheme();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
if (active && payload && payload.length) {
return (
<Box
sx={{
backgroundColor: theme.palette.background.paper,
border: 1,
borderColor: theme.palette.divider,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Typography
sx={{
color: theme.palette.text.secondary,
fontSize: 12,
fontWeight: 500,
}}
>
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
</Typography>
{Object.keys(config)
.reverse()
.map((key) => {
const { palette } = config[key];
const dotColor = theme.palette[palette].main;
return (
<Stack
key={`${key}-tooltip`}
direction="row"
alignItems="center"
gap={theme.spacing(3)}
mt={theme.spacing(1)}
>
<Box
width={theme.spacing(4)}
height={theme.spacing(4)}
sx={{ borderRadius: "50%", backgroundColor: dotColor }}
/>
<Typography
textTransform="capitalize"
sx={{ opacity: 0.8 }}
>
{config[key].text}
</Typography>
<Typography>{Math.floor(payload[0].payload[key])}</Typography>
</Stack>
);
})}
</Box>
);
}
return null;
};
@@ -1,5 +1,6 @@
import { BaseChart } from "@/Components/v2/design-elements";
import { FileText } from "lucide-react";
import type { Check, CheckAudits } from "@/Types/Check";
import { Pie, PieChart, ResponsiveContainer, Label } from "recharts";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
@@ -29,19 +30,20 @@ const CenterLabel = ({ viewBox, value }: any) => {
);
};
export const PiePageSpeed = ({ latestCheck }: { latestCheck: any }) => {
export const PiePageSpeed = ({ latestCheck }: { latestCheck?: Check }) => {
const { t } = useTranslation();
const theme = useTheme();
const [hoverTitle, setHoverTitle] = useState<string | null>(null);
if (!latestCheck) return null;
const LABELS: Record<string, string> = {
FCP: t("common.charts.pageSpeed.fcp"),
SI: t("common.charts.pageSpeed.si"),
LCP: t("common.charts.pageSpeed.lcp"),
TBT: t("common.charts.pageSpeed.tbt"),
CLS: t("common.charts.pageSpeed.cls"),
FCP: t("pages.pageSpeed.charts.common.fcp"),
SI: t("pages.pageSpeed.charts.common.si"),
LCP: t("pages.pageSpeed.charts.common.lcp"),
TBT: t("pages.pageSpeed.charts.common.tbt"),
CLS: t("pages.pageSpeed.charts.common.cls"),
};
const metrics = [
const metrics: { key: keyof CheckAudits; color: string; weight: number }[] = [
{ key: "fcp", color: alpha("#1DE9B6", 0.6), weight: 0.1 },
{ key: "si", color: alpha("#7C4DFF", 0.6), weight: 0.1 },
{ key: "lcp", color: alpha("#FFB200", 0.6), weight: 0.25 },
@@ -50,7 +52,8 @@ export const PiePageSpeed = ({ latestCheck }: { latestCheck: any }) => {
];
const scores = metrics.flatMap(({ key, color, weight }) => {
const val = Math.floor((latestCheck?.[key] ?? 0) * 100);
const audit = latestCheck?.audits?.[key];
const val = Math.floor((audit?.score ?? 0) * 100);
const inverse = 100 - val;
return [
{
@@ -71,12 +74,11 @@ export const PiePageSpeed = ({ latestCheck }: { latestCheck: any }) => {
});
const totalScore =
(latestCheck?.fcp || 0) * 0.1 +
(latestCheck?.si || 0) * 0.1 +
(latestCheck?.lcp || 0) * 0.25 +
(latestCheck?.tbt || 0) * 0.3 +
(latestCheck?.cls || 0) * 0.25;
(latestCheck.audits?.fcp?.score || 0) * 0.1 +
(latestCheck.audits?.si?.score || 0) * 0.1 +
(latestCheck.audits?.lcp?.score || 0) * 0.25 +
(latestCheck.audits?.tbt?.score || 0) * 0.3 +
(latestCheck.audits?.cls?.score || 0) * 0.25;
const pageSpeedPalette = getPageSpeedPalette(Math.floor(totalScore * 100));
const score = [
@@ -95,7 +97,7 @@ export const PiePageSpeed = ({ latestCheck }: { latestCheck: any }) => {
strokeWidth={1.5}
/>
}
title={t("common.charts.pageSpeed.title")}
title={t("pages.pageSpeed.charts.pie.title")}
>
<Tooltip
open={Boolean(hoverTitle)}
@@ -0,0 +1,105 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import { BarChart3 } from "lucide-react";
import { BaseChart } from "@/Components/v2/design-elements";
import Typography from "@mui/material/Typography";
import type { Check } from "@/Types/Check";
import { useTranslation } from "react-i18next";
import { getPageSpeedPalette } from "@/Utils/MonitorUtils";
import { useTheme } from "@mui/material/styles";
const MetricBox = ({
label,
value,
weight,
}: {
label: string;
value: number;
weight: number;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const palette = getPageSpeedPalette(value);
return (
<Stack
direction={"row"}
sx={{
border: 1,
borderStyle: "solid",
borderColor: theme.palette.divider,
borderRadius: theme.shape.borderRadius,
}}
>
<Stack
flex={1}
p={theme.spacing(4)}
>
<Typography textTransform={"uppercase"}>{label}</Typography>
<Stack
direction="row"
justifyContent={"space-between"}
>
<Typography>{`${value}%`}</Typography>
<Typography>{`${t("pages.pageSpeed.charts.legend.weight")}: ${weight}%`}</Typography>
</Stack>
</Stack>
<Box
width={4}
bgcolor={theme.palette[palette].light}
sx={{
borderTopRightRadius: theme.shape.borderRadius,
borderBottomRightRadius: theme.shape.borderRadius,
}}
/>
</Stack>
);
};
export const PiePageSpeedLegend = ({ latestCheck }: { latestCheck?: Check }) => {
const theme = useTheme();
const { t } = useTranslation();
if (!latestCheck) {
return null;
}
return (
<BaseChart
icon={
<BarChart3
size={20}
strokeWidth={1.5}
/>
}
title={t("pages.pageSpeed.charts.legend.title")}
>
<Stack gap={theme.spacing(4)}>
<MetricBox
label={t("pages.pageSpeed.charts.common.si")}
value={Math.floor((latestCheck.audits?.si?.score || 0) * 100)}
weight={10}
/>
<MetricBox
label={t("pages.pageSpeed.charts.common.fcp")}
value={Math.floor((latestCheck.audits?.fcp?.score || 0) * 100)}
weight={10}
/>
<MetricBox
label={t("pages.pageSpeed.charts.common.cls")}
value={Math.floor((latestCheck.audits?.cls?.score || 0) * 100)}
weight={25}
/>
<MetricBox
label={t("pages.pageSpeed.charts.common.tbt")}
value={Math.floor((latestCheck.audits?.tbt?.score || 0) * 100)}
weight={30}
/>
<MetricBox
label={t("pages.pageSpeed.charts.common.lcp")}
value={Math.floor((latestCheck.audits?.lcp?.score || 0) * 100)}
weight={25}
/>
</Stack>
</BaseChart>
);
};
+4 -1
View File
@@ -5,4 +5,7 @@ export * from "./charts/RadialAvgResponse";
export * from "./charts/HistogramDetails";
export * from "./charts/HistogramPageSpeed";
export * from "./charts/HistogramPageSpeedTooltip";
export * from "./charts/PiePagespeed";
export * from "./charts/PiePageSpeed";
export * from "./charts/PiePageSpeedLegend";
export * from "./charts/HistogramPageSpeedDetails";
export * from "./charts/HistogramPageSpeedDetailsTooltip";
+57 -2
View File
@@ -1,7 +1,62 @@
import { BasePage } from "@/Components/v2/design-elements";
import type { PageSpeedDetailsResponse } from "@/Types/Monitor";
import { HeaderMonitorControls } from "@/Components/v2/common";
import Stack from "@mui/material/Stack";
import {
HistogramPageSpeedDetails,
PiePageSpeed,
PiePageSpeedLegend,
MonitorStatBoxes,
} from "@/Components/v2/monitors";
import { useIsAdmin } from "@/Hooks/useIsAdmin";
import { useGet } from "@/Hooks/UseApi";
import { useParams } from "react-router-dom";
import { useTheme } from "@mui/material";
const PageSpeedDetails = () => {
return <BasePage>PageSpeed Details - Work in Progress</BasePage>;
};
const { monitorId } = useParams();
const isAdmin = useIsAdmin();
const theme = useTheme();
const {
data: monitorData,
isLoading,
error,
refetch,
} = useGet<PageSpeedDetailsResponse>(
monitorId ? `/monitors/pagespeed/details/${monitorId}?dateRange=day` : null
);
const monitor = monitorData?.monitor;
const monitorStats = monitorData?.monitorStats || null;
return (
<BasePage
loading={isLoading}
error={error}
>
<HeaderMonitorControls
path="pagespeed"
monitor={monitor}
isAdmin={isAdmin}
refetch={refetch}
/>
<MonitorStatBoxes
monitor={monitor}
monitorStats={monitorStats}
/>
<HistogramPageSpeedDetails
checks={monitor?.checks || []}
range="day"
/>
<Stack
direction={{ xs: "column", md: "row" }}
gap={theme.spacing(10)}
>
<PiePageSpeed latestCheck={monitor?.checks?.[0]} />
<PiePageSpeedLegend latestCheck={monitor?.checks?.[0]} />
</Stack>
</BasePage>
);
};
export default PageSpeedDetails;
+7 -1
View File
@@ -133,7 +133,13 @@ const Routes = () => {
/>
<Route
path="pagespeed/:monitorId"
element={<PageSpeedDetails />}
element={
<>
<ThemeProvider theme={v2theme}>
<PageSpeedDetails />
</ThemeProvider>
</>
}
/>
<Route
path="pagespeed/configure/:monitorId"
+5
View File
@@ -76,3 +76,8 @@ export interface MonitorDetailsResponse {
monitorData: MonitorData;
monitorStats: MonitorStats | null;
}
export interface PageSpeedDetailsResponse {
monitor: MonitorWithChecks;
monitorStats: MonitorStats | null;
}
+16 -1
View File
@@ -39,7 +39,7 @@
"status": "Status",
"type": "Type"
},
"empty": "Nothing h"
"empty": "Nothing here"
},
"charts": {
"labels": {
@@ -228,6 +228,21 @@
"headers": {
"pageSpeedScore": "PageSpeed score"
}
},
"charts": {
"common": {
"cls": "Cumulative Layout Shift (CLS)",
"fcp": "First Contentful Paint (FCP)",
"lcp": "Largest Contentful Paint (LCP)",
"si": "Speed Index (SI)",
"tbt": "Total Blocking Time (TBT)"
},
"pie": { "title": "Performance report" },
"legend": {
"title": "PageSpeed report",
"weight": "Weight"
}
}
}
},