From be1ca4ac289aebdccbbdbcd800c168801990bc08 Mon Sep 17 00:00:00 2001 From: mannilakash Date: Fri, 27 Feb 2026 11:57:46 -0500 Subject: [PATCH] feat: adding infrastructure display support to status page --- client/src/Hooks/useStatusPageForm.ts | 2 + client/src/Pages/StatusPage/Create/index.tsx | 52 +++++- .../Components/InfrastructureMetrics.tsx | 152 ++++++++++++++++++ .../Status/Components/MonitorsList.tsx | 109 +++++++++---- 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 | 28 +++- server/src/db/models/StatusPage.ts | 8 +- .../MongoStatusPagesRepository.ts | 1 + server/src/types/statusPage.ts | 5 +- 13 files changed, 341 insertions(+), 41 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/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx index 095ac3499..e4119e800 100644 --- a/client/src/Pages/StatusPage/Create/index.tsx +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -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 { StatusPageResponse } from "@/Types/StatusPage"; +import type { MonitorDisplayType, StatusPageResponse } from "@/Types/StatusPage"; import timezones from "@/Utils/timezones.json"; import { useNavigate, useParams } from "react-router-dom"; import axios from "axios"; @@ -49,10 +49,12 @@ const CreateStatusPage = () => { // 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 @@ -425,6 +452,23 @@ const CreateStatusPage = () => { /> )} /> + ( + + } + label={t( + "pages.statusPages.form.features.option.showInfrastructure.label" + )} + /> + )} + /> {/* { + const theme = useTheme(); + return ( + + + + {label} + + {(upperLabel || lowerLabel) && ( + + {upperLabel && ( + + + {upperLabel} + + {upperValue} + + )} + {lowerLabel && ( + + + {lowerLabel} + + {lowerValue} + + )} + + )} + + ); +}; + +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 cpuUsage = (latestCheck?.cpu?.usage_percent || 0) * 100; + const memoryUsage = (latestCheck?.memory?.usage_percent || 0) * 100; + const memoryUsed = latestCheck?.memory?.used_bytes || 0; + const memoryTotal = latestCheck?.memory?.total_bytes || 0; + + const disks = latestCheck?.disk ?? []; + const totalDiskUsage = disks.reduce((acc, disk) => acc + (disk?.usage_percent || 0), 0); + const diskCount = disks.length || 1; + const diskUsage = (totalDiskUsage / diskCount) * 100; + const diskUsed = disks.reduce((acc, disk) => acc + (disk?.used_bytes || 0), 0); + const diskTotal = disks.reduce((acc, disk) => acc + (disk?.total_bytes || 0), 0); + + return ( + + + + + + ); +}; diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx index b94b20a80..7e053c018 100644 --- a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx +++ b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx @@ -5,6 +5,7 @@ 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 { useSelector } from "react-redux"; @@ -32,10 +33,11 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { return ( - {statusPage.showCharts !== false && ( + {statusPage.showCharts && ( {t("pages.statusPages.monitorsList.chartTypeHeatmap")} { )} {monitors?.map((monitor) => { + const isInfrastructureMonitor = monitor?.type === "hardware"; return ( - - {monitor.name} - - {showURL && ( + + {monitor.name} + + + {t(`pages.statusPages.monitorsList.badge.${monitor?.type}`)} + + + {showURL && monitor?.url && ( { )} - + - {statusPage.showCharts !== false && ( - - {chartType === "histogram" ? ( - - ) : ( - + + {!isInfrastructureMonitor && ( + <> + {statusPage.showCharts !== false && ( + + {chartType === "histogram" ? ( + + ) : ( + + )} + )} - + + )} + + {isInfrastructureMonitor && statusPage.showInfrastructure !== false && ( + + + + )} ); 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..5a301c1a1 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 4c4965aec..16b159398 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1178,7 +1178,30 @@ }, "monitorsList": { "chartTypeHeatmap": "Heatmap", - "chartTypeHistogram": "Histogram" + "chartTypeHistogram": "Histogram", + "noData": "No data available", + "infrastructure": { + "title": "Infrastructure", + "cpu": "CPU", + "memory": "RAM", + "disk": "Disk", + "usage": "Usage", + "used": "Used", + "total": "Total" + }, + "uptime": { + "title": "Uptime", + "responseTime": "Response time" + }, + "badge": { + "http": "HTTP", + "ping": "Ping", + "port": "Port", + "docker": "Docker", + "pagespeed": "PageSpeed", + "hardware": "Infrastructure", + "game": "Game" + } }, "statusBar": { "allDown": "All systems are down", @@ -1270,6 +1293,9 @@ }, "showAdminLoginLink": { "label": "Show admin login link" + }, + "showInfrastructure": { + "label": "Show infrastructure metrics" } } } diff --git a/server/src/db/models/StatusPage.ts b/server/src/db/models/StatusPage.ts index 56c5ee299..6c1ada40e 100644 --- a/server/src/db/models/StatusPage.ts +++ b/server/src/db/models/StatusPage.ts @@ -43,9 +43,9 @@ const StatusPageSchema = new Schema( required: true, }, type: { - type: String, + type: [String], required: true, - default: "uptime", + default: [], enum: StatusPageTypes, }, companyName: { @@ -100,6 +100,10 @@ const StatusPageSchema = new Schema( type: Boolean, default: false, }, + showInfrastructure: { + type: Boolean, + default: false, + }, customCSS: { type: String, default: "", diff --git a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts index e19676285..eb2d92729 100644 --- a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts +++ b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts @@ -57,6 +57,7 @@ class MongoStatusPagesRepository implements IStatusPagesRepository { showCharts: doc.showCharts, showUptimePercentage: doc.showUptimePercentage, showAdminLoginLink: doc.showAdminLoginLink, + showInfrastructure: doc.showInfrastructure, customCSS: doc.customCSS, createdAt: this.toDateString(doc.createdAt), updatedAt: this.toDateString(doc.updatedAt), diff --git a/server/src/types/statusPage.ts b/server/src/types/statusPage.ts index b11cf53d6..0329f4e6f 100644 --- a/server/src/types/statusPage.ts +++ b/server/src/types/statusPage.ts @@ -1,4 +1,4 @@ -export const StatusPageTypes = ["uptime"] as const; +export const StatusPageTypes = ["uptime", "infrastructure"] as const; export type StatusPageType = (typeof StatusPageTypes)[number]; export interface StatusPageLogo { @@ -15,7 +15,7 @@ export interface StatusPage { id: string; userId: string; teamId: string; - type: StatusPageType; + type: StatusPageType[]; companyName: string; url: string; timezone?: string; @@ -28,6 +28,7 @@ export interface StatusPage { showCharts: boolean; showUptimePercentage: boolean; showAdminLoginLink: boolean; + showInfrastructure: boolean; customCSS: string; createdAt: string; updatedAt: string;