Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions client/src/Components/design-elements/Gauge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

import { useTheme } from "@mui/material/styles";
import { useTheme, type Theme } from "@mui/material/styles";
import { useMemo, useState, useEffect } from "react";
import { getInfraGaugeColor } from "@/Utils/MonitorUtils";

Expand All @@ -17,13 +17,18 @@ export const Gauge = ({
strokeWidth = 15,
precision = 1,
unit = "%",
// colorFn lets callers override the default color logic.
// The Gauge was originally designed for metrics where higher values are worse (e.g., CPU usage).
// PageSpeed scores are the opposite—higher is better—so a custom colorFn can be used to invert the behavior.
colorFn,
}: {
isLoading?: boolean;
progress?: number;
radius?: number;
strokeWidth?: number;
precision?: number;
unit?: string;
colorFn?: (val: number, theme: Theme) => string;
}) => {
const theme = useTheme();
const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE));
Expand All @@ -48,7 +53,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;
Expand Down
1 change: 1 addition & 0 deletions client/src/Hooks/useStatusPageForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
Expand Down
44 changes: 36 additions & 8 deletions client/src/Pages/StatusPage/Create/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { useStatusPageForm } from "@/Hooks/useStatusPageForm";
import type { StatusPageFormData } from "@/Validation/statusPage";
import { useGet, usePost, usePut, useDelete } from "@/Hooks/UseApi";
import type { Monitor } from "@/Types/Monitor";
import type { MonitorDisplayType, StatusPageResponse } from "@/Types/StatusPage";
import type { StatusPageType, StatusPageResponse } from "@/Types/StatusPage";
import { getMonitorTypeLabel } from "@/Types/StatusPage";
import timezones from "@/Utils/timezones.json";
import { useNavigate, useParams } from "react-router-dom";
Expand All @@ -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()}`;
})();

Expand All @@ -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);
Expand Down Expand Up @@ -91,14 +101,16 @@ const CreateStatusPage = () => {
}, [defaults, reset]);

const watchedMonitorIds: string[] = form.watch("monitors") ?? [];
const computedTypes: MonitorDisplayType[] = useMemo(() => {
const computedTypes: StatusPageType[] = useMemo(() => {
const selectedMonitors = (watchedMonitorIds ?? [])
.map((id) => monitors.find((m) => m.id === id))
.filter((m): m is Monitor => m !== undefined);

const typesSet = new Set<MonitorDisplayType>();
const typesSet = new Set<StatusPageType>();
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"];
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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}
Expand Down
15 changes: 13 additions & 2 deletions client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -26,8 +27,12 @@ interface MonitorsListProps {
}

const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => {
const bg =
monitorType === "hardware" ? theme.palette.info.light : theme.palette.success.light;
const monitorColors: Record<string, string> = {
hardware: theme.palette.info.light,
pagespeed: theme.palette.warning.light,
};

const bg = monitorColors[monitorType] || theme.palette.success.light;
return {
backgroundColor: bg,
color: theme.palette.background.paper,
Expand Down Expand Up @@ -105,6 +110,12 @@ const MonitorContent = ({
return <InfrastructureMetrics monitor={monitor} />;
}

if (monitor.type === "pagespeed") {
return statusPage.showPageSpeed === false ? null : (
<PageSpeedMetrics monitor={monitor} />
);
}

if (statusPage.showCharts === false) return null;

const checks = monitor.checks?.slice().reverse() ?? [];
Expand Down
122 changes: 122 additions & 0 deletions client/src/Pages/StatusPage/Status/Components/PageSpeedMetrics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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 type { CheckSnapshot } from "@/Types/Check";
import Grid from "@mui/material/Grid";
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();
return (
<Grid
size={{ xs: 12, md: 3 }}
display="flex"
flexDirection="column"
alignItems="center"
textAlign="center"
gap={theme.spacing(SPACING.LG)}
padding={theme.spacing(LAYOUT.XS)}
sx={{
borderRight: { xs: "none", md: `1px solid ${theme.palette.divider}` },
borderBottom: { xs: `1px solid ${theme.palette.divider}`, md: "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: CheckSnapshot, t: (key: string) => string): MetricConfig[] =>
[
{
key: "performance",
label: t("pages.statusPages.monitorsList.pagespeed.performance"),
progress: check.performance,
},
{
key: "accessibility",
label: t("pages.statusPages.monitorsList.pagespeed.accessibility"),
progress: check.accessibility,
},
{
key: "bestPractices",
label: t("pages.statusPages.monitorsList.pagespeed.bestPractices"),
progress: check.bestPractices,
},
{
key: "seo",
label: t("pages.statusPages.monitorsList.pagespeed.seo"),
progress: check.seo,
},
].filter((m): m is MetricConfig => m.progress !== undefined);

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);

return (
<Grid
container
alignItems="center"
padding={theme.spacing(LAYOUT.LG)}
flex={1}
>
{metrics.map((metric) => (
<MetricItem
key={metric.key}
label={metric.label}
progress={metric.progress}
/>
))}
</Grid>
);
};
4 changes: 3 additions & 1 deletion client/src/Pages/StatusPage/Status/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions client/src/Types/StatusPage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Monitor, MonitorType } from "@/Types/Monitor";
export type MonitorDisplayType = "uptime" | "infrastructure";
export const StatusPageTypes = ["uptime", "infrastructure", "pagespeed"] as const;
export type StatusPageType = (typeof StatusPageTypes)[number];

export const MONITOR_TYPE_KEYS: Partial<Record<MonitorType, string>> = {
http: "pages.common.monitors.monitorTypes.optionHttp",
Expand All @@ -25,7 +26,7 @@ export interface StatusPage {
id: string;
userId: string;
teamId: string;
type: MonitorDisplayType[];
type: StatusPageType[];
companyName: string;
url: string;
timezone?: string;
Expand All @@ -42,6 +43,7 @@ export interface StatusPage {
showUptimePercentage: boolean;
showAdminLoginLink: boolean;
showInfrastructure: boolean;
showPageSpeed: boolean;
customCSS: string;
createdAt: string;
updatedAt: string;
Expand Down
6 changes: 6 additions & 0 deletions client/src/Utils/MonitorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 "";

Expand Down
6 changes: 3 additions & 3 deletions client/src/Validation/statusPage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod";
import { StatusPageTypes } from "@/Types/StatusPage";

export const statusPageSchema = z.object({
companyName: z
Expand All @@ -14,16 +15,15 @@ export const statusPageSchema = z.object({
"URL can only contain lowercase letters, numbers, and hyphens"
),
timezone: z.string().optional(),
type: z
.array(z.enum(["uptime", "infrastructure"]))
.min(1, "At least one type is required"),
type: z.array(z.enum(StatusPageTypes)).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"),
isPublished: z.boolean(),
showCharts: z.boolean(),
showUptimePercentage: z.boolean(),
showAdminLoginLink: z.boolean(),
showInfrastructure: z.boolean(),
showPageSpeed: z.boolean(),
customCSS: z.string().optional(),
logo: z
.object({
Expand Down
1 change: 1 addition & 0 deletions client/src/Validation/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ const statusPageValidation = joi.object({
showCharts: joi.boolean(),
showAdminLoginLink: joi.boolean(),
showInfrastructure: joi.boolean(),
showPageSpeed: joi.boolean(),
});

const settingsValidation = joi.object({
Expand Down
Loading
Loading