feat: statusPage - adding pagespeed section

This commit is contained in:
mannilakash committed 2026-03-30 08:15:43 -04:00
1 parent 8ab049aa6c
commit 6cea199e4f
15 files changed
+231 -11

No files matched your search

@@ -17,6 +17,7 @@ export const Gauge = ({
strokeWidth = 15,
precision = 1,
unit = "%",
colorFn,
}: {
isLoading?: boolean;
progress?: number;
@@ -24,6 +25,7 @@ export const Gauge = ({
strokeWidth?: number;
precision?: number;
unit?: string;
colorFn?: (val: number, theme: any) => string;
}) => {
const theme = useTheme();
const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE));
@@ -48,7 +50,8 @@ export const Gauge = ({
return () => clearTimeout(timer);
}, [progress, circumference, strokeLength]);
const fillColor = getInfraGaugeColor(progressWithinRange, theme);
const resolvedColorFn = colorFn ?? getInfraGaugeColor;
const fillColor = resolvedColorFn(progressWithinRange, theme);
if (isLoading) {
return;
+1
View File
@@ -35,6 +35,7 @@ export const useStatusPageForm = ({
showUptimePercentage: data?.showUptimePercentage ?? true,
showAdminLoginLink: data?.showAdminLoginLink ?? false,
showInfrastructure: data?.showInfrastructure ?? false,
showPageSpeed: data?.showPageSpeed ?? false,
customCSS: data?.customCSS || "",
logo: transformLogo(data?.logo),
};
+33 -5
View File
@@ -37,9 +37,17 @@ import { HeaderConfigStatusControls } from "./Components/HeaderConfigStatusContr
const monitorsUrl = (() => {
const params = new URLSearchParams();
["http", "ping", "port", "docker", "game", "grpc", "websocket", "hardware"].forEach(
(type) => params.append("type", type)
);
[
"http",
"ping",
"port",
"docker",
"game",
"grpc",
"websocket",
"hardware",
"pagespeed",
].forEach((type) => params.append("type", type));
return `/monitors/team?${params.toString()}`;
})();
@@ -59,7 +67,9 @@ const CreateStatusPage = () => {
// Fetch existing status page data when configuring
const { data: statusPageData, isLoading: isLoadingStatusPage } =
useGet<StatusPageResponse>(
isCreate ? null : `/status-page/${url}?type=uptime&type=infrastructure`
isCreate
? null
: `/status-page/${url}?type=uptime&type=infrastructure&type=pagespeed`
);
const { data: monitorsResponse } = useGet<Monitor[]>(monitorsUrl);
@@ -98,7 +108,9 @@ const CreateStatusPage = () => {
const typesSet = new Set<MonitorDisplayType>();
selectedMonitors.forEach((m) => {
typesSet.add(m.type === "hardware" ? "infrastructure" : "uptime");
if (m.type === "hardware") typesSet.add("infrastructure");
else if (m.type === "pagespeed") typesSet.add("pagespeed");
else typesSet.add("uptime");
});
return typesSet.size ? Array.from(typesSet) : ["uptime"];
@@ -139,6 +151,7 @@ const CreateStatusPage = () => {
fd.append("showUptimePercentage", String(data.showUptimePercentage));
fd.append("showAdminLoginLink", String(data.showAdminLoginLink));
fd.append("showInfrastructure", String(data.showInfrastructure));
fd.append("showPageSpeed", String(data.showPageSpeed));
data.monitors.forEach((monitorId) => {
fd.append("monitors[]", monitorId);
@@ -475,6 +488,21 @@ const CreateStatusPage = () => {
/>
)}
/>
<Controller
name="showPageSpeed"
control={control}
render={({ field }) => (
<FormControlLabel
control={
<Checkbox
checked={field.value}
onChange={field.onChange}
/>
}
label={t("pages.statusPages.form.features.option.showPageSpeed.label")}
/>
)}
/>
{/* <Controller
name="showUptimePercentage"
control={control}
@@ -6,6 +6,7 @@ import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/common"
import { StatusLabel, BaseBox } from "@/Components/design-elements";
import { SwitchComponent } from "@/Components/inputs";
import { InfrastructureMetrics } from "@/Pages/StatusPage/Status/Components/InfrastructureMetrics";
import { PageSpeedMetrics } from "@/Pages/StatusPage/Status/Components/PageSpeedMetrics";
import { useTheme, type Theme } from "@mui/material/styles";
import { useSelector } from "react-redux";
@@ -26,8 +27,14 @@ interface MonitorsListProps {
}
const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => {
const monitorColors = {
hardware: theme.palette.info.light,
pagespeed: theme.palette.warning.light,
};
const bg =
monitorType === "hardware" ? theme.palette.info.light : theme.palette.success.light;
monitorColors[monitorType as keyof typeof monitorColors] ||
theme.palette.success.light;
return {
backgroundColor: bg,
color: theme.palette.background.paper,
@@ -105,6 +112,11 @@ const MonitorContent = ({
return <InfrastructureMetrics monitor={monitor} />;
}
if (monitor.type === "pagespeed") {
if (statusPage.showPageSpeed === false) return null;
return <PageSpeedMetrics monitor={monitor} />;
}
if (statusPage.showCharts === false) return null;
const checks = monitor.checks?.slice().reverse() ?? [];
@@ -0,0 +1,148 @@
import Typography from "@mui/material/Typography";
import { Gauge } from "@/Components/design-elements";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import type { Monitor } from "@/Types/Monitor";
import Grid from "@mui/material/Grid";
import useMediaQuery from "@mui/material/useMediaQuery";
import Box from "@mui/material/Box";
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
import { getPageSpeedGaugeColor } from "@/Utils/MonitorUtils";
const GAUGE_RADIUS = 60;
const GAUGE_STROKE_WIDTH = 12;
interface StatusPageMonitor extends Monitor {
checks?: Monitor["recentChecks"];
}
interface MetricConfig {
key: string;
label: string;
progress: number;
}
const MetricItem = ({ label, progress }: MetricConfig) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("sm"));
return (
<Grid
size={isSmall ? 12 : 3}
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
textAlign: "center",
gap: theme.spacing(SPACING.LG),
padding: theme.spacing(LAYOUT.XS),
borderRight: isSmall ? "none" : `1px solid ${theme.palette.divider}`,
borderBottom: isSmall ? `1px solid ${theme.palette.divider}` : "none",
"&:last-child": {
borderRight: "none",
borderBottom: "none",
paddingBottom: theme.spacing(SPACING.LG),
},
}}
>
<Box
display="flex"
flexDirection="column"
alignItems="center"
>
<Gauge
progress={progress}
radius={GAUGE_RADIUS}
strokeWidth={GAUGE_STROKE_WIDTH}
colorFn={getPageSpeedGaugeColor}
/>
<Typography variant="body2">{label}</Typography>
</Box>
</Grid>
);
};
const buildMetrics = (
check: NonNullable<Monitor["recentChecks"]>[number],
t: (key: string) => string
): MetricConfig[] => {
const metrics: MetricConfig[] = [];
if (typeof check.performance === "number") {
metrics.push({
key: "performance",
label: t("pages.statusPages.monitorsList.pagespeed.performance"),
progress: check.performance,
});
}
if (typeof check.accessibility === "number") {
metrics.push({
key: "accessibility",
label: t("pages.statusPages.monitorsList.pagespeed.accessibility"),
progress: check.accessibility,
});
}
if (typeof check.bestPractices === "number") {
metrics.push({
key: "bestPractices",
label: t("pages.statusPages.monitorsList.pagespeed.bestPractices"),
progress: check.bestPractices,
});
}
if (typeof check.seo === "number") {
metrics.push({
key: "seo",
label: t("pages.statusPages.monitorsList.pagespeed.seo"),
progress: check.seo,
});
}
return metrics;
};
export const PageSpeedMetrics = ({ monitor }: { monitor: StatusPageMonitor }) => {
const theme = useTheme();
const { t } = useTranslation();
const latestCheck = monitor.recentChecks?.[0] ?? monitor.checks?.[0];
if (!latestCheck) {
return (
<Typography
variant="body2"
color={theme.palette.text.secondary}
>
{t("pages.statusPages.monitorsList.noData")}
</Typography>
);
}
const metrics = buildMetrics(latestCheck, t);
if (metrics.length === 0) {
return (
<Typography
variant="body2"
color={theme.palette.text.secondary}
>
{t("pages.statusPages.monitorsList.noData")}
</Typography>
);
}
return (
<Grid
container
alignItems="center"
padding={theme.spacing(LAYOUT.LG)}
sx={{ flex: 1 }}
>
{metrics.map((metric) => (
<MetricItem
key={metric.key}
label={metric.label}
progress={metric.progress}
/>
))}
</Grid>
);
};
+3 -1
View File
@@ -23,7 +23,9 @@ const StatusPageView = () => {
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const isPublic = location.pathname.startsWith("/status/public");
const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null;
const apiUrl = url
? `/status-page/${url}?type=uptime&type=infrastructure&type=pagespeed`
: null;
const { data, isLoading, error } = useGet<StatusPageResponse>(
apiUrl,
+2 -1
View File
@@ -1,5 +1,5 @@
import type { Monitor, MonitorType } from "@/Types/Monitor";
export type MonitorDisplayType = "uptime" | "infrastructure";
export type MonitorDisplayType = "uptime" | "infrastructure" | "pagespeed";
export const MONITOR_TYPE_KEYS: Partial<Record<MonitorType, string>> = {
http: "pages.common.monitors.monitorTypes.optionHttp",
@@ -42,6 +42,7 @@ export interface StatusPage {
showUptimePercentage: boolean;
showAdminLoginLink: boolean;
showInfrastructure: boolean;
showPageSpeed: boolean;
customCSS: string;
createdAt: string;
updatedAt: string;
+6
View File
@@ -78,6 +78,12 @@ export const getPageSpeedPalette = (score: number): PaletteKey => {
else return "error";
};
export const getPageSpeedGaugeColor = (val: number, theme: any) => {
if (val >= 90) return theme.palette.success.main;
else if (val >= 50) return theme.palette.warning.light;
else return theme.palette.error.light;
};
export const formatUrl = (url: string, maxLength: number = 55) => {
if (!url) return "";
+2 -1
View File
@@ -15,7 +15,7 @@ export const statusPageSchema = z.object({
),
timezone: z.string().optional(),
type: z
.array(z.enum(["uptime", "infrastructure"]))
.array(z.enum(["uptime", "infrastructure", "pagespeed"]))
.min(1, "At least one type is required"),
color: z.string().min(1, "Color is required"),
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
@@ -24,6 +24,7 @@ export const statusPageSchema = z.object({
showUptimePercentage: z.boolean(),
showAdminLoginLink: z.boolean(),
showInfrastructure: z.boolean(),
showPageSpeed: z.boolean(),
customCSS: z.string().optional(),
logo: z
.object({
+1
View File
@@ -324,6 +324,7 @@ const statusPageValidation = joi.object({
showCharts: joi.boolean(),
showAdminLoginLink: joi.boolean(),
showInfrastructure: joi.boolean(),
showPageSpeed: joi.boolean(),
});
const settingsValidation = joi.object({
+10
View File
@@ -1202,6 +1202,13 @@
"used": "Used",
"total": "Total"
},
"pagespeed": {
"title": "PageSpeed",
"performance": "Performance",
"accessibility": "Accessibility",
"bestPractices": "Best Practices",
"seo": "SEO"
},
"uptime": {
"title": "Uptime",
"responseTime": "Response time"
@@ -1300,6 +1307,9 @@
},
"showInfrastructure": {
"label": "Show infrastructure metrics"
},
"showPageSpeed": {
"label": "Show PageSpeed scores"
}
}
}
+4
View File
@@ -104,6 +104,10 @@ const StatusPageSchema = new Schema<StatusPageDocument>(
type: Boolean,
default: false,
},
showPageSpeed: {
type: Boolean,
default: false,
},
customCSS: {
type: String,
default: "",
@@ -58,6 +58,7 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
showUptimePercentage: doc.showUptimePercentage,
showAdminLoginLink: doc.showAdminLoginLink,
showInfrastructure: doc.showInfrastructure,
showPageSpeed: doc.showPageSpeed,
customCSS: doc.customCSS,
createdAt: this.toDateString(doc.createdAt),
updatedAt: this.toDateString(doc.updatedAt),
+2 -1
View File
@@ -1,4 +1,4 @@
export const StatusPageTypes = ["uptime", "infrastructure"] as const;
export const StatusPageTypes = ["uptime", "infrastructure", "pagespeed"] as const;
export type StatusPageType = (typeof StatusPageTypes)[number];
export interface StatusPageLogo {
@@ -29,6 +29,7 @@ export interface StatusPage {
showUptimePercentage: boolean;
showAdminLoginLink: boolean;
showInfrastructure: boolean;
showPageSpeed: boolean;
customCSS: string;
createdAt: string;
updatedAt: string;
@@ -32,6 +32,7 @@ export const createStatusPageBodyValidation = z
showUptimePercentage: booleanCoercion,
showAdminLoginLink: booleanCoercion.optional(),
showInfrastructure: booleanCoercion.optional(),
showPageSpeed: booleanCoercion.optional(),
removeLogo: z.union([z.literal("true"), z.literal("false")]).optional(),
})
.strip();