fix: refactoring changes and adding type migration to statusPageType

This commit is contained in:
mannilakash committed 2026-03-11 14:03:25 -04:00
1 parent be1ca4ac28
commit a9e9097a1f
7 files changed
+307 -176

No files matched your search

+5 -5
View File
@@ -302,35 +302,35 @@ const CreateMonitorPage = () => {
>
<RadioWithDescription
value="http"
label={t("pages.createMonitor.form.type.optionHttp")}
label={t("pages.common.monitors.monitorTypes.optionHttp")}
description={t(
"pages.createMonitor.form.type.optionHttpDescription"
)}
/>
<RadioWithDescription
value="ping"
label={t("pages.createMonitor.form.type.optionPing")}
label={t("pages.common.monitors.monitorTypes.optionPing")}
description={t(
"pages.createMonitor.form.type.optionPingDescription"
)}
/>
<RadioWithDescription
value="docker"
label={t("pages.createMonitor.form.type.optionDocker")}
label={t("pages.common.monitors.monitorTypes.optionDocker")}
description={t(
"pages.createMonitor.form.type.optionDockerDescription"
)}
/>
<RadioWithDescription
value="port"
label={t("pages.createMonitor.form.type.optionPort")}
label={t("pages.common.monitors.monitorTypes.optionPort")}
description={t(
"pages.createMonitor.form.type.optionPortDescription"
)}
/>
<RadioWithDescription
value="game"
label={t("pages.createMonitor.form.type.optionGame")}
label={t("pages.common.monitors.monitorTypes.optionGame")}
description={t(
"pages.createMonitor.form.type.optionGameDescription"
)}
+3 -1
View File
@@ -330,7 +330,9 @@ const CreateStatusPage = () => {
}}
>
<GripVertical size={20} />
<Typography flexGrow={1}>{monitor.name}</Typography>
<Typography
flexGrow={1}
>{`${monitor.name} (${t(`pages.common.monitors.monitorTypes.option${monitor?.type.charAt(0).toUpperCase() + monitor?.type.slice(1)}`)})`}</Typography>
<IconButton
size="small"
onClick={() => {
@@ -5,6 +5,9 @@ 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";
interface StatusPageMonitor extends Monitor {
checks?: Monitor["recentChecks"];
@@ -13,34 +16,62 @@ interface StatusPageMonitor extends Monitor {
infrastructureDisk?: number;
}
interface MetricItemProps {
interface MetricDetailRowProps {
label: string;
progress: number;
upperLabel?: string;
upperValue?: string;
lowerLabel?: string;
lowerValue?: string;
value: string;
}
const MetricItem = ({
label,
progress,
upperLabel,
upperValue,
lowerLabel,
lowerValue,
}: MetricItemProps) => {
const MetricDetailRow = ({ label, value }: MetricDetailRowProps) => {
const theme = useTheme();
return (
<Stack
direction="row"
alignItems="center"
gap={`${theme.spacing(4)} ${theme.spacing(10)}`}
flex={1}
justifyContent="space-between"
>
<Stack
<Typography
variant="body2"
color={theme.palette.text.secondary}
>
{label}
</Typography>
<Typography variant="body2">{value}</Typography>
</Stack>
);
};
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 (
<Grid
size={isSmall ? 12 : 4}
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
textAlign: "center",
gap: theme.spacing(2),
padding: theme.spacing(2),
paddingBottom: theme.spacing(4),
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(2),
},
}}
>
<Box
display="flex"
flexDirection="column"
alignItems="center"
spacing={theme.spacing(2)}
>
<Gauge
progress={progress}
@@ -48,44 +79,22 @@ const MetricItem = ({
strokeWidth={12}
/>
<Typography variant="body2">{label}</Typography>
</Stack>
{(upperLabel || lowerLabel) && (
<Stack
spacing={theme.spacing(1)}
flex={1}
paddingBottom={theme.spacing(10)}
</Box>
{details && details.length > 0 && (
<Box
width="100%"
paddingX={theme.spacing(10)}
>
{upperLabel && (
<Stack
direction="row"
justifyContent="space-between"
>
<Typography
variant="body2"
color="text.secondary"
>
{upperLabel}
</Typography>
<Typography variant="body2">{upperValue}</Typography>
</Stack>
)}
{lowerLabel && (
<Stack
direction="row"
justifyContent="space-between"
>
<Typography
variant="body2"
color="text.secondary"
>
{lowerLabel}
</Typography>
<Typography variant="body2">{lowerValue}</Typography>
</Stack>
)}
</Stack>
{details.map((detail) => (
<MetricDetailRow
key={detail.label}
label={detail.label}
value={detail.value}
/>
))}
</Box>
)}
</Stack>
</Grid>
);
};
@@ -99,54 +108,93 @@ export const InfrastructureMetrics = ({ monitor }: { monitor: StatusPageMonitor
return (
<Typography
variant="body2"
color="text.secondary"
color={theme.palette.text.secondary}
>
{t("pages.statusPages.monitorsList.noData")}
</Typography>
);
}
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);
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 (
<Stack
direction={{ xs: "column", sm: "row" }}
gap={theme.spacing(10)}
columnGap={theme.spacing(15)}
padding={theme.spacing(8)}
<Grid
container
alignItems="center"
padding={theme.spacing(4)}
>
<MetricItem
label={t("pages.statusPages.monitorsList.infrastructure.cpu")}
progress={cpuUsage}
upperLabel={t("pages.statusPages.monitorsList.infrastructure.usage")}
upperValue={`${cpuUsage.toFixed(1)}%`}
/>
<MetricItem
label={t("pages.statusPages.monitorsList.infrastructure.memory")}
progress={memoryUsage}
upperLabel={t("pages.statusPages.monitorsList.infrastructure.used")}
upperValue={prettyBytes(memoryUsed)}
lowerLabel={t("pages.statusPages.monitorsList.infrastructure.total")}
lowerValue={prettyBytes(memoryTotal)}
/>
<MetricItem
label={t("pages.statusPages.monitorsList.infrastructure.disk")}
progress={diskUsage}
upperLabel={t("pages.statusPages.monitorsList.infrastructure.used")}
upperValue={prettyBytes(diskUsed)}
lowerLabel={t("pages.statusPages.monitorsList.infrastructure.total")}
lowerValue={prettyBytes(diskTotal)}
/>
</Stack>
{metrics.map(
({ key, labelKey, hasData, progress, details }) =>
hasData && (
<MetricItem
key={key}
label={t(labelKey)}
progress={progress}
details={details}
/>
)
)}
</Grid>
);
};
@@ -7,7 +7,7 @@ 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";
@@ -23,6 +23,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<string, { color: string; bg: string }> = {
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(1)} ${theme.spacing(2)}`,
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 <InfrastructureMetrics monitor={monitor} />;
}
if (statusPage.showCharts === false) {
return null;
}
return (
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1, mb: theme.spacing(2) }}>
{chartType === "histogram" ? (
<HistogramResponseTime
height={{ xs: 50, md: 100 }}
gap={{ xs: theme.spacing(1), md: theme.spacing(5) }}
checks={monitor?.checks?.slice().reverse() ?? []}
/>
) : (
<HeatmapResponseTime checks={monitor?.checks?.slice().reverse() ?? []} />
)}
</Box>
);
};
export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
const theme = useTheme();
const { t } = useTranslation();
@@ -55,7 +136,6 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
)}
{monitors?.map((monitor) => {
const isInfrastructureMonitor = monitor?.type === "hardware";
return (
<BaseBox
key={monitor.id}
@@ -87,27 +167,22 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
</Typography>
<Typography
variant="caption"
sx={{
backgroundColor:
monitor?.type === "hardware"
? theme.palette.info.light
: theme.palette.success.light,
color:
monitor?.type === "hardware"
? theme.palette.info.dark
: theme.palette.success.dark,
padding: `${theme.spacing(0.5)} ${theme.spacing(1.5)}`,
borderRadius: theme.shape.borderRadius,
fontWeight: 600,
}}
sx={getMonitorBadgeStyles(monitor?.type ?? "", theme)}
>
{t(`pages.statusPages.monitorsList.badge.${monitor?.type}`)}
{t(
`pages.common.monitors.monitorTypes.option${monitor?.type.charAt(0).toUpperCase() + monitor?.type.slice(1)}`
)}
</Typography>
</Stack>
{showURL && monitor?.url && (
<Typography
variant="body2"
color="text.secondary"
color={theme.palette.text.secondary}
sx={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{monitor.url}
</Typography>
@@ -115,48 +190,11 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
</Box>
<StatusLabel status={monitor?.status} />
</Stack>
{!isInfrastructureMonitor && (
<>
{statusPage.showCharts !== false && (
<Box
sx={{
overflow: "hidden",
minWidth: 0,
flex: 1,
mb: theme.spacing(2),
}}
>
{chartType === "histogram" ? (
<HistogramResponseTime
height={{ xs: 50, md: 100 }}
gap={{ xs: theme.spacing(0.5), md: theme.spacing(5) }}
checks={monitor?.checks?.slice().reverse() ?? []}
/>
) : (
<HeatmapResponseTime
checks={monitor?.checks?.slice().reverse() ?? []}
/>
)}
</Box>
)}
</>
)}
{isInfrastructureMonitor && statusPage.showInfrastructure !== false && (
<BaseBox
key={monitor.id}
padding={theme.spacing(4)}
>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
gap={theme.spacing(8)}
></Stack>
<InfrastructureMetrics monitor={monitor} />
</BaseBox>
)}
<MonitorContent
monitor={monitor}
statusPage={statusPage}
chartType={chartType}
/>
</BaseBox>
);
})}
+12 -16
View File
@@ -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,19 +545,12 @@
},
"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"
},
@@ -1184,6 +1188,7 @@
"title": "Infrastructure",
"cpu": "CPU",
"memory": "RAM",
"memoryText": "Memory",
"disk": "Disk",
"usage": "Usage",
"used": "Used",
@@ -1192,15 +1197,6 @@
"uptime": {
"title": "Uptime",
"responseTime": "Response time"
},
"badge": {
"http": "HTTP",
"ping": "Ping",
"port": "Port",
"docker": "Docker",
"pagespeed": "PageSpeed",
"hardware": "Infrastructure",
"game": "Game"
}
},
"statusBar": {
@@ -0,0 +1,45 @@
import mongoose from "mongoose";
import { logger } from "@/utils/logger.js";
import StatusPageModel from "../models/StatusPage.js";
/**
* Migration: Convert StatusPage `type` field from string to array of strings
*
* Old format: { type: "uptime" }
* New format: { type: ["uptime"] }
*/
export async function migrateStatusPageTypeToArray(): Promise<void> {
const SERVICE_NAME = "Migration:MigrateStatusPageTypeToArray";
try {
logger.info({ service: SERVICE_NAME, message: "Starting migration of StatusPage type field from string to array" });
const db = mongoose.connection.db;
if (!db) {
throw new Error("Database connection is not initialized");
}
// Find all status pages where `type` is a string (not yet migrated)
const result = await StatusPageModel.updateMany({ type: { $type: "string" } }, [
{
$set: {
type: ["$type"],
},
},
]);
if (result.modifiedCount === 0) {
logger.info({ service: SERVICE_NAME, message: "No StatusPage documents needed migration" });
return;
}
logger.info({
service: SERVICE_NAME,
message: `Migration complete. Converted ${result.modifiedCount} StatusPage document(s) from string type to array type`,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error({ service: SERVICE_NAME, message: `Error during StatusPage type migration: ${errorMessage}` });
throw error;
}
}
+2
View File
@@ -3,6 +3,7 @@ import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js";
import MigrationModel from "../models/Migration.js";
import { migrateStatusPageTypeToArray } from "./0005_migrateStatusPageTypeToArray.js";
type MigrationEntry = {
name: string;
@@ -14,6 +15,7 @@ const migrations: MigrationEntry[] = [
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
{ name: "0005_migrateStatusPageTypeToArray", execute: migrateStatusPageTypeToArray },
];
const runMigrations = async (logger?: { info: Function; error: Function }) => {