diff --git a/client/src/Pages/StatusPage/Status/Components/InfrastructureMetrics.tsx b/client/src/Pages/StatusPage/Status/Components/InfrastructureMetrics.tsx deleted file mode 100644 index 32a15d64b..000000000 --- a/client/src/Pages/StatusPage/Status/Components/InfrastructureMetrics.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import { Gauge } from "@/Components/design-elements"; -import { useTheme } from "@mui/material/styles"; -import { useTranslation } from "react-i18next"; -import prettyBytes from "pretty-bytes"; -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"; - -const GAUGE_RADIUS = 60; -const GAUGE_STROKE_WIDTH = 12; -const PERCENTAGE_MULTIPLIER = 100; - -interface StatusPageMonitor extends Monitor { - checks?: Monitor["recentChecks"]; -} - -interface MetricDetail { - label: string; - value: string; -} - -interface MetricConfig { - key: string; - label: string; - hasData: boolean; - progress: number; - details: MetricDetail[]; -} - -const MetricDetailRow = ({ label, value }: MetricDetail) => { - const theme = useTheme(); - return ( - - - {label} - - {value} - - ); -}; - -interface MetricItemProps { - label: string; - progress: number; - details?: MetricDetail[]; -} - -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) => ( - - ))} - - )} - - ); -}; - -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(); - - const latestCheck = monitor.recentChecks?.[0] ?? monitor.checks?.[0]; - - if (!latestCheck) { - return ( - - {t("pages.statusPages.monitorsList.noData")} - - ); - } - - 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, label, progress, details }) => ( - - ))} - - ); -}; diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx deleted file mode 100644 index 01e7d1975..000000000 --- a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import Stack from "@mui/material/Stack"; -import { useTranslation } from "react-i18next"; -import Box from "@mui/material/Box"; -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, 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 { getMonitorTypeLabel } from "@/Types/StatusPage"; -import type { RootState } from "@/Types/state"; -import { LAYOUT, SPACING } from "@/Utils/Theme/constants"; - -interface StatusPageMonitor extends Monitor { - checks?: Monitor["recentChecks"]; -} - -interface MonitorsListProps { - statusPage: StatusPage; - monitors: StatusPageMonitor[]; -} - -const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => { - const bg = - monitorType === "hardware" ? theme.palette.info.light : theme.palette.success.light; - return { - backgroundColor: bg, - color: theme.palette.background.paper, - padding: `${theme.spacing(SPACING.SM)} ${theme.spacing(SPACING.LG)}`, - borderRadius: theme.shape.borderRadius, - }; -}; - -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: StatusPageMonitor; - statusPage: StatusPage; - chartType: string; -}) => { - const theme = useTheme(); - - if (monitor.type === "hardware") { - if (statusPage.showInfrastructure === false) return null; - return ; - } - - if (statusPage.showCharts === false) return null; - - const checks = monitor.checks?.slice().reverse() ?? []; - return ( - - {chartType === "histogram" ? ( - - ) : ( - - )} - - ); -}; - -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")} - setChartType(e.target.checked ? "histogram" : "heatmap")} - /> - - {t("pages.statusPages.monitorsList.chartTypeHistogram")} - - - )} - - {monitors.map((monitor) => ( - - - - - ))} - - ); -}; diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx b/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx deleted file mode 100644 index 49f70d952..000000000 --- a/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { - AlertTriangle, - CircleCheck, - CircleX, - Loader, - PauseCircle, - ShieldAlert, - Wrench, -} from "lucide-react"; -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; - -import { useTranslation } from "react-i18next"; -import { useTheme } from "@mui/material"; -import type { Monitor, MonitorStatus } from "@/Types/Monitor"; - -type SemanticTone = "up" | "down" | "warn" | "info"; - -interface StatusDisplay { - icon: JSX.Element; - msg?: string; - tone: SemanticTone; -} - -const getMonitorStatus = (monitors: Monitor[], t: Function): StatusDisplay => { - if (monitors.length === 0) { - return { - msg: t("pages.statusPages.statusBar.noMonitors"), - tone: "warn", - icon: , - }; - } - - const allOf = (...statuses: MonitorStatus[]) => - monitors.every((m) => statuses.includes(m.status)); - const someOf = (...statuses: MonitorStatus[]) => - monitors.some((m) => statuses.includes(m.status)); - const noneOf = (...statuses: MonitorStatus[]) => - monitors.every((m) => !statuses.includes(m.status)); - - if (allOf("up")) { - return { - msg: t("pages.statusPages.statusBar.allUp"), - tone: "up", - icon: , - }; - } - if (allOf("breached")) { - return { - msg: t("pages.statusPages.statusBar.allBreached"), - tone: "down", - icon: , - }; - } - if (allOf("maintenance")) { - return { - msg: t("pages.statusPages.statusBar.allMaintenance"), - tone: "warn", - icon: , - }; - } - if (allOf("down")) { - return { - msg: t("pages.statusPages.statusBar.allDown"), - tone: "down", - icon: , - }; - } - if (allOf("paused")) { - return { - msg: t("pages.statusPages.statusBar.allPaused"), - tone: "warn", - icon: , - }; - } - if (allOf("initializing")) { - return { - msg: t("pages.statusPages.statusBar.allInitializing"), - tone: "warn", - icon: , - }; - } - - // Breached takes highest priority in mixed states - if (someOf("breached") && someOf("down")) { - return { - msg: t("pages.statusPages.statusBar.breachedAndDown"), - tone: "down", - icon: , - }; - } - if (someOf("breached")) { - return { - msg: t("pages.statusPages.statusBar.breached"), - tone: "down", - icon: , - }; - } - if (someOf("maintenance") && someOf("down")) { - return { - msg: t("pages.statusPages.statusBar.maintenanceAndDown"), - tone: "down", - icon: , - }; - } - if (someOf("maintenance") && noneOf("down")) { - return { - msg: t("pages.statusPages.statusBar.maintenance"), - tone: "warn", - icon: , - }; - } - if (someOf("down")) { - return { - msg: t("pages.statusPages.statusBar.degraded"), - tone: "warn", - icon: , - }; - } - if (someOf("paused")) { - return { - msg: t("pages.statusPages.statusBar.partiallyPaused"), - tone: "warn", - icon: , - }; - } - if (someOf("initializing")) { - return { - msg: t("pages.statusPages.statusBar.initializing"), - tone: "info", - icon: , - }; - } - - return { - msg: t("pages.statusPages.statusBar.unknown"), - tone: "warn", - icon: , - }; -}; - -const toneToCssVar: Record = { - up: "var(--sp-up)", - down: "var(--sp-down)", - warn: "var(--sp-warn)", - info: "var(--sp-up)", -}; - -interface StatusBarProps { - monitors: Monitor[]; -} - -export const StatusBar = ({ monitors }: StatusBarProps) => { - const theme = useTheme(); - const { t } = useTranslation(); - const monitorsStatus = getMonitorStatus(monitors, t); - - return ( - - {monitorsStatus.icon} - {monitorsStatus.msg} - - ); -}; diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx index 86b9d90ad..1f9cca9ee 100644 --- a/client/src/Pages/StatusPage/Status/index.tsx +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -1,12 +1,9 @@ import { BasePage, BaseFallback } from "@/Components/design-elements"; -import { StatusBar } from "@/Pages/StatusPage/Status/Components/StatusBar"; -import { MonitorsList } from "@/Pages/StatusPage/Status/Components/MonitorsList"; import Typography from "@mui/material/Typography"; import { Link } from "react-router-dom"; import Stack from "@mui/material/Stack"; -import Box from "@mui/material/Box"; -import { useMediaQuery, useTheme } from "@mui/material"; +import { useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useIsAdmin } from "@/Hooks/useIsAdmin"; import { useLocation, useParams } from "react-router-dom"; @@ -43,7 +40,6 @@ const StatusPageView = () => { const { url } = useParams(); const isAdmin = useIsAdmin(); const location = useLocation(); - 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; @@ -88,17 +84,6 @@ const StatusPageView = () => { ); } - let sx: React.CSSProperties = {}; - if (isPublic) { - sx.paddingTop = theme.spacing(20); - sx.paddingLeft = isSmall ? "5vw" : "20vw"; - sx.paddingRight = isSmall ? "5vw" : "20vw"; - } - - const logoSrc = statusPage.logo?.data - ? `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}` - : null; - const ThemedRenderer = THEMED_RENDERERS[resolveStatusPageTheme(statusPage.theme)]; const themedRenderer = ( { /> ); - if (themedRenderer) { - // Public route: render directly on the viewport, themed background covers everything. - if (isPublic) { - return ( - - {themedRenderer} - - ); - } - - // Admin preview: wrap in a mock browser frame inside the admin shell. - const publicUrl = `${window.location.origin}/status/public/${statusPage.url}`; + // Public route: render directly on the viewport, themed background covers everything. + if (isPublic) { return ( - - - - {themedRenderer} - - + {themedRenderer} + ); } + // Admin preview: wrap in a mock browser frame inside the admin shell. + const publicUrl = `${window.location.origin}/status/public/${statusPage.url}`; return ( - - + - - {logoSrc && ( - - )} - - - - + {themedRenderer} + + ); };