From 894b6a7a7d86636e725ac5c89c0bc6f3ed5999a7 Mon Sep 17 00:00:00 2001 From: mannilakash Date: Mon, 16 Mar 2026 17:46:13 -0400 Subject: [PATCH 1/5] feat: status-page infra section FE --- client/src/Hooks/useStatusPageForm.ts | 2 + client/src/Pages/CreateMonitor/index.tsx | 14 +- client/src/Pages/StatusPage/Create/index.tsx | 56 ++++- .../Components/InfrastructureMetrics.tsx | 200 ++++++++++++++++++ .../Status/Components/MonitorsList.tsx | 160 +++++++++++--- client/src/Pages/StatusPage/Status/index.tsx | 5 +- .../Components/StatusPagesTable.tsx | 2 +- client/src/Types/StatusPage.ts | 5 +- client/src/Validation/statusPage.ts | 4 + client/src/Validation/validation.js | 9 +- client/src/locales/en.json | 41 +++- 11 files changed, 438 insertions(+), 60 deletions(-) create mode 100644 client/src/Pages/StatusPage/Status/Components/InfrastructureMetrics.tsx diff --git a/client/src/Hooks/useStatusPageForm.ts b/client/src/Hooks/useStatusPageForm.ts index a079095cc..0011ea761 100644 --- a/client/src/Hooks/useStatusPageForm.ts +++ b/client/src/Hooks/useStatusPageForm.ts @@ -27,12 +27,14 @@ export const useStatusPageForm = ({ companyName: data?.companyName || "", url: data?.url || generateDefaultUrl(), timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, + type: data?.type || ["uptime"], color: data?.color || "#4169E1", monitors: data?.monitors || [], isPublished: data?.isPublished ?? false, showCharts: data?.showCharts ?? true, showUptimePercentage: data?.showUptimePercentage ?? true, showAdminLoginLink: data?.showAdminLoginLink ?? false, + showInfrastructure: data?.showInfrastructure ?? false, customCSS: data?.customCSS || "", logo: transformLogo(data?.logo), }; diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index b80cddaaa..15b76eab3 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -302,49 +302,49 @@ const CreateMonitorPage = () => { > { // Fetch existing status page data when configuring const { data: statusPageData, isLoading: isLoadingStatusPage } = - useGet(isCreate ? null : `/status-page/${url}?type=uptime`); + useGet( + isCreate ? null : `/status-page/${url}?type=uptime&type=infrastructure` + ); const { data: monitorsResponse } = useGet( - "/monitors/team?type=http&type=ping&type=port&type=docker" + "/monitors/team?type=http&type=ping&type=port&type=docker&type=hardware" ); const monitors = monitorsResponse ?? []; @@ -81,6 +83,27 @@ const CreateStatusPage = () => { reset(defaults); }, [defaults, reset]); + const watchedMonitorIds: string[] = form.watch("monitors") ?? []; + useEffect(() => { + const selectedMonitors = (watchedMonitorIds ?? []) + .map((id) => monitors.find((m) => m.id === id)) + .filter((m): m is Monitor => m !== undefined); + + const typesSet: Set = new Set(); + selectedMonitors.forEach((m) => { + if (m.type === "hardware") { + typesSet.add("infrastructure"); + } else { + typesSet.add("uptime"); + } + }); + + const computedTypes: MonitorDisplayType[] = typesSet.size + ? Array.from(typesSet) + : ["uptime"]; + form.setValue("type", computedTypes); + }, [watchedMonitorIds, monitors, form]); + const onError = (errors: any) => { logger.debug("Status page validation errors", errors); }; @@ -103,7 +126,6 @@ const CreateStatusPage = () => { const onSubmit = async (data: StatusPageFormData) => { const fd = new FormData(); - fd.append("type", "uptime"); fd.append("isPublished", String(data.isPublished)); if (data.companyName) fd.append("companyName", data.companyName); if (data.url) fd.append("url", data.url); @@ -112,11 +134,16 @@ const CreateStatusPage = () => { fd.append("showCharts", String(data.showCharts)); fd.append("showUptimePercentage", String(data.showUptimePercentage)); fd.append("showAdminLoginLink", String(data.showAdminLoginLink)); + fd.append("showInfrastructure", String(data.showInfrastructure)); data.monitors.forEach((monitorId) => { fd.append("monitors[]", monitorId); }); + data.type.forEach((type) => { + fd.append("type[]", type); + }); + // Handle logo upload if (data.logo === null) { // Signal to remove the logo @@ -303,7 +330,9 @@ const CreateStatusPage = () => { }} > - {monitor.name} + {`${monitor.name} (${t(`pages.common.monitors.monitorTypes.option${monitor?.type.charAt(0).toUpperCase() + monitor?.type.slice(1)}`)})`} { @@ -425,6 +454,23 @@ const CreateStatusPage = () => { /> )} /> + ( + + } + label={t( + "pages.statusPages.form.features.option.showInfrastructure.label" + )} + /> + )} + /> {/* { + const theme = useTheme(); + return ( + + + {label} + + {value} + + ); +}; + +interface MetricItemProps { + label: string; + progress: number; + details?: MetricDetailRowProps[]; +} + +const MetricItem = ({ label, progress, details }: MetricItemProps) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + return ( + + + + {label} + + {details && details.length > 0 && ( + + {details.map((detail) => ( + + ))} + + )} + + ); +}; + +export const InfrastructureMetrics = ({ monitor }: { monitor: StatusPageMonitor }) => { + const theme = useTheme(); + const { t } = useTranslation(); + + const latestCheck = monitor.recentChecks?.[0] ?? monitor.checks?.[0]; + + if (!latestCheck) { + return ( + + {t("pages.statusPages.monitorsList.noData")} + + ); + } + + const metrics = [ + { + key: t("pages.statusPages.monitorsList.infrastructure.cpu"), + labelKey: t("pages.statusPages.monitorsList.infrastructure.cpu"), + hasData: latestCheck.cpu && typeof latestCheck.cpu.usage_percent === "number", + progress: latestCheck.cpu?.usage_percent ? latestCheck.cpu.usage_percent * 100 : 0, + details: [ + { + label: t("pages.statusPages.monitorsList.infrastructure.usage"), + value: `${((latestCheck.cpu?.usage_percent || 0) * 100).toFixed(2)}%`, + }, + ], + }, + { + key: t("pages.statusPages.monitorsList.infrastructure.memoryText"), + labelKey: t("pages.statusPages.monitorsList.infrastructure.memory"), + hasData: + latestCheck.memory && + typeof latestCheck.memory.usage_percent === "number" && + typeof latestCheck.memory.used_bytes === "number" && + typeof latestCheck.memory.total_bytes === "number", + progress: latestCheck.memory?.usage_percent + ? latestCheck.memory.usage_percent * 100 + : 0, + details: [ + { + label: t("pages.statusPages.monitorsList.infrastructure.used"), + value: prettyBytes(latestCheck.memory?.used_bytes || 0), + }, + { + label: t("pages.statusPages.monitorsList.infrastructure.total"), + value: prettyBytes(latestCheck.memory?.total_bytes || 0), + }, + ], + }, + { + key: t("pages.statusPages.monitorsList.infrastructure.disk"), + labelKey: t("pages.statusPages.monitorsList.infrastructure.disk"), + hasData: latestCheck.disk && latestCheck.disk.length > 0, + progress: latestCheck.disk + ? (latestCheck.disk.reduce((acc, disk) => acc + (disk?.usage_percent || 0), 0) / + latestCheck.disk.length) * + 100 + : 0, + details: [ + { + label: t("pages.statusPages.monitorsList.infrastructure.used"), + value: prettyBytes( + latestCheck?.disk?.reduce((acc, disk) => acc + (disk?.used_bytes || 0), 0) ?? + 0 + ), + }, + { + label: t("pages.statusPages.monitorsList.infrastructure.total"), + value: prettyBytes( + latestCheck?.disk?.reduce((acc, disk) => acc + (disk?.total_bytes || 0), 0) ?? + 0 + ), + }, + ], + }, + ]; + + return ( + + {metrics.map( + ({ key, labelKey, hasData, progress, details }) => + hasData && ( + + ) + )} + + ); +}; diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx index b94b20a80..c9212f0c4 100644 --- a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx +++ b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx @@ -5,13 +5,15 @@ import Typography from "@mui/material/Typography"; 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 { useTheme } from "@mui/material/styles"; +import { useTheme, type Theme } from "@mui/material/styles"; import { useSelector } from "react-redux"; import { useState } from "react"; import type { Monitor } from "@/Types/Monitor"; import type { StatusPage } from "@/Types/StatusPage"; import type { RootState } from "@/Types/state"; +import { LAYOUT, SPACING } from "@/Utils/Theme/constants"; interface StatusPageMonitor extends Monitor { checks?: Monitor["recentChecks"]; @@ -22,6 +24,87 @@ interface MonitorsListProps { monitors: StatusPageMonitor[]; } +const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => { + // for future: specific badge styles can be defined here based on monitor type + const badgeConfig: Record = { + hardware: { + color: theme.palette.background.paper, + bg: theme.palette.info.light, + }, + http: { + color: theme.palette.background.paper, + bg: theme.palette.success.light, + }, + ping: { + color: theme.palette.background.paper, + bg: theme.palette.success.light, + }, + port: { + color: theme.palette.background.paper, + bg: theme.palette.success.light, + }, + docker: { + color: theme.palette.background.paper, + bg: theme.palette.success.light, + }, + pagespeed: { + color: theme.palette.background.paper, + bg: theme.palette.success.light, + }, + game: { + color: theme.palette.background.paper, + bg: theme.palette.success.light, + }, + }; + + const config = badgeConfig[monitorType] ?? badgeConfig.http; + return { + backgroundColor: config.bg, + color: config.color, + padding: `${theme.spacing(SPACING.SM)} ${theme.spacing(SPACING.LG)}`, + borderRadius: theme.shape.borderRadius, + }; +}; + +// Extracted component for monitor content below the header row +const MonitorContent = ({ + monitor, + statusPage, + chartType, +}: { + monitor: Monitor & { checks?: Monitor["recentChecks"] }; + statusPage: StatusPage; + chartType: string; +}) => { + const theme = useTheme(); + const isInfrastructureMonitor = monitor?.type === "hardware"; + + if (isInfrastructureMonitor) { + if (statusPage.showInfrastructure === false) { + return null; + } + return ; + } + + if (statusPage.showCharts === false) { + return null; + } + + return ( + + {chartType === "histogram" ? ( + + ) : ( + + )} + + ); +}; + export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { const theme = useTheme(); const { t } = useTranslation(); @@ -31,11 +114,12 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram"); return ( - - {statusPage.showCharts !== false && ( + + {statusPage.showCharts && ( {t("pages.statusPages.monitorsList.chartTypeHeatmap")} { return ( - - {monitor.name} - - {showURL && ( + + {monitor.name} + + + {t( + `pages.common.monitors.monitorTypes.option${monitor?.type.charAt(0).toUpperCase() + monitor?.type.slice(1)}` + )} + + + {showURL && monitor?.url && ( {monitor.url} )} - + - {statusPage.showCharts !== false && ( - - {chartType === "histogram" ? ( - - ) : ( - - )} - - )} + ); })} diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx index 0ca629581..77aa0892a 100644 --- a/client/src/Pages/StatusPage/Status/index.tsx +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -23,16 +23,17 @@ const StatusPageView = () => { const isSmall = useMediaQuery(theme.breakpoints.down("md")); const isPublic = location.pathname.startsWith("/status/public"); - const apiUrl = url ? `/status-page/${url}?type=uptime` : null; + const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null; const { data, isLoading, error } = useGet(apiUrl); const statusPage = data?.statusPage; const monitors = data?.monitors ?? []; + const infrastructureMonitors = data?.infrastructureMonitors ?? []; if (!statusPage) return null; - if (monitors?.length === 0) { + if (monitors?.length === 0 && infrastructureMonitors?.length === 0) { return ( row.type, + render: (row) => row.type.join(", "), }, { id: "status", diff --git a/client/src/Types/StatusPage.ts b/client/src/Types/StatusPage.ts index 900373f3d..370cc23b5 100644 --- a/client/src/Types/StatusPage.ts +++ b/client/src/Types/StatusPage.ts @@ -1,10 +1,11 @@ import type { Monitor } from "@/Types/Monitor"; +export type MonitorDisplayType = "uptime" | "infrastructure"; export interface StatusPage { id: string; userId: string; teamId: string; - type: string; + type: MonitorDisplayType[]; companyName: string; url: string; timezone?: string; @@ -20,6 +21,7 @@ export interface StatusPage { showCharts: boolean; showUptimePercentage: boolean; showAdminLoginLink: boolean; + showInfrastructure: boolean; customCSS: string; createdAt: string; updatedAt: string; @@ -28,4 +30,5 @@ export interface StatusPage { export interface StatusPageResponse { statusPage: StatusPage; monitors: Monitor[]; + infrastructureMonitors: Monitor[]; } diff --git a/client/src/Validation/statusPage.ts b/client/src/Validation/statusPage.ts index fbe6a0690..43d7198f0 100644 --- a/client/src/Validation/statusPage.ts +++ b/client/src/Validation/statusPage.ts @@ -14,12 +14,16 @@ 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"), 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(), customCSS: z.string().optional(), logo: z .object({ diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index fa92f2e99..c11543cad 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -288,7 +288,13 @@ const logoImageValidation = joi .optional(); // Make entire object optional const statusPageValidation = joi.object({ - type: joi.string().valid("uptime").required(), + type: joi + .alternatives() + .try( + joi.string().valid("uptime", "infrastructure"), + joi.array().items(joi.string().valid("uptime", "infrastructure")) + ) + .required(), isPublished: joi.bool(), companyName: joi .string() @@ -317,6 +323,7 @@ const statusPageValidation = joi.object({ showUptimePercentage: joi.boolean(), showCharts: joi.boolean(), showAdminLoginLink: joi.boolean(), + showInfrastructure: joi.boolean(), }); const settingsValidation = joi.object({ diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 04e18e3dc..dbe037447 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -407,6 +407,17 @@ "paused": "paused", "total": "total", "up": "up" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "Game", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "Port", + "optionPagespeed": "PageSpeed", + "optionHardware": "Infrastructure", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" } } }, @@ -534,21 +545,13 @@ }, "type": { "description": "Select the type of check to perform", - "optionDocker": "Docker", "optionDockerDescription": "Use Docker to monitor if a container is running.", - "optionGame": "Game", "optionGameDescription": "Monitor if a specific game server is online.", - "optionGrpc": "gRPC", "optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.", - "optionWebSocket": "WebSocket", "optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.", - "optionHttp": "HTTP(S)", "optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.", - "optionPing": "Ping", "optionPingDescription": "Use ICMP Ping to monitor if a server is online.", - "optionPort": "Port", - "optionPortDescription": "Monitor if a specific port on a server is open.", - "title": "Type" + "optionPortDescription": "Monitor if a specific port on a server is open." }, "thresholds": { "title": "Alert thresholds", @@ -1178,7 +1181,22 @@ }, "monitorsList": { "chartTypeHeatmap": "Heatmap", - "chartTypeHistogram": "Histogram" + "chartTypeHistogram": "Histogram", + "noData": "No data available", + "infrastructure": { + "title": "Infrastructure", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "Memory", + "disk": "Disk", + "usage": "Usage", + "used": "Used", + "total": "Total" + }, + "uptime": { + "title": "Uptime", + "responseTime": "Response time" + } }, "statusBar": { "allDown": "All systems are down", @@ -1270,6 +1288,9 @@ }, "showAdminLoginLink": { "label": "Show admin login link" + }, + "showInfrastructure": { + "label": "Show infrastructure metrics" } } } From d3d00353a144a94c0a1853ad770b5821cfb4a380 Mon Sep 17 00:00:00 2001 From: mannilakash Date: Tue, 17 Mar 2026 06:04:42 -0400 Subject: [PATCH 2/5] fix: minor refactoring infrastructureMetrics --- client/src/Pages/StatusPage/Create/index.tsx | 39 ++-- .../Components/InfrastructureMetrics.tsx | 208 +++++++++++------- 2 files changed, 153 insertions(+), 94 deletions(-) diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx index 3b64738e5..1574c21c1 100644 --- a/client/src/Pages/StatusPage/Create/index.tsx +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -21,7 +21,7 @@ import { import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useStatusPageForm } from "@/Hooks/useStatusPageForm"; @@ -39,6 +39,22 @@ interface TimezoneOption { name: string; } +const MONITOR_TYPE_KEYS: Record = { + http: "pages.common.monitors.monitorTypes.optionHttp", + ping: "pages.common.monitors.monitorTypes.optionPing", + docker: "pages.common.monitors.monitorTypes.optionDocker", + port: "pages.common.monitors.monitorTypes.optionPort", + game: "pages.common.monitors.monitorTypes.optionGame", + grpc: "pages.common.monitors.monitorTypes.optionGrpc", + websocket: "pages.common.monitors.monitorTypes.optionWebSocket", + hardware: "pages.common.monitors.monitorTypes.optionHardware", +}; + +const getMonitorTypeLabel = (type: string, t: (key: string) => string): string => { + const i18nMonitorKey = MONITOR_TYPE_KEYS[type]; + return i18nMonitorKey ? t(i18nMonitorKey) : type; +}; + const CreateStatusPage = () => { const theme = useTheme(); const { t } = useTranslation(); @@ -84,25 +100,22 @@ const CreateStatusPage = () => { }, [defaults, reset]); const watchedMonitorIds: string[] = form.watch("monitors") ?? []; - useEffect(() => { + const computedTypes: MonitorDisplayType[] = useMemo(() => { const selectedMonitors = (watchedMonitorIds ?? []) .map((id) => monitors.find((m) => m.id === id)) .filter((m): m is Monitor => m !== undefined); - const typesSet: Set = new Set(); + const typesSet = new Set(); selectedMonitors.forEach((m) => { - if (m.type === "hardware") { - typesSet.add("infrastructure"); - } else { - typesSet.add("uptime"); - } + typesSet.add(m.type === "hardware" ? "infrastructure" : "uptime"); }); - const computedTypes: MonitorDisplayType[] = typesSet.size - ? Array.from(typesSet) - : ["uptime"]; + return typesSet.size ? Array.from(typesSet) : ["uptime"]; + }, [JSON.stringify(watchedMonitorIds), monitors]); + + useEffect(() => { form.setValue("type", computedTypes); - }, [watchedMonitorIds, monitors, form]); + }, [computedTypes]); const onError = (errors: any) => { logger.debug("Status page validation errors", errors); @@ -332,7 +345,7 @@ const CreateStatusPage = () => { {`${monitor.name} (${t(`pages.common.monitors.monitorTypes.option${monitor?.type.charAt(0).toUpperCase() + monitor?.type.slice(1)}`)})`} + >{`${monitor.name} (${getMonitorTypeLabel(monitor.type, t)})`} { diff --git a/client/src/Pages/StatusPage/Status/Components/InfrastructureMetrics.tsx b/client/src/Pages/StatusPage/Status/Components/InfrastructureMetrics.tsx index 9be17105c..32a15d64b 100644 --- a/client/src/Pages/StatusPage/Status/Components/InfrastructureMetrics.tsx +++ b/client/src/Pages/StatusPage/Status/Components/InfrastructureMetrics.tsx @@ -10,19 +10,28 @@ import useMediaQuery from "@mui/material/useMediaQuery"; import Box from "@mui/material/Box"; import { LAYOUT, SPACING } from "@/Utils/Theme/constants"; +const GAUGE_RADIUS = 60; +const GAUGE_STROKE_WIDTH = 12; +const PERCENTAGE_MULTIPLIER = 100; + interface StatusPageMonitor extends Monitor { checks?: Monitor["recentChecks"]; - infrastructureCPU?: number; - infrastructureMemory?: number; - infrastructureDisk?: number; } -interface MetricDetailRowProps { +interface MetricDetail { label: string; value: string; } -const MetricDetailRow = ({ label, value }: MetricDetailRowProps) => { +interface MetricConfig { + key: string; + label: string; + hasData: boolean; + progress: number; + details: MetricDetail[]; +} + +const MetricDetailRow = ({ label, value }: MetricDetail) => { const theme = useTheme(); return ( { interface MetricItemProps { label: string; progress: number; - details?: MetricDetailRowProps[]; + details?: MetricDetail[]; } const MetricItem = ({ label, progress, details }: MetricItemProps) => { @@ -75,8 +84,8 @@ const MetricItem = ({ label, progress, details }: MetricItemProps) => { > {label} @@ -98,6 +107,92 @@ const MetricItem = ({ label, progress, details }: MetricItemProps) => { ); }; +type LatestCheck = NonNullable[number]; + +const buildCpuMetric = ( + check: LatestCheck, + t: (key: string) => string +): MetricConfig | null => { + if (!check.cpu || typeof check.cpu.usage_percent !== "number") { + return null; + } + const usagePercent = (check.cpu.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER; + return { + key: "cpu", + label: t("pages.statusPages.monitorsList.infrastructure.cpu"), + hasData: true, + progress: usagePercent, + details: [ + { + label: t("pages.statusPages.monitorsList.infrastructure.usage"), + value: `${usagePercent.toFixed(2)}%`, + }, + ], + }; +}; + +const buildMemoryMetric = ( + check: LatestCheck, + t: (key: string) => string +): MetricConfig | null => { + if ( + !check.memory || + typeof check.memory.usage_percent !== "number" || + typeof check.memory.used_bytes !== "number" || + typeof check.memory.total_bytes !== "number" + ) { + return null; + } + return { + key: "memory", + label: t("pages.statusPages.monitorsList.infrastructure.memory"), + hasData: true, + progress: (check.memory.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER, + details: [ + { + label: t("pages.statusPages.monitorsList.infrastructure.used"), + value: prettyBytes(check.memory.used_bytes ?? 0), + }, + { + label: t("pages.statusPages.monitorsList.infrastructure.total"), + value: prettyBytes(check.memory.total_bytes ?? 0), + }, + ], + }; +}; + +const buildDiskMetric = ( + check: LatestCheck, + t: (key: string) => string +): MetricConfig | null => { + if (!check.disk || check.disk.length === 0) { + return null; + } + const disks = check.disk; + const avgUsagePercent = + (disks.reduce((acc, disk) => acc + (disk?.usage_percent ?? 0), 0) / disks.length) * + PERCENTAGE_MULTIPLIER; + const totalUsedBytes = disks.reduce((acc, disk) => acc + (disk?.used_bytes ?? 0), 0); + const totalTotalBytes = disks.reduce((acc, disk) => acc + (disk?.total_bytes ?? 0), 0); + + return { + key: "disk", + label: t("pages.statusPages.monitorsList.infrastructure.disk"), + hasData: true, + progress: avgUsagePercent, + details: [ + { + label: t("pages.statusPages.monitorsList.infrastructure.used"), + value: prettyBytes(totalUsedBytes), + }, + { + label: t("pages.statusPages.monitorsList.infrastructure.total"), + value: prettyBytes(totalTotalBytes), + }, + ], + }; +}; + export const InfrastructureMetrics = ({ monitor }: { monitor: StatusPageMonitor }) => { const theme = useTheme(); const { t } = useTranslation(); @@ -115,68 +210,22 @@ export const InfrastructureMetrics = ({ monitor }: { monitor: StatusPageMonitor ); } - const metrics = [ - { - key: t("pages.statusPages.monitorsList.infrastructure.cpu"), - labelKey: t("pages.statusPages.monitorsList.infrastructure.cpu"), - hasData: latestCheck.cpu && typeof latestCheck.cpu.usage_percent === "number", - progress: latestCheck.cpu?.usage_percent ? latestCheck.cpu.usage_percent * 100 : 0, - details: [ - { - label: t("pages.statusPages.monitorsList.infrastructure.usage"), - value: `${((latestCheck.cpu?.usage_percent || 0) * 100).toFixed(2)}%`, - }, - ], - }, - { - key: t("pages.statusPages.monitorsList.infrastructure.memoryText"), - labelKey: t("pages.statusPages.monitorsList.infrastructure.memory"), - hasData: - latestCheck.memory && - typeof latestCheck.memory.usage_percent === "number" && - typeof latestCheck.memory.used_bytes === "number" && - typeof latestCheck.memory.total_bytes === "number", - progress: latestCheck.memory?.usage_percent - ? latestCheck.memory.usage_percent * 100 - : 0, - details: [ - { - label: t("pages.statusPages.monitorsList.infrastructure.used"), - value: prettyBytes(latestCheck.memory?.used_bytes || 0), - }, - { - label: t("pages.statusPages.monitorsList.infrastructure.total"), - value: prettyBytes(latestCheck.memory?.total_bytes || 0), - }, - ], - }, - { - key: t("pages.statusPages.monitorsList.infrastructure.disk"), - labelKey: t("pages.statusPages.monitorsList.infrastructure.disk"), - hasData: latestCheck.disk && latestCheck.disk.length > 0, - progress: latestCheck.disk - ? (latestCheck.disk.reduce((acc, disk) => acc + (disk?.usage_percent || 0), 0) / - latestCheck.disk.length) * - 100 - : 0, - details: [ - { - label: t("pages.statusPages.monitorsList.infrastructure.used"), - value: prettyBytes( - latestCheck?.disk?.reduce((acc, disk) => acc + (disk?.used_bytes || 0), 0) ?? - 0 - ), - }, - { - label: t("pages.statusPages.monitorsList.infrastructure.total"), - value: prettyBytes( - latestCheck?.disk?.reduce((acc, disk) => acc + (disk?.total_bytes || 0), 0) ?? - 0 - ), - }, - ], - }, - ]; + const metrics: MetricConfig[] = [ + buildCpuMetric(latestCheck, t), + buildMemoryMetric(latestCheck, t), + buildDiskMetric(latestCheck, t), + ].filter((m): m is MetricConfig => m !== null); + + if (metrics.length === 0) { + return ( + + {t("pages.statusPages.monitorsList.noData")} + + ); + } return ( - {metrics.map( - ({ key, labelKey, hasData, progress, details }) => - hasData && ( - - ) - )} + {metrics.map(({ key, label, progress, details }) => ( + + ))} ); }; From 622446c308d4d41b449b12ae0b0924cd1d8e463c Mon Sep 17 00:00:00 2001 From: mannilakash Date: Tue, 17 Mar 2026 15:01:30 -0400 Subject: [PATCH 3/5] fix: refactoring monitorsList and minor fixes based on feedback --- client/src/Pages/StatusPage/Create/index.tsx | 19 +- .../Status/Components/MonitorsList.tsx | 201 ++++++++---------- client/src/Types/StatusPage.ts | 22 +- client/src/locales/en.json | 3 +- 4 files changed, 109 insertions(+), 136 deletions(-) diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx index 1574c21c1..ea4452dae 100644 --- a/client/src/Pages/StatusPage/Create/index.tsx +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -29,6 +29,7 @@ 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 { getMonitorTypeLabel } from "@/Types/StatusPage"; import timezones from "@/Utils/timezones.json"; import { useNavigate, useParams } from "react-router-dom"; import axios from "axios"; @@ -39,22 +40,6 @@ interface TimezoneOption { name: string; } -const MONITOR_TYPE_KEYS: Record = { - http: "pages.common.monitors.monitorTypes.optionHttp", - ping: "pages.common.monitors.monitorTypes.optionPing", - docker: "pages.common.monitors.monitorTypes.optionDocker", - port: "pages.common.monitors.monitorTypes.optionPort", - game: "pages.common.monitors.monitorTypes.optionGame", - grpc: "pages.common.monitors.monitorTypes.optionGrpc", - websocket: "pages.common.monitors.monitorTypes.optionWebSocket", - hardware: "pages.common.monitors.monitorTypes.optionHardware", -}; - -const getMonitorTypeLabel = (type: string, t: (key: string) => string): string => { - const i18nMonitorKey = MONITOR_TYPE_KEYS[type]; - return i18nMonitorKey ? t(i18nMonitorKey) : type; -}; - const CreateStatusPage = () => { const theme = useTheme(); const { t } = useTranslation(); @@ -111,7 +96,7 @@ const CreateStatusPage = () => { }); return typesSet.size ? Array.from(typesSet) : ["uptime"]; - }, [JSON.stringify(watchedMonitorIds), monitors]); + }, [watchedMonitorIds, monitors]); useEffect(() => { form.setValue("type", computedTypes); diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx index c9212f0c4..248a69c9b 100644 --- a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx +++ b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx @@ -12,6 +12,7 @@ import { useSelector } from "react-redux"; import { useState } from "react"; import type { Monitor } from "@/Types/Monitor"; import type { StatusPage } from "@/Types/StatusPage"; +import { getMonitorTypeLabel } from "@/Types/StatusPage"; import type { RootState } from "@/Types/state"; import { LAYOUT, SPACING } from "@/Utils/Theme/constants"; @@ -25,81 +26,98 @@ interface MonitorsListProps { } const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => { - // for future: specific badge styles can be defined here based on monitor type - const badgeConfig: Record = { - hardware: { - color: theme.palette.background.paper, - bg: theme.palette.info.light, - }, - http: { - color: theme.palette.background.paper, - bg: theme.palette.success.light, - }, - ping: { - color: theme.palette.background.paper, - bg: theme.palette.success.light, - }, - port: { - color: theme.palette.background.paper, - bg: theme.palette.success.light, - }, - docker: { - color: theme.palette.background.paper, - bg: theme.palette.success.light, - }, - pagespeed: { - color: theme.palette.background.paper, - bg: theme.palette.success.light, - }, - game: { - color: theme.palette.background.paper, - bg: theme.palette.success.light, - }, - }; - - const config = badgeConfig[monitorType] ?? badgeConfig.http; + const bg = + monitorType === "hardware" ? theme.palette.info.light : theme.palette.success.light; return { - backgroundColor: config.bg, - color: config.color, + backgroundColor: bg, + color: theme.palette.background.paper, padding: `${theme.spacing(SPACING.SM)} ${theme.spacing(SPACING.LG)}`, borderRadius: theme.shape.borderRadius, }; }; -// Extracted component for monitor content below the header row +const MonitorHeader = ({ + monitor, + showURL, +}: { + monitor: StatusPageMonitor; + showURL: boolean; +}) => { + const theme = useTheme(); + const { t } = useTranslation(); + + return ( + + + + + {monitor.name} + + + {getMonitorTypeLabel(monitor.type, t)} + + + {showURL && monitor.url && ( + + {monitor.url} + + )} + + + + ); +}; + const MonitorContent = ({ monitor, statusPage, chartType, }: { - monitor: Monitor & { checks?: Monitor["recentChecks"] }; + monitor: StatusPageMonitor; statusPage: StatusPage; chartType: string; }) => { const theme = useTheme(); - const isInfrastructureMonitor = monitor?.type === "hardware"; - if (isInfrastructureMonitor) { - if (statusPage.showInfrastructure === false) { - return null; - } + if (monitor.type === "hardware") { + if (statusPage.showInfrastructure === false) return null; return ; } - if (statusPage.showCharts === false) { - return null; - } + if (statusPage.showCharts === false) return null; + const checks = monitor.checks?.slice().reverse() ?? []; return ( {chartType === "histogram" ? ( ) : ( - + )} ); @@ -108,17 +126,15 @@ const MonitorContent = ({ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { const theme = useTheme(); const { t } = useTranslation(); - const showURL = useSelector((state: RootState) => state.ui?.showURL); - const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram"); return ( {statusPage.showCharts && ( {t("pages.statusPages.monitorsList.chartTypeHeatmap")} @@ -126,9 +142,7 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { dualOption value={chartType} checked={chartType === "histogram"} - onChange={(e) => { - setChartType(e.target.checked ? "histogram" : "heatmap"); - }} + onChange={(e) => setChartType(e.target.checked ? "histogram" : "heatmap")} /> {t("pages.statusPages.monitorsList.chartTypeHistogram")} @@ -136,69 +150,22 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { )} - {monitors?.map((monitor) => { - return ( - - - - - - {monitor.name} - - - {t( - `pages.common.monitors.monitorTypes.option${monitor?.type.charAt(0).toUpperCase() + monitor?.type.slice(1)}` - )} - - - {showURL && monitor?.url && ( - - {monitor.url} - - )} - - - - - - ); - })} + {monitors.map((monitor) => ( + + + + + ))} ); }; diff --git a/client/src/Types/StatusPage.ts b/client/src/Types/StatusPage.ts index 370cc23b5..9b4ee313f 100644 --- a/client/src/Types/StatusPage.ts +++ b/client/src/Types/StatusPage.ts @@ -1,6 +1,26 @@ -import type { Monitor } from "@/Types/Monitor"; +import type { Monitor, MonitorType } from "@/Types/Monitor"; export type MonitorDisplayType = "uptime" | "infrastructure"; +export const MONITOR_TYPE_KEYS: Partial> = { + http: "pages.common.monitors.monitorTypes.optionHttp", + ping: "pages.common.monitors.monitorTypes.optionPing", + docker: "pages.common.monitors.monitorTypes.optionDocker", + port: "pages.common.monitors.monitorTypes.optionPort", + game: "pages.common.monitors.monitorTypes.optionGame", + grpc: "pages.common.monitors.monitorTypes.optionGrpc", + websocket: "pages.common.monitors.monitorTypes.optionWebSocket", + hardware: "pages.common.monitors.monitorTypes.optionHardware", + pagespeed: "pages.common.monitors.monitorTypes.optionPagespeed", +}; + +export const getMonitorTypeLabel = ( + type: MonitorType, + t: (key: string) => string +): string => { + const key = MONITOR_TYPE_KEYS[type]; + return key ? t(key) : type; +}; + export interface StatusPage { id: string; userId: string; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index dbe037447..92a21939f 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -551,7 +551,8 @@ "optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.", "optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.", "optionPingDescription": "Use ICMP Ping to monitor if a server is online.", - "optionPortDescription": "Monitor if a specific port on a server is open." + "optionPortDescription": "Monitor if a specific port on a server is open.", + "title": "Type" }, "thresholds": { "title": "Alert thresholds", From 115fcc67f6f4d2ef19875b5e33976b798faea25f Mon Sep 17 00:00:00 2001 From: mannilakash Date: Tue, 17 Mar 2026 15:14:44 -0400 Subject: [PATCH 4/5] fix: fetching game/grpc/websocket as well for statusPage --- client/src/Pages/StatusPage/Create/index.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx index ea4452dae..f1ecc674c 100644 --- a/client/src/Pages/StatusPage/Create/index.tsx +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -54,9 +54,15 @@ const CreateStatusPage = () => { isCreate ? null : `/status-page/${url}?type=uptime&type=infrastructure` ); - const { data: monitorsResponse } = useGet( - "/monitors/team?type=http&type=ping&type=port&type=docker&type=hardware" - ); + const monitorsUrl = useMemo(() => { + const params = new URLSearchParams(); + ["http", "ping", "port", "docker", "game", "grpc", "websocket", "hardware"].forEach( + (type) => params.append("type", type) + ); + return `/monitors/team?${params.toString()}`; + }, []); + + const { data: monitorsResponse } = useGet(monitorsUrl); const monitors = monitorsResponse ?? []; const { post, loading: isSubmittingPost } = usePost(); From 5a12105c3dd82fb4683e136c7bc701815dffc386 Mon Sep 17 00:00:00 2001 From: mannilakash Date: Wed, 18 Mar 2026 17:02:17 -0400 Subject: [PATCH 5/5] fix: statusPageMonitor removal of infrastructureMonitors[] --- client/src/Pages/StatusPage/Create/index.tsx | 16 ++++++++-------- client/src/Pages/StatusPage/Status/index.tsx | 3 +-- client/src/Types/StatusPage.ts | 1 - 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx index f1ecc674c..f09d9e47d 100644 --- a/client/src/Pages/StatusPage/Create/index.tsx +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -35,6 +35,14 @@ import { useNavigate, useParams } from "react-router-dom"; import axios from "axios"; import { HeaderConfigStatusControls } from "./Components/HeaderConfigStatusControls"; +const monitorsUrl = (() => { + const params = new URLSearchParams(); + ["http", "ping", "port", "docker", "game", "grpc", "websocket", "hardware"].forEach( + (type) => params.append("type", type) + ); + return `/monitors/team?${params.toString()}`; +})(); + interface TimezoneOption { _id: string; name: string; @@ -54,14 +62,6 @@ const CreateStatusPage = () => { isCreate ? null : `/status-page/${url}?type=uptime&type=infrastructure` ); - const monitorsUrl = useMemo(() => { - const params = new URLSearchParams(); - ["http", "ping", "port", "docker", "game", "grpc", "websocket", "hardware"].forEach( - (type) => params.append("type", type) - ); - return `/monitors/team?${params.toString()}`; - }, []); - const { data: monitorsResponse } = useGet(monitorsUrl); const monitors = monitorsResponse ?? []; diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx index 77aa0892a..3bb14261f 100644 --- a/client/src/Pages/StatusPage/Status/index.tsx +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -29,11 +29,10 @@ const StatusPageView = () => { const statusPage = data?.statusPage; const monitors = data?.monitors ?? []; - const infrastructureMonitors = data?.infrastructureMonitors ?? []; if (!statusPage) return null; - if (monitors?.length === 0 && infrastructureMonitors?.length === 0) { + if (monitors.length === 0) { return (