Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c27393942e | ||
|
|
d1077a1314 | ||
|
|
22bcb74653 | ||
|
|
78f5ce180b | ||
|
|
a9e9097a1f | ||
|
|
be1ca4ac28 |
No files matched your search
@@ -27,12 +27,14 @@ export const useStatusPageForm = ({
|
|||||||
companyName: data?.companyName || "",
|
companyName: data?.companyName || "",
|
||||||
url: data?.url || generateDefaultUrl(),
|
url: data?.url || generateDefaultUrl(),
|
||||||
timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
type: data?.type || ["uptime"],
|
||||||
color: data?.color || "#4169E1",
|
color: data?.color || "#4169E1",
|
||||||
monitors: data?.monitors || [],
|
monitors: data?.monitors || [],
|
||||||
isPublished: data?.isPublished ?? false,
|
isPublished: data?.isPublished ?? false,
|
||||||
showCharts: data?.showCharts ?? true,
|
showCharts: data?.showCharts ?? true,
|
||||||
showUptimePercentage: data?.showUptimePercentage ?? true,
|
showUptimePercentage: data?.showUptimePercentage ?? true,
|
||||||
showAdminLoginLink: data?.showAdminLoginLink ?? false,
|
showAdminLoginLink: data?.showAdminLoginLink ?? false,
|
||||||
|
showInfrastructure: data?.showInfrastructure ?? false,
|
||||||
customCSS: data?.customCSS || "",
|
customCSS: data?.customCSS || "",
|
||||||
logo: transformLogo(data?.logo),
|
logo: transformLogo(data?.logo),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -302,49 +302,49 @@ const CreateMonitorPage = () => {
|
|||||||
>
|
>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="http"
|
value="http"
|
||||||
label={t("pages.createMonitor.form.type.optionHttp")}
|
label={t("pages.common.monitors.monitorTypes.optionHttp")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionHttpDescription"
|
"pages.createMonitor.form.type.optionHttpDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="ping"
|
value="ping"
|
||||||
label={t("pages.createMonitor.form.type.optionPing")}
|
label={t("pages.common.monitors.monitorTypes.optionPing")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionPingDescription"
|
"pages.createMonitor.form.type.optionPingDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="docker"
|
value="docker"
|
||||||
label={t("pages.createMonitor.form.type.optionDocker")}
|
label={t("pages.common.monitors.monitorTypes.optionDocker")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionDockerDescription"
|
"pages.createMonitor.form.type.optionDockerDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="port"
|
value="port"
|
||||||
label={t("pages.createMonitor.form.type.optionPort")}
|
label={t("pages.common.monitors.monitorTypes.optionPort")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionPortDescription"
|
"pages.createMonitor.form.type.optionPortDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="game"
|
value="game"
|
||||||
label={t("pages.createMonitor.form.type.optionGame")}
|
label={t("pages.common.monitors.monitorTypes.optionGame")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionGameDescription"
|
"pages.createMonitor.form.type.optionGameDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="grpc"
|
value="grpc"
|
||||||
label={t("pages.createMonitor.form.type.optionGrpc")}
|
label={t("pages.common.monitors.monitorTypes.optionGrpc")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionGrpcDescription"
|
"pages.createMonitor.form.type.optionGrpcDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="websocket"
|
value="websocket"
|
||||||
label={t("pages.createMonitor.form.type.optionWebSocket")}
|
label={t("pages.common.monitors.monitorTypes.optionWebSocket")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionWebSocketDescription"
|
"pages.createMonitor.form.type.optionWebSocketDescription"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ import {
|
|||||||
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useStatusPageForm } from "@/Hooks/useStatusPageForm";
|
import { useStatusPageForm } from "@/Hooks/useStatusPageForm";
|
||||||
import type { StatusPageFormData } from "@/Validation/statusPage";
|
import type { StatusPageFormData } from "@/Validation/statusPage";
|
||||||
import { useGet, usePost, usePut, useDelete } from "@/Hooks/UseApi";
|
import { useGet, usePost, usePut, useDelete } from "@/Hooks/UseApi";
|
||||||
import type { Monitor } from "@/Types/Monitor";
|
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 timezones from "@/Utils/timezones.json";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -39,6 +39,22 @@ interface TimezoneOption {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MONITOR_TYPE_KEYS: Record<string, string> = {
|
||||||
|
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 CreateStatusPage = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -49,10 +65,12 @@ const CreateStatusPage = () => {
|
|||||||
|
|
||||||
// Fetch existing status page data when configuring
|
// Fetch existing status page data when configuring
|
||||||
const { data: statusPageData, isLoading: isLoadingStatusPage } =
|
const { data: statusPageData, isLoading: isLoadingStatusPage } =
|
||||||
useGet<StatusPageResponse>(isCreate ? null : `/status-page/${url}?type=uptime`);
|
useGet<StatusPageResponse>(
|
||||||
|
isCreate ? null : `/status-page/${url}?type=uptime&type=infrastructure`
|
||||||
|
);
|
||||||
|
|
||||||
const { data: monitorsResponse } = useGet<Monitor[]>(
|
const { data: monitorsResponse } = useGet<Monitor[]>(
|
||||||
"/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 ?? [];
|
const monitors = monitorsResponse ?? [];
|
||||||
|
|
||||||
@@ -81,6 +99,24 @@ const CreateStatusPage = () => {
|
|||||||
reset(defaults);
|
reset(defaults);
|
||||||
}, [defaults, reset]);
|
}, [defaults, reset]);
|
||||||
|
|
||||||
|
const watchedMonitorIds: string[] = form.watch("monitors") ?? [];
|
||||||
|
const computedTypes: MonitorDisplayType[] = useMemo(() => {
|
||||||
|
const selectedMonitors = (watchedMonitorIds ?? [])
|
||||||
|
.map((id) => monitors.find((m) => m.id === id))
|
||||||
|
.filter((m): m is Monitor => m !== undefined);
|
||||||
|
|
||||||
|
const typesSet = new Set<MonitorDisplayType>();
|
||||||
|
selectedMonitors.forEach((m) => {
|
||||||
|
typesSet.add(m.type === "hardware" ? "infrastructure" : "uptime");
|
||||||
|
});
|
||||||
|
|
||||||
|
return typesSet.size ? Array.from(typesSet) : ["uptime"];
|
||||||
|
}, [JSON.stringify(watchedMonitorIds), monitors]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.setValue("type", computedTypes);
|
||||||
|
}, [computedTypes]);
|
||||||
|
|
||||||
const onError = (errors: any) => {
|
const onError = (errors: any) => {
|
||||||
logger.debug("Status page validation errors", errors);
|
logger.debug("Status page validation errors", errors);
|
||||||
};
|
};
|
||||||
@@ -103,7 +139,6 @@ const CreateStatusPage = () => {
|
|||||||
|
|
||||||
const onSubmit = async (data: StatusPageFormData) => {
|
const onSubmit = async (data: StatusPageFormData) => {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("type", "uptime");
|
|
||||||
fd.append("isPublished", String(data.isPublished));
|
fd.append("isPublished", String(data.isPublished));
|
||||||
if (data.companyName) fd.append("companyName", data.companyName);
|
if (data.companyName) fd.append("companyName", data.companyName);
|
||||||
if (data.url) fd.append("url", data.url);
|
if (data.url) fd.append("url", data.url);
|
||||||
@@ -112,11 +147,16 @@ const CreateStatusPage = () => {
|
|||||||
fd.append("showCharts", String(data.showCharts));
|
fd.append("showCharts", String(data.showCharts));
|
||||||
fd.append("showUptimePercentage", String(data.showUptimePercentage));
|
fd.append("showUptimePercentage", String(data.showUptimePercentage));
|
||||||
fd.append("showAdminLoginLink", String(data.showAdminLoginLink));
|
fd.append("showAdminLoginLink", String(data.showAdminLoginLink));
|
||||||
|
fd.append("showInfrastructure", String(data.showInfrastructure));
|
||||||
|
|
||||||
data.monitors.forEach((monitorId) => {
|
data.monitors.forEach((monitorId) => {
|
||||||
fd.append("monitors[]", monitorId);
|
fd.append("monitors[]", monitorId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
data.type.forEach((type) => {
|
||||||
|
fd.append("type[]", type);
|
||||||
|
});
|
||||||
|
|
||||||
// Handle logo upload
|
// Handle logo upload
|
||||||
if (data.logo === null) {
|
if (data.logo === null) {
|
||||||
// Signal to remove the logo
|
// Signal to remove the logo
|
||||||
@@ -303,7 +343,9 @@ const CreateStatusPage = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GripVertical size={20} />
|
<GripVertical size={20} />
|
||||||
<Typography flexGrow={1}>{monitor.name}</Typography>
|
<Typography
|
||||||
|
flexGrow={1}
|
||||||
|
>{`${monitor.name} (${getMonitorTypeLabel(monitor.type, t)})`}</Typography>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -425,6 +467,23 @@ const CreateStatusPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
name="showInfrastructure"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={t(
|
||||||
|
"pages.statusPages.form.features.option.showInfrastructure.label"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
{/* <Controller
|
{/* <Controller
|
||||||
name="showUptimePercentage"
|
name="showUptimePercentage"
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
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 (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">{value}</Typography>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Grid
|
||||||
|
size={isSmall ? 12 : 4}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
textAlign: "center",
|
||||||
|
gap: theme.spacing(SPACING.LG),
|
||||||
|
padding: theme.spacing(LAYOUT.XS),
|
||||||
|
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(SPACING.LG),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Gauge
|
||||||
|
progress={progress}
|
||||||
|
radius={GAUGE_RADIUS}
|
||||||
|
strokeWidth={GAUGE_STROKE_WIDTH}
|
||||||
|
/>
|
||||||
|
<Typography variant="body2">{label}</Typography>
|
||||||
|
</Box>
|
||||||
|
{details && details.length > 0 && (
|
||||||
|
<Box
|
||||||
|
width="100%"
|
||||||
|
paddingX={theme.spacing(LAYOUT.LG)}
|
||||||
|
>
|
||||||
|
{details.map((detail) => (
|
||||||
|
<MetricDetailRow
|
||||||
|
key={detail.label}
|
||||||
|
label={detail.label}
|
||||||
|
value={detail.value}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type LatestCheck = NonNullable<Monitor["recentChecks"]>[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 (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
>
|
||||||
|
{t("pages.statusPages.monitorsList.noData")}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics: MetricConfig[] = [
|
||||||
|
buildCpuMetric(latestCheck, t),
|
||||||
|
buildMemoryMetric(latestCheck, t),
|
||||||
|
buildDiskMetric(latestCheck, t),
|
||||||
|
].filter((m): m is MetricConfig => m !== null);
|
||||||
|
|
||||||
|
if (metrics.length === 0) {
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
>
|
||||||
|
{t("pages.statusPages.monitorsList.noData")}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
alignItems="center"
|
||||||
|
padding={theme.spacing(LAYOUT.XS)}
|
||||||
|
>
|
||||||
|
{metrics.map(({ key, label, progress, details }) => (
|
||||||
|
<MetricItem
|
||||||
|
key={key}
|
||||||
|
label={label}
|
||||||
|
progress={progress}
|
||||||
|
details={details}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,13 +5,15 @@ import Typography from "@mui/material/Typography";
|
|||||||
import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/common";
|
import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/common";
|
||||||
import { StatusLabel, BaseBox } from "@/Components/design-elements";
|
import { StatusLabel, BaseBox } from "@/Components/design-elements";
|
||||||
import { SwitchComponent } from "@/Components/inputs";
|
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 { useSelector } from "react-redux";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { Monitor } from "@/Types/Monitor";
|
import type { Monitor } from "@/Types/Monitor";
|
||||||
import type { StatusPage } from "@/Types/StatusPage";
|
import type { StatusPage } from "@/Types/StatusPage";
|
||||||
import type { RootState } from "@/Types/state";
|
import type { RootState } from "@/Types/state";
|
||||||
|
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
|
||||||
|
|
||||||
interface StatusPageMonitor extends Monitor {
|
interface StatusPageMonitor extends Monitor {
|
||||||
checks?: Monitor["recentChecks"];
|
checks?: Monitor["recentChecks"];
|
||||||
@@ -22,6 +24,87 @@ interface MonitorsListProps {
|
|||||||
monitors: StatusPageMonitor[];
|
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(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 <InfrastructureMetrics monitor={monitor} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusPage.showCharts === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1, mb: theme.spacing(SPACING.LG) }}>
|
||||||
|
{chartType === "histogram" ? (
|
||||||
|
<HistogramResponseTime
|
||||||
|
height={{ xs: 50, md: 100 }}
|
||||||
|
gap={{ xs: theme.spacing(SPACING.SM), md: theme.spacing(LAYOUT.SM) }}
|
||||||
|
checks={monitor?.checks?.slice().reverse() ?? []}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<HeatmapResponseTime checks={monitor?.checks?.slice().reverse() ?? []} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -31,11 +114,12 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
|||||||
const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram");
|
const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={theme.spacing(8)}>
|
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||||
{statusPage.showCharts !== false && (
|
{statusPage.showCharts && (
|
||||||
<Stack
|
<Stack
|
||||||
direction={"row"}
|
direction={"row"}
|
||||||
alignItems={"center"}
|
alignItems={"center"}
|
||||||
|
gap={theme.spacing(LAYOUT.SM)}
|
||||||
>
|
>
|
||||||
<Typography>{t("pages.statusPages.monitorsList.chartTypeHeatmap")}</Typography>
|
<Typography>{t("pages.statusPages.monitorsList.chartTypeHeatmap")}</Typography>
|
||||||
<SwitchComponent
|
<SwitchComponent
|
||||||
@@ -56,52 +140,62 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
|||||||
return (
|
return (
|
||||||
<BaseBox
|
<BaseBox
|
||||||
key={monitor.id}
|
key={monitor.id}
|
||||||
padding={theme.spacing(4)}
|
padding={theme.spacing(LAYOUT.MD)}
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
gap={theme.spacing(4)}
|
gap={theme.spacing(LAYOUT.XS)}
|
||||||
mb={statusPage.showCharts !== false ? theme.spacing(4) : 0}
|
mb={theme.spacing(LAYOUT.XS)}
|
||||||
>
|
>
|
||||||
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
|
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
|
||||||
<Typography
|
<Stack
|
||||||
variant="h6"
|
direction="row"
|
||||||
sx={{
|
alignItems="center"
|
||||||
overflow: "hidden",
|
gap={theme.spacing(SPACING.LG)}
|
||||||
textOverflow: "ellipsis",
|
mb={theme.spacing(SPACING.SM)}
|
||||||
whiteSpace: "nowrap",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{monitor.name}
|
<Typography
|
||||||
</Typography>
|
variant="h6"
|
||||||
{showURL && (
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{monitor.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={getMonitorBadgeStyles(monitor?.type ?? "", theme)}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
`pages.common.monitors.monitorTypes.option${monitor?.type.charAt(0).toUpperCase() + monitor?.type.slice(1)}`
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
{showURL && monitor?.url && (
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="text.secondary"
|
color={theme.palette.text.secondary}
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{monitor.url}
|
{monitor.url}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<StatusLabel status={monitor.status} />
|
<StatusLabel status={monitor?.status} />
|
||||||
</Stack>
|
</Stack>
|
||||||
{statusPage.showCharts !== false && (
|
<MonitorContent
|
||||||
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
|
monitor={monitor}
|
||||||
{chartType === "histogram" ? (
|
statusPage={statusPage}
|
||||||
<HistogramResponseTime
|
chartType={chartType}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -23,16 +23,17 @@ const StatusPageView = () => {
|
|||||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
const isPublic = location.pathname.startsWith("/status/public");
|
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<StatusPageResponse>(apiUrl);
|
const { data, isLoading, error } = useGet<StatusPageResponse>(apiUrl);
|
||||||
|
|
||||||
const statusPage = data?.statusPage;
|
const statusPage = data?.statusPage;
|
||||||
const monitors = data?.monitors ?? [];
|
const monitors = data?.monitors ?? [];
|
||||||
|
const infrastructureMonitors = data?.infrastructureMonitors ?? [];
|
||||||
|
|
||||||
if (!statusPage) return null;
|
if (!statusPage) return null;
|
||||||
|
|
||||||
if (monitors?.length === 0) {
|
if (monitors?.length === 0 && infrastructureMonitors?.length === 0) {
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export const StatusPagesTable = ({
|
|||||||
{
|
{
|
||||||
id: "type",
|
id: "type",
|
||||||
content: t("common.table.headers.type"),
|
content: t("common.table.headers.type"),
|
||||||
render: (row) => row.type,
|
render: (row) => row.type.join(", "),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "status",
|
id: "status",
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { Monitor } from "@/Types/Monitor";
|
import type { Monitor } from "@/Types/Monitor";
|
||||||
|
export type MonitorDisplayType = "uptime" | "infrastructure";
|
||||||
|
|
||||||
export interface StatusPage {
|
export interface StatusPage {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
type: string;
|
type: MonitorDisplayType[];
|
||||||
companyName: string;
|
companyName: string;
|
||||||
url: string;
|
url: string;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
@@ -20,6 +21,7 @@ export interface StatusPage {
|
|||||||
showCharts: boolean;
|
showCharts: boolean;
|
||||||
showUptimePercentage: boolean;
|
showUptimePercentage: boolean;
|
||||||
showAdminLoginLink: boolean;
|
showAdminLoginLink: boolean;
|
||||||
|
showInfrastructure: boolean;
|
||||||
customCSS: string;
|
customCSS: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -28,4 +30,5 @@ export interface StatusPage {
|
|||||||
export interface StatusPageResponse {
|
export interface StatusPageResponse {
|
||||||
statusPage: StatusPage;
|
statusPage: StatusPage;
|
||||||
monitors: Monitor[];
|
monitors: Monitor[];
|
||||||
|
infrastructureMonitors?: Monitor[];
|
||||||
}
|
}
|
||||||
@@ -14,12 +14,16 @@ export const statusPageSchema = z.object({
|
|||||||
"URL can only contain lowercase letters, numbers, and hyphens"
|
"URL can only contain lowercase letters, numbers, and hyphens"
|
||||||
),
|
),
|
||||||
timezone: z.string().optional(),
|
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"),
|
color: z.string().min(1, "Color is required"),
|
||||||
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
|
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
|
||||||
isPublished: z.boolean(),
|
isPublished: z.boolean(),
|
||||||
showCharts: z.boolean(),
|
showCharts: z.boolean(),
|
||||||
showUptimePercentage: z.boolean(),
|
showUptimePercentage: z.boolean(),
|
||||||
showAdminLoginLink: z.boolean(),
|
showAdminLoginLink: z.boolean(),
|
||||||
|
showInfrastructure: z.boolean(),
|
||||||
customCSS: z.string().optional(),
|
customCSS: z.string().optional(),
|
||||||
logo: z
|
logo: z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -288,7 +288,13 @@ const logoImageValidation = joi
|
|||||||
.optional(); // Make entire object optional
|
.optional(); // Make entire object optional
|
||||||
|
|
||||||
const statusPageValidation = joi.object({
|
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(),
|
isPublished: joi.bool(),
|
||||||
companyName: joi
|
companyName: joi
|
||||||
.string()
|
.string()
|
||||||
@@ -317,6 +323,7 @@ const statusPageValidation = joi.object({
|
|||||||
showUptimePercentage: joi.boolean(),
|
showUptimePercentage: joi.boolean(),
|
||||||
showCharts: joi.boolean(),
|
showCharts: joi.boolean(),
|
||||||
showAdminLoginLink: joi.boolean(),
|
showAdminLoginLink: joi.boolean(),
|
||||||
|
showInfrastructure: joi.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const settingsValidation = joi.object({
|
const settingsValidation = joi.object({
|
||||||
|
|||||||
@@ -407,6 +407,17 @@
|
|||||||
"paused": "paused",
|
"paused": "paused",
|
||||||
"total": "total",
|
"total": "total",
|
||||||
"up": "up"
|
"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": {
|
"type": {
|
||||||
"description": "Select the type of check to perform",
|
"description": "Select the type of check to perform",
|
||||||
"optionDocker": "Docker",
|
|
||||||
"optionDockerDescription": "Use Docker to monitor if a container is running.",
|
"optionDockerDescription": "Use Docker to monitor if a container is running.",
|
||||||
"optionGame": "Game",
|
|
||||||
"optionGameDescription": "Monitor if a specific game server is online.",
|
"optionGameDescription": "Monitor if a specific game server is online.",
|
||||||
"optionGrpc": "gRPC",
|
|
||||||
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
|
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
|
||||||
"optionWebSocket": "WebSocket",
|
|
||||||
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.",
|
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.",
|
||||||
"optionHttp": "HTTP(S)",
|
|
||||||
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
|
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
|
||||||
"optionPing": "Ping",
|
|
||||||
"optionPingDescription": "Use ICMP Ping to monitor if a server is online.",
|
"optionPingDescription": "Use ICMP Ping to monitor if a server is online.",
|
||||||
"optionPort": "Port",
|
|
||||||
"optionPortDescription": "Monitor if a specific port on a server is open.",
|
"optionPortDescription": "Monitor if a specific port on a server is open.",
|
||||||
"title": "Type"
|
"title": "Type"
|
||||||
},
|
},
|
||||||
@@ -1178,7 +1182,22 @@
|
|||||||
},
|
},
|
||||||
"monitorsList": {
|
"monitorsList": {
|
||||||
"chartTypeHeatmap": "Heatmap",
|
"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": {
|
"statusBar": {
|
||||||
"allDown": "All systems are down",
|
"allDown": "All systems are down",
|
||||||
@@ -1270,6 +1289,9 @@
|
|||||||
},
|
},
|
||||||
"showAdminLoginLink": {
|
"showAdminLoginLink": {
|
||||||
"label": "Show admin login link"
|
"label": "Show admin login link"
|
||||||
|
},
|
||||||
|
"showInfrastructure": {
|
||||||
|
"label": "Show infrastructure metrics"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
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(
|
||||||
|
{
|
||||||
|
$expr: {
|
||||||
|
$eq: [{ $type: "$type" }, "string"], // only scalar string, not existing array
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
|
|||||||
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
|
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
|
||||||
import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js";
|
import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js";
|
||||||
import MigrationModel from "../models/Migration.js";
|
import MigrationModel from "../models/Migration.js";
|
||||||
|
import { migrateStatusPageTypeToArray } from "./0005_migrateStatusPageTypeToArray.js";
|
||||||
|
|
||||||
type MigrationEntry = {
|
type MigrationEntry = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -14,6 +15,7 @@ const migrations: MigrationEntry[] = [
|
|||||||
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
|
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
|
||||||
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
|
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
|
||||||
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
|
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
|
||||||
|
{ name: "0005_migrateStatusPageTypeToArray", execute: migrateStatusPageTypeToArray },
|
||||||
];
|
];
|
||||||
|
|
||||||
const runMigrations = async (logger?: { info: Function; error: Function }) => {
|
const runMigrations = async (logger?: { info: Function; error: Function }) => {
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ const StatusPageSchema = new Schema<StatusPageDocument>(
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: [String],
|
||||||
required: true,
|
required: true,
|
||||||
default: "uptime",
|
default: [],
|
||||||
enum: StatusPageTypes,
|
enum: StatusPageTypes,
|
||||||
},
|
},
|
||||||
companyName: {
|
companyName: {
|
||||||
@@ -100,6 +100,10 @@ const StatusPageSchema = new Schema<StatusPageDocument>(
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
showInfrastructure: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
customCSS: {
|
customCSS: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
|
|||||||
showCharts: doc.showCharts,
|
showCharts: doc.showCharts,
|
||||||
showUptimePercentage: doc.showUptimePercentage,
|
showUptimePercentage: doc.showUptimePercentage,
|
||||||
showAdminLoginLink: doc.showAdminLoginLink,
|
showAdminLoginLink: doc.showAdminLoginLink,
|
||||||
|
showInfrastructure: doc.showInfrastructure,
|
||||||
customCSS: doc.customCSS,
|
customCSS: doc.customCSS,
|
||||||
createdAt: this.toDateString(doc.createdAt),
|
createdAt: this.toDateString(doc.createdAt),
|
||||||
updatedAt: this.toDateString(doc.updatedAt),
|
updatedAt: this.toDateString(doc.updatedAt),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const StatusPageTypes = ["uptime"] as const;
|
export const StatusPageTypes = ["uptime", "infrastructure"] as const;
|
||||||
export type StatusPageType = (typeof StatusPageTypes)[number];
|
export type StatusPageType = (typeof StatusPageTypes)[number];
|
||||||
|
|
||||||
export interface StatusPageLogo {
|
export interface StatusPageLogo {
|
||||||
@@ -15,7 +15,7 @@ export interface StatusPage {
|
|||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
type: StatusPageType;
|
type: StatusPageType[];
|
||||||
companyName: string;
|
companyName: string;
|
||||||
url: string;
|
url: string;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
@@ -28,6 +28,7 @@ export interface StatusPage {
|
|||||||
showCharts: boolean;
|
showCharts: boolean;
|
||||||
showUptimePercentage: boolean;
|
showUptimePercentage: boolean;
|
||||||
showAdminLoginLink: boolean;
|
showAdminLoginLink: boolean;
|
||||||
|
showInfrastructure: boolean;
|
||||||
customCSS: string;
|
customCSS: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ export const getStatusPageParamValidation = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getStatusPageQueryValidation = z.object({
|
export const getStatusPageQueryValidation = z.object({
|
||||||
type: z.literal("uptime"),
|
type: z.array(z.enum(["uptime", "infrastructure"])),
|
||||||
timeFrame: z.coerce.number().optional(),
|
timeFrame: z.coerce.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createStatusPageBodyValidation = z
|
export const createStatusPageBodyValidation = z
|
||||||
.object({
|
.object({
|
||||||
type: z.literal("uptime"),
|
type: z.array(z.enum(["uptime", "infrastructure"])),
|
||||||
companyName: z.string().min(1, "Company name is required"),
|
companyName: z.string().min(1, "Company name is required"),
|
||||||
url: z.string().regex(/^[a-zA-Z0-9_-]+$/, {
|
url: z.string().regex(/^[a-zA-Z0-9_-]+$/, {
|
||||||
message: "URL can only contain letters, numbers, underscores, and hyphens",
|
message: "URL can only contain letters, numbers, underscores, and hyphens",
|
||||||
|
|||||||
Reference in new issue
Block a user