From 794018a6abbf95d3028ce1c7331eb184217b6ab6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sat, 24 Jan 2026 20:41:21 +0000 Subject: [PATCH 001/133] simplify breadcrumbs --- .../v2/design-elements/Breadcrumb.tsx | 125 ++++++------------ 1 file changed, 43 insertions(+), 82 deletions(-) diff --git a/client/src/Components/v2/design-elements/Breadcrumb.tsx b/client/src/Components/v2/design-elements/Breadcrumb.tsx index 4489f992d..492ed0ec9 100644 --- a/client/src/Components/v2/design-elements/Breadcrumb.tsx +++ b/client/src/Components/v2/design-elements/Breadcrumb.tsx @@ -2,25 +2,21 @@ import MuiBreadcrumbs from "@mui/material/Breadcrumbs"; import Typography from "@mui/material/Typography"; import { Link, useLocation } from "react-router-dom"; import { useTheme } from "@mui/material/styles"; -import { ChevronRight, Home } from "lucide-react"; -import { useTranslation } from "react-i18next"; +import { ChevronRight } from "lucide-react"; -export const Breadcrumb = ({ breadcrumbOverride }: { breadcrumbOverride?: string[] }) => { - const { t } = useTranslation(); +const isId = (segment: string): boolean => { + return segment.length === 24 || /^[a-f0-9-]{36}$/.test(segment); +}; + +export const Breadcrumb = () => { const theme = useTheme(); const location = useLocation(); - // Don't show breadcrumb on dashboard - if (location.pathname === "/" || location.pathname === "/uptime") { - return null; - } + const segments = location.pathname.split("/").filter((x) => x); - const pathnames = breadcrumbOverride || location.pathname.split("/").filter((x) => x); - - // Don't show if only one level (e.g., /uptime) - if (pathnames.length === 0) { - return null; - } + // Build simplified breadcrumb: "uptime" or "uptime / details" + const basePage = segments[0] || "home"; + const hasDetailsPage = segments.length > 1 && isId(segments[1]); return ( - - + {hasDetailsPage ? ( + + + {basePage.charAt(0).toUpperCase() + basePage.slice(1)} + + + ) : ( - Home + {basePage.charAt(0).toUpperCase() + basePage.slice(1)} - - {pathnames.map((value, index) => { - const last = index === pathnames.length - 1; - const to = `/${pathnames.slice(0, index + 1).join("/")}`; - - let displayName = ""; - - // Paths that are IDs should be detail pages - if (value.length === 24 || value.match(/^[a-f0-9-]{36}$/)) { - displayName = "Details"; - } else { - displayName = - t(`common.breadcrumbs.${value}`) || - value.charAt(0).toUpperCase() + value.slice(1); - } - - return last ? ( - - {displayName} - - ) : ( - - - {displayName} - - - ); - })} + )} + {hasDetailsPage && ( + + Details + + )} ); }; From d42d5f232577991992d0e85bfc81042ed8e581eb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 25 Jan 2026 02:09:33 +0000 Subject: [PATCH 002/133] initial commit --- .../common/controls/HeaderMonitorControls.tsx | 19 ++++ client/src/Components/v2/common/index.ts | 1 + .../v2/design-elements/BasePage.tsx | 2 +- .../src/Components/v2/design-elements/Dot.tsx | 23 +++++ .../v2/design-elements/MonitorStatus.tsx | 59 ++++++++++++ .../v2/design-elements/PulseDot.tsx | 48 ++++++++++ .../Components/v2/design-elements/index.tsx | 3 + client/src/Pages/Uptime/Details/index.tsx | 96 +++++++++++++++++++ .../Uptime/Details/{index.jsx => old.jsx} | 0 client/src/Routes/index.jsx | 10 +- client/src/Types/Check.ts | 5 + client/src/Types/Monitor.ts | 25 +++++ 12 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 client/src/Components/v2/common/controls/HeaderMonitorControls.tsx create mode 100644 client/src/Components/v2/design-elements/Dot.tsx create mode 100644 client/src/Components/v2/design-elements/MonitorStatus.tsx create mode 100644 client/src/Components/v2/design-elements/PulseDot.tsx create mode 100644 client/src/Pages/Uptime/Details/index.tsx rename client/src/Pages/Uptime/Details/{index.jsx => old.jsx} (100%) diff --git a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx new file mode 100644 index 000000000..9a1f99380 --- /dev/null +++ b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx @@ -0,0 +1,19 @@ +import Stack from "@mui/material/Stack"; +import { MonitorStatus } from "@/Components/v2/design-elements"; + +import type { Monitor } from "@/Types/Monitor.js"; + +interface HeaderMonitorControlsProps { + monitor: Monitor; +} + +export const HeaderMonitorControls = ({ monitor }: HeaderMonitorControlsProps) => { + return ( + + + + ); +}; diff --git a/client/src/Components/v2/common/index.ts b/client/src/Components/v2/common/index.ts index e354983eb..16baac7fe 100644 --- a/client/src/Components/v2/common/index.ts +++ b/client/src/Components/v2/common/index.ts @@ -2,3 +2,4 @@ export * from "../common/charts/HistogramResponseTime"; export * from "../common/charts/HeatmapResponseTime"; export * from "../common/charts/HeatmapResponseTimeTooltip"; export * from "../common/controls/HeaderCreate"; +export * from "../common/controls/HeaderMonitorControls"; diff --git a/client/src/Components/v2/design-elements/BasePage.tsx b/client/src/Components/v2/design-elements/BasePage.tsx index c853a8d30..c8618fa67 100644 --- a/client/src/Components/v2/design-elements/BasePage.tsx +++ b/client/src/Components/v2/design-elements/BasePage.tsx @@ -55,7 +55,7 @@ export const BasePage = ({ spacing={theme.spacing(10)} {...props} > - + {children} ); diff --git a/client/src/Components/v2/design-elements/Dot.tsx b/client/src/Components/v2/design-elements/Dot.tsx new file mode 100644 index 000000000..541e90812 --- /dev/null +++ b/client/src/Components/v2/design-elements/Dot.tsx @@ -0,0 +1,23 @@ +import type { CSSProperties } from "react"; + +interface DotProps { + color?: string; + size?: string; + style?: CSSProperties; +} + +export const Dot = ({ color = "gray", size = "4px", style }: DotProps) => { + return ( + + ); +}; diff --git a/client/src/Components/v2/design-elements/MonitorStatus.tsx b/client/src/Components/v2/design-elements/MonitorStatus.tsx new file mode 100644 index 000000000..0ecb77df8 --- /dev/null +++ b/client/src/Components/v2/design-elements/MonitorStatus.tsx @@ -0,0 +1,59 @@ +import type { Monitor } from "@/Types/Monitor"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { PulseDot, Dot } from "@/Components/v2/design-elements"; +import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils"; +import { useTheme } from "@mui/material/styles"; +import prettyMilliseconds from "pretty-ms"; +import { typographyLevels } from "@/Utils/Theme/v2Palette"; +import useMediaQuery from "@mui/material/useMediaQuery"; +export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + + if (!monitor) { + return null; + } + return ( + + + {monitor.name} + + + + + {formatUrl(monitor?.url)} + + {!isSmall && ( + <> + + + Checking every {prettyMilliseconds(monitor?.interval, { verbose: true })} + + + )} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/PulseDot.tsx b/client/src/Components/v2/design-elements/PulseDot.tsx new file mode 100644 index 000000000..ab792b628 --- /dev/null +++ b/client/src/Components/v2/design-elements/PulseDot.tsx @@ -0,0 +1,48 @@ +import { Box, Stack, useTheme } from "@mui/material"; + +interface PulseDotProps { + color: string; +} + +export const PulseDot = ({ color }: PulseDotProps) => { + const theme = useTheme(); + + return ( + + + + ); +}; diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx index 374dabef7..ce2a3b48d 100644 --- a/client/src/Components/v2/design-elements/index.tsx +++ b/client/src/Components/v2/design-elements/index.tsx @@ -8,3 +8,6 @@ export * from "./BaseBox"; export * from "./Table"; export { DataTable as Table } from "./Table"; export * from "./StatusLabel"; +export * from "./PulseDot"; +export * from "./Dot"; +export * from "./MonitorStatus"; diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx new file mode 100644 index 000000000..b2f32276b --- /dev/null +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -0,0 +1,96 @@ +import { useState, useMemo } from "react"; +import { useParams } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { useGet } from "@/Hooks/UseApi"; +import type { MonitorDetailsResponse } from "@/Types/Monitor"; +import type { ChecksResponse } from "@/Types/Check"; +import type { RootState } from "@/Types/state"; +import useCertificateFetch from "./Hooks/useCertificateFetch"; +import { BasePage } from "@/Components/v2/design-elements"; +import { HeaderMonitorControls } from "@/Components/v2/common"; + +const certificateDateFormat = "MMM D, YYYY h A"; + +const UptimeDetailsPage = () => { + const { monitorId } = useParams<{ monitorId: string }>(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + const [dateRange, setDateRange] = useState("recent"); + + const monitorDetailsUrl = useMemo(() => { + if (!monitorId) { + return null; + } + const params = new URLSearchParams(); + params.append("dateRange", dateRange); + params.append("normalize", "true"); + return `/monitors/uptime/details/${monitorId}?${params.toString()}`; + }, [monitorId, dateRange]); + + const { + data: monitorDetailsData, + isLoading: monitorIsLoading, + error: monitorError, + refetch: refetchMonitor, + } = useGet(monitorDetailsUrl); + + const monitorData = monitorDetailsData?.monitorData; + const monitor = monitorData?.monitor; + const monitorStats = monitorDetailsData?.monitorStats ?? null; + + const [certificateExpiry] = useCertificateFetch({ + monitor, + monitorId, + certificateDateFormat, + uiTimezone, + }); + + const checksUrl = useMemo(() => { + if (!monitorId || !monitor?.type) { + return null; + } + const params = new URLSearchParams(); + params.append("type", monitor.type); + params.append("sortOrder", "desc"); + params.append("dateRange", dateRange); + params.append("page", String(page)); + params.append("rowsPerPage", String(rowsPerPage)); + return `/checks/${monitorId}?${params.toString()}`; + }, [monitorId, monitor?.type, dateRange, page, rowsPerPage]); + + const { + data: checksData, + isLoading: checksIsLoading, + error: checksError, + } = useGet(checksUrl); + + const checks = checksData?.checks ?? []; + const checksCount = checksData?.checksCount ?? 0; + + // TODO: Add UI components here + // Placeholder to consume all data fetching results + console.log({ + monitorIsLoading, + monitorError, + refetchMonitor, + monitorStats, + certificateExpiry, + checksIsLoading, + checksError, + checks, + checksCount, + setPage, + setRowsPerPage, + setDateRange, + }); + + return ( + + + + ); +}; + +export default UptimeDetailsPage; diff --git a/client/src/Pages/Uptime/Details/index.jsx b/client/src/Pages/Uptime/Details/old.jsx similarity index 100% rename from client/src/Pages/Uptime/Details/index.jsx rename to client/src/Pages/Uptime/Details/old.jsx diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index e37bae063..715d4374e 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -16,7 +16,7 @@ import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed.jsx"; // Uptime import Uptime from "../Pages/Uptime/Monitors"; -import UptimeDetails from "../Pages/Uptime/Details/index.jsx"; +import UptimeDetails from "../Pages/Uptime/Details"; import UptimeCreate from "../Pages/Uptime/Create/index.jsx"; // PageSpeed @@ -103,7 +103,13 @@ const Routes = () => { /> } + element={ + <> + + + + + } /> Date: Sun, 25 Jan 2026 03:20:58 +0000 Subject: [PATCH 003/133] monitor controls header --- .../common/controls/HeaderMonitorControls.tsx | 80 ++++++++++++++++++- .../Components/v2/design-elements/Icon.tsx | 13 +++ .../Components/v2/design-elements/index.tsx | 1 + client/src/Pages/Uptime/Details/index.tsx | 17 +++- client/src/locales/en.json | 4 +- 5 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 client/src/Components/v2/design-elements/Icon.tsx diff --git a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx index 9a1f99380..f908e4185 100644 --- a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx +++ b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx @@ -1,19 +1,93 @@ import Stack from "@mui/material/Stack"; -import { MonitorStatus } from "@/Components/v2/design-elements"; +import { Icon, MonitorStatus } from "@/Components/v2/design-elements"; +import { Button } from "@/Components/v2/inputs"; +import { Settings, Pause, Play, Mail, Bug } from "lucide-react"; + +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { useTheme } from "@mui/material/styles"; import type { Monitor } from "@/Types/Monitor.js"; interface HeaderMonitorControlsProps { + path: string; monitor: Monitor; + isAdmin: boolean; } -export const HeaderMonitorControls = ({ monitor }: HeaderMonitorControlsProps) => { +export const HeaderMonitorControls = ({ + path, + monitor, + isAdmin, +}: HeaderMonitorControlsProps) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const theme = useTheme(); return ( + + + + {isAdmin && ( + + )} + {isAdmin && ( + + )} + ); }; diff --git a/client/src/Components/v2/design-elements/Icon.tsx b/client/src/Components/v2/design-elements/Icon.tsx new file mode 100644 index 000000000..bc49ddcc0 --- /dev/null +++ b/client/src/Components/v2/design-elements/Icon.tsx @@ -0,0 +1,13 @@ +import type { LucideIcon } from "lucide-react"; + +interface IconProps { + icon: LucideIcon; + size?: number; + strokeWidth?: number; +} + +const Icon = ({ icon: Icon, size = 20, strokeWidth = 1.5 }: IconProps) => { + return ; +}; + +export default Icon; diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx index ce2a3b48d..e71cfb2ec 100644 --- a/client/src/Components/v2/design-elements/index.tsx +++ b/client/src/Components/v2/design-elements/index.tsx @@ -11,3 +11,4 @@ export * from "./StatusLabel"; export * from "./PulseDot"; export * from "./Dot"; export * from "./MonitorStatus"; +export { default as Icon } from "./Icon"; diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index b2f32276b..a816bd4d9 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -1,3 +1,7 @@ +import { BasePage } from "@/Components/v2/design-elements"; +import { HeaderMonitorControls } from "@/Components/v2/common"; + +import { useIsAdmin } from "@/Hooks/useIsAdmin"; import { useState, useMemo } from "react"; import { useParams } from "react-router-dom"; import { useSelector } from "react-redux"; @@ -6,12 +10,11 @@ import type { MonitorDetailsResponse } from "@/Types/Monitor"; import type { ChecksResponse } from "@/Types/Check"; import type { RootState } from "@/Types/state"; import useCertificateFetch from "./Hooks/useCertificateFetch"; -import { BasePage } from "@/Components/v2/design-elements"; -import { HeaderMonitorControls } from "@/Components/v2/common"; const certificateDateFormat = "MMM D, YYYY h A"; const UptimeDetailsPage = () => { + const isAdmin = useIsAdmin(); const { monitorId } = useParams<{ monitorId: string }>(); const uiTimezone = useSelector((state: RootState) => state.ui.timezone); @@ -86,9 +89,17 @@ const UptimeDetailsPage = () => { setDateRange, }); + if (!monitor) { + return null; + } + return ( - + ); }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index de02815b1..11b003442 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -4,7 +4,9 @@ "monitoringAgentName": "Capture", "buttons": { "toggleTheme": "Toggles light & dark", - "create": "Create" + "testNotifications": "Test notifications", + "create": "Create", + "incidents": "Incidents" }, "toasts": { "networkError": "Network error", From a2569c50c78a396e593bd7a65aee05fe479eae03 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 25 Jan 2026 04:09:40 +0000 Subject: [PATCH 004/133] stat boxes --- .../common/controls/HeaderMonitorControls.tsx | 8 +- .../Components/v2/design-elements/StatBox.tsx | 86 +++++++++++++++++++ .../Components/v2/design-elements/Tooltip.tsx | 79 +++++++++++++++++ .../Components/v2/design-elements/index.tsx | 2 + .../v2/monitors/MonitorStatBoxes.tsx | 69 +++++++++++++++ client/src/Components/v2/monitors/index.tsx | 1 + client/src/Pages/Uptime/Details/index.tsx | 39 ++++++--- client/src/Types/Monitor.ts | 13 ++- client/src/locales/en.json | 6 ++ 9 files changed, 286 insertions(+), 17 deletions(-) create mode 100644 client/src/Components/v2/design-elements/StatBox.tsx create mode 100644 client/src/Components/v2/design-elements/Tooltip.tsx create mode 100644 client/src/Components/v2/monitors/MonitorStatBoxes.tsx diff --git a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx index f908e4185..35abb1cd9 100644 --- a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx +++ b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx @@ -11,7 +11,7 @@ import type { Monitor } from "@/Types/Monitor.js"; interface HeaderMonitorControlsProps { path: string; - monitor: Monitor; + monitor?: Monitor; isAdmin: boolean; } @@ -23,9 +23,13 @@ export const HeaderMonitorControls = ({ const { t } = useTranslation(); const navigate = useNavigate(); const theme = useTheme(); + + if (!monitor) { + return null; + } return ( ; + +export const GradientBox = ({ children, palette, sx }: GradientBox) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const isLight = theme.palette.mode === "light"; + const paper = theme.palette.background.paper; + const paperStart = lighten(paper, isLight ? 0.06 : 0.08); + const paperEnd = paper; + const bg = palette + ? `linear-gradient(135deg, ${theme.palette[palette].light} 0%, ${theme.palette[palette].main} 100%)` + : `linear-gradient(135deg, ${paperStart} 0%, ${paperEnd} 100%)`; + + return ( + + {children} + + ); +}; + +type StatBoxProps = React.PropsWithChildren<{ + title: string; + subtitle: string; + palette?: PaletteKey; + sx?: SxProps; + tooltip?: string; + onClick?: () => void; +}>; + +export const StatBox = ({ + title, + subtitle, + palette, + children, + sx, + tooltip, + onClick, +}: StatBoxProps) => { + const theme = useTheme(); + const textColor = palette ? theme.palette[palette].contrastText : "inherit"; + + return ( + + + + {title} + {tooltip && ( + + )} + + {subtitle} + {children} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/Tooltip.tsx b/client/src/Components/v2/design-elements/Tooltip.tsx new file mode 100644 index 000000000..f83b1331c --- /dev/null +++ b/client/src/Components/v2/design-elements/Tooltip.tsx @@ -0,0 +1,79 @@ +import MuiTooltip from "@mui/material/Tooltip"; +import { useTheme } from "@mui/material/styles"; +import { Info } from "lucide-react"; + +import type { TooltipProps } from "@mui/material/Tooltip"; + +type StyledTooltipProps = TooltipProps; + +export const Tooltip = ({ placement = "top", ...props }: StyledTooltipProps) => { + const theme = useTheme(); + + return ( + + {props.children} + + ); +}; + +export interface TooltipWithInfoProps extends Omit { + iconSize?: number; + iconColor?: string; +} + +export const TooltipWithInfo = ({ + iconSize = 14, + iconColor, + ...props +}: TooltipWithInfoProps) => { + const theme = useTheme(); + + const defaultColor = theme.palette.text.secondary; + + return ( + + + { + e.currentTarget.style.opacity = "1"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.opacity = "0.7"; + }} + /> + + + ); +}; diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx index e71cfb2ec..967e61160 100644 --- a/client/src/Components/v2/design-elements/index.tsx +++ b/client/src/Components/v2/design-elements/index.tsx @@ -12,3 +12,5 @@ export * from "./PulseDot"; export * from "./Dot"; export * from "./MonitorStatus"; export { default as Icon } from "./Icon"; +export * from "./Tooltip"; +export * from "./StatBox"; diff --git a/client/src/Components/v2/monitors/MonitorStatBoxes.tsx b/client/src/Components/v2/monitors/MonitorStatBoxes.tsx new file mode 100644 index 000000000..1e19d44ea --- /dev/null +++ b/client/src/Components/v2/monitors/MonitorStatBoxes.tsx @@ -0,0 +1,69 @@ +import Stack from "@mui/material/Stack"; +import { StatBox } from "@/Components/v2/design-elements"; + +import { useTheme } from "@mui/material/styles"; +import type { MonitorStats, Monitor } from "@/Types/Monitor"; +import { getHumanReadableDuration } from "@/Utils/timeUtils.js"; +import { getStatusPalette } from "@/Utils/MonitorUtils"; +import { useTranslation } from "react-i18next"; + +interface MonitorStatBoxesProps { + monitor?: Monitor; + monitorStats: MonitorStats | null; + certificateExpiry?: string; +} + +export const MonitorStatBoxes = ({ + monitor, + monitorStats, + certificateExpiry, +}: MonitorStatBoxesProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + if (!monitorStats || !monitor) { + return null; + } + + const timeOfLastFailure = monitorStats?.timeOfLastFailure || 0; + const timeSinceLastFailure = + timeOfLastFailure > 0 + ? Date.now() - timeOfLastFailure + : Date.now() - new Date(monitorStats?.createdAt); + + // Determine time since last check + const timeOfLastCheck = monitorStats?.lastCheckTimestamp || 0; + const timeSinceLastCheck = Date.now() - timeOfLastCheck; + + const streakTime = getHumanReadableDuration(timeSinceLastFailure); + + const lastCheckTime = getHumanReadableDuration(timeSinceLastCheck); + const palette = getStatusPalette(monitor?.status); + + return ( + + + + + + {monitor?.type === "http" && ( + + )} + + ); +}; diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx index a7cfa34fd..60bfcc78d 100644 --- a/client/src/Components/v2/monitors/index.tsx +++ b/client/src/Components/v2/monitors/index.tsx @@ -1 +1,2 @@ export * from "./ControlsFilter"; +export * from "./MonitorStatBoxes"; diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index a816bd4d9..cdb69b5cd 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -9,10 +9,15 @@ import { useGet } from "@/Hooks/UseApi"; import type { MonitorDetailsResponse } from "@/Types/Monitor"; import type { ChecksResponse } from "@/Types/Check"; import type { RootState } from "@/Types/state"; -import useCertificateFetch from "./Hooks/useCertificateFetch"; +import { MonitorStatBoxes } from "@/Components/v2/monitors"; +import { formatDateWithTz } from "@/Utils/timeUtils"; const certificateDateFormat = "MMM D, YYYY h A"; +interface CertificateResponse { + certificateDate: string; +} + const UptimeDetailsPage = () => { const isAdmin = useIsAdmin(); const { monitorId } = useParams<{ monitorId: string }>(); @@ -43,12 +48,22 @@ const UptimeDetailsPage = () => { const monitor = monitorData?.monitor; const monitorStats = monitorDetailsData?.monitorStats ?? null; - const [certificateExpiry] = useCertificateFetch({ - monitor, - monitorId, - certificateDateFormat, - uiTimezone, - }); + // Certificate fetch - only for HTTP monitors + const certificateUrl = useMemo(() => { + if (!monitorId || monitor?.type !== "http") { + return null; + } + return `/monitors/certificate/${monitorId}`; + }, [monitorId, monitor?.type]); + + const { data: certificateData } = useGet(certificateUrl); + + const certificateExpiry = useMemo(() => { + if (!certificateData?.certificateDate) { + return undefined; + } + return formatDateWithTz(certificateData.certificateDate, certificateDateFormat, uiTimezone) ?? "N/A"; + }, [certificateData, uiTimezone]); const checksUrl = useMemo(() => { if (!monitorId || !monitor?.type) { @@ -79,7 +94,6 @@ const UptimeDetailsPage = () => { monitorError, refetchMonitor, monitorStats, - certificateExpiry, checksIsLoading, checksError, checks, @@ -89,10 +103,6 @@ const UptimeDetailsPage = () => { setDateRange, }); - if (!monitor) { - return null; - } - return ( { monitor={monitor} isAdmin={isAdmin} /> + ); }; diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 414dd0164..e6098121a 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -56,11 +56,18 @@ export interface MonitorGroupedCheck { } export interface MonitorStats { + id: string; + monitorId: string; + avgResponseTime: number; totalChecks: number; - upChecks: number; - downChecks: number; + totalUpChecks: number; + totalDownChecks: number; uptimePercentage: number; - averageResponseTime: number; + lastCheckTimestamp: number; + lastResponseTime: number; + timeOfLastFailure?: number; + createdAt: string; + updatedAt: string; } export interface MonitorData { diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 11b003442..318398579 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -169,6 +169,12 @@ "pause": "Pause", "resume": "Resume", "delete": "Delete" + }, + "statBoxes": { + "activeFor": "Active for", + "lastCheck": "Last check", + "lastResponseTime": "Last response time", + "certificateExpiry": "Certificate expiry" } } }, From 71bd1fa28f6ef1690c5ebfb023e374c547099bc4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 25 Jan 2026 15:56:15 +0000 Subject: [PATCH 005/133] time range header --- .../v2/common/controls/HeaderTimeRange.tsx | 70 +++++++++++++++++++ client/src/Components/v2/common/index.ts | 1 + client/src/Pages/Uptime/Details/index.tsx | 32 ++++----- 3 files changed, 85 insertions(+), 18 deletions(-) create mode 100644 client/src/Components/v2/common/controls/HeaderTimeRange.tsx diff --git a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx new file mode 100644 index 000000000..3ca27ffbc --- /dev/null +++ b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx @@ -0,0 +1,70 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; +import ToggleButton from "@mui/material/ToggleButton"; +import { useTheme } from "@mui/material/styles"; + +interface MonitorTimeFrameHeaderProps { + isLoading?: boolean; + hasDateRange?: boolean; + dateRange: string; + setDateRange: (dateRange: string) => void; +} + +export const HeaderTimeRange = ({ + isLoading = false, + hasDateRange = true, + dateRange, + setDateRange, +}: MonitorTimeFrameHeaderProps) => { + const theme = useTheme(); + + const handleChange = ( + _event: React.MouseEvent, + newValue: string | null + ) => { + if (newValue !== null) { + setDateRange(newValue); + } + }; + + let timeFramePicker = null; + + if (hasDateRange) { + timeFramePicker = ( + + Recent + Day + Week + Month + + ); + } + + return ( + + + Showing statistics for past{" "} + {dateRange === "recent" + ? "2 hours" + : dateRange === "day" + ? "24 hours" + : dateRange === "week" + ? "7 days" + : "30 days"} + . + + {timeFramePicker} + + ); +}; diff --git a/client/src/Components/v2/common/index.ts b/client/src/Components/v2/common/index.ts index 16baac7fe..d0d1262b7 100644 --- a/client/src/Components/v2/common/index.ts +++ b/client/src/Components/v2/common/index.ts @@ -3,3 +3,4 @@ export * from "../common/charts/HeatmapResponseTime"; export * from "../common/charts/HeatmapResponseTimeTooltip"; export * from "../common/controls/HeaderCreate"; export * from "../common/controls/HeaderMonitorControls"; +export * from "../common/controls/HeaderTimeRange"; diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index cdb69b5cd..7e88fde2b 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -1,5 +1,5 @@ import { BasePage } from "@/Components/v2/design-elements"; -import { HeaderMonitorControls } from "@/Components/v2/common"; +import { HeaderMonitorControls, HeaderTimeRange } from "@/Components/v2/common"; import { useIsAdmin } from "@/Hooks/useIsAdmin"; import { useState, useMemo } from "react"; @@ -62,7 +62,13 @@ const UptimeDetailsPage = () => { if (!certificateData?.certificateDate) { return undefined; } - return formatDateWithTz(certificateData.certificateDate, certificateDateFormat, uiTimezone) ?? "N/A"; + return ( + formatDateWithTz( + certificateData.certificateDate, + certificateDateFormat, + uiTimezone + ) ?? "N/A" + ); }, [certificateData, uiTimezone]); const checksUrl = useMemo(() => { @@ -87,22 +93,6 @@ const UptimeDetailsPage = () => { const checks = checksData?.checks ?? []; const checksCount = checksData?.checksCount ?? 0; - // TODO: Add UI components here - // Placeholder to consume all data fetching results - console.log({ - monitorIsLoading, - monitorError, - refetchMonitor, - monitorStats, - checksIsLoading, - checksError, - checks, - checksCount, - setPage, - setRowsPerPage, - setDateRange, - }); - return ( { monitorStats={monitorStats} certificateExpiry={certificateExpiry} /> + ); }; From 8af8efbe3bf1f6f2f293d015fee6227e5518f410 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 25 Jan 2026 16:09:48 +0000 Subject: [PATCH 006/133] toggle-button-group --- .../v2/common/controls/HeaderTimeRange.tsx | 3 +- .../src/Components/v2/inputs/ToggleButton.tsx | 52 +++++++++++++++++++ client/src/Components/v2/inputs/index.tsx | 1 + 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 client/src/Components/v2/inputs/ToggleButton.tsx diff --git a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx index 3ca27ffbc..772683b43 100644 --- a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx +++ b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx @@ -1,7 +1,6 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; -import ToggleButton from "@mui/material/ToggleButton"; +import { ToggleButtonGroup, ToggleButton } from "@/Components/v2/inputs"; import { useTheme } from "@mui/material/styles"; interface MonitorTimeFrameHeaderProps { diff --git a/client/src/Components/v2/inputs/ToggleButton.tsx b/client/src/Components/v2/inputs/ToggleButton.tsx new file mode 100644 index 000000000..40b4309b8 --- /dev/null +++ b/client/src/Components/v2/inputs/ToggleButton.tsx @@ -0,0 +1,52 @@ +import ToggleButton from "@mui/material/ToggleButton"; +import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; +import type { ToggleButtonProps } from "@mui/material/ToggleButton"; +import type { ToggleButtonGroupProps } from "@mui/material/ToggleButtonGroup"; +import { useTheme } from "@mui/material/styles"; + +export const ToggleButtonInput = ({ sx, ...props }: ToggleButtonProps) => { + return ( + + ); +}; + +export const ToggleButtonGroupInput = ({ sx, ...props }: ToggleButtonGroupProps) => { + const theme = useTheme(); + return ( + + ); +}; diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx index 9a76f6f7f..07e919932 100644 --- a/client/src/Components/v2/inputs/index.tsx +++ b/client/src/Components/v2/inputs/index.tsx @@ -4,3 +4,4 @@ export { SelectInput as Select } from "./Select"; export { CheckboxInput as Checkbox } from "./Checkbox"; export { AutoCompleteInput as Autocomplete } from "./AutoComplete"; export { TextInput as TextField } from "./TextInput"; +export { ToggleButtonInput as ToggleButton, ToggleButtonGroupInput as ToggleButtonGroup } from "./ToggleButton"; From 79dfa886f1deb51864653e6bcc658314ecc2a4b6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 25 Jan 2026 16:31:54 +0000 Subject: [PATCH 007/133] upchart --- .../v2/design-elements/BaseChart.tsx | 72 +++++++++ .../Components/v2/design-elements/index.tsx | 1 + .../v2/monitors/charts/HistogramStatus.tsx | 142 ++++++++++++++++++ client/src/Components/v2/monitors/index.tsx | 1 + client/src/Pages/Uptime/Details/index.tsx | 14 ++ 5 files changed, 230 insertions(+) create mode 100644 client/src/Components/v2/design-elements/BaseChart.tsx create mode 100644 client/src/Components/v2/monitors/charts/HistogramStatus.tsx diff --git a/client/src/Components/v2/design-elements/BaseChart.tsx b/client/src/Components/v2/design-elements/BaseChart.tsx new file mode 100644 index 000000000..fd9251915 --- /dev/null +++ b/client/src/Components/v2/design-elements/BaseChart.tsx @@ -0,0 +1,72 @@ +import { BaseBox } from "../design-elements"; +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import type { ResponsiveStyleValue } from "@mui/system"; + +import { useTheme } from "@mui/material/styles"; + +type BaseChartProps = React.PropsWithChildren<{ + icon: React.ReactNode; + title: string; + width?: number | string; + maxWidth?: number | string; + padding?: number | string | ResponsiveStyleValue; +}>; + +export const BaseChart = ({ + children, + icon, + title, + width = "100%", + maxWidth = "100%", + padding, +}: BaseChartProps) => { + const theme = useTheme(); + return ( + + + + {icon && ( + + {icon} + + )} + {title} + + {children} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx index 967e61160..6ebffc624 100644 --- a/client/src/Components/v2/design-elements/index.tsx +++ b/client/src/Components/v2/design-elements/index.tsx @@ -14,3 +14,4 @@ export * from "./MonitorStatus"; export { default as Icon } from "./Icon"; export * from "./Tooltip"; export * from "./StatBox"; +export * from "./BaseChart"; diff --git a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx new file mode 100644 index 000000000..d6b378c25 --- /dev/null +++ b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx @@ -0,0 +1,142 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { BaseChart } from "@/Components/v2/design-elements"; +import { TrendingUp, AlertTriangle } from "lucide-react"; +import { useTheme } from "@mui/material/styles"; +import { useSelector } from "react-redux"; +import { formatDateWithTz } from "@/Utils/timeUtils"; +import { useTranslation } from "react-i18next"; +import { ResponsiveContainer, BarChart, XAxis, Bar, Cell, Tooltip } from "recharts"; +import { getResponseTimeColor } from "@/Utils/MonitorUtils"; + +const XLabel = ({ p1, p2, range }: { p1: any; p2: any; range: string }) => { + const theme = useTheme(); + const uiTimezone = useSelector((state: any) => state.ui.timezone); + const dateFormat = range === "day" ? "MMM D, h:mm A" : "MMM D"; + return ( + <> + + {formatDateWithTz(p1.bucketDate, dateFormat, uiTimezone)} + + + {formatDateWithTz(p2.bucketDate, dateFormat, uiTimezone)} + + + ); +}; + +export const HistogramStatus = ({ monitorData, checks = [], range }) => { + console.log(checks); + const { t } = useTranslation(); + const theme = useTheme(); + const CustomTooltip = ({ active, payload }: any) => { + const uiTimezone = useSelector((state: any) => state.ui.timezone); + if (!active || !payload?.length) return null; + const d = payload[0]?.payload as any & { + avgResponseTime?: number; + }; + const avg = d?.avgResponseTime ?? 0; + const titleText = t("common.charts.uptime.avgResponseTime"); + const fmt = range === "30d" ? "MMM D, YYYY" : "ddd, MMM D, YYYY, h:mm A"; + let dateLabel = ""; + if (d?.bucketDate) { + const base = new Date(d.bucketDate); + const midBucket = + range === "30d" ? new Date(base.getTime() + 12 * 60 * 60 * 1000) : base; + dateLabel = formatDateWithTz(midBucket.toISOString(), fmt, uiTimezone); + } + return ( + + {dateLabel ? ( + + {dateLabel} + + ) : null} + {titleText} + {Math.floor(avg)} ms + + ); + }; + + return ( + } + title={"Test"} + > + + + + Checks: {"totalChecks"} + + + + + + } + /> + } + /> + + {checks?.map((groupedCheck) => { + const fillColor = getResponseTimeColor(groupedCheck.avgResponseTime); + return ( + + ); + })} + + + + + + ); +}; diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx index 60bfcc78d..214e10674 100644 --- a/client/src/Components/v2/monitors/index.tsx +++ b/client/src/Components/v2/monitors/index.tsx @@ -1,2 +1,3 @@ export * from "./ControlsFilter"; export * from "./MonitorStatBoxes"; +export * from "./charts/HistogramStatus"; diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index 7e88fde2b..55d53f29e 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -1,6 +1,9 @@ import { BasePage } from "@/Components/v2/design-elements"; import { HeaderMonitorControls, HeaderTimeRange } from "@/Components/v2/common"; +import Stack from "@mui/material/Stack"; +import { HistogramStatus } from "@/Components/v2/monitors"; +import { useTheme } from "@mui/material/styles"; import { useIsAdmin } from "@/Hooks/useIsAdmin"; import { useState, useMemo } from "react"; import { useParams } from "react-router-dom"; @@ -19,6 +22,7 @@ interface CertificateResponse { } const UptimeDetailsPage = () => { + const theme = useTheme(); const isAdmin = useIsAdmin(); const { monitorId } = useParams<{ monitorId: string }>(); const uiTimezone = useSelector((state: RootState) => state.ui.timezone); @@ -111,6 +115,16 @@ const UptimeDetailsPage = () => { dateRange={dateRange} setDateRange={setDateRange} /> + + + ); }; From 3208cc9e2c919a355d60b5ab06a5217d19df66d8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 25 Jan 2026 16:48:56 +0000 Subject: [PATCH 008/133] strings and icons --- .../v2/monitors/charts/HistogramStatus.tsx | 63 +++++++++++++------ client/src/Pages/Uptime/Details/index.tsx | 11 ++++ client/src/Types/Monitor.ts | 16 +++-- client/src/locales/en.json | 7 +++ 4 files changed, 70 insertions(+), 27 deletions(-) diff --git a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx index d6b378c25..ed78a1726 100644 --- a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx +++ b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx @@ -1,17 +1,25 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import { BaseChart } from "@/Components/v2/design-elements"; -import { TrendingUp, AlertTriangle } from "lucide-react"; import { useTheme } from "@mui/material/styles"; import { useSelector } from "react-redux"; import { formatDateWithTz } from "@/Utils/timeUtils"; import { useTranslation } from "react-i18next"; import { ResponsiveContainer, BarChart, XAxis, Bar, Cell, Tooltip } from "recharts"; import { getResponseTimeColor } from "@/Utils/MonitorUtils"; +import type { GroupedCheck } from "@/Types/Check"; +import type { MonitorData } from "@/Types/Monitor"; +import type { RootState } from "@/Types/state"; -const XLabel = ({ p1, p2, range }: { p1: any; p2: any; range: string }) => { +interface XLabelProps { + p1: GroupedCheck; + p2: GroupedCheck; + range: string; +} + +const XLabel = ({ p1, p2, range }: XLabelProps) => { const theme = useTheme(); - const uiTimezone = useSelector((state: any) => state.ui.timezone); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); const dateFormat = range === "day" ? "MMM D, h:mm A" : "MMM D"; return ( <> @@ -23,7 +31,7 @@ const XLabel = ({ p1, p2, range }: { p1: any; p2: any; range: string }) => { fontSize={11} fill={theme.palette.text.secondary} > - {formatDateWithTz(p1.bucketDate, dateFormat, uiTimezone)} + {formatDateWithTz(p1._id, dateFormat, uiTimezone)} { fontSize={11} fill={theme.palette.text.secondary} > - {formatDateWithTz(p2.bucketDate, dateFormat, uiTimezone)} + {formatDateWithTz(p2._id, dateFormat, uiTimezone)} ); }; -export const HistogramStatus = ({ monitorData, checks = [], range }) => { - console.log(checks); +interface HistogramStatusProps { + title: string; + icon: React.ReactNode; + monitorData?: MonitorData; + checks?: GroupedCheck[]; + range: string; +} + +export const HistogramStatus = ({ + title, + icon, + monitorData, + checks = [], + range, +}: HistogramStatusProps) => { const { t } = useTranslation(); const theme = useTheme(); - const CustomTooltip = ({ active, payload }: any) => { - const uiTimezone = useSelector((state: any) => state.ui.timezone); + const CustomTooltip = ({ + active, + payload, + }: { + active?: boolean; + payload?: Array<{ payload: GroupedCheck }>; + }) => { + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); if (!active || !payload?.length) return null; - const d = payload[0]?.payload as any & { - avgResponseTime?: number; - }; + const d = payload[0]?.payload; const avg = d?.avgResponseTime ?? 0; - const titleText = t("common.charts.uptime.avgResponseTime"); + const titleText = t("common.charts.labels.averageRepsonseTime"); const fmt = range === "30d" ? "MMM D, YYYY" : "ddd, MMM D, YYYY, h:mm A"; let dateLabel = ""; - if (d?.bucketDate) { - const base = new Date(d.bucketDate); + if (d?._id) { + const base = new Date(d._id); const midBucket = range === "30d" ? new Date(base.getTime() + 12 * 60 * 60 * 1000) : base; dateLabel = formatDateWithTz(midBucket.toISOString(), fmt, uiTimezone); @@ -83,10 +108,12 @@ export const HistogramStatus = ({ monitorData, checks = [], range }) => { ); }; + const totalChecks = checks.reduce((acc, check) => acc + (check.totalChecks || 0), 0); + return ( } - title={"Test"} + icon={icon} + title={title} > { justifyContent="space-between" > - Checks: {"totalChecks"} + Total checks: {totalChecks} { gap={theme.spacing(8)} > } monitorData={monitorData} checks={monitorData?.groupedUpChecks} range={dateRange} /> + } + monitorData={monitorData} + checks={monitorData?.groupedDownChecks} + range={dateRange} + /> ); diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index e6098121a..cd5245386 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -1,4 +1,4 @@ -import type { Check } from "@/Types/Check"; +import type { Check, GroupedCheck } from "@/Types/Check"; export type MonitorStatus = boolean | undefined; export const MonitorTypes = [ @@ -48,12 +48,6 @@ export interface MonitorsWithChecksResponse { summary: MonitorsSummary; } -export interface MonitorGroupedCheck { - time: number; - responseTime: number; - originalResponseTime: number; - totalChecks: number; -} export interface MonitorStats { id: string; @@ -72,10 +66,14 @@ export interface MonitorStats { export interface MonitorData { monitor: Monitor; - groupedChecks: MonitorGroupedCheck[]; + groupedChecks: GroupedCheck[]; + groupedUpChecks: GroupedCheck[]; + groupedDownChecks: GroupedCheck[]; + groupedAvgResponseTime: number; + groupedUptimePercentage: number; } export interface MonitorDetailsResponse { monitorData: MonitorData; - monitorStats: MonitorStats; + monitorStats: MonitorStats | null; } diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 318398579..db3a54485 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -24,6 +24,13 @@ "type": "Type" }, "empty": "Nothing h" + }, + "charts": { + "labels": { + "uptime": "Uptime", + "downtime": "Downtime", + "averageRepsonseTime": "Average response time" + } } }, "submit": "Submit", From e354fa7ebfff7e1d724ce1578665fb3b33848adf Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 02:06:37 +0000 Subject: [PATCH 009/133] response time chart --- .../Components/v1/Charts/BarChart/index.jsx | 2 +- .../Charts/DePINStatusPageBarChart/index.jsx | 2 +- .../Charts/MonitorDetailsAreaChart/index.jsx | 2 +- .../v1/Charts/StatusPageBarChart/index.jsx | 2 +- .../Components/v1/Charts/Utils/chartUtils.jsx | 2 +- .../v1/MonitorDetailsControlHeader/status.jsx | 2 +- .../v2/monitors/MonitorStatBoxes.tsx | 2 +- .../v2/monitors/charts/HistogramDetails.tsx | 164 ++++++++++++++++++ .../v2/monitors/charts/HistogramStatus.tsx | 2 +- .../v2/monitors/charts/RadialAvgResponse.tsx | 91 ++++++++++ client/src/Components/v2/monitors/index.tsx | 2 + .../Checks/Components/IncidentTable/index.jsx | 2 +- .../Components/IncidentDetailsModal/index.jsx | 2 +- .../Components/IncidentTable/index.jsx | 2 +- .../LatestIncidentsPanel/IncidentItem.jsx | 2 +- .../hooks/useGetIncidentsDuration.js | 2 +- .../Diagnostics/components/stats/index.jsx | 2 +- client/src/Pages/Logs/Diagnostics/index.jsx | 2 +- .../hooks/useMaintenanceActions.jsx | 2 +- .../hooks/useMaintenanceData.jsx | 2 +- .../Maintenance/MaintenanceTable/index.jsx | 2 +- .../Details/Components/Charts/AreaChart.jsx | 2 +- .../Components/PageSpeedStatusBoxes/index.jsx | 2 +- .../Monitors/Components/Card/index.jsx | 2 +- .../Details/Components/ChartBoxes/index.jsx | 2 +- .../Components/Charts/CustomLabels.jsx | 2 +- .../Components/ResponseTable/index.jsx | 2 +- .../Components/UptimeStatusBoxes/index.jsx | 2 +- .../Details/Hooks/useCertificateFetch.jsx | 2 +- client/src/Pages/Uptime/Details/index.tsx | 16 +- client/src/Utils/TimeUtils.ts | 16 +- .../{timeUtils.js => timeUtilsLegacy.js} | 30 ++++ client/src/locales/en.json | 4 +- 33 files changed, 338 insertions(+), 37 deletions(-) create mode 100644 client/src/Components/v2/monitors/charts/HistogramDetails.tsx create mode 100644 client/src/Components/v2/monitors/charts/RadialAvgResponse.tsx rename client/src/Utils/{timeUtils.js => timeUtilsLegacy.js} (87%) diff --git a/client/src/Components/v1/Charts/BarChart/index.jsx b/client/src/Components/v1/Charts/BarChart/index.jsx index 9f0ed8aa6..77b836cae 100644 --- a/client/src/Components/v1/Charts/BarChart/index.jsx +++ b/client/src/Components/v1/Charts/BarChart/index.jsx @@ -1,6 +1,6 @@ import { useTheme } from "@emotion/react"; import { Box, Stack, Tooltip, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { useEffect, useState } from "react"; import "./index.css"; import { useSelector } from "react-redux"; diff --git a/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx b/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx index b37990e46..8b6c2b165 100644 --- a/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx +++ b/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx @@ -5,7 +5,7 @@ import { Stack, Box, Tooltip, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useState, useEffect } from "react"; import PropTypes from "prop-types"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { useSelector } from "react-redux"; const PlaceholderCheck = ({ daysToShow }) => { diff --git a/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx b/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx index a9ef04a53..dafca7b40 100644 --- a/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx +++ b/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx @@ -12,7 +12,7 @@ import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useMemo, useState } from "react"; import { useSelector } from "react-redux"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { tooltipDateFormatLookup, tickDateFormatLookup, diff --git a/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx b/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx index 309201b84..2dc5e61f0 100644 --- a/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx +++ b/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx @@ -1,6 +1,6 @@ import { useTheme } from "@emotion/react"; import { Box, Stack, Tooltip, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { useEffect, useState, forwardRef } from "react"; import { useSelector } from "react-redux"; import PropTypes from "prop-types"; diff --git a/client/src/Components/v1/Charts/Utils/chartUtils.jsx b/client/src/Components/v1/Charts/Utils/chartUtils.jsx index 1bab80595..7797ba462 100644 --- a/client/src/Components/v1/Charts/Utils/chartUtils.jsx +++ b/client/src/Components/v1/Charts/Utils/chartUtils.jsx @@ -2,7 +2,7 @@ import PropTypes from "prop-types"; import { useSelector } from "react-redux"; import { useTheme } from "@mui/material"; import { Text } from "recharts"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { Box, Stack, Typography } from "@mui/material"; import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions.js"; /** diff --git a/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx index febf449d4..c6a3cfcfa 100644 --- a/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx +++ b/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx @@ -4,7 +4,7 @@ import PulseDot from "../Animated/PulseDot.jsx"; import Typography from "@mui/material/Typography"; import Dot from "../Dot/index.jsx"; // Utils -import { formatDurationRounded } from "../../../Utils/timeUtils.js"; +import { formatDurationRounded } from "../../../Utils/timeUtilsLegacy.js"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js"; diff --git a/client/src/Components/v2/monitors/MonitorStatBoxes.tsx b/client/src/Components/v2/monitors/MonitorStatBoxes.tsx index 1e19d44ea..4b3a49fe9 100644 --- a/client/src/Components/v2/monitors/MonitorStatBoxes.tsx +++ b/client/src/Components/v2/monitors/MonitorStatBoxes.tsx @@ -3,7 +3,7 @@ import { StatBox } from "@/Components/v2/design-elements"; import { useTheme } from "@mui/material/styles"; import type { MonitorStats, Monitor } from "@/Types/Monitor"; -import { getHumanReadableDuration } from "@/Utils/timeUtils.js"; +import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js"; import { getStatusPalette } from "@/Utils/MonitorUtils"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Components/v2/monitors/charts/HistogramDetails.tsx b/client/src/Components/v2/monitors/charts/HistogramDetails.tsx new file mode 100644 index 000000000..f3ca2acda --- /dev/null +++ b/client/src/Components/v2/monitors/charts/HistogramDetails.tsx @@ -0,0 +1,164 @@ +import { BaseChart, BaseBox } from "@/Components/v2/design-elements"; +import { Clock } from "lucide-react"; +import { + AreaChart, + Area, + XAxis, + Tooltip, + CartesianGrid, + ResponsiveContainer, + Text, +} from "recharts"; +import Typography from "@mui/material/Typography"; + +import { useSelector } from "react-redux"; +import { + formatDateWithTz, + tickDateFormatLookup, + tooltipDateFormatLookup, +} from "@/Utils/TimeUtils"; +import { useTheme } from "@mui/material/styles"; +import type { GroupedCheck } from "@/Types/Check"; +import type { RootState } from "@/Types/state"; + +type XTickProps = { + x: number; + y: number; + payload: { value: any }; + range: string; +}; + +export const XTick = ({ x, y, payload, range }: XTickProps) => { + const format = tickDateFormatLookup(range); + const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + return ( + + {formatDateWithTz(payload?.value, format, uiTimezone)} + + ); +}; + +type ResponseTimeToolTipProps = { + active?: boolean | undefined; + payload?: any[]; + label?: string | number; + range: string; + theme: any; + uiTimezone: string; +}; + +const ResponseTimeToolTip = ({ + active, + payload, + label, + range, + theme, + uiTimezone, +}: ResponseTimeToolTipProps) => { + if (!label) return null; + if (!payload) return null; + if (!active) return null; + + const format = tooltipDateFormatLookup(range); + const responseTime = Math.floor(payload?.[0]?.payload?.avgResponseTime || 0); + return ( + + {formatDateWithTz(String(label), format, uiTimezone)} + Response time: {responseTime} ms + + ); +}; + +export const HistogramDetails = ({ + checks, + range, +}: { + checks: GroupedCheck[]; + range: string; +}) => { + console.log(range); + const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + return ( + + } + title="Response times" + > + + + + + + + + + + ( + + )} + /> + + ( + + )} + /> + + + + + ); +}; diff --git a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx index ed78a1726..2805013eb 100644 --- a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx +++ b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx @@ -3,7 +3,7 @@ import Typography from "@mui/material/Typography"; import { BaseChart } from "@/Components/v2/design-elements"; import { useTheme } from "@mui/material/styles"; import { useSelector } from "react-redux"; -import { formatDateWithTz } from "@/Utils/timeUtils"; +import { formatDateWithTz } from "@/Utils/timeUtilsLegacy"; import { useTranslation } from "react-i18next"; import { ResponsiveContainer, BarChart, XAxis, Bar, Cell, Tooltip } from "recharts"; import { getResponseTimeColor } from "@/Utils/MonitorUtils"; diff --git a/client/src/Components/v2/monitors/charts/RadialAvgResponse.tsx b/client/src/Components/v2/monitors/charts/RadialAvgResponse.tsx new file mode 100644 index 000000000..b54bfc764 --- /dev/null +++ b/client/src/Components/v2/monitors/charts/RadialAvgResponse.tsx @@ -0,0 +1,91 @@ +import { BaseChart } from "@/Components/v2/design-elements"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Gauge } from "lucide-react"; +import { Cell, RadialBarChart, RadialBar, ResponsiveContainer } from "recharts"; + +import { useTranslation } from "react-i18next"; +import { getResponseTimeColor } from "@/Utils/MonitorUtils"; +import { useTheme } from "@mui/material/styles"; + +export const RadialAvgResponse = ({ avg, max }: { avg: number; max: number }) => { + const { t } = useTranslation(); + const theme = useTheme(); + const chartData = [ + { name: "max", value: max - avg, color: "transparent" }, + { name: "avg", value: avg, color: "red" }, + ]; + + const palette = getResponseTimeColor(avg); + const msg: Record = { + success: "Excellent", + warning: "Average", + danger: "Poor", + }; + + return ( + + } + title={t("common.charts.labels.averageResponseTime")} + > + + + + + + + + + + + {t("common.charts.labels.low")} + {t("common.charts.labels.high")} + + + + {msg[palette]} + + {`${avg?.toFixed()}ms`} + + + + ); +}; diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx index 214e10674..de38d2cab 100644 --- a/client/src/Components/v2/monitors/index.tsx +++ b/client/src/Components/v2/monitors/index.tsx @@ -1,3 +1,5 @@ export * from "./ControlsFilter"; export * from "./MonitorStatBoxes"; export * from "./charts/HistogramStatus"; +export * from "./charts/RadialAvgResponse"; +export * from "./charts/HistogramDetails"; diff --git a/client/src/Pages/Checks/Components/IncidentTable/index.jsx b/client/src/Pages/Checks/Components/IncidentTable/index.jsx index 31475f8b1..857f31bff 100644 --- a/client/src/Pages/Checks/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Checks/Components/IncidentTable/index.jsx @@ -13,7 +13,7 @@ import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; //Utils -import { formatDateWithTz } from "@/Utils/timeUtils.js"; +import { formatDateWithTz } from "@/Utils/timeUtilsLegacy.js"; import { useSelector } from "react-redux"; import { useState } from "react"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx index d68b0b85e..bfca9e93f 100644 --- a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx +++ b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx @@ -9,7 +9,7 @@ import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx"; import { useEffect, useState } from "react"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { formatDateWithTz } from "@/Utils/timeUtils"; +import { formatDateWithTz } from "@/Utils/timeUtilsLegacy.js"; import { useSelector } from "react-redux"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx index 05242c1f3..2a03a8822 100644 --- a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx @@ -9,7 +9,7 @@ import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; import IncidentActionsMenu from "./IncidentActionsMenu.jsx"; //Utils -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { useSelector } from "react-redux"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx index 7c6fb611a..ca5bb640f 100644 --- a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx +++ b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx @@ -2,7 +2,7 @@ import PropTypes from "prop-types"; import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import { getHumanReadableDuration } from "@/Utils/timeUtils.js"; +import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js"; import Icon from "@/Components/v1/Icon"; import { useTranslation } from "react-i18next"; import useGetIncidentsDuration from "../../hooks/useGetIncidentsDuration"; diff --git a/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js b/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js index e9b1da6d1..a4f434a80 100644 --- a/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js +++ b/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js @@ -1,4 +1,4 @@ -import { getHumanReadableDuration } from "@/Utils/timeUtils.js"; +import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js"; const useGetIncidentsDuration = (incident, isActive) => { if (!incident?.startTime) { return "-"; diff --git a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx index b169a6a85..13ed63618 100644 --- a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx +++ b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx @@ -7,7 +7,7 @@ import CircularProgress from "@mui/material/CircularProgress"; import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; -import { getHumanReadableDuration } from "../../../../../Utils/timeUtils.js"; +import { getHumanReadableDuration } from "../../../../../Utils/timeUtilsLegacy.js"; import { formatBytes } from "../../utils/utils.js"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/Logs/Diagnostics/index.jsx index 07b504c77..194e13821 100644 --- a/client/src/Pages/Logs/Diagnostics/index.jsx +++ b/client/src/Pages/Logs/Diagnostics/index.jsx @@ -7,7 +7,7 @@ import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import { useFetchDiagnostics } from "../../../Hooks/logHooks.js"; -import { getHumanReadableDuration } from "../../../Utils/timeUtils.js"; +import { getHumanReadableDuration } from "../../../Utils/timeUtilsLegacy.js"; import { formatBytes, getPercentage } from "./utils/utils.js"; const Diagnostics = () => { diff --git a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx index 22a61084d..a5e853160 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx @@ -6,7 +6,7 @@ import { MS_PER_HOUR, MS_PER_DAY, MS_PER_WEEK, -} from "../../../../Utils/timeUtils.js"; +} from "../../../../Utils/timeUtilsLegacy.js"; const useMaintenanceActions = () => { const MS_LOOKUP = { diff --git a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx index 3414334a7..4ab56bef9 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx @@ -6,7 +6,7 @@ import { MS_PER_HOUR, MS_PER_DAY, MS_PER_WEEK, -} from "../../../../Utils/timeUtils.js"; +} from "../../../../Utils/timeUtilsLegacy.js"; const useMaintenanceData = () => { const REVERSE_REPEAT_LOOKUP = { 0: "none", diff --git a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx index 765858050..11875bb6a 100644 --- a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx +++ b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx @@ -6,7 +6,7 @@ import Icon from "@/Components/v1/Icon"; import ActionsMenu from "./ActionsMenu/index.jsx"; import { memo } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { formatDurationRounded } from "../../../Utils/timeUtils.js"; +import { formatDurationRounded } from "../../../Utils/timeUtilsLegacy.js"; import { StatusLabel } from "@/Components/v1/Label/index.jsx"; import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx index 06b92c933..d8c118237 100644 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx +++ b/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx @@ -11,7 +11,7 @@ import { import { useTheme } from "@emotion/react"; import { useMemo, useState } from "react"; import { Box, Stack, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../../Utils/timeUtilsLegacy.js"; import { useSelector } from "react-redux"; const config = { diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx index 872b1e293..6ff26c739 100644 --- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx +++ b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx @@ -1,7 +1,7 @@ import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; import StatBox from "@/Components/v1/StatBox/index.jsx"; import { Typography } from "@mui/material"; -import { getHumanReadableDuration } from "../../../../../Utils/timeUtils.js"; +import { getHumanReadableDuration } from "../../../../../Utils/timeUtilsLegacy.js"; import { useTranslation } from "react-i18next"; const PageSpeedStatusBoxes = ({ shouldRender, monitorStats }) => { diff --git a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx index 4642e2336..4a85863c4 100644 --- a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx +++ b/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx @@ -6,7 +6,7 @@ import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; import { useSelector } from "react-redux"; -import { formatDateWithTz, formatDurationSplit } from "@/Utils/timeUtils.js"; +import { formatDateWithTz, formatDurationSplit } from "@/Utils/timeUtilsLegacy.js"; import { useMonitorUtils } from "@/Hooks/useMonitorUtils.js"; import { useState } from "react"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx index b8496f5ea..73d241dce 100644 --- a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx +++ b/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx @@ -7,7 +7,7 @@ import DownBarChart from "../Charts/DownBarChart.jsx"; import ResponseGaugeChart from "../Charts/ResponseGaugeChart.jsx"; import SkeletonLayout from "./skeleton.jsx"; // Utils -import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../../Utils/timeUtilsLegacy.js"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { useState } from "react"; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx b/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx index 472c8d058..ffdaa5bbb 100644 --- a/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx +++ b/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx @@ -1,6 +1,6 @@ import PropTypes from "prop-types"; import { useSelector } from "react-redux"; -import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../../Utils/timeUtilsLegacy.js"; const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => { const uiTimezone = useSelector((state) => state.ui.timezone); diff --git a/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx b/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx index 2d7991324..b115f67b0 100644 --- a/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx +++ b/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx @@ -5,7 +5,7 @@ import Table from "@/Components/v1/Table/index.jsx"; import TablePagination from "@/Components/v1/Table/TablePagination/index.jsx"; import { StatusLabel } from "@/Components/v1/Label/index.jsx"; import { useTranslation } from "react-i18next"; -import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../../Utils/timeUtilsLegacy.js"; import SkeletonLayout from "./skeleton.jsx"; import Tooltip from "@mui/material/Tooltip"; import Box from "@mui/material/Box"; diff --git a/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx index 252873cf9..8c05e6c74 100644 --- a/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx +++ b/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx @@ -2,7 +2,7 @@ import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; import StatBox from "@/Components/v1/StatBox/index.jsx"; import PropTypes from "prop-types"; -import { getHumanReadableDuration } from "../../../../../Utils/timeUtils.js"; +import { getHumanReadableDuration } from "../../../../../Utils/timeUtilsLegacy.js"; import { useTheme } from "@mui/material/styles"; import { Typography } from "@mui/material"; import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; diff --git a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx b/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx index e74a99d82..8912438d8 100644 --- a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx +++ b/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx @@ -1,7 +1,7 @@ import { logger } from "../../../../Utils/Logger.js"; import { useEffect, useState } from "react"; import { networkService } from "../../../../main.jsx"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; const useCertificateFetch = ({ monitor, diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index fb1ee586a..79758041c 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -1,7 +1,11 @@ import { BasePage } from "@/Components/v2/design-elements"; import { HeaderMonitorControls, HeaderTimeRange } from "@/Components/v2/common"; import Stack from "@mui/material/Stack"; -import { HistogramStatus } from "@/Components/v2/monitors"; +import { + HistogramStatus, + RadialAvgResponse, + HistogramDetails, +} from "@/Components/v2/monitors"; import { TrendingUp, AlertTriangle } from "lucide-react"; import { useTheme } from "@mui/material/styles"; @@ -14,7 +18,7 @@ import type { MonitorDetailsResponse } from "@/Types/Monitor"; import type { ChecksResponse } from "@/Types/Check"; import type { RootState } from "@/Types/state"; import { MonitorStatBoxes } from "@/Components/v2/monitors"; -import { formatDateWithTz } from "@/Utils/timeUtils"; +import { formatDateWithTz } from "@/Utils/timeUtilsLegacy"; import { t } from "i18next"; const certificateDateFormat = "MMM D, YYYY h A"; @@ -135,7 +139,15 @@ const UptimeDetailsPage = () => { checks={monitorData?.groupedDownChecks} range={dateRange} /> + + ); }; diff --git a/client/src/Utils/TimeUtils.ts b/client/src/Utils/TimeUtils.ts index 3b629baf0..a19a0bf8b 100644 --- a/client/src/Utils/TimeUtils.ts +++ b/client/src/Utils/TimeUtils.ts @@ -25,10 +25,10 @@ export const formatDateWithTz = (timestamp: string, format: string, timezone: st export const tickDateFormatLookup = (range: string) => { const tickFormatLookup: Record = { - "1h": "h:mm A", - "24h": "h:mm A", - "7d": "MM/D, h:mm A", - "30d": "ddd. M/D", + recent: "h:mm A", + day: "h:mm A", + week: "MM/D, h:mm A", + month: "ddd. M/D", }; const format = tickFormatLookup[range]; if (format === undefined) { @@ -39,10 +39,10 @@ export const tickDateFormatLookup = (range: string) => { export const tooltipDateFormatLookup = (range: string) => { const dateFormatLookup: Record = { - "1h": "ddd. MMMM D, YYYY, hh:mm A", - "24h": "ddd. MMMM D, YYYY, hh:mm A", - "7d": "ddd. MMMM D, YYYY, hh:mm A", - "30d": "ddd. MMMM D, YYYY", + recent: "ddd. MMMM D, YYYY, hh:mm A", + day: "ddd. MMMM D, YYYY, hh:mm A", + week: "ddd. MMMM D, YYYY, hh:mm A", + month: "ddd. MMMM D, YYYY", }; const format = dateFormatLookup[range]; if (format === undefined) { diff --git a/client/src/Utils/timeUtils.js b/client/src/Utils/timeUtilsLegacy.js similarity index 87% rename from client/src/Utils/timeUtils.js rename to client/src/Utils/timeUtilsLegacy.js index f6e2dcec0..365572f6b 100644 --- a/client/src/Utils/timeUtils.js +++ b/client/src/Utils/timeUtilsLegacy.js @@ -135,3 +135,33 @@ export const formatDateWithTz = (timestamp, format, timezone) => { const formattedDate = dayjs(timestamp).tz(timezone).format(format); return formattedDate; }; + +export const tickDateFormatLookup = (range) => { + switch (range) { + case "recent": + return "h:mm A"; + case "day": + return "h A"; + case "week": + return "MMM D"; + case "month": + return "MMM D"; + default: + return "MMM D, h A"; + } +}; + +export const tooltipDateFormatLookup = (range) => { + switch (range) { + case "recent": + return "MMM D, h:mm A"; + case "day": + return "MMM D, h:mm A"; + case "week": + return "ddd, MMM D"; + case "month": + return "ddd, MMM D"; + default: + return "MMM D, h:mm A"; + } +}; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index db3a54485..8cba70259 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -29,7 +29,9 @@ "labels": { "uptime": "Uptime", "downtime": "Downtime", - "averageRepsonseTime": "Average response time" + "averageResponseTime": "Average response time", + "low": "Low", + "high": "High" } } }, From a64885e609e2927da79e8d26f7fb60b466bb98ff Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 02:37:59 +0000 Subject: [PATCH 010/133] checks table --- client/src/App.jsx | 9 +- .../common/controls/HeaderMonitorControls.tsx | 25 +++-- .../v2/design-elements/StatusLabel.tsx | 7 +- .../Uptime/Details/Components/ChecksTable.tsx | 100 ++++++++++++++++++ client/src/Pages/Uptime/Details/index.tsx | 12 ++- 5 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 client/src/Pages/Uptime/Details/Components/ChecksTable.tsx diff --git a/client/src/App.jsx b/client/src/App.jsx index af2f6c11c..da83f9061 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -37,7 +37,14 @@ function App() { - + ); } diff --git a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx index 35abb1cd9..fb9161425 100644 --- a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx +++ b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx @@ -6,6 +6,7 @@ import { Settings, Pause, Play, Mail, Bug } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@mui/material/styles"; +import { usePost } from "@/Hooks/UseApi"; import type { Monitor } from "@/Types/Monitor.js"; @@ -13,16 +14,23 @@ interface HeaderMonitorControlsProps { path: string; monitor?: Monitor; isAdmin: boolean; + refetch: Function; } export const HeaderMonitorControls = ({ path, monitor, isAdmin, + refetch, }: HeaderMonitorControlsProps) => { const { t } = useTranslation(); const navigate = useNavigate(); const theme = useTheme(); + const { + post, + loading: isPosting, + // error: postError, + } = usePost(); if (!monitor) { return null; @@ -43,11 +51,10 @@ export const HeaderMonitorControls = ({ - - - - - - - ); -}; - -OptionsHeader.propTypes = { - shouldRender: PropTypes.bool, - selectedMonitor: PropTypes.string, - setSelectedMonitor: PropTypes.func, - monitors: PropTypes.object, - filter: PropTypes.string, - setFilter: PropTypes.func, - dateRange: PropTypes.string, - setDateRange: PropTypes.func, -}; - -export default OptionsHeader; diff --git a/client/src/Pages/Checks/Components/OptionsHeader/skeleton.jsx b/client/src/Pages/Checks/Components/OptionsHeader/skeleton.jsx deleted file mode 100644 index 8a8990dc4..000000000 --- a/client/src/Pages/Checks/Components/OptionsHeader/skeleton.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Stack, Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Checks/Components/StatusBoxes/StatusBox.jsx b/client/src/Pages/Checks/Components/StatusBoxes/StatusBox.jsx deleted file mode 100644 index d7f02ada3..000000000 --- a/client/src/Pages/Checks/Components/StatusBoxes/StatusBox.jsx +++ /dev/null @@ -1,129 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Stack, Typography } from "@mui/material"; -import Background from "@/assets/Images/background-grid.svg?react"; -import Icon from "@/Components/v1/Icon"; - -const StatusBox = ({ title, value, status }) => { - const theme = useTheme(); - let sharedStyles = { - position: "absolute", - right: 8, - "& svg": { - width: 20, - height: 20, - opacity: 0.9, - "& path": { stroke: theme.palette.primary.contrastTextTertiary, strokeWidth: 1.7 }, - }, - }; - - let color; - let icon; - if (status === "up") { - color = theme.palette.success.lowContrast; - icon = ( - - - - ); - } else if (status === "down") { - color = theme.palette.error.lowContrast; - icon = ( - - - - ); - } else if (status === "paused") { - color = theme.palette.warning.lowContrast; - icon = ( - - - - ); - } else { - color = theme.palette.accent.main; - icon = ( - - - - ); - } - - return ( - - - - - - - - {title} - - {icon} - - - {value} - - - # - - - - - ); -}; - -StatusBox.propTypes = { - title: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - status: PropTypes.string, -}; - -export default StatusBox; diff --git a/client/src/Pages/Checks/Components/StatusBoxes/index.jsx b/client/src/Pages/Checks/Components/StatusBoxes/index.jsx deleted file mode 100644 index bb993111a..000000000 --- a/client/src/Pages/Checks/Components/StatusBoxes/index.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from "prop-types"; -import { Stack } from "@mui/material"; -import StatusBox from "./StatusBox.jsx"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import SkeletonLayout from "./skeleton.jsx"; - -const StatusBoxes = ({ isLoading, summary }) => { - const theme = useTheme(); - const { t } = useTranslation(); - if (isLoading) return ; - return ( - - - - - - - ); -}; - -StatusBoxes.propTypes = { - isLoading: PropTypes.bool, - summary: PropTypes.object, -}; - -export default StatusBoxes; diff --git a/client/src/Pages/Checks/Components/StatusBoxes/skeleton.jsx b/client/src/Pages/Checks/Components/StatusBoxes/skeleton.jsx deleted file mode 100644 index b9aa016b7..000000000 --- a/client/src/Pages/Checks/Components/StatusBoxes/skeleton.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -const SkeletonLayout = () => { - const theme = useTheme(); - return ( - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Checks/old.jsx b/client/src/Pages/Checks/old.jsx deleted file mode 100644 index bcd036a58..000000000 --- a/client/src/Pages/Checks/old.jsx +++ /dev/null @@ -1,100 +0,0 @@ -// Components -import { Stack } from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import IncidentTable from "./Components/IncidentTable/index.jsx"; -import OptionsHeader from "./Components/OptionsHeader/index.jsx"; -import StatusBoxes from "./Components/StatusBoxes/index.jsx"; - -//Utils -import { useTheme } from "@emotion/react"; -import { useFetchMonitorsByTeamId } from "@/Hooks/monitorHooks.js"; -import { useFetchChecksSummaryByTeamId } from "@/Hooks/checkHooks.js"; -import { useState, useEffect } from "react"; -import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; -import { useTranslation } from "react-i18next"; -import { useParams } from "react-router-dom"; - -//Constants -const Checks = () => { - // Redux state - const { t } = useTranslation(); - - const BREADCRUMBS = [{ name: t("checksPageTitle"), path: "/checks" }]; - - // Local state - const [selectedMonitor, setSelectedMonitor] = useState("0"); - const [filter, setFilter] = useState(undefined); - const [dateRange, setDateRange] = useState("hour"); - const [monitorLookup, setMonitorLookup] = useState(undefined); - const [updateTrigger, setUpdateTrigger] = useState(false); - - //Hooks - - //Utils - const theme = useTheme(); - const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({}); - const [summary, isLoadingSummary, networkErrorSummary] = useFetchChecksSummaryByTeamId({ - dateRange, - updateTrigger, - }); - const { monitorId } = useParams(); - - useEffect(() => { - if (monitorId) { - setSelectedMonitor(monitorId); - } - }, [monitorId]); - - useEffect(() => { - const monitorLookup = monitors?.reduce((acc, monitor) => { - acc[monitor.id] = { - id: monitor.id, - name: monitor.name, - type: monitor.type, - }; - return acc; - }, {}); - setMonitorLookup(monitorLookup); - }, [monitors]); - - if (networkError || networkErrorSummary) { - return ( - - - - ); - } - - return ( - - - - - - - - ); -}; - -export default Checks; diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 7916e2c06..e4fe01412 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -200,8 +200,28 @@ class MongoChecksRepository implements IChecksRepository { return documents.map((doc) => this.toEntity(doc)); }; + private toDocument = (check: Partial): CheckDocument => { + // Map id to _id for MongoDB storage + const { id, metadata, ...rest } = check; + return { + _id: id ? new mongoose.Types.ObjectId(id) : new mongoose.Types.ObjectId(), + metadata: metadata ? { + monitorId: new mongoose.Types.ObjectId(metadata.monitorId), + teamId: new mongoose.Types.ObjectId(metadata.teamId), + type: metadata.type, + } : { + monitorId: new mongoose.Types.ObjectId(), + teamId: new mongoose.Types.ObjectId(), + type: "http", + }, + ...rest, + } as unknown as CheckDocument; + }; + createChecks = async (checks: Check[]) => { - return await CheckModel.insertMany(checks); + const docs = checks.map((check) => this.toDocument(check)); + const inserted = await CheckModel.insertMany(docs); + return this.mapDocuments(inserted as unknown as CheckDocument[]); }; findByMonitorId = async ( From 396d5b369bd2202f3abbd510732fdf31e1bc0e25 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 28 Jan 2026 20:20:18 +0000 Subject: [PATCH 072/133] remove unused --- .../Details/Components/Charts/AreaChart.jsx | 330 ----------------- .../Components/Charts/AreaChartLegend.jsx | 62 ---- .../Details/Components/Charts/PieChart.jsx | 327 ----------------- .../Components/Charts/PieChartLegend.jsx | 101 ------ .../Components/PageSpeedAreaChart/index.jsx | 60 ---- .../PageSpeedAreaChart/skeleton.jsx | 17 - .../Components/PageSpeedStatusBoxes/index.jsx | 49 --- .../Components/PerformanceReport/index.jsx | 61 ---- .../Components/PerformanceReport/skeleton.jsx | 17 - client/src/Pages/PageSpeed/Details/old.jsx | 120 ------- .../Monitors/Components/Card/index.jsx | 331 ------------------ .../Monitors/Components/MonitorGrid/index.jsx | 22 -- client/src/Pages/PageSpeed/Monitors/old.jsx | 86 ----- .../Details/Hooks/useCertificateFetch.jsx | 44 --- client/src/Pages/Uptime/Details/old.jsx | 190 ---------- .../Monitors/Components/Filter/index.jsx | 162 --------- .../Monitors/Components/Skeleton/index.jsx | 61 ---- .../Monitors/Components/StatusBoxes/index.jsx | 42 --- .../Components/StatusBoxes/skeleton.jsx | 31 -- .../Components/StatusBoxes/statusBox.jsx | 115 ------ .../Components/UptimeDataTable/index.jsx | 241 ------------- client/src/Pages/Uptime/Monitors/old.jsx | 225 ------------ 22 files changed, 2694 deletions(-) delete mode 100644 client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/old.jsx delete mode 100644 client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx delete mode 100644 client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx delete mode 100644 client/src/Pages/PageSpeed/Monitors/old.jsx delete mode 100644 client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx delete mode 100644 client/src/Pages/Uptime/Details/old.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/old.jsx diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx deleted file mode 100644 index d8c118237..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx +++ /dev/null @@ -1,330 +0,0 @@ -import PropTypes from "prop-types"; -import { - AreaChart, - Area, - XAxis, - Tooltip, - CartesianGrid, - ResponsiveContainer, - Text, -} from "recharts"; -import { useTheme } from "@emotion/react"; -import { useMemo, useState } from "react"; -import { Box, Stack, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../../../Utils/timeUtilsLegacy.js"; -import { useSelector } from "react-redux"; - -const config = { - seo: { - id: "seo", - text: "SEO", - color: "secondary", - }, - performance: { - id: "performance", - text: "performance", - color: "success", - }, - bestPractices: { - id: "bestPractices", - text: "best practices", - color: "warning", - }, - accessibility: { - id: "accessibility", - text: "accessibility", - color: "accent", - }, -}; - -/** - * Custom tooltip for the area chart. - * @param {Object} props - * @param {boolean} props.active - Whether the tooltip is active. - * @param {Array} props.payload - The payload data for the tooltip. - * @param {string} props.label - The label for the tooltip. - * @returns {JSX.Element|null} The tooltip element or null if not active. - */ - -const CustomToolTip = ({ active, payload, label, config }) => { - const theme = useTheme(); - const uiTimezone = useSelector((state) => state.ui.timezone); - - if (active && payload && payload.length) { - return ( - - - {formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)} - - {Object.keys(config) - .reverse() - .map((key) => { - const { color } = config[key]; - const dotColor = theme.palette[color].main; - - return ( - - - - {config[key].text} - {" "} - {payload[0].payload[key]} - - ); - })} - - ); - } - return null; -}; - -CustomToolTip.propTypes = { - active: PropTypes.bool, - payload: PropTypes.array, - label: PropTypes.string, - config: PropTypes.object, -}; - -/** - * Processes data to insert gaps with null values based on the interval. - * @param {Array} data - * @param {number} interval - The interval in milliseconds for gaps. - * @returns {Array} The formatted data with gaps. - */ -const processDataWithGaps = (data, interval) => { - if (data.length === 0) return []; - let formattedData = []; - let last = new Date(data[0].createdAt).getTime(); - - // Helper function to add a null entry - const addNullEntry = (timestamp) => { - formattedData.push({ - accessibility: "N/A", - bestPractices: "N/A", - performance: "N/A", - seo: "N/A", - createdAt: timestamp, - }); - }; - - data.forEach((entry) => { - const current = new Date(entry.createdAt).getTime(); - - if (current - last > interval * 2) { - // Insert null entries for each interval - let temp = last + interval; - while (temp < current) { - addNullEntry(new Date(temp).toISOString()); - temp += interval; - } - } - - formattedData.push(entry); - last = current; - }); - - return formattedData; -}; - -/** - * Custom tick component to render ticks on the XAxis. - * - * @param {Object} props - * @param {number} props.x - The x coordinate for the tick. - * @param {number} props.y - The y coordinate for the tick. - * @param {Object} props.payload - The data object containing the tick value. - * @param {number} props.index - The index of the tick in the array of ticks. - * - * @returns {JSX.Element|null} The tick element or null if the tick should be hidden. - */ -const CustomTick = ({ x, y, payload, index }) => { - const theme = useTheme(); - const uiTimezone = useSelector((state) => state.ui.timezone); - - // Render nothing for the first tick - if (index === 0) return null; - - return ( - - {formatDateWithTz(payload?.value, "h:mm a", uiTimezone)} - - ); -}; -CustomTick.propTypes = { - x: PropTypes.number, - y: PropTypes.number, - payload: PropTypes.shape({ - value: PropTypes.string.isRequired, - }), - index: PropTypes.number, -}; - -/** - * A chart displaying pagespeed details over time. - * @param {Object} props - * @param {Array} props.data - The data to display in the chart. - * @param {number} props.interval - The interval in milliseconds for processing gaps. - * @returns {JSX.Element} The area chart component. - */ - -const PageSpeedAreaChart = ({ data, monitor, metrics }) => { - const theme = useTheme(); - const [isHovered, setIsHovered] = useState(false); - const memoizedData = useMemo( - () => processDataWithGaps(data, monitor.interval), - [data[0]] - ); - - const filteredConfig = Object.keys(config).reduce((result, key) => { - if (metrics[key]) { - result[key] = config[key]; - } - return result; - }, {}); - - return ( - - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - - } - axisLine={false} - tickLine={false} - height={18} - minTickGap={0} - interval="equidistantPreserveStart" - /> - } - /> - - {Object.values(filteredConfig).map(({ id, color }) => { - /* TODO not working? */ - const startColor = theme.palette[color].main; - const endColor = theme.palette[color].lowContrast; - - return ( - - - - - ); - })} - - {Object.keys(filteredConfig).map((key) => { - const { color } = filteredConfig[key]; - const strokeColor = theme.palette[color].main; - const bgColor = theme.palette.primary.main; - - return ( - - ); - })} - - - ); -}; - -PageSpeedAreaChart.propTypes = { - data: PropTypes.arrayOf( - PropTypes.shape({ - createdAt: PropTypes.string.isRequired, - accessibility: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - bestPractices: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - performance: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - seo: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - }) - ).isRequired, - monitor: PropTypes.object.isRequired, - metrics: PropTypes.object, -}; - -export default PageSpeedAreaChart; diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx deleted file mode 100644 index 525e4d0d2..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Box, Typography, Divider } from "@mui/material"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx"; -import { useTranslation } from "react-i18next"; -import { useTheme } from "@emotion/react"; - -const AreaChartLegend = ({ metrics, handleMetrics }) => { - const theme = useTheme(); - const { t } = useTranslation(); - return ( - - } - header="Metrics" - > - - - {t("shown")} - - - - handleMetrics("accessibility")} - /> - - handleMetrics("bestPractices")} - /> - - handleMetrics("performance")} - /> - - handleMetrics("seo")} - /> - - - ); -}; - -export default AreaChartLegend; diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx deleted file mode 100644 index eec075684..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx +++ /dev/null @@ -1,327 +0,0 @@ -import PropTypes from "prop-types"; -import { PieChart as MuiPieChart } from "@mui/x-charts/PieChart"; -import { useDrawingArea } from "@mui/x-charts"; -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { Box } from "@mui/material"; - -/** - * Renders a centered label within a pie chart. - * - * @param {Object} props - * @param {string | number} props.value - The value to display in the label. - * @param {string} props.color - The color of the text. - * @returns {JSX.Element} - */ -const PieCenterLabel = ({ value, color, setExpand }) => { - const { width, height } = useDrawingArea(); - return ( - setExpand(true)} - > - - - {value} - - - ); -}; - -PieCenterLabel.propTypes = { - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - color: PropTypes.string, - setExpand: PropTypes.func, -}; - -/** - * A component that renders a label on a pie chart slice. - * The label is positioned relative to the center of the pie chart and is optionally highlighted. - * - * @param {Object} props - * @param {number} props.value - The value to display inside the pie slice. - * @param {number} props.startAngle - The starting angle of the pie slice in degrees. - * @param {number} props.endAngle - The ending angle of the pie slice in degrees. - * @param {string} props.color - The color of the label text when highlighted. - * @param {boolean} props.highlighted - Determines if the label should be highlighted or not. - * @returns {JSX.Element} - */ -const PieValueLabel = ({ value, startAngle, endAngle, color, highlighted }) => { - const { width, height } = useDrawingArea(); - - // Compute the midpoint angle in radians - const angle = (((startAngle + endAngle) / 2) * Math.PI) / 180; - const radius = height / 4; // length from center of the circle to where the text is positioned - - // Calculate x and y positions - const x = Math.sin(angle) * radius; - const y = -Math.cos(angle) * radius; - - return ( - - - +{value} - - - ); -}; - -// Validate props using PropTypes -PieValueLabel.propTypes = { - value: PropTypes.number.isRequired, - startAngle: PropTypes.number.isRequired, - endAngle: PropTypes.number.isRequired, - color: PropTypes.string.isRequired, - highlighted: PropTypes.bool.isRequired, -}; - -/** - * Weight constants for different performance metrics. - * @type {Object} - */ -const weights = { - fcp: 10, - si: 10, - lcp: 25, - tbt: 30, - cls: 25, -}; - -const PieChart = ({ audits }) => { - const theme = useTheme(); - const [highlightedItem, setHighLightedItem] = useState(null); - const [expand, setExpand] = useState(false); - - /** - * Retrieves color properties based on the performance value. - * - * @param {number} value - The performance score used to determine the color properties. - * @returns {{stroke: string, strokeBg: string, text: string, bg: string}} The color properties for the given performance value. - */ - const getColors = (value) => { - if (value >= 90 && value <= 100) - return { - stroke: theme.palette.success.main, - strokeBg: theme.palette.success.lowContrast, - text: theme.palette.success.contrastText, - bg: theme.palette.success.lowContrast, - }; - else if (value >= 50 && value < 90) - return { - stroke: theme.palette.warning.main, - strokeBg: theme.palette.warning.lowContrast, - text: theme.palette.warning.contrastText, - bg: theme.palette.warning.lowContrast, - }; - else if (value >= 0 && value < 50) - return { - stroke: theme.palette.error.contrastText, - strokeBg: theme.palette.error.lowContrast, - text: theme.palette.error.contrastText, - bg: theme.palette.error.lowContrast, - }; - return { - stroke: theme.palette.tertiary.contrastText, - strokeBg: theme.palette.tertiary.contrastText, - text: theme.palette.tertiary.contrastText, - bg: theme.palette.tertiary.main, - }; - }; - - /** - * Calculates and formats the data needed for rendering a pie chart based on audit scores and weights. - * This function generates properties for each pie slice, including angles, radii, and colors. - * It also calculates performance based on the weighted values. - * - * @returns {Array} An array of objects, each representing the properties for a slice of the pie chart. - * @returns {number} performance - A variable updated with the rounded sum of weighted values. - */ - let performance = 0; - const getPieData = (audits) => { - if (typeof audits === "undefined") return undefined; - - let data = []; - let startAngle = 0; - const padding = 3; // padding between arcs - const max = 360 - padding * (Object.keys(audits).length - 1); // _id is a child of audits - - Object.keys(audits).forEach((key) => { - if (audits[key].score) { - let value = audits[key].score * weights[key]; - let endAngle = startAngle + (weights[key] * max) / 100; - - let theme = getColors(audits[key].score * 100); - data.push({ - id: key, - data: [ - { - value: value, - color: theme.stroke, - label: key.toUpperCase(), - }, - { - value: weights[key] - value, - color: theme.strokeBg, - label: "", - }, - ], - arcLabel: (item) => `${item.label}`, - arcLabelRadius: 95, - startAngle: startAngle, - endAngle: endAngle, - innerRadius: 73, - outerRadius: 80, - cornerRadius: 2, - highlightScope: { faded: "global", highlighted: "series" }, - faded: { - innerRadius: 73, - outerRadius: 80, - additionalRadius: -20, - arcLabelRadius: 5, - }, - cx: pieSize.width / 2, - }); - - performance += Math.floor(value); - startAngle = endAngle + padding; - } - }); - - return data; - }; - - const pieSize = { width: 230, height: 230 }; - const pieData = getPieData(audits); - const colorMap = getColors(performance); - - return ( - setExpand(false)} - sx={{ - display: "flex", - justifyContent: "center", - alignItems: "center", - width: "100%", - }} - > - {expand ? ( - - - {pieData?.map((pie) => ( - - ))} - - ) : ( - - - - )} - - ); -}; - -PieChart.propTypes = { - audits: PropTypes.object, -}; - -export default PieChart; diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx deleted file mode 100644 index 5cfdad608..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Stack, Box, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import PropTypes from "prop-types"; - -const PieChartLegend = ({ audits }) => { - const theme = useTheme(); - - return ( - - } - header="Performance metrics" - sx={{ flex: 1 }} - > - {typeof audits !== "undefined" && - Object.keys(audits).map((key) => { - if (key === "_id") return; - - let audit = audits[key]; - let score = audit.score * 100; - let bg = - score >= 90 - ? theme.palette.success.main - : score >= 50 - ? theme.palette.warning.main - : score >= 0 - ? theme.palette.error.main - : theme.palette.tertiary.main; - - // Find the position where the number ends and the unit begins - const match = audit?.displayValue?.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/); - let value; - let unit; - if (match) { - value = match[1]; - match[2] === "s" ? (unit = "seconds") : (unit = match[2]); - } else { - value = audit.displayValue; - } - - return ( - - - - {audit.title} - - - {value} - - {unit} - - - - - - ); - })} - - ); -}; - -PieChartLegend.propTypes = { - audits: PropTypes.object, -}; - -export default PieChartLegend; diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx deleted file mode 100644 index 9020ddd6d..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; -import AreaChart from "../Charts/AreaChart.jsx"; -import AreaChartLegend from "../Charts/AreaChartLegend.jsx"; -import SkeletonLayout from "./skeleton.jsx"; -import Icon from "@/Components/v1/Icon"; -import { Stack } from "@mui/material"; -import PropTypes from "prop-types"; - -import { useTheme } from "@emotion/react"; - -const PageSpeedAreaChart = ({ shouldRender, monitor, metrics, handleMetrics }) => { - const theme = useTheme(); - - if (!shouldRender) { - return ; - } - - const data = monitor?.checks ? [...monitor.checks].reverse() : []; - - return ( - - - } - header="Score history" - height="100%" - borderRadiusRight={16} - Legend={ - - } - > - - - - ); -}; - -PageSpeedAreaChart.propTypes = { - shouldRender: PropTypes.bool, - monitor: PropTypes.object, - metrics: PropTypes.object, - handleMetrics: PropTypes.func, -}; - -export default PageSpeedAreaChart; diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx deleted file mode 100644 index b793908d4..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Box, Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx deleted file mode 100644 index 6ff26c739..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; -import StatBox from "@/Components/v1/StatBox/index.jsx"; -import { Typography } from "@mui/material"; -import { getHumanReadableDuration } from "../../../../../Utils/timeUtilsLegacy.js"; -import { useTranslation } from "react-i18next"; - -const PageSpeedStatusBoxes = ({ shouldRender, monitorStats }) => { - const lastChecked = monitorStats?.lastCheckTimestamp - ? Date.now() - monitorStats.lastCheckTimestamp - : 0; - - // Determine time since last failure - const timeOfLastFailure = monitorStats?.timeOfLastFailure; - const timeSinceLastFailure = - timeOfLastFailure > 0 - ? Date.now() - timeOfLastFailure - : Date.now() - new Date(monitorStats?.createdAt); - - const streakTime = getHumanReadableDuration(timeSinceLastFailure); - - const time = getHumanReadableDuration(lastChecked); - - const { t } = useTranslation(); - - return ( - - - {streakTime} - {t("ago")} - - } - /> - - {time} - {t("ago")} - - } - /> - - ); -}; - -export default PageSpeedStatusBoxes; diff --git a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx deleted file mode 100644 index a2e1b5f96..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import PieChart from "../Charts/PieChart.jsx"; -import { Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import PieChartLegend from "../Charts/PieChartLegend.jsx"; -import SkeletonLayout from "./skeleton.jsx"; -import { useTranslation } from "react-i18next"; - -const PerformanceReport = ({ shouldRender, audits }) => { - const theme = useTheme(); - const { t } = useTranslation(); - - if (!shouldRender) { - return ; - } - - return ( - - } - header="Performance report" - Legend={} - borderRadiusRight={16} - > - - - {t("pageSpeedDetailsPerformanceReport")}{" "} - - {t("pageSpeedDetailsPerformanceReportCalculator")} - - - - ); -}; - -export default PerformanceReport; diff --git a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx b/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx deleted file mode 100644 index b793908d4..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Box, Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/PageSpeed/Details/old.jsx b/client/src/Pages/PageSpeed/Details/old.jsx deleted file mode 100644 index 3190787b3..000000000 --- a/client/src/Pages/PageSpeed/Details/old.jsx +++ /dev/null @@ -1,120 +0,0 @@ -// Components -import { Stack, Typography } from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx"; -import PageSpeedStatusBoxes from "./Components/PageSpeedStatusBoxes/index.jsx"; -import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx"; -import PageSpeedAreaChart from "./Components/PageSpeedAreaChart/index.jsx"; -import PerformanceReport from "./Components/PerformanceReport/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -// Utils -import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -import { useParams } from "react-router-dom"; -import { useFetchPageSpeedMonitorById } from "../../../Hooks/monitorHooks.js"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -// Constants -const BREADCRUMBS = [ - { name: "pagespeed", path: "/pagespeed" }, - { name: "details", path: `` }, -]; - -const PageSpeedDetails = () => { - const theme = useTheme(); - const { t } = useTranslation(); - const isAdmin = useIsAdmin(); - const { monitorId } = useParams(); - - // Local state - const [metrics, setMetrics] = useState({ - accessibility: true, - bestPractices: true, - performance: true, - seo: true, - }); - const [trigger, setTrigger] = useState(false); - // Network - const [monitor, monitorStats, isLoading, networkError] = useFetchPageSpeedMonitorById({ - monitorId, - dateRange: "day", - updateTrigger: trigger, - }); - - // Handlers - const handleMetrics = (id) => { - setMetrics((prev) => ({ ...prev, [id]: !prev[id] })); - }; - - const triggerUpdate = () => { - setTrigger(!trigger); - }; - if (networkError === true) { - return ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - // Empty view, displayed when loading is complete and there are no checks - if (!isLoading && monitor?.checks?.length === 0) { - return ( - - - - - {t("distributedUptimeDetailsNoMonitorHistory")} - - - ); - } - - return ( - - - - - - - - - - ); -}; - -export default PageSpeedDetails; diff --git a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx deleted file mode 100644 index 4a85863c4..000000000 --- a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx +++ /dev/null @@ -1,331 +0,0 @@ -import PropTypes from "prop-types"; -import Icon from "@/Components/v1/Icon"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import { Box, Grid, Stack, Typography } from "@mui/material"; -import { useNavigate } from "react-router-dom"; -import { useTheme } from "@emotion/react"; -import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; -import { useSelector } from "react-redux"; -import { formatDateWithTz, formatDurationSplit } from "@/Utils/timeUtilsLegacy.js"; -import { useMonitorUtils } from "@/Hooks/useMonitorUtils.js"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import IconBox from "@/Components/v1/IconBox/index.jsx"; -/** - * CustomToolTip displays a tooltip with formatted date and score information. - * @param {Object} props - * @param {Array} props.payload - Data to display in the tooltip - * @returns {JSX.Element} The rendered tooltip component - */ -const CustomToolTip = ({ payload }) => { - const theme = useTheme(); - const uiTimezone = useSelector((state) => state.ui.timezone); - - return ( - - - {formatDateWithTz( - payload[0]?.payload.createdAt, - "ddd, MMMM D, YYYY, h:mm A", - uiTimezone - )} - - - - - - {payload[0]?.name} - {" "} - {payload[0]?.payload.score} - - - ); -}; - -CustomToolTip.propTypes = { - payload: PropTypes.array, -}; - -/* TODO separate utils in folder*/ -/** - * Processes the raw data to include a score for each entry. - * @param {Array} data - The raw data array. - * @returns {Array} - The formatted data array with scores. - */ -const processData = (data) => { - if (data.length === 0) return []; - let formattedData = []; - - const calculateScore = (entry) => { - return ( - (entry.accessibility + entry.bestPractices + entry.performance + entry.seo) / 4 - ); - }; - - data.forEach((entry) => { - entry = { ...entry, score: calculateScore(entry) }; - formattedData.push(entry); - }); - - return formattedData; -}; - -/* TODO separate component*/ -/** - * Renders an area chart displaying page speed scores. - * @param {Object} props - * @param {Array} props.data - The raw data to be displayed in the chart. - * @param {string} props.status - The status of the page speed which determines the chart's color scheme. - * @returns {JSX.Element} - The rendered area chart. - */ -const PagespeedAreaChart = ({ data, status }) => { - const theme = useTheme(); - const [isHovered, setIsHovered] = useState(false); - const { statusToTheme } = useMonitorUtils(); - - const themeColor = statusToTheme[status]; - - const formattedData = processData(data); - - return ( - - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - - } - /> - - - - - - - - - - ); -}; - -PagespeedAreaChart.propTypes = { - data: PropTypes.arrayOf( - PropTypes.shape({ - accessibility: PropTypes.number.isRequired, - bestPractices: PropTypes.number.isRequired, - performance: PropTypes.number.isRequired, - seo: PropTypes.number.isRequired, - }) - ).isRequired, - status: PropTypes.string.isRequired, -}; - -/* TODO separate component */ -/** - * Renders a card displaying monitor details and an area chart. - * @param {Object} props - * @param {Object} props.monitor - The monitor data to be displayed in the card. - * @returns {JSX.Element} - The rendered card. - */ -const Card = ({ monitor }) => { - const { determineState, pagespeedStatusMsg } = useMonitorUtils(); - const theme = useTheme(); - const { t } = useTranslation(); - const navigate = useNavigate(); - const monitorState = determineState(monitor); - - return ( - - navigate(`/pagespeed/${monitor.id}`)} - border={1} - borderColor={theme.palette.primary.lowContrast} - borderRadius={theme.shape.borderRadius} - backgroundColor={theme.palette.primary.main} - sx={{ - display: "grid", - gridTemplateColumns: "34px 2fr 1fr", - columnGap: theme.spacing(5), - gridTemplateRows: "34px 1fr 3fr", - cursor: "pointer", - "&:hover": { - backgroundColor: theme.palette.tertiary.main, - }, - "& path": { - transition: "stroke-width 400ms ease", - }, - }} - > - - - - - {monitor.name} - - - - {monitor.url} - - - - - - - {t("checkingEvery")}{" "} - {(() => { - const { time, format } = formatDurationSplit(monitor?.interval); - return ( - <> - - {time}{" "} - - {format} - - ); - })()} - - - - - ); -}; - -Card.propTypes = { - monitor: PropTypes.object.isRequired, -}; - -export default Card; diff --git a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx deleted file mode 100644 index 9df5f7577..000000000 --- a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Grid } from "@mui/material"; -import Card from "../Card/index.jsx"; - -const MonitorGrid = ({ monitors, shouldRender = true }) => { - if (!shouldRender) return null; - - return ( - - {monitors?.map((monitor) => ( - - ))} - - ); -}; - -export default MonitorGrid; diff --git a/client/src/Pages/PageSpeed/Monitors/old.jsx b/client/src/Pages/PageSpeed/Monitors/old.jsx deleted file mode 100644 index f1bfcd0dd..000000000 --- a/client/src/Pages/PageSpeed/Monitors/old.jsx +++ /dev/null @@ -1,86 +0,0 @@ -// Components -import { useState } from "react"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import { Stack } from "@mui/material"; -import CreateMonitorHeader from "@/Components/v1/MonitorCreateHeader/index.jsx"; -import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx"; -import MonitorGrid from "./Components/MonitorGrid/index.jsx"; -import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; -import FallbackPageSpeedWarning from "@/Components/v1/Fallback/FallbackPageSpeedWarning.jsx"; - -// Utils -import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -import { useFetchMonitorsWithChecks } from "@/Hooks/monitorHooks.js"; -import { useFetchSettings } from "@/Hooks/settingsHooks.js"; -// Constants -const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }]; -const TYPES = ["pagespeed"]; -const PageSpeed = () => { - const theme = useTheme(); - const isAdmin = useIsAdmin(); - - const [ - summary, - monitorsWithChecks, - monitorsWithChecksCount, - monitorsWithChecksIsLoading, - monitorsWithChecksNetworkError, - ] = useFetchMonitorsWithChecks({ - types: TYPES, - limit: 10, - page: null, - rowsPerPage: null, - filter: null, - field: null, - order: null, - monitorUpdateTrigger: null, - }); - - const [settingsData, setSettingsData] = useState(undefined); - const [isSettingsLoading, settingsError] = useFetchSettings({ - setSettingsData, - setIsApiKeySet: () => {}, - setIsEmailPasswordSet: () => {}, - }); - - return ( - <> - - ) - } - > - - - - - - - - - ); -}; - -export default PageSpeed; diff --git a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx b/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx deleted file mode 100644 index 8912438d8..000000000 --- a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import { logger } from "../../../../Utils/Logger.js"; -import { useEffect, useState } from "react"; -import { networkService } from "../../../../main.jsx"; -import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; - -const useCertificateFetch = ({ - monitor, - monitorId, - certificateDateFormat, - uiTimezone, -}) => { - const [certificateExpiry, setCertificateExpiry] = useState(undefined); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - const fetchCertificate = async () => { - if (monitor?.type !== "http") { - return; - } - - try { - setIsLoading(true); - const res = await networkService.getCertificateExpiry({ - monitorId: monitorId, - }); - if (res?.data?.data?.certificateDate) { - const date = res.data.data.certificateDate; - setCertificateExpiry( - formatDateWithTz(date, certificateDateFormat, uiTimezone) ?? "N/A" - ); - } - } catch (error) { - setCertificateExpiry("N/A"); - logger.error(error); - } finally { - setIsLoading(false); - } - }; - fetchCertificate(); - }, [monitorId, certificateDateFormat, uiTimezone, monitor]); - return [certificateExpiry, isLoading]; -}; - -export default useCertificateFetch; diff --git a/client/src/Pages/Uptime/Details/old.jsx b/client/src/Pages/Uptime/Details/old.jsx deleted file mode 100644 index 144ceb4dc..000000000 --- a/client/src/Pages/Uptime/Details/old.jsx +++ /dev/null @@ -1,190 +0,0 @@ -// Components -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx"; -import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx"; -import ChartBoxes from "./Components/ChartBoxes/index.jsx"; -import ResponseTimeChart from "./Components/Charts/ResponseTimeChart.jsx"; -import ResponseTable from "./Components/ResponseTable/index.jsx"; -import UptimeStatusBoxes from "./Components/UptimeStatusBoxes/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; - -// Utils -import { useState } from "react"; -import { useParams } from "react-router-dom"; -import { useSelector } from "react-redux"; -import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -import { useFetchUptimeMonitorById } from "../../../Hooks/monitorHooks.js"; -import useCertificateFetch from "./Hooks/useCertificateFetch.jsx"; -import { useFetchChecksByMonitor } from "../../../Hooks/checkHooks.js"; -import { useTranslation } from "react-i18next"; - -// Constants -const BREADCRUMBS = [ - { name: "uptime", path: "/uptime" }, - { name: "details", path: "" }, - // { name: "details", path: `/uptime/${monitorId}` }, Is this needed? We can't click on this anywy -]; - -const certificateDateFormat = "MMM D, YYYY h A"; - -const UptimeDetails = () => { - // Redux state - const uiTimezone = useSelector((state) => state.ui.timezone); - - // Local state - const [dateRange, setDateRange] = useState("recent"); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(5); - const [trigger, setTrigger] = useState(false); - - // Utils - const dateFormat = - dateRange === "day" || dateRange === "recent" ? "MMM D, h A" : "MMM D"; - const { monitorId } = useParams(); - const theme = useTheme(); - const isAdmin = useIsAdmin(); - const { t } = useTranslation(); - - const [monitorData, monitorStats, monitorIsLoading, monitorNetworkError] = - useFetchUptimeMonitorById({ - monitorId, - dateRange, - trigger, - }); - - const monitor = monitorData?.monitor; - - const [certificateExpiry, certificateIsLoading] = useCertificateFetch({ - monitor, - monitorId, - certificateDateFormat, - uiTimezone, - }); - - const monitorType = monitor?.type; - - const [checks, checksCount, checksAreLoading, checksNetworkError] = - useFetchChecksByMonitor({ - monitorId, - type: monitorType, - sortOrder: "desc", - limit: null, - dateRange, - filter: null, - page, - rowsPerPage, - }); - - // Handlers - const triggerUpdate = () => { - setTrigger(!trigger); - }; - - const handlePageChange = (_, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(event.target.value); - }; - - if (monitorNetworkError || checksNetworkError) { - return ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - // Empty view, displayed when loading is complete and there are no checks - if (!monitorIsLoading && !checksAreLoading && checksCount === 0) { - return ( - - - - - - - - - {t("distributedUptimeDetailsNoMonitorHistory")} - - - ); - } - - return ( - - - - - - - - - - ); -}; - -export default UptimeDetails; diff --git a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx deleted file mode 100644 index 62ba0be13..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useTheme } from "@emotion/react"; -import PropTypes from "prop-types"; -import FilterHeader from "@/Components/v1/FilterHeader/index.jsx"; -import { useMemo } from "react"; -import { Box, Button } from "@mui/material"; -import Icon from "@/Components/v1/Icon"; -import { useTranslation } from "react-i18next"; - -/** - * Filter Component - * - * A high-level component that provides filtering options for type, status, and state. - * It allows users to select multiple options for each filter and reset the filters. - * - * @component - * @param {Object} props - The component props. - * @param {string[]} props.selectedTypes - An array of selected type values. - * @param {function} props.setSelectedTypes - A function to set the selected type values. - * @param {string[]} props.selectedStatus - An array of selected status values. - * @param {function} props.setSelectedStatus - A function to set the selected status values. - * @param {string[]} props.selectedState - An array of selected state values. - * @param {function} props.setSelectedState - A function to set the selected state values. - * @param {function} props.setToFilterStatus - A function to set the filter status based on selected status values. - * @param {function} props.setToFilterActive - A function to set the filter active state based on selected state values. - * @param {function} props.handleReset - A function to reset all filters. - * - * @returns {JSX.Element} The rendered Filter component. - */ - -const getTypeOptions = () => [ - { value: "http", label: "HTTP(S)" }, - { value: "ping", label: "Ping" }, - { value: "docker", label: "Docker" }, - { value: "port", label: "Port" }, - { value: "game", label: "Game" }, -]; - -// These functions were moved inline to ensure translations are applied correctly - -const Filter = ({ - selectedTypes, - setSelectedTypes, - selectedStatus, - setSelectedStatus, - selectedState, - setSelectedState, - setToFilterStatus, - setToFilterActive, - handleReset, -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - - const typeOptions = getTypeOptions(); - // Create status options with translations - const statusOptions = [ - { value: "Up", label: t("monitorStatus.up") }, - { value: "Down", label: t("monitorStatus.down") }, - ]; - // Create state options with translations - const stateOptions = [ - { value: "Active", label: t("monitorState.active") }, - { value: "Paused", label: t("monitorState.paused") }, - ]; - - const handleTypeChange = (event) => { - const selectedValues = event.target.value; - setSelectedTypes(selectedValues.length > 0 ? selectedValues : undefined); - }; - - const handleStatusChange = (event) => { - const selectedValues = event.target.value; - setSelectedStatus(selectedValues.length > 0 ? selectedValues : undefined); - - if (selectedValues.length === 0 || selectedValues.length === 2) { - setToFilterStatus(undefined); - } else { - setToFilterStatus(selectedValues[0] === "Up" ? "true" : "false"); - } - }; - - const handleStateChange = (event) => { - const selectedValues = event.target.value; - setSelectedState(selectedValues); - - if (selectedValues.length === 0 || selectedValues.length === 2) { - setToFilterActive(undefined); - } else { - setToFilterActive(selectedValues[0] === "Active" ? "true" : "false"); - } - }; - - const isFilterActive = useMemo(() => { - return ( - (selectedTypes?.length ?? 0) > 0 || - (selectedState?.length ?? 0) > 0 || - (selectedStatus?.length ?? 0) > 0 - ); - }, [selectedState, selectedTypes, selectedStatus]); - - return ( - - - - - - - ); -}; - -Filter.propTypes = { - selectedTypes: PropTypes.arrayOf(PropTypes.string), - setSelectedTypes: PropTypes.func.isRequired, - selectedStatus: PropTypes.arrayOf(PropTypes.string), - setSelectedStatus: PropTypes.func.isRequired, - selectedState: PropTypes.arrayOf(PropTypes.string), - setSelectedState: PropTypes.func.isRequired, - setToFilterStatus: PropTypes.func.isRequired, - setToFilterActive: PropTypes.func.isRequired, - handleReset: PropTypes.func.isRequired, -}; - -export default Filter; diff --git a/client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx b/client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx deleted file mode 100644 index 4e55be536..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - <> - - - - - - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx deleted file mode 100644 index 5cba9e3b1..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import PropTypes from "prop-types"; -import { Stack } from "@mui/material"; -import StatusBox from "./statusBox.jsx"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import SkeletonLayout from "./skeleton.jsx"; - -const StatusBoxes = ({ shouldRender, monitorsSummary }) => { - const theme = useTheme(); - const { t } = useTranslation(); - if (!shouldRender) return ; - return ( - - - - - - ); -}; - -StatusBoxes.propTypes = { - monitorsSummary: PropTypes.object, - shouldRender: PropTypes.bool, -}; - -export default StatusBoxes; diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx deleted file mode 100644 index df6e08ccd..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -const SkeletonLayout = () => { - const theme = useTheme(); - return ( - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx deleted file mode 100644 index 136c3575d..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx +++ /dev/null @@ -1,115 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Stack, Typography } from "@mui/material"; -import Icon from "@/Components/v1/Icon"; -import Background from "@/assets/Images/background-grid.svg?react"; - -const StatusBox = ({ title, value, status }) => { - const theme = useTheme(); - let sharedStyles = { - position: "absolute", - right: 8, - opacity: 0.5, - "& svg path": { stroke: theme.palette.primary.contrastTextTertiary }, - }; - - let color; - let icon; - if (status === "up") { - color = theme.palette.success.lowContrast; - icon = ( - - - - ); - } else if (status === "down") { - color = theme.palette.error.lowContrast; - icon = ( - - - - ); - } else if (status === "paused") { - color = theme.palette.warning.lowContrast; - icon = ( - - - - ); - } - - return ( - - - - - - - - {title} - - {icon} - - - {value} - - - # - - - - - ); -}; - -StatusBox.propTypes = { - title: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - status: PropTypes.string, -}; - -export default StatusBox; diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx deleted file mode 100644 index 8c3239dd3..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx +++ /dev/null @@ -1,241 +0,0 @@ -// Components -import { Box, Stack } from "@mui/material"; -import DataTable from "@/Components/v1/Table/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import Host from "@/Components/v1/Host/index.jsx"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import BarChart from "@/Components/v1/Charts/BarChart/index.jsx"; -import ActionsMenu from "@/Components/v1/ActionsMenu/index.jsx"; - -import LoadingSpinner from "../LoadingSpinner/index.jsx"; -import TableSkeleton from "@/Components/v1/Table/skeleton.jsx"; - -// Utils -import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; -import { useNavigate } from "react-router-dom"; -import PropTypes from "prop-types"; -import { useTranslation } from "react-i18next"; - -/** - * UptimeDataTable displays a table of uptime monitors with sorting, searching, and action capabilities - * @param {Object} props - Component props - * @param {boolean} props.isAdmin - Whether the current user has admin privileges - * @param {boolean} props.isLoading - Loading state of the table - * @param {Array<{ - * id: string, - * url: string, - * title: string, - * percentage: number, - * percentageColor: string, - * monitor: { - * id: string, - * type: string, - * checks: Array - * } - * }>} props.monitors - Array of monitor objects to display - * @param {number} props.monitorCount - Total count of monitors - * @param {Object} props.sort - Current sort configuration - * @param {string} props.sort.field - Field to sort by - * @param {'asc'|'desc'} props.sort.order - Sort direction - * @param {Function} props.setSort - Callback to update sort configuration - * @param {string} props.search - Current search query - * @param {Function} props.setSearch - Callback to update search query - * @param {boolean} props.isSearching - Whether a search is in progress - * @param {Function} props.setIsLoading - Callback to update loading state - * @param {Function} props.triggerUpdate - Callback to trigger a data refresh - * @returns {JSX.Element} Rendered component - */ -const UptimeDataTable = ({ - isAdmin, - isSearching, - filteredMonitors, - sort, - setSort, - triggerUpdate, - monitorsAreLoading, -}) => { - // Utils - const navigate = useNavigate(); - const { determineState } = useMonitorUtils(); - const theme = useTheme(); - const { t } = useTranslation(); - - // Local state - // Handlers - const handleSort = (field) => { - let order = ""; - if (sort?.field !== field) { - order = "desc"; - } else { - order = sort?.order === "asc" ? "desc" : "asc"; - } - setSort({ field, order }); - }; - - const headers = [ - { - id: "name", - content: ( - handleSort("name")} - > - {t("host")} - - {sort?.order === "asc" ? ( - - ) : ( - - )} - - - ), - render: (row) => ( - - ), - }, - { - id: "status", - content: ( - handleSort("status")} - > - {" "} - {t("status")} - - {sort?.order === "asc" ? ( - - ) : ( - - )} - - - ), - render: (row) => { - const status = determineState(row.monitor); - return ( - - ); - }, - }, - { - id: "responseTime", - content: t("responseTime"), - render: (row) => ( - - - - ), - }, - { - id: "type", - content: t("type"), - render: (row) => ( - - {row.monitor.type === "http" ? "HTTP(s)" : row.monitor.type} - - ), - }, - { - id: "actions", - content: t("actions"), - render: (row) => ( - - ), - }, - ]; - - if (monitorsAreLoading) { - return ; - } - - return ( - - - { - navigate(`/uptime/${row.id}`); - }, - emptyView: "No monitors found", - }} - /> - - ); -}; - -UptimeDataTable.propTypes = { - isSearching: PropTypes.bool, - setSort: PropTypes.func, - setSearch: PropTypes.func, - triggerUpdate: PropTypes.func, - debouncedSearch: PropTypes.string, - onSearchChange: PropTypes.func, - isAdmin: PropTypes.bool, - monitors: PropTypes.array, - filteredMonitors: PropTypes.array, - monitorCount: PropTypes.number, - monitorsAreLoading: PropTypes.bool, - sort: PropTypes.shape({ - field: PropTypes.string, - order: PropTypes.oneOf(["asc", "desc"]), - }), -}; - -export default UptimeDataTable; diff --git a/client/src/Pages/Uptime/Monitors/old.jsx b/client/src/Pages/Uptime/Monitors/old.jsx deleted file mode 100644 index 67019362f..000000000 --- a/client/src/Pages/Uptime/Monitors/old.jsx +++ /dev/null @@ -1,225 +0,0 @@ -// Required Data -// 1. Monitor summary -// 2. List of monitors filtered by search term with 25 checks each -// 2a.List of monitors must have the total number of monitors that match. - -// Components -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import Greeting from "../../../Utils/greeting.jsx"; -import StatusBoxes from "./Components/StatusBoxes/index.jsx"; -import UptimeDataTable from "./Components/UptimeDataTable/index.jsx"; -import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; -import CreateMonitorHeader from "@/Components/v1/MonitorCreateHeader/index.jsx"; -import SearchComponent from "./Components/SearchComponent/index.jsx"; -import Filter from "./Components/Filter/index.jsx"; -import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; - -import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx"; - -// MUI Components -import { Stack, Box, Button } from "@mui/material"; -// Utils -import { useState, useCallback, useEffect } from "react"; -import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { useSelector, useDispatch } from "react-redux"; -import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; -import PropTypes from "prop-types"; -import { - useFetchMonitorsWithChecks, - useFetchMonitorsByTeamId, -} from "@/Hooks/monitorHooks.js"; -import { useTranslation } from "react-i18next"; - -const TYPES = ["http", "ping", "docker", "port", "game"]; -const CreateMonitorButton = ({ shouldRender }) => { - // Utils - const navigate = useNavigate(); - const { t } = useTranslation(); - if (shouldRender === false) { - return; - } - - return ( - - - - ); -}; - -CreateMonitorButton.propTypes = { - shouldRender: PropTypes.bool, -}; - -const UptimeMonitors = () => { - // Redux state - const rowsPerPage = useSelector((state) => state.ui?.monitors?.rowsPerPage ?? 10); - - // Local state - const [search, setSearch] = useState(undefined); - const [page, setPage] = useState(undefined); - const [sort, setSort] = useState(undefined); - const [isSearching, setIsSearching] = useState(false); - const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false); - const [selectedTypes, setSelectedTypes] = useState(undefined); - const [selectedState, setSelectedState] = useState(undefined); - const [selectedStatus, setSelectedStatus] = useState(undefined); - const [toFilterStatus, setToFilterStatus] = useState(undefined); - const [toFilterActive, setToFilterActive] = useState(undefined); - - // Utils - const theme = useTheme(); - const isAdmin = useIsAdmin(); - const dispatch = useDispatch(); - const { t } = useTranslation(); - - const BREADCRUMBS = [{ name: t("menu.uptime"), path: "/uptime" }]; - - // Handlers - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - dispatch( - setRowsPerPage({ - value: parseInt(event.target.value, 10), - table: "monitors", - }) - ); - setPage(0); - }; - - const triggerUpdate = useCallback(() => { - setMonitorUpdateTrigger((prev) => !prev); - }, []); - - const handleReset = () => { - setSelectedState(undefined); - setSelectedTypes(undefined); - setSelectedStatus(undefined); - setToFilterStatus(undefined); - setToFilterActive(undefined); - }; - - const filterLookup = new Map([ - [toFilterStatus, "status"], - [toFilterActive, "isActive"], - ]); - - const activeFilter = [...filterLookup].find(([key]) => key !== undefined); - const field = activeFilter?.[1] || (search ? "name" : sort?.field); - const filter = activeFilter?.[0] || search; - - const effectiveTypes = selectedTypes?.length ? selectedTypes : TYPES; - - const [ - summary, - monitorsWithChecks, - monitorsWithChecksCount, - monitorsWithChecksIsLoading, - networkError, - ] = useFetchMonitorsWithChecks({ - types: effectiveTypes, - limit: 25, - page: page, - rowsPerPage: rowsPerPage, - filter: filter, - field: field, - order: sort?.order, - monitorUpdateTrigger, - }); - - const [monitors, listIsLoading, listNetworkError] = useFetchMonitorsByTeamId({ - type: ["http", "ping", "docker", "port", "game"], - }); - - useEffect(() => { - if (isSearching) { - setPage(undefined); - } - }, [isSearching]); - - const isLoading = monitorsWithChecksIsLoading; - - return ( - <> - - - - - - - - - - - - - - - - - - ); -}; - -export default UptimeMonitors; From 7f1c996632728bde3e270d3b3773fd8010ec4c04 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 28 Jan 2026 20:21:41 +0000 Subject: [PATCH 073/133] format --- .../checks/MongoChecksRepistory.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index e4fe01412..1f65673f3 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -205,15 +205,17 @@ class MongoChecksRepository implements IChecksRepository { const { id, metadata, ...rest } = check; return { _id: id ? new mongoose.Types.ObjectId(id) : new mongoose.Types.ObjectId(), - metadata: metadata ? { - monitorId: new mongoose.Types.ObjectId(metadata.monitorId), - teamId: new mongoose.Types.ObjectId(metadata.teamId), - type: metadata.type, - } : { - monitorId: new mongoose.Types.ObjectId(), - teamId: new mongoose.Types.ObjectId(), - type: "http", - }, + metadata: metadata + ? { + monitorId: new mongoose.Types.ObjectId(metadata.monitorId), + teamId: new mongoose.Types.ObjectId(metadata.teamId), + type: metadata.type, + } + : { + monitorId: new mongoose.Types.ObjectId(), + teamId: new mongoose.Types.ObjectId(), + type: "http", + }, ...rest, } as unknown as CheckDocument; }; From 5596e45ff3895a71ca7bfc2943ff6b58560a5699 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 28 Jan 2026 20:48:02 +0000 Subject: [PATCH 074/133] initial commit --- client/src/Pages/Infrastructure/Monitors/index.tsx | 5 +++++ .../src/Pages/Infrastructure/Monitors/{index.jsx => old.jsx} | 0 2 files changed, 5 insertions(+) create mode 100644 client/src/Pages/Infrastructure/Monitors/index.tsx rename client/src/Pages/Infrastructure/Monitors/{index.jsx => old.jsx} (100%) diff --git a/client/src/Pages/Infrastructure/Monitors/index.tsx b/client/src/Pages/Infrastructure/Monitors/index.tsx new file mode 100644 index 000000000..3ed96f6a1 --- /dev/null +++ b/client/src/Pages/Infrastructure/Monitors/index.tsx @@ -0,0 +1,5 @@ +const InfrastructureMonitors = () => { + return null; +}; + +export default InfrastructureMonitors; diff --git a/client/src/Pages/Infrastructure/Monitors/index.jsx b/client/src/Pages/Infrastructure/Monitors/old.jsx similarity index 100% rename from client/src/Pages/Infrastructure/Monitors/index.jsx rename to client/src/Pages/Infrastructure/Monitors/old.jsx From c18159afd39dc1355a35341d2fd6e4bdbb92db6e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 28 Jan 2026 21:34:43 +0000 Subject: [PATCH 075/133] v2 infra migration --- .../Components/v2/design-elements/Gauge.tsx | 106 +++++++ .../Components/v2/design-elements/index.tsx | 1 + .../Components/v2/monitors/ControlsFilter.tsx | 40 +-- .../Monitors/Components/Filters/index.jsx | 94 ------ .../Monitors/Components/MonitorsTable.tsx | 275 ++++++++++++++++++ .../Components/MonitorsTable/index.jsx | 162 ----------- .../Components/MonitorsTableMenu/index.jsx | 196 ------------- .../Pages/Infrastructure/Monitors/index.tsx | 197 ++++++++++++- .../src/Pages/Infrastructure/Monitors/old.jsx | 139 --------- .../Components/LoadingSpinner/index.jsx | 46 --- .../Components/SearchComponent/index.jsx | 49 ---- .../Uptime/Monitors/Hooks/useDebounce.jsx | 18 -- client/src/Routes/index.jsx | 10 +- client/src/locales/en.json | 9 + 14 files changed, 617 insertions(+), 725 deletions(-) create mode 100644 client/src/Components/v2/design-elements/Gauge.tsx delete mode 100644 client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx create mode 100644 client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable.tsx delete mode 100644 client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx delete mode 100644 client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx delete mode 100644 client/src/Pages/Infrastructure/Monitors/old.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/Components/LoadingSpinner/index.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/Hooks/useDebounce.jsx diff --git a/client/src/Components/v2/design-elements/Gauge.tsx b/client/src/Components/v2/design-elements/Gauge.tsx new file mode 100644 index 000000000..34c57487d --- /dev/null +++ b/client/src/Components/v2/design-elements/Gauge.tsx @@ -0,0 +1,106 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; + +import { useTheme } from "@mui/material/styles"; +import { useMemo, useState, useEffect } from "react"; +import { getInfraGaugeColor } from "@/Utils/MonitorUtils"; + +const MINIMUM_VALUE = 0; +const MAXIMUM_VALUE = 100; + +export const Gauge = ({ + isLoading = false, + progress = 0, + radius = 70, + strokeWidth = 15, + precision = 1, + unit = "%", +}: { + isLoading?: boolean; + progress?: number; + radius?: number; + strokeWidth?: number; + precision?: number; + unit?: string; +}) => { + const theme = useTheme(); + const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE)); + + // Calculate the length of the stroke for the circle + const { circumference, totalSize, strokeLength } = useMemo( + () => ({ + circumference: 2 * Math.PI * radius, + totalSize: radius * 2 + strokeWidth * 2, + strokeLength: (progress / 100) * (2 * Math.PI * radius), + }), + [radius, strokeWidth, progress] + ); + + const [offset, setOffset] = useState(circumference); + useEffect(() => { + setOffset(circumference); + const timer = setTimeout(() => { + setOffset(circumference - strokeLength); + }, 100); + + return () => clearTimeout(timer); + }, [progress, circumference, strokeLength]); + + const fillColor = getInfraGaugeColor(progressWithinRange, theme); + + if (isLoading) { + return; + } + + return ( + + + + + + + + {`${progressWithinRange.toFixed(precision)}${unit}`} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx index 6ebffc624..000dab1b2 100644 --- a/client/src/Components/v2/design-elements/index.tsx +++ b/client/src/Components/v2/design-elements/index.tsx @@ -15,3 +15,4 @@ export { default as Icon } from "./Icon"; export * from "./Tooltip"; export * from "./StatBox"; export * from "./BaseChart"; +export * from "./Gauge"; diff --git a/client/src/Components/v2/monitors/ControlsFilter.tsx b/client/src/Components/v2/monitors/ControlsFilter.tsx index d7eecefb5..34eedd190 100644 --- a/client/src/Components/v2/monitors/ControlsFilter.tsx +++ b/client/src/Components/v2/monitors/ControlsFilter.tsx @@ -11,6 +11,7 @@ const statuses = ["up", "down"]; const states = ["active", "paused"]; export const ControlsFilter = ({ + showTypes = true, selectedTypes, setSelectedTypes, selectedStatus, @@ -19,8 +20,9 @@ export const ControlsFilter = ({ setSelectedState, onClearFilters, }: { - selectedTypes: MonitorType[]; - setSelectedTypes: React.Dispatch>; + showTypes?: boolean; + selectedTypes?: MonitorType[]; + setSelectedTypes?: React.Dispatch>; selectedStatus: string; setSelectedStatus: React.Dispatch>; selectedState: string; @@ -30,27 +32,29 @@ export const ControlsFilter = ({ const theme = useTheme(); const isSmall = useMediaQuery(theme.breakpoints.down("md")); const isFilterActive = - selectedTypes.length > 0 || selectedStatus !== "" || selectedState !== ""; + (selectedTypes?.length ?? 0) > 0 || selectedStatus !== "" || selectedState !== ""; return ( - + {showTypes && setSelectedTypes && ( + + )} setSelectedInterface(e.target.value)} - items={availableInterfaces.map((interfaceName) => ({ - _id: interfaceName, - name: interfaceName, - }))} - sx={{ minWidth: 200 }} - /> - )} - - - - - ); -}; - -Network.propTypes = { - net: PropTypes.array, - checks: PropTypes.array, - isLoading: PropTypes.bool.isRequired, - dateRange: PropTypes.string.isRequired, - setDateRange: PropTypes.func.isRequired, -}; - -export default Network; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx deleted file mode 100644 index 52fcfad87..000000000 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import { - Card, - CardContent, - Skeleton, - Table, - TableHead, - TableRow, - TableCell, - TableBody, -} from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - - - - Name - Bytes Sent - Bytes Received - Packets Sent - Packets Received - Errors In - Errors Out - Drops In - Drops Out - - - - {Array.from({ length: 5 }).map((_, idx) => ( - - {Array.from({ length: 9 }).map((__, colIdx) => ( - - - - ))} - - ))} - -
-
-
- ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx deleted file mode 100644 index d411886c7..000000000 --- a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx +++ /dev/null @@ -1,115 +0,0 @@ -// Components -import { Stack, Typography } from "@mui/material"; -import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; -import StatBox from "@/Components/v1/StatBox/index.jsx"; - -//Utils -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; -import { useTranslation } from "react-i18next"; - -const InfraStatBoxes = ({ shouldRender, monitor }) => { - // Utils - const { formatBytes } = useHardwareUtils(); - const { determineState } = useMonitorUtils(); - const { t } = useTranslation(); - - const latestCheck = monitor?.recentChecks?.[0]; - - // Get data from latest check - const physicalCores = latestCheck?.cpu?.physical_core ?? 0; - const logicalCores = latestCheck?.cpu?.logical_core ?? 0; - const cpuFrequency = latestCheck?.cpu?.frequency ?? 0; - const cpuTemperature = - latestCheck?.cpu?.temperature?.length > 0 - ? latestCheck.cpu.temperature.reduce((acc, curr) => acc + curr, 0) / - latestCheck.cpu.temperature.length - : 0; - const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0; - const diskTotalBytes = latestCheck?.disk[0]?.total_bytes ?? 0; - const os = latestCheck?.host?.os ?? undefined; - const platform = latestCheck?.host?.platform ?? undefined; - const osPlatform = - typeof os === "undefined" && typeof platform === "undefined" - ? undefined - : `${os} ${platform}`; - - return ( - - - - {physicalCores} - - {physicalCores === 1 ? "core" : "cores"} - - - } - /> - - {logicalCores} - - {logicalCores === 1 ? "core" : "cores"} - - - } - /> - - {(cpuFrequency / 1000).toFixed(2)} - Ghz - - } - /> - - {cpuTemperature.toFixed(2)} - °C - - } - /> - - - {/* - {(uptimePercentage * 100).toFixed(2)} - % - - } - /> */} - - - ); -}; - -export default InfraStatBoxes; diff --git a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx deleted file mode 100644 index 8938847a4..000000000 --- a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx +++ /dev/null @@ -1,270 +0,0 @@ -import { Typography, Tooltip } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; - -// Constants -const BASE_BOX_PADDING_VERTICAL = 4; -const BASE_BOX_PADDING_HORIZONTAL = 8; -const TYPOGRAPHY_PADDING = 8; -const CHART_CONTAINER_HEIGHT = 300; - -const useHardwareUtils = () => { - const theme = useTheme(); - const { t } = useTranslation(); - - const getDimensions = () => { - const totalTypographyPadding = parseInt(theme.spacing(TYPOGRAPHY_PADDING), 10) * 2; - const totalChartContainerPadding = - parseInt(theme.spacing(BASE_BOX_PADDING_VERTICAL), 10) * 2; - return { - baseBoxPaddingVertical: BASE_BOX_PADDING_VERTICAL, - baseBoxPaddingHorizontal: BASE_BOX_PADDING_HORIZONTAL, - totalContainerPadding: parseInt(theme.spacing(BASE_BOX_PADDING_VERTICAL), 10) * 2, - areaChartHeight: - CHART_CONTAINER_HEIGHT - totalChartContainerPadding - totalTypographyPadding, - }; - }; - - const formatBytes = (bytes, space = false) => { - if (bytes === undefined || bytes === null) - return ( - <> - {0} - {space ? " " : ""} - {t("gb")} - - ); - if (typeof bytes !== "number") - return ( - <> - {0} - {space ? " " : ""} - {t("gb")} - - ); - if (bytes === 0) - return ( - <> - {0} - {space ? " " : ""} - {t("gb")} - - ); - - const GB = bytes / (1024 * 1024 * 1024); - const MB = bytes / (1024 * 1024); - - if (GB >= 1) { - return ( - <> - {Number(GB.toFixed(2))} - {space ? " " : ""} - {t("gb")} - - ); - } else { - return ( - <> - {Number(MB.toFixed(2))} - {space ? " " : ""} - {t("mb")} - - ); - } - }; - - const formatBytesPerSecondString = (bytesPerSec, space = false) => { - if ( - bytesPerSec === undefined || - bytesPerSec === null || - typeof bytesPerSec !== "number" || - bytesPerSec === 0 - ) { - return `0${space ? " " : ""}B/s`; - } - - const GB = bytesPerSec / (1024 * 1024 * 1024); - const MB = bytesPerSec / (1024 * 1024); - const KB = bytesPerSec / 1024; - - if (GB >= 1) { - return `${Number(GB.toFixed(1))}${space ? " " : ""}GB/s`; - } else if (MB >= 1) { - return `${Number(MB.toFixed(1))}${space ? " " : ""}MB/s`; - } else if (KB >= 1) { - return `${Number(KB.toFixed(1))}${space ? " " : ""}KB/s`; - } else { - return `${Number(bytesPerSec.toFixed(1))}${space ? " " : ""}B/s`; - } - }; - - const formatPacketsPerSecondString = (packetsPerSec, space = false) => { - if ( - packetsPerSec === undefined || - packetsPerSec === null || - typeof packetsPerSec !== "number" || - packetsPerSec === 0 - ) { - return `0${space ? " " : ""}pps`; - } - - const M = packetsPerSec / (1000 * 1000); - const K = packetsPerSec / 1000; - - if (M >= 1) { - return `${Number(M.toFixed(1))}${space ? " " : ""}Mpps`; - } else if (K >= 1) { - return `${Number(K.toFixed(1))}${space ? " " : ""}Kpps`; - } else { - return `${Math.round(packetsPerSec)}${space ? " " : ""}pps`; - } - }; - - const formatDeviceName = (device) => { - const deviceStr = String(device || ""); - - // Show full device path - return ( - - - {deviceStr} - - - ); - }; - - const formatMountpoint = (mountpoint) => { - const mountpointStr = String(mountpoint || ""); - - if (!mountpointStr) { - return ( - - - N/A - - - ); - } - - // Show full mountpoint path - return ( - - - {mountpointStr} - - - ); - }; - - /** - * Converts a decimal value to a percentage - * - * @function decimalToPercentage - * @param {number} value - Decimal value to convert - * @returns {number} Percentage representation - * - * @example - * decimalToPercentage(0.75) // Returns 75 - * decimalToPercentage(null) // Returns 0 - */ - const decimalToPercentage = (value) => { - if (value === null || value === undefined) return 0; - return value * 100; - }; - - const buildTemps = (checks) => { - let numCores = 1; - if (checks === null) return { temps: [], tempKeys: [] }; - - for (const check of checks) { - if (check?.avgTemperature?.length > numCores) { - numCores = check.avgTemperature.length; - break; - } - } - const temps = checks.map((check) => { - // If there's no data, set the temperature to 0 - if ( - check?.avgTemperature?.length === 0 || - check?.avgTemperature === undefined || - check?.avgTemperature === null - ) { - check.avgTemperature = Array(numCores).fill(0); - } - const res = check?.avgTemperature?.reduce( - (acc, cur, idx) => { - acc[`core${idx + 1}`] = cur; - return acc; - }, - { - _id: check._id, - } - ); - return res; - }); - if (temps.length === 0 || !temps[0]) { - return { temps: [], tempKeys: [] }; - } - - return { - tempKeys: Object.keys(temps[0] || {}).filter((key) => key !== "_id"), - temps, - }; - }; - - return { - formatBytes, - formatDeviceName, - formatMountpoint, - decimalToPercentage, - buildTemps, - getDimensions, - formatBytesPerSecondString, - formatPacketsPerSecondString, - }; -}; - -export { useHardwareUtils }; diff --git a/client/src/Pages/Infrastructure/Details/old.jsx b/client/src/Pages/Infrastructure/Details/old.jsx deleted file mode 100644 index 6ae630822..000000000 --- a/client/src/Pages/Infrastructure/Details/old.jsx +++ /dev/null @@ -1,142 +0,0 @@ -// Components -import { Stack, Typography, Tab } from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx"; -import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx"; -import StatusBoxes from "./Components/StatusBoxes/index.jsx"; -import GaugeBoxes from "./Components/GaugeBoxes/index.jsx"; -import AreaChartBoxes from "./Components/AreaChartBoxes/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import NetworkStats from "./Components/NetworkStats/index.jsx"; -import CustomTabList from "@/Components/v1/Tab/index.jsx"; -import TabContext from "@mui/lab/TabContext"; - -// Utils -import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -import { useFetchHardwareMonitorById } from "../../../Hooks/monitorHooks.js"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useParams } from "react-router-dom"; - -// Constants -const BREADCRUMBS = [ - { name: "infrastructure monitors", path: "/infrastructure" }, - { name: "details", path: "" }, -]; -const InfrastructureDetails = () => { - // Local state - const [dateRange, setDateRange] = useState("recent"); - const [trigger, setTrigger] = useState(false); - const [tab, setTab] = useState("details"); - - // Utils - const theme = useTheme(); - const { monitorId } = useParams(); - const { t } = useTranslation(); - const isAdmin = useIsAdmin(); - - const [monitor, isLoading, networkError] = useFetchHardwareMonitorById({ - monitorId, - dateRange, - updateTrigger: trigger, - }); - - const triggerUpdate = () => { - setTrigger(!trigger); - }; - - if (networkError === true) { - return ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - if (!isLoading && monitor?.stats?.checks?.length === 0) { - return ( - - - - - {t("distributedUptimeDetailsNoMonitorHistory")} - - - ); - } - - return ( - - - - - setTab(v)} - > - - - - {tab === "details" && ( - <> - - - - - - )} - {tab === "network" && ( - - )} - - - ); -}; - -export default InfrastructureDetails; From 6395e8a10e0dbe137ec3094c1d5dfbb8adfbd1ac Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 Jan 2026 17:52:10 +0000 Subject: [PATCH 087/133] iniital commit --- client/src/Pages/Notifications/index.tsx | 25 +++++++++++++++++++ .../Notifications/{index.jsx => old.jsx} | 0 client/src/Routes/index.jsx | 10 ++++++-- client/src/locales/en.json | 11 ++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 client/src/Pages/Notifications/index.tsx rename client/src/Pages/Notifications/{index.jsx => old.jsx} (100%) diff --git a/client/src/Pages/Notifications/index.tsx b/client/src/Pages/Notifications/index.tsx new file mode 100644 index 000000000..cc8d68285 --- /dev/null +++ b/client/src/Pages/Notifications/index.tsx @@ -0,0 +1,25 @@ +import { BasePageWithStates } from "@/Components/v2/design-elements"; + +import { useTranslation } from "react-i18next"; +const NotificationsPage = () => { + const { t } = useTranslation(); + + return ( + + Notifications + + ); +}; +export default NotificationsPage; diff --git a/client/src/Pages/Notifications/index.jsx b/client/src/Pages/Notifications/old.jsx similarity index 100% rename from client/src/Pages/Notifications/index.jsx rename to client/src/Pages/Notifications/old.jsx diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index f9b5cb8a1..e0725e8e2 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -43,7 +43,7 @@ import CreateStatus from "../Pages/StatusPage/Create/index.jsx"; import StatusPages from "../Pages/StatusPage/StatusPages/index.jsx"; import Status from "../Pages/StatusPage/Status/index.jsx"; -import Notifications from "../Pages/Notifications/index.jsx"; +import Notifications from "../Pages/Notifications"; import CreateNotifications from "../Pages/Notifications/create/index.jsx"; // Settings @@ -210,7 +210,13 @@ const Routes = () => { } + element={ + <> + + + + + } /> Date: Thu, 29 Jan 2026 18:14:23 +0000 Subject: [PATCH 088/133] v2-table --- .../components/NotificationsTable.tsx | 96 +++++++++++++++++++ client/src/Pages/Notifications/index.tsx | 31 ++++-- client/src/Types/Notification.ts | 17 ++++ client/src/locales/en.json | 5 + 4 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 client/src/Pages/Notifications/components/NotificationsTable.tsx create mode 100644 client/src/Types/Notification.ts diff --git a/client/src/Pages/Notifications/components/NotificationsTable.tsx b/client/src/Pages/Notifications/components/NotificationsTable.tsx new file mode 100644 index 000000000..0e804d276 --- /dev/null +++ b/client/src/Pages/Notifications/components/NotificationsTable.tsx @@ -0,0 +1,96 @@ +import { ActionsMenu, type ActionMenuItem } from "@/Components/v2/actions-menu"; +import Typography from "@mui/material/Typography"; +import type { Header } from "@/Components/v2/design-elements/Table"; +import { Table } from "@/Components/v2/design-elements"; + +import type { Notification } from "@/Types/Notification"; +import { useNavigate } from "react-router"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material"; + +interface NotificationsTableProps { + notifications: Notification[]; + setSelectedChannel: Function; +} + +export const NotificationsTable = ({ + notifications, + setSelectedChannel, +}: NotificationsTableProps) => { + const navigate = useNavigate(); + const { t } = useTranslation(); + const theme = useTheme(); + + const getActions = (channel: Notification): ActionMenuItem[] => { + return [ + { + id: 1, + label: t("pages.common.monitors.actions.configure"), + action: () => { + navigate(`/notification-channels/${channel.id}/configure`); + }, + closeMenu: true, + }, + + { + id: 7, + label: ( + + {t("pages.common.monitors.actions.delete")} + + ), + action: async () => { + setSelectedChannel(channel); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = () => { + const headers: Header[] = [ + { + id: "name", + content: t("common.table.headers.name"), + render: (row) => { + return {row?.notificationName}; + }, + }, + + { + id: "type", + content: t("common.table.headers.type"), + render: (row) => { + return {row?.type}; + }, + }, + { + id: "destination", + content: t("pages.notifications.table.headers.destination"), + render: (row) => { + return {row?.address}; + }, + }, + { + id: "actions", + content: t("common.table.headers.actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + const headers = getHeaders(); + + return ( + { + navigate(`/notification-channels/${row.id}/configure`); + }} + /> + ); +}; diff --git a/client/src/Pages/Notifications/index.tsx b/client/src/Pages/Notifications/index.tsx index cc8d68285..c5d710394 100644 --- a/client/src/Pages/Notifications/index.tsx +++ b/client/src/Pages/Notifications/index.tsx @@ -1,25 +1,42 @@ import { BasePageWithStates } from "@/Components/v2/design-elements"; +import { NotificationsTable } from "@/Pages/Notifications/components/NotificationsTable"; +import { useState } from "react"; +import { useGet } from "@/Hooks/UseApi"; import { useTranslation } from "react-i18next"; +import type { Notification } from "@/Types/Notification"; + const NotificationsPage = () => { const { t } = useTranslation(); + const [selectedChannel, setSelectedChannel] = useState(null); + + const { + data: notifications, + isLoading, + isValidating, + error, + refetch, + } = useGet("/notifications/team"); + return ( - Notifications + ); }; + export default NotificationsPage; diff --git a/client/src/Types/Notification.ts b/client/src/Types/Notification.ts new file mode 100644 index 000000000..669c840ca --- /dev/null +++ b/client/src/Types/Notification.ts @@ -0,0 +1,17 @@ +export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as const; +export type NotificationChannel = (typeof NotificationChannels)[number]; + +export interface Notification { + id: string; + userId: string; + teamId: string; + type: NotificationChannel; + notificationName: string; + address?: string; + phone?: string; + homeserverUrl?: string; + roomId?: string; + accessToken?: string; + createdAt: string; + updatedAt: string; +} diff --git a/client/src/locales/en.json b/client/src/locales/en.json index f955f3c22..960397021 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -337,6 +337,11 @@ "Keep administrators informed of system changes" ], "title": "Notification channles are used to:" + }, + "table": { + "headers": { + "destination": "Destination" + } } } }, From c1258d3cdf0c10c3f4eedaa5376d93bfea1caa34 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 Jan 2026 18:18:52 +0000 Subject: [PATCH 089/133] v2 --- .../components/NotificationsTable.tsx | 4 +-- client/src/Pages/Notifications/index.tsx | 27 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/client/src/Pages/Notifications/components/NotificationsTable.tsx b/client/src/Pages/Notifications/components/NotificationsTable.tsx index 0e804d276..306d8efa8 100644 --- a/client/src/Pages/Notifications/components/NotificationsTable.tsx +++ b/client/src/Pages/Notifications/components/NotificationsTable.tsx @@ -27,7 +27,7 @@ export const NotificationsTable = ({ id: 1, label: t("pages.common.monitors.actions.configure"), action: () => { - navigate(`/notification-channels/${channel.id}/configure`); + navigate(`/notifications/${channel.id}`); }, closeMenu: true, }, @@ -89,7 +89,7 @@ export const NotificationsTable = ({ headers={headers} data={notifications} onRowClick={(row) => { - navigate(`/notification-channels/${row.id}/configure`); + navigate(`/notifications/${row.id}`); }} /> ); diff --git a/client/src/Pages/Notifications/index.tsx b/client/src/Pages/Notifications/index.tsx index c5d710394..6e7dcc456 100644 --- a/client/src/Pages/Notifications/index.tsx +++ b/client/src/Pages/Notifications/index.tsx @@ -1,8 +1,9 @@ import { BasePageWithStates } from "@/Components/v2/design-elements"; import { NotificationsTable } from "@/Pages/Notifications/components/NotificationsTable"; +import { Dialog } from "@/Components/v2/inputs"; import { useState } from "react"; -import { useGet } from "@/Hooks/UseApi"; +import { useGet, useDelete } from "@/Hooks/UseApi"; import { useTranslation } from "react-i18next"; import type { Notification } from "@/Types/Notification"; @@ -10,6 +11,7 @@ const NotificationsPage = () => { const { t } = useTranslation(); const [selectedChannel, setSelectedChannel] = useState(null); + const isDialogOpen = Boolean(selectedChannel); const { data: notifications, @@ -17,7 +19,20 @@ const NotificationsPage = () => { isValidating, error, refetch, - } = useGet("/notifications/team"); + } = useGet("/notifications/team", {}, { keepPreviousData: true }); + + const { deleteFn, loading: isDeleting } = useDelete(); + + const handleConfirm = async () => { + if (!selectedChannel) return; + await deleteFn(`/notifications/${selectedChannel.id}`); + setSelectedChannel(null); + refetch(); + }; + + const handleCancel = () => { + setSelectedChannel(null); + }; return ( { notifications={notifications ?? []} setSelectedChannel={setSelectedChannel} /> + ); }; From b1db74d96fc7ee8d2602392b80243ddadc8fecb0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 Jan 2026 18:19:55 +0000 Subject: [PATCH 090/133] remove unused --- .../Notifications/components/ActionMenu.jsx | 83 ------------ client/src/Pages/Notifications/old.jsx | 121 ------------------ 2 files changed, 204 deletions(-) delete mode 100644 client/src/Pages/Notifications/components/ActionMenu.jsx delete mode 100644 client/src/Pages/Notifications/old.jsx diff --git a/client/src/Pages/Notifications/components/ActionMenu.jsx b/client/src/Pages/Notifications/components/ActionMenu.jsx deleted file mode 100644 index a3ccc4bbb..000000000 --- a/client/src/Pages/Notifications/components/ActionMenu.jsx +++ /dev/null @@ -1,83 +0,0 @@ -// Components -import Menu from "@mui/material/Menu"; -import IconButton from "@mui/material/IconButton"; -import Icon from "@/Components/v1/Icon"; -import MenuItem from "@mui/material/MenuItem"; - -// Utils -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import PropTypes from "prop-types"; -import { useTranslation } from "react-i18next"; - -const ActionMenu = ({ notification, onDelete }) => { - const theme = useTheme(); - const navigate = useNavigate(); - const [anchorEl, setAnchorEl] = useState(null); - const open = Boolean(anchorEl); - const { t } = useTranslation(); - // Handlers - const handleClick = (event) => { - event.stopPropagation(); - setAnchorEl(event.currentTarget); - }; - - const handleClose = (event) => { - if (event) { - event.stopPropagation(); - } - setAnchorEl(null); - }; - - const handleRemove = (e) => { - e.stopPropagation(); - onDelete(notification.id); - handleClose(); - }; - - const handleConfigure = (e) => { - e.stopPropagation(); - navigate(`/notifications/${notification.id}`); - handleClose(); - }; - - return ( - <> - e.stopPropagation()} - > - - - - e.stopPropagation()} - onMouseDown={(e) => e.stopPropagation()} - > - {t("configure")} - - {t("delete")} - - - - ); -}; - -ActionMenu.propTypes = { - notification: PropTypes.object, - onDelete: PropTypes.func, -}; - -export default ActionMenu; diff --git a/client/src/Pages/Notifications/old.jsx b/client/src/Pages/Notifications/old.jsx deleted file mode 100644 index 70ed3ef96..000000000 --- a/client/src/Pages/Notifications/old.jsx +++ /dev/null @@ -1,121 +0,0 @@ -// Components -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import Button from "@mui/material/Button"; -import DataTable from "@/Components/v1/Table/index.jsx"; -import ActionMenu from "./components/ActionMenu.jsx"; -import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; - -// Utils -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { - useGetNotificationsByTeamId, - useDeleteNotification, -} from "../../Hooks/useNotifications.js"; -import { useTranslation } from "react-i18next"; - -const Notifications = () => { - const navigate = useNavigate(); - const theme = useTheme(); - const BREADCRUMBS = [{ name: "notifications", path: "/notifications" }]; - const [updateTrigger, setUpdateTrigger] = useState(false); - const [notifications, isLoading, error] = useGetNotificationsByTeamId(updateTrigger); - const [deleteNotification, isDeleting, deleteError] = useDeleteNotification(); - const { t } = useTranslation(); - // Handlers - const triggerUpdate = () => { - setUpdateTrigger(!updateTrigger); - }; - - const onDelete = (id) => { - deleteNotification(id, triggerUpdate); - }; - - const headers = [ - { - id: "name", - content: "Name", - render: (row) => { - return row.notificationName; - }, - }, - { - id: "target", - content: "Target", - render: (row) => { - return row.address; - }, - }, - { - id: "platform", - content: "Platform", - render: (row) => { - return row?.config?.platform || row.type; - }, - }, - - { - id: "actions", - content: "Actions", - onClick: (e) => { - e.stopPropagation(); - }, - render: (row) => { - return ( - - ); - }, - }, - ]; - - return ( - <> - - - - - - - {t("notifications.createTitle")} - navigate(`/notifications/${row.id}`), - rowSX: { - cursor: "pointer", - "&:hover td": { - backgroundColor: theme.palette.tertiary.main, - transition: "background-color .3s ease", - }, - }, - }} - headers={headers} - data={notifications} - /> - - - - ); -}; - -export default Notifications; From fc98192bcff381746ebf5c6d000e230142511c2c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 Jan 2026 18:20:05 +0000 Subject: [PATCH 091/133] format --- client/src/Types/Notification.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/client/src/Types/Notification.ts b/client/src/Types/Notification.ts index 669c840ca..5b9645c9d 100644 --- a/client/src/Types/Notification.ts +++ b/client/src/Types/Notification.ts @@ -1,4 +1,11 @@ -export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as const; +export const NotificationChannels = [ + "email", + "slack", + "discord", + "webhook", + "pager_duty", + "matrix", +] as const; export type NotificationChannel = (typeof NotificationChannels)[number]; export interface Notification { From a154ad11b5ba12c47f17694d85ed7bcb9cbaaaab Mon Sep 17 00:00:00 2001 From: mannilakash Date: Thu, 29 Jan 2026 13:21:56 -0500 Subject: [PATCH 092/133] fix: latest incident panel: Box -> Grid --- .../LatestIncidentsPanel/IncidentItem.jsx | 67 +++++++++++-------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx index c9bf9abd2..e8001087b 100644 --- a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx +++ b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx @@ -6,6 +6,7 @@ import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js"; import Icon from "@/Components/v1/Icon"; import { useTranslation } from "react-i18next"; import useGetIncidentsDuration from "../../hooks/useGetIncidentsDuration"; +import { Grid } from "@mui/material"; const IncidentItem = ({ incident }) => { const { t } = useTranslation(); @@ -28,30 +29,24 @@ const IncidentItem = ({ incident }) => { }; return ( - - { > {incident.monitorName || t("incidentsPage.unknownMonitor")} - + - + { textTransform: "capitalize", }} /> - + - - {duration} - - + {duration} + + ); }; From c0ba278e47c7b831df975bac11c360a8faa3426c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 Jan 2026 18:34:50 +0000 Subject: [PATCH 093/133] loading state --- client/src/Pages/Notifications/index.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/src/Pages/Notifications/index.tsx b/client/src/Pages/Notifications/index.tsx index 6e7dcc456..daf59e74a 100644 --- a/client/src/Pages/Notifications/index.tsx +++ b/client/src/Pages/Notifications/index.tsx @@ -1,14 +1,17 @@ import { BasePageWithStates } from "@/Components/v2/design-elements"; import { NotificationsTable } from "@/Pages/Notifications/components/NotificationsTable"; import { Dialog } from "@/Components/v2/inputs"; +import { HeaderCreate } from "@/Components/v2/common"; import { useState } from "react"; import { useGet, useDelete } from "@/Hooks/UseApi"; import { useTranslation } from "react-i18next"; import type { Notification } from "@/Types/Notification"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; const NotificationsPage = () => { const { t } = useTranslation(); + const isAdmin = useIsAdmin(); const [selectedChannel, setSelectedChannel] = useState(null); const isDialogOpen = Boolean(selectedChannel); @@ -46,6 +49,11 @@ const NotificationsPage = () => { actionButtonText={t("pages.notifications.fallback.actionButton")} actionLink="/notifications/create" > + Date: Thu, 29 Jan 2026 18:41:46 +0000 Subject: [PATCH 094/133] initial commit --- client/package-lock.json | 4 ++-- client/package.json | 4 ++-- client/src/Pages/Notifications/create/index.tsx | 5 +++++ .../Pages/Notifications/create/{index.jsx => old.jsx} | 0 client/src/Routes/index.jsx | 10 ++++++++-- 5 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 client/src/Pages/Notifications/create/index.tsx rename client/src/Pages/Notifications/create/{index.jsx => old.jsx} (100%) diff --git a/client/package-lock.json b/client/package-lock.json index b4ad9f73c..d860ae844 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -29,7 +29,7 @@ "pretty-ms": "^9.3.0", "react": "18.3.1", "react-dom": "^18.2.0", - "react-hook-form": "7.63.0", + "react-hook-form": "^7.63.0", "react-i18next": "^15.4.0", "react-icons": "5.5.0", "react-redux": "9.2.0", @@ -40,7 +40,7 @@ "redux-persist": "6.0.0", "swr": "^2.3.6", "vite-plugin-svgr": "^4.2.0", - "zod": "4.1.11" + "zod": "^4.1.11" }, "devDependencies": { "@types/node": "24.5.2", diff --git a/client/package.json b/client/package.json index a5c17e343..09599143a 100644 --- a/client/package.json +++ b/client/package.json @@ -34,7 +34,7 @@ "pretty-ms": "^9.3.0", "react": "18.3.1", "react-dom": "^18.2.0", - "react-hook-form": "7.63.0", + "react-hook-form": "^7.63.0", "react-i18next": "^15.4.0", "react-icons": "5.5.0", "react-redux": "9.2.0", @@ -45,7 +45,7 @@ "redux-persist": "6.0.0", "swr": "^2.3.6", "vite-plugin-svgr": "^4.2.0", - "zod": "4.1.11" + "zod": "^4.1.11" }, "devDependencies": { "@types/node": "24.5.2", diff --git a/client/src/Pages/Notifications/create/index.tsx b/client/src/Pages/Notifications/create/index.tsx new file mode 100644 index 000000000..82cee4861 --- /dev/null +++ b/client/src/Pages/Notifications/create/index.tsx @@ -0,0 +1,5 @@ +const NotificationsCreatePage = () => { + return
Create Notifications Page
; +}; + +export default NotificationsCreatePage; diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/old.jsx similarity index 100% rename from client/src/Pages/Notifications/create/index.jsx rename to client/src/Pages/Notifications/create/old.jsx diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index e0725e8e2..4806e45bd 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -44,7 +44,7 @@ import StatusPages from "../Pages/StatusPage/StatusPages/index.jsx"; import Status from "../Pages/StatusPage/Status/index.jsx"; import Notifications from "../Pages/Notifications"; -import CreateNotifications from "../Pages/Notifications/create/index.jsx"; +import CreateNotifications from "../Pages/Notifications/create"; // Settings import Account from "../Pages/Account/index.jsx"; @@ -220,7 +220,13 @@ const Routes = () => { /> } + element={ + <> + + + + + } /> Date: Thu, 29 Jan 2026 19:29:36 +0000 Subject: [PATCH 095/133] form --- .../v2/design-elements/SplitBox.tsx | 78 +++++++++++++++++++ .../Components/v2/design-elements/index.tsx | 1 + client/src/Hooks/useNotificationForm.ts | 28 +++++++ .../src/Pages/Notifications/create/index.tsx | 75 +++++++++++++++++- client/src/Validation/notifications.ts | 51 ++++++++++++ client/src/locales/en.json | 8 ++ 6 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 client/src/Components/v2/design-elements/SplitBox.tsx create mode 100644 client/src/Hooks/useNotificationForm.ts create mode 100644 client/src/Validation/notifications.ts diff --git a/client/src/Components/v2/design-elements/SplitBox.tsx b/client/src/Components/v2/design-elements/SplitBox.tsx new file mode 100644 index 000000000..3a778dbcf --- /dev/null +++ b/client/src/Components/v2/design-elements/SplitBox.tsx @@ -0,0 +1,78 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; +export const SplitBox = ({ + left, + right, +}: { + left: React.ReactNode; + right: React.ReactNode; +}) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + return ( + + + {left} + + + {right} + + + ); +}; + +export const ConfigBox = ({ + title, + subtitle, + leftContent, + rightContent, +}: { + title: string; + subtitle: string; + leftContent?: React.ReactNode; + rightContent: React.ReactNode; +}) => { + const theme = useTheme(); + return ( + + + {title} + + {subtitle} + {leftContent} + + } + right={rightContent} + /> + ); +}; diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx index 33248a345..a96fea052 100644 --- a/client/src/Components/v2/design-elements/index.tsx +++ b/client/src/Components/v2/design-elements/index.tsx @@ -17,3 +17,4 @@ export * from "./StatBox"; export * from "./BaseChart"; export * from "./Gauge"; export * from "./Tabs"; +export * from "./SplitBox"; diff --git a/client/src/Hooks/useNotificationForm.ts b/client/src/Hooks/useNotificationForm.ts new file mode 100644 index 000000000..dd882db67 --- /dev/null +++ b/client/src/Hooks/useNotificationForm.ts @@ -0,0 +1,28 @@ +import { useMemo } from "react"; +import { notificationSchema } from "@/Validation/notifications"; +import type { Notification } from "@/Types/Notification"; + +interface UseNotificationFormOptions { + data?: Notification | null; +} + +export const useNotificationForm = ({ data = null }: UseNotificationFormOptions = {}) => { + return useMemo(() => { + const defaults = + data?.type === "matrix" + ? { + type: "matrix" as const, + notificationName: data.notificationName || "", + homeserverUrl: data.homeserverUrl || "", + roomId: data.roomId || "", + accessToken: data.accessToken || "", + } + : { + type: (data?.type || "email") as Exclude, + notificationName: data?.notificationName || "", + address: data?.address || "", + }; + + return { schema: notificationSchema, defaults }; + }, [data]); +}; diff --git a/client/src/Pages/Notifications/create/index.tsx b/client/src/Pages/Notifications/create/index.tsx index 82cee4861..58b14a426 100644 --- a/client/src/Pages/Notifications/create/index.tsx +++ b/client/src/Pages/Notifications/create/index.tsx @@ -1,5 +1,78 @@ +import { BasePage, ConfigBox } from "@/Components/v2/design-elements"; +import { TextField } from "@/Components/v2/inputs"; + +import { useEffect } from "react"; +import { useParams } from "react-router-dom"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useGet } from "@/Hooks/UseApi"; +import { useNotificationForm } from "@/Hooks/useNotificationForm"; +import type { NotificationFormData } from "@/Validation/notifications"; +import type { Notification } from "@/Types/Notification"; +import { useTranslation } from "react-i18next"; + const NotificationsCreatePage = () => { - return
Create Notifications Page
; + const { t } = useTranslation(); + const { notificationId } = useParams<{ notificationId: string }>(); + const isEditMode = Boolean(notificationId); + + const { data: existingNotification, isLoading: notificationIsLoading } = + useGet(isEditMode ? `/notifications/${notificationId}` : null); + + const { schema, defaults } = useNotificationForm({ data: existingNotification }); + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: defaults, + }); + + const { + register, + control, + watch, + reset, + formState: { errors }, + } = form; + + // Reset form when defaults change (i.e., when data is fetched) + useEffect(() => { + reset(defaults); + }, [defaults, reset]); + + const watchedType = watch("type"); + + // Suppress unused variable warnings - these will be used when UI is built + void register; + void control; + + return ( + + ( + + )} + /> + } + /> + + ); }; export default NotificationsCreatePage; diff --git a/client/src/Validation/notifications.ts b/client/src/Validation/notifications.ts new file mode 100644 index 000000000..c78cf1ef9 --- /dev/null +++ b/client/src/Validation/notifications.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; + +const baseSchema = z.object({ + notificationName: z + .string() + .min(1, "Notification name is required") + .max(100, "Notification name must be at most 100 characters"), +}); + +const emailSchema = baseSchema.extend({ + type: z.literal("email"), + address: z.string().min(1, "Email is required").email("Please enter a valid email address"), +}); + +const slackSchema = baseSchema.extend({ + type: z.literal("slack"), + address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"), +}); + +const discordSchema = baseSchema.extend({ + type: z.literal("discord"), + address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"), +}); + +const webhookSchema = baseSchema.extend({ + type: z.literal("webhook"), + address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"), +}); + +const pagerDutySchema = baseSchema.extend({ + type: z.literal("pager_duty"), + address: z.string().min(1, "Integration key is required"), +}); + +const matrixSchema = baseSchema.extend({ + type: z.literal("matrix"), + homeserverUrl: z.string().min(1, "Homeserver URL is required").url("Please enter a valid URL"), + roomId: z.string().min(1, "Room ID is required"), + accessToken: z.string().min(1, "Access token is required"), +}); + +export const notificationSchema = z.discriminatedUnion("type", [ + emailSchema, + slackSchema, + discordSchema, + webhookSchema, + pagerDutySchema, + matrixSchema, +]); + +export type NotificationFormData = z.infer; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 960397021..b98b43b7d 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -342,6 +342,14 @@ "headers": { "destination": "Destination" } + }, + "form": { + "notificationName": { + "description": "A descriptive name for the notification channel", + "optionName": "Channel name", + "placeholder": "e.g. Production Alerts", + "title": "Channel name" + } } } }, From 22ae6b4be618cdee1b6604925560d383a5efddf0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 Jan 2026 19:34:49 +0000 Subject: [PATCH 096/133] type --- .../src/Pages/Notifications/create/index.tsx | 37 ++++++++++++++++++- client/src/locales/en.json | 5 +++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/client/src/Pages/Notifications/create/index.tsx b/client/src/Pages/Notifications/create/index.tsx index 58b14a426..1c4d586a9 100644 --- a/client/src/Pages/Notifications/create/index.tsx +++ b/client/src/Pages/Notifications/create/index.tsx @@ -1,5 +1,7 @@ import { BasePage, ConfigBox } from "@/Components/v2/design-elements"; -import { TextField } from "@/Components/v2/inputs"; +import { TextField, Select } from "@/Components/v2/inputs"; +import MenuItem from "@mui/material/MenuItem"; +import Typography from "@mui/material/Typography"; import { useEffect } from "react"; import { useParams } from "react-router-dom"; @@ -10,6 +12,7 @@ import { useNotificationForm } from "@/Hooks/useNotificationForm"; import type { NotificationFormData } from "@/Validation/notifications"; import type { Notification } from "@/Types/Notification"; import { useTranslation } from "react-i18next"; +import { NotificationChannels } from "@/Types/Notification"; const NotificationsCreatePage = () => { const { t } = useTranslation(); @@ -71,6 +74,38 @@ const NotificationsCreatePage = () => { /> } /> + { + return ( + + ); + }} + /> + } + /> ); }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index b98b43b7d..7ffa5e00e 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -349,6 +349,11 @@ "optionName": "Channel name", "placeholder": "e.g. Production Alerts", "title": "Channel name" + }, + "type": { + "description": "Select the type of notification channel to create.", + "optionType": "Type", + "title": "Channel type" } } } From a25ff03b71d9f0e16476c71c3804bfdf7d049869 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 Jan 2026 20:12:50 +0000 Subject: [PATCH 097/133] remaining fields --- .../src/Pages/Notifications/create/index.tsx | 143 ++++++++++++++++-- client/src/locales/en.json | 31 ++++ 2 files changed, 163 insertions(+), 11 deletions(-) diff --git a/client/src/Pages/Notifications/create/index.tsx b/client/src/Pages/Notifications/create/index.tsx index 1c4d586a9..29a6ea2f6 100644 --- a/client/src/Pages/Notifications/create/index.tsx +++ b/client/src/Pages/Notifications/create/index.tsx @@ -3,7 +3,7 @@ import { TextField, Select } from "@/Components/v2/inputs"; import MenuItem from "@mui/material/MenuItem"; import Typography from "@mui/material/Typography"; -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { useParams } from "react-router-dom"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -19,8 +19,9 @@ const NotificationsCreatePage = () => { const { notificationId } = useParams<{ notificationId: string }>(); const isEditMode = Boolean(notificationId); - const { data: existingNotification, isLoading: notificationIsLoading } = - useGet(isEditMode ? `/notifications/${notificationId}` : null); + const { data: existingNotification } = useGet( + isEditMode ? `/notifications/${notificationId}` : null + ); const { schema, defaults } = useNotificationForm({ data: existingNotification }); @@ -34,7 +35,7 @@ const NotificationsCreatePage = () => { control, watch, reset, - formState: { errors }, + formState: {}, } = form; // Reset form when defaults change (i.e., when data is fetched) @@ -44,6 +45,32 @@ const NotificationsCreatePage = () => { const watchedType = watch("type"); + const addressConfig = useMemo(() => { + if (watchedType === "pager_duty") { + return { + title: t("pages.notifications.form.pagerDuty.title"), + description: t("pages.notifications.form.pagerDuty.description"), + fieldLabel: t("pages.notifications.form.pagerDuty.optionIntegrationKey"), + placeholder: t("pages.notifications.form.pagerDuty.placeholder"), + }; + } + if (watchedType === "email") { + return { + title: t("pages.notifications.form.address.title"), + description: t("pages.notifications.form.address.description"), + fieldLabel: t("pages.notifications.form.address.optionAddress"), + placeholder: t("pages.notifications.form.address.placeholderEmail"), + }; + } + return { + title: t("pages.notifications.form.address.title"), + description: t("pages.notifications.form.address.description"), + fieldLabel: t("pages.notifications.form.address.optionAddress"), + placeholder: t("pages.notifications.form.address.placeholderWebhook"), + }; + }, [watchedType, t]); + + // Suppress unused variable warnings - these will be used when UI is built void register; void control; @@ -58,17 +85,15 @@ const NotificationsCreatePage = () => { name="notificationName" control={control} defaultValue={defaults.notificationName} - render={({ field }) => ( + render={({ field, fieldState }) => ( )} /> @@ -82,12 +107,12 @@ const NotificationsCreatePage = () => { name="type" control={control} defaultValue={defaults.type} - render={({ field }) => { + render={({ field, fieldState }) => { return ( - {NotificationChannels.map((type: string) => { - return ( - - {type} - - ); - })} - - ); - }} + render={({ field, fieldState }) => ( + + )} /> } /> @@ -157,76 +160,86 @@ const NotificationsCreatePage = () => { )} {watchedType === "matrix" && ( ( - - )} - /> - } - /> - )} - {watchedType === "matrix" && ( - ( - - )} - /> - } - /> - )} - {watchedType === "matrix" && ( - ( - - )} - /> + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + } /> )} + + + + ); }; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 4806e45bd..10ee6f9cb 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -231,7 +231,13 @@ const Routes = () => { } + element={ + <> + + + + + } /> Date: Thu, 29 Jan 2026 20:47:47 +0000 Subject: [PATCH 099/133] submit --- .../src/Pages/Notifications/create/index.tsx | 120 +++++++++--------- 1 file changed, 61 insertions(+), 59 deletions(-) diff --git a/client/src/Pages/Notifications/create/index.tsx b/client/src/Pages/Notifications/create/index.tsx index 30e098857..b5b98a7a0 100644 --- a/client/src/Pages/Notifications/create/index.tsx +++ b/client/src/Pages/Notifications/create/index.tsx @@ -7,9 +7,10 @@ import { useTheme } from "@mui/material/styles"; import { useEffect, useMemo } from "react"; import { useParams } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useGet } from "@/Hooks/UseApi"; +import { useGet, usePost } from "@/Hooks/UseApi"; import { useNotificationForm } from "@/Hooks/useNotificationForm"; import type { NotificationFormData } from "@/Validation/notifications"; import type { Notification } from "@/Types/Notification"; @@ -17,73 +18,74 @@ import { useTranslation } from "react-i18next"; import { NotificationChannels } from "@/Types/Notification"; const NotificationsCreatePage = () => { - const { t } = useTranslation(); - const theme = useTheme(); - const { notificationId } = useParams<{ notificationId: string }>(); - const isEditMode = Boolean(notificationId); + const { t } = useTranslation(); + const theme = useTheme(); + const navigate = useNavigate(); + const { notificationId } = useParams<{ notificationId: string }>(); + const isEditMode = Boolean(notificationId); - const { data: existingNotification } = useGet( - isEditMode ? `/notifications/${notificationId}` : null - ); + const { data: existingNotification } = useGet( + isEditMode ? `/notifications/${notificationId}` : null + ); - const { schema, defaults } = useNotificationForm({ data: existingNotification }); + const { post, loading: isSubmitting } = usePost(); - const form = useForm({ - resolver: zodResolver(schema), - defaultValues: defaults, - }); + const { schema, defaults } = useNotificationForm({ data: existingNotification }); - const { - control, - watch, - reset, - handleSubmit, - } = form; + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: defaults, + }); - useEffect(() => { - reset(defaults); - }, [defaults, reset]); + const { control, watch, reset, handleSubmit } = form; - const watchedType = watch("type"); + useEffect(() => { + reset(defaults); + }, [defaults, reset]); - const addressConfig = useMemo(() => { - if (watchedType === "pager_duty") { - return { - title: t("pages.notifications.form.pagerDuty.title"), - description: t("pages.notifications.form.pagerDuty.description"), - fieldLabel: t("pages.notifications.form.pagerDuty.optionIntegrationKey"), - placeholder: t("pages.notifications.form.pagerDuty.placeholder"), - }; - } - if (watchedType === "email") { - return { - title: t("pages.notifications.form.address.title"), - description: t("pages.notifications.form.address.description"), - fieldLabel: t("pages.notifications.form.address.optionAddress"), - placeholder: t("pages.notifications.form.address.placeholderEmail"), - }; - } - return { - title: t("pages.notifications.form.address.title"), - description: t("pages.notifications.form.address.description"), - fieldLabel: t("pages.notifications.form.address.optionAddress"), - placeholder: t("pages.notifications.form.address.placeholderWebhook"), - }; - }, [watchedType, t]); + const watchedType = watch("type"); - const onSubmit = (data: NotificationFormData) => { - console.log(data); - }; + const addressConfig = useMemo(() => { + if (watchedType === "pager_duty") { + return { + title: t("pages.notifications.form.pagerDuty.title"), + description: t("pages.notifications.form.pagerDuty.description"), + fieldLabel: t("pages.notifications.form.pagerDuty.optionIntegrationKey"), + placeholder: t("pages.notifications.form.pagerDuty.placeholder"), + }; + } + if (watchedType === "email") { + return { + title: t("pages.notifications.form.address.title"), + description: t("pages.notifications.form.address.description"), + fieldLabel: t("pages.notifications.form.address.optionAddress"), + placeholder: t("pages.notifications.form.address.placeholderEmail"), + }; + } + return { + title: t("pages.notifications.form.address.title"), + description: t("pages.notifications.form.address.description"), + fieldLabel: t("pages.notifications.form.address.optionAddress"), + placeholder: t("pages.notifications.form.address.placeholderWebhook"), + }; + }, [watchedType, t]); - const handleTest = () => { - console.log("Test notification"); - }; + const onSubmit = async (data: NotificationFormData) => { + const result = await post("/notifications", data); + if (result) { + navigate("/notifications"); + } + }; - return ( - + const handleTest = () => { + console.log("Test notification"); + }; + + return ( + { {t("common.buttons.test")} From 2a3683c74a8bca9d6eebf7d65e6b4e6297ae3a14 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 Jan 2026 21:08:26 +0000 Subject: [PATCH 103/133] breadcurmbs --- .../v2/design-elements/Breadcrumb.tsx | 23 +++++++++++++++---- .../components/NotificationsTable.tsx | 4 ++-- client/src/Routes/index.jsx | 2 +- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/client/src/Components/v2/design-elements/Breadcrumb.tsx b/client/src/Components/v2/design-elements/Breadcrumb.tsx index 492ed0ec9..1dea2dff6 100644 --- a/client/src/Components/v2/design-elements/Breadcrumb.tsx +++ b/client/src/Components/v2/design-elements/Breadcrumb.tsx @@ -8,15 +8,28 @@ const isId = (segment: string): boolean => { return segment.length === 24 || /^[a-f0-9-]{36}$/.test(segment); }; +const actionSegments = ["create", "configure"]; + export const Breadcrumb = () => { const theme = useTheme(); const location = useLocation(); const segments = location.pathname.split("/").filter((x) => x); - // Build simplified breadcrumb: "uptime" or "uptime / details" + // Build simplified breadcrumb: "uptime" or "uptime / details" or "uptime / create" const basePage = segments[0] || "home"; - const hasDetailsPage = segments.length > 1 && isId(segments[1]); + + const secondSegment = segments[1]; + const isActionPage = secondSegment && actionSegments.includes(secondSegment); // create/config + const isDetailsPage = secondSegment && isId(secondSegment); // details + const hasSubPage = isActionPage || isDetailsPage; + + const getSubPageLabel = (): string => { + if (isActionPage) { + return secondSegment.charAt(0).toUpperCase() + secondSegment.slice(1); + } + return "Details"; + }; return ( { }, }} > - {hasDetailsPage ? ( + {hasSubPage ? ( { {basePage.charAt(0).toUpperCase() + basePage.slice(1)} )} - {hasDetailsPage && ( + {hasSubPage && ( { color: theme.palette.primary.main, }} > - Details + {getSubPageLabel()} )} diff --git a/client/src/Pages/Notifications/components/NotificationsTable.tsx b/client/src/Pages/Notifications/components/NotificationsTable.tsx index 306d8efa8..2885a34bb 100644 --- a/client/src/Pages/Notifications/components/NotificationsTable.tsx +++ b/client/src/Pages/Notifications/components/NotificationsTable.tsx @@ -27,7 +27,7 @@ export const NotificationsTable = ({ id: 1, label: t("pages.common.monitors.actions.configure"), action: () => { - navigate(`/notifications/${channel.id}`); + navigate(`/notifications/configure/${channel.id}`); }, closeMenu: true, }, @@ -89,7 +89,7 @@ export const NotificationsTable = ({ headers={headers} data={notifications} onRowClick={(row) => { - navigate(`/notifications/${row.id}`); + navigate(`/notifications/configure/${row.id}`); }} /> ); diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 10ee6f9cb..498be4ba7 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -230,7 +230,7 @@ const Routes = () => { /> From 348eea5475cce9b04b0e2c3eb92f99431ba7c221 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 Jan 2026 21:08:36 +0000 Subject: [PATCH 104/133] breadcrumb fix --- client/src/Components/v2/design-elements/Breadcrumb.tsx | 6 ++++-- client/src/locales/en.json | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/client/src/Components/v2/design-elements/Breadcrumb.tsx b/client/src/Components/v2/design-elements/Breadcrumb.tsx index 1dea2dff6..e0a310c9f 100644 --- a/client/src/Components/v2/design-elements/Breadcrumb.tsx +++ b/client/src/Components/v2/design-elements/Breadcrumb.tsx @@ -3,6 +3,7 @@ import Typography from "@mui/material/Typography"; import { Link, useLocation } from "react-router-dom"; import { useTheme } from "@mui/material/styles"; import { ChevronRight } from "lucide-react"; +import { useTranslation } from "react-i18next"; const isId = (segment: string): boolean => { return segment.length === 24 || /^[a-f0-9-]{36}$/.test(segment); @@ -11,13 +12,14 @@ const isId = (segment: string): boolean => { const actionSegments = ["create", "configure"]; export const Breadcrumb = () => { + const { t } = useTranslation(); const theme = useTheme(); const location = useLocation(); const segments = location.pathname.split("/").filter((x) => x); // Build simplified breadcrumb: "uptime" or "uptime / details" or "uptime / create" - const basePage = segments[0] || "home"; + const basePage = segments[0] || t("common.breadcrumbs.home"); const secondSegment = segments[1]; const isActionPage = secondSegment && actionSegments.includes(secondSegment); // create/config @@ -28,7 +30,7 @@ export const Breadcrumb = () => { if (isActionPage) { return secondSegment.charAt(0).toUpperCase() + secondSegment.slice(1); } - return "Details"; + return t("common.breadcrumbs.details"); }; return ( diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 79e901dc1..80da1835c 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -2,6 +2,10 @@ "common": { "appName": "Checkmate", "monitoringAgentName": "Capture", + "breadcrumbs": { + "home": "Home", + "details": "Details" + }, "labels": { "responseTime": "Response time" }, From 2dca5ccb6715eb118b6fed4fa8a679167f06c53e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 Jan 2026 21:09:00 +0000 Subject: [PATCH 105/133] format --- client/src/Validation/notifications.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/src/Validation/notifications.ts b/client/src/Validation/notifications.ts index c78cf1ef9..e505c695f 100644 --- a/client/src/Validation/notifications.ts +++ b/client/src/Validation/notifications.ts @@ -9,7 +9,10 @@ const baseSchema = z.object({ const emailSchema = baseSchema.extend({ type: z.literal("email"), - address: z.string().min(1, "Email is required").email("Please enter a valid email address"), + address: z + .string() + .min(1, "Email is required") + .email("Please enter a valid email address"), }); const slackSchema = baseSchema.extend({ @@ -34,7 +37,10 @@ const pagerDutySchema = baseSchema.extend({ const matrixSchema = baseSchema.extend({ type: z.literal("matrix"), - homeserverUrl: z.string().min(1, "Homeserver URL is required").url("Please enter a valid URL"), + homeserverUrl: z + .string() + .min(1, "Homeserver URL is required") + .url("Please enter a valid URL"), roomId: z.string().min(1, "Room ID is required"), accessToken: z.string().min(1, "Access token is required"), }); From a0aee182dec300cb0f0b032bd55cff783d82d689 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 Jan 2026 21:15:14 +0000 Subject: [PATCH 106/133] remove unused --- .../NotificationIntegrationModal.jsx | 419 ------------------ .../Components/TabComponent.jsx | 115 ----- .../Components/TabPanel.jsx | 41 -- .../Hooks/useNotification.js | 129 ------ client/src/Hooks/useNotifications.js | 152 +------ client/src/Pages/Notifications/create/old.jsx | 320 ------------- client/src/Pages/Notifications/utils.js | 44 -- 7 files changed, 1 insertion(+), 1219 deletions(-) delete mode 100644 client/src/Components/v1/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx delete mode 100644 client/src/Components/v1/NotificationIntegrationModal/Components/TabComponent.jsx delete mode 100644 client/src/Components/v1/NotificationIntegrationModal/Components/TabPanel.jsx delete mode 100644 client/src/Components/v1/NotificationIntegrationModal/Hooks/useNotification.js delete mode 100644 client/src/Pages/Notifications/create/old.jsx delete mode 100644 client/src/Pages/Notifications/utils.js diff --git a/client/src/Components/v1/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx b/client/src/Components/v1/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx deleted file mode 100644 index 2aae996f6..000000000 --- a/client/src/Components/v1/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx +++ /dev/null @@ -1,419 +0,0 @@ -import { useState, useMemo, useEffect, useCallback } from "react"; -import { useTranslation } from "react-i18next"; -import PropTypes from "prop-types"; - -import { - Dialog, - DialogContent, - DialogActions, - Button, - Typography, - Box, - Tabs, - Tab, - Stack, -} from "@mui/material"; -import { useTheme } from "@emotion/react"; -import TabPanel from "./TabPanel.jsx"; -import TabComponent from "./TabComponent.jsx"; -import useNotifications from "../Hooks/useNotification.js"; - -// Define constants for notification types to avoid magic values -const NOTIFICATION_TYPES = { - SLACK: "slack", - DISCORD: "discord", - TELEGRAM: "telegram", - WEBHOOK: "webhook", -}; - -// Define constants for field IDs -const FIELD_IDS = { - WEBHOOK: "webhook", - TOKEN: "token", - CHAT_ID: "chatId", - URL: "url", -}; - -const NotificationIntegrationModal = ({ - open, - onClose, - monitor, - setMonitor, - // Optional prop to configure available notification types - notificationTypes = null, -}) => { - const { t } = useTranslation(); - const theme = useTheme(); - const [tabValue, setTabValue] = useState(0); - - const [loading, _, sendTestNotification] = useNotifications(); - - // Helper to get the field state key with error handling - const getFieldKey = useCallback( - (typeId, fieldId) => { - if (typeof typeId !== "string" || typeId === "") { - throw new Error(t("errorInvalidTypeId")); - } - - if (typeof fieldId !== "string" || fieldId === "") { - throw new Error(t("errorInvalidFieldId")); - } - - return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`; - }, - [t] - ); - - // Define notification types - const DEFAULT_NOTIFICATION_TYPES = [ - { - id: NOTIFICATION_TYPES.SLACK, - label: t("notifications.slack.label"), - description: t("notifications.slack.description"), - fields: [ - { - id: FIELD_IDS.WEBHOOK, - label: t("notifications.slack.webhookLabel"), - placeholder: t("notifications.slack.webhookPlaceholder"), - type: "text", - }, - ], - }, - { - id: NOTIFICATION_TYPES.DISCORD, - label: t("notifications.discord.label"), - description: t("notifications.discord.description"), - fields: [ - { - id: FIELD_IDS.WEBHOOK, - label: t("notifications.discord.webhookLabel"), - placeholder: t("notifications.discord.webhookPlaceholder"), - type: "text", - }, - ], - }, - { - id: NOTIFICATION_TYPES.TELEGRAM, - label: t("notifications.telegram.label"), - description: t("notifications.telegram.description"), - fields: [ - { - id: FIELD_IDS.TOKEN, - label: t("notifications.telegram.tokenLabel"), - placeholder: t("notifications.telegram.tokenPlaceholder"), - type: "text", - }, - { - id: FIELD_IDS.CHAT_ID, - label: t("notifications.telegram.chatIdLabel"), - placeholder: t("notifications.telegram.chatIdPlaceholder"), - type: "text", - }, - ], - }, - { - id: NOTIFICATION_TYPES.WEBHOOK, - label: t("notifications.webhook.label"), - description: t("notifications.webhook.description"), - fields: [ - { - id: FIELD_IDS.URL, - label: t("notifications.webhook.urlLabel"), - placeholder: t("notifications.webhook.urlPlaceholder"), - type: "text", - }, - ], - }, - ]; - - // Use provided notification types or default to our translated ones - const activeNotificationTypes = notificationTypes || DEFAULT_NOTIFICATION_TYPES; - - // Memoized function to initialize integrations state - const initialIntegrationsState = useMemo(() => { - const state = {}; - - activeNotificationTypes.forEach((type) => { - // Add enabled flag for each notification type - state[type.id] = false; - - // Add state for each field in the notification type - type.fields.forEach((field) => { - const fieldKey = getFieldKey(type.id, field.id); - state[fieldKey] = ""; - }); - }); - - return state; - }, [activeNotificationTypes, getFieldKey]); // Only recompute when these dependencies change - - const [integrations, setIntegrations] = useState(initialIntegrationsState); - - useEffect(() => { - if (open) { - const extractNotificationValues = () => { - const values = {}; - - if (!monitor?.notifications || !Array.isArray(monitor.notifications)) { - return values; - } - - monitor.notifications.forEach((notification) => { - // Handle notification based on its structure - if (notification.type === "webhook" && notification.platform) { - if (typeof notification.config === "undefined") return; - const platform = notification.platform; - values[platform] = true; // Set platform as enabled - - // Extract configuration based on platform - switch (platform) { - case NOTIFICATION_TYPES.SLACK: - case NOTIFICATION_TYPES.DISCORD: - if (notification.config.webhookUrl) { - values[getFieldKey(platform, FIELD_IDS.WEBHOOK)] = - notification.config.webhookUrl; - } - break; - case NOTIFICATION_TYPES.TELEGRAM: - if (notification.config.botToken) { - values[getFieldKey(platform, FIELD_IDS.TOKEN)] = - notification.config.botToken; - } - if (notification.config.chatId) { - values[getFieldKey(platform, FIELD_IDS.CHAT_ID)] = - notification.config.chatId; - } - break; - case NOTIFICATION_TYPES.WEBHOOK: - if (notification.config.webhookUrl) { - values[getFieldKey(platform, FIELD_IDS.URL)] = - notification.config.webhookUrl; - } - break; - } - } - }); - - return values; - }; - - const extractedValues = extractNotificationValues(); - setIntegrations((prev) => ({ - ...prev, - ...extractedValues, - })); - } - }, [open, monitor, getFieldKey]); - - const handleChangeTab = (event, newValue) => { - setTabValue(newValue); - }; - - const handleIntegrationChange = (type, checked) => { - setIntegrations((prev) => ({ - ...prev, - [type]: checked, - })); - }; - - const handleInputChange = (type, value) => { - setIntegrations((prev) => ({ - ...prev, - [type]: value, - })); - }; - - const handleTestNotification = async (type) => { - // Get the notification type details - const notificationType = activeNotificationTypes.find((t) => t.id === type); - - if (typeof notificationType === "undefined") { - return; - } - - // Prepare config object based on notification type - const config = {}; - - // Add each field value to the config object - notificationType.fields.forEach((field) => { - const fieldKey = getFieldKey(type, field.id); - config[field.id] = integrations[fieldKey]; - }); - - await sendTestNotification(type, config); - }; - - // In NotificationIntegrationModal.jsx, update the handleSave function: - - const handleSave = () => { - // Get existing notifications - const notifications = [...(monitor?.notifications || [])]; - - // Get all notification types IDs - const existingTypes = activeNotificationTypes.map((type) => type.id); - - // Filter out notifications that are configurable in this modal - const filteredNotifications = notifications.filter((notification) => { - if (notification.platform) { - return !existingTypes.includes(notification.platform); - } - - return !existingTypes.includes(notification.type); - }); - - // Add each enabled notification with its configured fields - activeNotificationTypes.forEach((type) => { - if (integrations[type.id]) { - let notificationObject = { - type: "webhook", - platform: type.id, // Set platform to identify the specific service - config: {}, - }; - - // Configure based on notification type - switch (type.id) { - case "slack": - case "discord": - notificationObject.config.webhookUrl = - integrations[getFieldKey(type.id, "webhook")]; - break; - case "telegram": - notificationObject.config.botToken = - integrations[getFieldKey(type.id, "token")]; - notificationObject.config.chatId = - integrations[getFieldKey(type.id, "chatId")]; - break; - case "webhook": - notificationObject.config.webhookUrl = - integrations[getFieldKey(type.id, "url")]; - break; - } - - filteredNotifications.push(notificationObject); - } - }); - - // Update monitor with new notifications - setMonitor((prev) => ({ - ...prev, - notifications: filteredNotifications, - })); - - onClose(); - }; - - return ( - - - - {/* Left sidebar with tabs */} - - - {t("notifications.addOrEditNotifications")} - - - - {activeNotificationTypes.map((type) => ( - - ))} - - - - {/* Right side content */} - - {activeNotificationTypes.map((type, index) => ( - - - - ))} - - - - - - - - ); -}; - -NotificationIntegrationModal.propTypes = { - open: PropTypes.bool.isRequired, - onClose: PropTypes.func.isRequired, - monitor: PropTypes.object.isRequired, - setMonitor: PropTypes.func.isRequired, - notificationTypes: PropTypes.array, -}; - -export default NotificationIntegrationModal; diff --git a/client/src/Components/v1/NotificationIntegrationModal/Components/TabComponent.jsx b/client/src/Components/v1/NotificationIntegrationModal/Components/TabComponent.jsx deleted file mode 100644 index 87fb2c301..000000000 --- a/client/src/Components/v1/NotificationIntegrationModal/Components/TabComponent.jsx +++ /dev/null @@ -1,115 +0,0 @@ -import React from "react"; -import { Typography, Box, Button, CircularProgress } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { useTheme } from "@emotion/react"; -import TextInput from "../../Inputs/TextInput/index.jsx"; -import Checkbox from "../../Inputs/Checkbox/index.jsx"; - -const TabComponent = ({ - type, - integrations, - handleIntegrationChange, - handleInputChange, - handleTestNotification, - isLoading, -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - - // Helper to get the field state key (e.g., slackWebhook, telegramToken) - const getFieldKey = (typeId, fieldId) => { - return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`; - }; - - // Check if all fields have values to enable test button - const areAllFieldsFilled = () => { - return type.fields.every((field) => { - const fieldKey = getFieldKey(type.id, field.id); - return integrations[fieldKey]; - }); - }; - - return ( - <> - - {type.label} - - - - {type.description} - - - - handleIntegrationChange(type.id, e.target.checked)} - disabled={isLoading} - /> - - - {type.fields.map((field) => { - const fieldKey = getFieldKey(type.id, field.id); - - return ( - - - {field.label} - - - handleInputChange(fieldKey, e.target.value)} - disabled={!integrations[type.id] || isLoading} - /> - - ); - })} - - - - - - ); -}; - -export default TabComponent; diff --git a/client/src/Components/v1/NotificationIntegrationModal/Components/TabPanel.jsx b/client/src/Components/v1/NotificationIntegrationModal/Components/TabPanel.jsx deleted file mode 100644 index 93546e7bc..000000000 --- a/client/src/Components/v1/NotificationIntegrationModal/Components/TabPanel.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; -import { Box } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -/** - * TabPanel component that displays content for the selected tab. - * - * @component - * @param {Object} props - The component props. - * @param {React.ReactNode} props.children - The content to be displayed when this tab panel is selected. - * @param {number} props.value - The currently selected tab value. - * @param {number} props.index - The index of this specific tab panel. - * @param {Object} props.other - Any additional props to be spread to the root element. - * @returns {React.ReactElement|null} The rendered tab panel or null if not selected. - */ -function TabPanel({ children, value, index, ...other }) { - const theme = useTheme(); - - return ( - - ); -} - -TabPanel.propTypes = { - children: PropTypes.node, - - index: PropTypes.number.isRequired, - - value: PropTypes.number.isRequired, -}; - -export default TabPanel; diff --git a/client/src/Components/v1/NotificationIntegrationModal/Hooks/useNotification.js b/client/src/Components/v1/NotificationIntegrationModal/Hooks/useNotification.js deleted file mode 100644 index c0712fa5f..000000000 --- a/client/src/Components/v1/NotificationIntegrationModal/Hooks/useNotification.js +++ /dev/null @@ -1,129 +0,0 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { networkService } from "../../../../Utils/NetworkService.js"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; - -// Define constants for notification types to avoid magic values -const NOTIFICATION_TYPES = { - SLACK: "slack", - DISCORD: "discord", - TELEGRAM: "telegram", - WEBHOOK: "webhook", -}; - -// Define constants for field IDs -const FIELD_IDS = { - WEBHOOK: "webhook", - TOKEN: "token", - CHAT_ID: "chatId", - URL: "url", -}; - -/** - * Custom hook for notification-related operations - */ -const useNotifications = () => { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(undefined); - const { t } = useTranslation(); - - /** - * Send a test notification - * @param {string} type - The notification type (slack, discord, telegram, webhook) - * @param {object} config - Configuration object with necessary params - */ - const sendTestNotification = async (type, config) => { - setLoading(true); - setError(undefined); - - // Validation based on notification type - let payload = { platform: type }; - let isValid = true; - let errorMessage = ""; - - switch (type) { - case NOTIFICATION_TYPES.SLACK: - payload.webhookUrl = config.webhook; - if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") { - isValid = false; - errorMessage = t("notifications.slack.webhookRequired"); - } - break; - - case NOTIFICATION_TYPES.DISCORD: - payload.webhookUrl = config.webhook; - if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") { - isValid = false; - errorMessage = t("notifications.discord.webhookRequired"); - } - break; - - case NOTIFICATION_TYPES.TELEGRAM: - payload.botToken = config.token; - payload.chatId = config.chatId; - if ( - typeof payload.botToken === "undefined" || - payload.botToken === "" || - typeof payload.chatId === "undefined" || - payload.chatId === "" - ) { - isValid = false; - errorMessage = t("notifications.telegram.fieldsRequired"); - } - break; - - case NOTIFICATION_TYPES.WEBHOOK: - payload.webhookUrl = config.url; - payload.platform = NOTIFICATION_TYPES.SLACK; - if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") { - isValid = false; - errorMessage = t("notifications.webhook.urlRequired"); - } - break; - - default: - isValid = false; - errorMessage = t("notifications.unsupportedType"); - } - - // If validation fails, show error and return - if (isValid === false) { - createToast({ - body: errorMessage, - variant: "error", - }); - setLoading(false); - return; - } - - try { - const response = await networkService.testNotification({ - platform: type, - payload: payload, - }); - - if (response.data.success === true) { - createToast({ - body: t("notifications.testSuccess"), - variant: "info", - }); - } else { - throw new Error(response.data.msg || t("notifications.testFailed")); - } - } catch (error) { - const errorMsg = - error.response?.data?.msg || error.message || t("notifications.networkError"); - createToast({ - body: `${t("notifications.testFailed")}: ${errorMsg}`, - variant: "error", - }); - setError(errorMsg); - } finally { - setLoading(false); - } - }; - - return [loading, error, sendTestNotification]; -}; - -export default useNotifications; diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js index 6c5919fac..6cf84e45f 100644 --- a/client/src/Hooks/useNotifications.js +++ b/client/src/Hooks/useNotifications.js @@ -1,36 +1,7 @@ import { useState, useEffect, useCallback } from "react"; import { createToast } from "../Utils/toastUtils.jsx"; import { networkService } from "../main.jsx"; -import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { NOTIFICATION_TYPES } from "../Pages/Notifications/utils.js"; - -const useCreateNotification = () => { - const navigate = useNavigate(); - const { t } = useTranslation(); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const createNotification = async (notification) => { - try { - setIsLoading(true); - await networkService.createNotification({ notification }); - createToast({ - body: t("notifications.create.success"), - }); - navigate("/notifications"); - } catch (error) { - setError(error); - createToast({ - body: t("notifications.create.failed"), - }); - } finally { - setIsLoading(false); - } - }; - - return [createNotification, isLoading, error]; -}; const useGetNotificationsByTeamId = (updateTrigger) => { const [notifications, setNotifications] = useState([]); @@ -60,119 +31,6 @@ const useGetNotificationsByTeamId = (updateTrigger) => { return [notifications, isLoading, error]; }; -const useDeleteNotification = () => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const { t } = useTranslation(); - - const deleteNotification = async (id, callback) => { - try { - setIsLoading(true); - await networkService.deleteNotificationById({ id }); - createToast({ - body: t("notifications.delete.success"), - }); - if (callback) { - callback(); - } - } catch (error) { - setError(error); - createToast({ - body: t("notifications.delete.failed"), - }); - } finally { - setIsLoading(false); - } - }; - - return [deleteNotification, isLoading, error]; -}; - -const useGetNotificationById = (id, setNotification) => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const getNotificationById = useCallback(async () => { - try { - setIsLoading(true); - const response = await networkService.getNotificationById({ id }); - - const notification = response?.data?.data ?? null; - - const notificationData = { - address: notification?.address, - notificationName: notification?.notificationName, - type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?.id, - }; - - setNotification(notificationData); - } catch (error) { - setError(error); - } finally { - setIsLoading(false); - } - }, [id, setNotification]); - - useEffect(() => { - if (id) { - getNotificationById(); - } - }, [getNotificationById, id]); - - return [isLoading, error]; -}; - -const useEditNotification = () => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const { t } = useTranslation(); - const navigate = useNavigate(); - const editNotification = async (id, notification) => { - try { - setIsLoading(true); - await networkService.editNotification({ id, notification }); - createToast({ - body: t("notifications.edit.success"), - }); - navigate(`/notifications`); - } catch (error) { - setError(error); - createToast({ - body: t("notifications.edit.failed"), - }); - } finally { - setIsLoading(false); - } - }; - - return [editNotification, isLoading, error]; -}; - -const useTestNotification = () => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const { t } = useTranslation(); - - const testNotification = async (notification) => { - try { - setIsLoading(true); - await networkService.testNotification({ notification }); - createToast({ - body: t("notifications.test.success"), - }); - } catch (error) { - setError(error); - createToast({ - body: error?.response?.data?.msg || t("notifications.test.failed"), - }); - } finally { - setIsLoading(false); - } - }; - - return [testNotification, isLoading, error]; -}; - const useTestAllNotifications = () => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(undefined); @@ -197,12 +55,4 @@ const useTestAllNotifications = () => { return [testAllNotifications, isLoading, error]; }; -export { - useCreateNotification, - useGetNotificationsByTeamId, - useDeleteNotification, - useGetNotificationById, - useEditNotification, - useTestNotification, - useTestAllNotifications, -}; +export { useGetNotificationsByTeamId, useTestAllNotifications }; diff --git a/client/src/Pages/Notifications/create/old.jsx b/client/src/Pages/Notifications/create/old.jsx deleted file mode 100644 index 14b72ab61..000000000 --- a/client/src/Pages/Notifications/create/old.jsx +++ /dev/null @@ -1,320 +0,0 @@ -// Components -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import Button from "@mui/material/Button"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import Box from "@mui/material/Box"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; - -// Utils -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { - useCreateNotification, - useGetNotificationById, - useEditNotification, - useTestNotification, - useDeleteNotification, -} from "../../../Hooks/useNotifications.js"; -import { notificationValidation } from "../../../Validation/validation.js"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import { useTranslation } from "react-i18next"; -import { useParams, useNavigate } from "react-router-dom"; -import { - NOTIFICATION_TYPES, - TITLE_MAP, - DESCRIPTION_MAP, - LABEL_MAP, - PLACEHOLDER_MAP, -} from "../utils.js"; - -// Setup - -const CreateNotifications = () => { - const { notificationId } = useParams(); - const navigate = useNavigate(); - const theme = useTheme(); - const [createNotification, isCreating] = useCreateNotification(); - const [editNotification, isEditing] = useEditNotification(); - const [testNotification, isTesting] = useTestNotification(); - const [deleteNotification, isDeleting] = useDeleteNotification(); - - const BREADCRUMBS = [ - { name: "notifications", path: "/notifications" }, - { - name: notificationId ? "edit" : "create", - path: notificationId ? `/notifications/${notificationId}` : "/notifications/create", - }, - ]; - - // Redux state - - // local state - const [notification, setNotification] = useState({ - notificationName: "", - address: "", - type: NOTIFICATION_TYPES[0].id, - }); - const [errors, setErrors] = useState({}); - const { t } = useTranslation(); - - const [notificationIsLoading] = useGetNotificationById(notificationId, setNotification); - - const [isOpen, setIsOpen] = useState(false); - - const getNotificationTypeValue = (typeId) => { - return NOTIFICATION_TYPES.find((type) => type.id === typeId)?.value || "email"; - }; - - const extractError = (error, field) => - error?.details.find((d) => d.path.includes(field))?.message; - - // handlers - const onSubmit = (e) => { - e.preventDefault(); - const form = { - ...notification, - type: getNotificationTypeValue(notification.type), - }; - - let error = null; - - error = notificationValidation.validate(form, { abortEarly: false }).error; - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - createToast({ body: Object.values(newErrors)[0] }); - setErrors(newErrors); - return; - } - - if (notificationId) { - editNotification(notificationId, form); - } else { - createNotification(form); - } - }; - - const onChange = (e) => { - const { name, value } = e.target; - let rawNotification = { ...notification, [name]: value }; - let newNotification = { - ...rawNotification, - type: getNotificationTypeValue(rawNotification.type), - }; - - const { error } = notificationValidation.validate(newNotification, { - abortEarly: false, - }); - let validationError = { ...errors }; - - if (name === "type") { - validationError["type"] = extractError(error, "type"); - validationError["address"] = extractError(error, "address"); - } else { - validationError[name] = extractError(error, name); - } - - setNotification(rawNotification); - setErrors(validationError); - }; - - const onTestNotification = () => { - const form = { - ...notification, - type: getNotificationTypeValue(notification.type), - }; - - let error = null; - - error = notificationValidation.validate(form, { abortEarly: false }).error; - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - createToast({ body: Object.values(newErrors)[0] }); - setErrors(newErrors); - return; - } - - testNotification(form); - }; - - const onDelete = () => { - if (notificationId) { - deleteNotification(notificationId, () => navigate("/notifications")); - } - }; - - const type = getNotificationTypeValue(notification.type); - return ( - - - {t("createNotifications.title")} - - - - - {t("createNotifications.nameSettings.title")} - - - {t("createNotifications.nameSettings.description")} - - - - - - - - - - {t("createNotifications.typeSettings.title")} - - - {t("createNotifications.typeSettings.description")} - - - - + Select a game + + )} + /> + )} + + {/* Display name field - common to all types */} + ( + + )} + /> + + } + /> + + {/* Submit Button */} + + + + + ); +}; + +export default CreateMonitorPage; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 498be4ba7..c2613a16c 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -60,6 +60,8 @@ import withAdminCheck from "@/Components/v1/HOC/withAdminCheck"; import BulkImport from "../Pages/Uptime/BulkImport/index.jsx"; import Logs from "../Pages/Logs/index.jsx"; +import CreateMonitor from "@/Pages/CreateMonitor"; + const Routes = () => { const mode = useSelector((state) => state.ui.mode); const AdminCheckedRegister = withAdminCheck(AuthRegister); @@ -78,6 +80,16 @@ const Routes = () => { path="/" element={} /> + + + + + + } + /> ; + +// Type-specific schemas exported for individual use +export { + httpSchema, + pingSchema, + portSchema, + dockerSchema, + gameSchema, + pagespeedSchema, + hardwareSchema, +}; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 80da1835c..d9f2e7ac6 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -220,6 +220,37 @@ } } }, + "createMonitor": { + "form": { + "type": { + "description": "Select the type of check to perform", + "optionType": "Type", + "title": "Type", + "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.", + "optionDocker": "Docker", + "optionDockerDescription": "Use Docker to monitor if a container is running.", + "optionPort": "Port", + "optionPortDescription": "Monitor if a specific port on a server is open.", + "optionGame": "Game", + "optionGameDescription": "Monitor if a specific game server is online." + }, + "general": { + "title": "General settings", + "description": { + "http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.", + "ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.", + "docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).", + "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.", + "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.", + "pagespeed": "Here you can select the URL of the host, together with the type of monitor.", + "infrastructure": "Here you can select the URL of the host, together with the friendly name and authorization secret to connect to the server agent." + } + } + } + }, "uptime": { "table": { "headers": { From 84e4ea7b649802dbbef7738ba472c79d4aab82a2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 Jan 2026 22:20:50 +0000 Subject: [PATCH 108/133] forward ref --- client/src/Components/v2/inputs/Select.tsx | 32 ++++++++++++++-------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/client/src/Components/v2/inputs/Select.tsx b/client/src/Components/v2/inputs/Select.tsx index 057b0e72c..8be20897d 100644 --- a/client/src/Components/v2/inputs/Select.tsx +++ b/client/src/Components/v2/inputs/Select.tsx @@ -1,7 +1,7 @@ import Select from "@mui/material/Select"; -import React from "react"; -import { useTheme } from "@mui/material/styles"; import type { SelectProps } from "@mui/material/Select"; +import React, { forwardRef } from "react"; +import { useTheme } from "@mui/material/styles"; import Stack from "@mui/material/Stack"; import { FieldLabel } from "./FieldLabel"; import { ChevronDown } from "lucide-react"; @@ -14,18 +14,21 @@ interface SelectInputProps extends Omit, "label"> { placeholderColor?: string; } -export const SelectInput = ({ - fieldLabel, - required, - placeholder, - placeholderColor, - ...props -}: SelectInputProps) => { +const SelectInputInner = ( + { + fieldLabel, + required, + placeholder, + placeholderColor, + ...props + }: SelectInputProps, + ref: React.ForwardedRef +) => { const theme = useTheme(); const emptyPlaceholderColor = placeholderColor || theme.palette.text.disabled; - const renderValue = (selected: any) => { - const isMultiple = Boolean((props as any).multiple); + const renderValue = (selected: unknown) => { + const isMultiple = Boolean((props as { multiple?: boolean }).multiple); const isEmpty = isMultiple ? !Array.isArray(selected) || selected.length === 0 : selected === undefined || selected === null || selected === ""; @@ -39,7 +42,7 @@ export const SelectInput = ({ const capitalized = items.map( (item) => item.charAt(0).toUpperCase() + item.slice(1) ); - return ({capitalized.join(" | ")}) as any; + return {capitalized.join(" | ")}; } const nodes = React.Children.toArray(props.children as React.ReactNode); @@ -59,6 +62,7 @@ export const SelectInput = ({ const select = ( {...props} + ref={ref} displayEmpty renderValue={renderValue} inputProps={{ @@ -100,3 +104,7 @@ export const SelectInput = ({ return select; }; + +export const SelectInput = forwardRef(SelectInputInner) as ( + props: SelectInputProps & { ref?: React.ForwardedRef } +) => React.ReactElement; From 841d25ce3e0db0ce9ad15b8564061a7e75d5d63f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 Jan 2026 22:47:29 +0000 Subject: [PATCH 109/133] general --- client/src/Pages/CreateMonitor/index.tsx | 134 +++++++++++++++++------ client/src/Validation/monitor.ts | 4 +- client/src/locales/en.json | 32 +++++- 3 files changed, 132 insertions(+), 38 deletions(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 80f818407..e0b5e2930 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -8,6 +8,7 @@ import Stack from "@mui/material/Stack"; import RadioGroup from "@mui/material/RadioGroup"; import FormControl from "@mui/material/FormControl"; import { useTranslation } from "react-i18next"; +import MenuItem from "@mui/material/MenuItem"; import { BasePage, ConfigBox } from "@/Components/v2/design-elements"; import { RadioWithDescription, Button, TextField, Select } from "@/Components/v2/inputs"; @@ -15,14 +16,15 @@ import { useGet } from "@/Hooks/UseApi"; import { useMonitorForm } from "@/Hooks/useMonitorForm"; import type { Monitor, MonitorType } from "@/Types/Monitor"; import type { MonitorFormData } from "@/Validation/monitor"; -import MenuItem from "@mui/material/MenuItem"; interface GeneralSettingsConfig { urlLabel: string; urlPlaceholder: string; namePlaceholder: string; + showUrl: boolean; showPort: boolean; showGameSelect: boolean; + showSecret: boolean; } const getGeneralSettingsConfig = ( @@ -31,39 +33,67 @@ const getGeneralSettingsConfig = ( ): GeneralSettingsConfig => { const configs: Record = { http: { - urlLabel: t("monitorType.http.label"), - urlPlaceholder: t("monitorType.http.placeholder"), - namePlaceholder: t("monitorType.http.namePlaceholder"), + urlLabel: t("pages.createMonitor.form.general.option.url.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, showPort: false, showGameSelect: false, + showSecret: false, }, ping: { - urlLabel: t("monitorType.ping.label"), - urlPlaceholder: t("monitorType.ping.placeholder"), - namePlaceholder: t("monitorType.ping.namePlaceholder"), + urlLabel: t("pages.createMonitor.form.general.option.host.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.host.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, showPort: false, showGameSelect: false, + showSecret: false, }, docker: { - urlLabel: t("monitorType.docker.label"), - urlPlaceholder: t("monitorType.docker.placeholder"), - namePlaceholder: t("monitorType.docker.namePlaceholder"), + urlLabel: t("pages.createMonitor.form.general.option.container.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.container.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, showPort: false, showGameSelect: false, + showSecret: false, }, port: { - urlLabel: t("monitorType.port.label"), - urlPlaceholder: t("monitorType.port.placeholder"), - namePlaceholder: t("monitorType.port.namePlaceholder"), + urlLabel: t("pages.createMonitor.form.general.option.url.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, showPort: true, showGameSelect: false, + showSecret: false, }, game: { - urlLabel: t("monitorType.game.label"), - urlPlaceholder: t("monitorType.game.placeholder"), - namePlaceholder: t("monitorType.game.namePlaceholder"), + urlLabel: t("pages.createMonitor.form.general.option.url.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, showPort: true, showGameSelect: true, + showSecret: false, + }, + pagespeed: { + urlLabel: t("pages.createMonitor.form.general.option.url.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, + showPort: false, + showGameSelect: false, + showSecret: false, + }, + hardware: { + urlLabel: t("pages.createMonitor.form.general.option.url.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, + showPort: false, + showGameSelect: false, + showSecret: true, }, }; return configs[type] || configs.http; @@ -156,6 +186,20 @@ const CreateMonitorPage = () => { label={t("pages.createMonitor.form.type.optionGame")} description={t("pages.createMonitor.form.type.optionGameDescription")} /> + + )} @@ -169,22 +213,24 @@ const CreateMonitorPage = () => { subtitle={t(`pages.createMonitor.form.general.description.${watchedType}`)} rightContent={ - {/* URL/Host/Container field */} - ( - - )} - /> + {/* URL/Host/Container field - not shown for hardware */} + {generalSettingsConfig.showUrl && ( + ( + + )} + /> + )} {/* Port field - only for port and game types */} {generalSettingsConfig.showPort && ( @@ -225,6 +271,28 @@ const CreateMonitorPage = () => { /> )} + {/* Secret field - only for hardware type */} + {generalSettingsConfig.showSecret && ( + ( + + )} + /> + )} + {/* Display name field - common to all types */} { Date: Thu, 29 Jan 2026 23:04:40 +0000 Subject: [PATCH 110/133] notifications --- client/src/Pages/CreateMonitor/index.tsx | 85 +++++++++++++++++++++++- client/src/locales/en.json | 7 +- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index e0b5e2930..d5e10d97b 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -9,12 +9,23 @@ import RadioGroup from "@mui/material/RadioGroup"; import FormControl from "@mui/material/FormControl"; import { useTranslation } from "react-i18next"; import MenuItem from "@mui/material/MenuItem"; +import Typography from "@mui/material/Typography"; +import Divider from "@mui/material/Divider"; +import IconButton from "@mui/material/IconButton"; +import { Trash2 } from "lucide-react"; import { BasePage, ConfigBox } from "@/Components/v2/design-elements"; -import { RadioWithDescription, Button, TextField, Select } from "@/Components/v2/inputs"; +import { + RadioWithDescription, + Button, + TextField, + Select, + Autocomplete, +} from "@/Components/v2/inputs"; import { useGet } from "@/Hooks/UseApi"; import { useMonitorForm } from "@/Hooks/useMonitorForm"; import type { Monitor, MonitorType } from "@/Types/Monitor"; +import type { Notification } from "@/Types/Notification"; import type { MonitorFormData } from "@/Validation/monitor"; interface GeneralSettingsConfig { @@ -109,6 +120,9 @@ const CreateMonitorPage = () => { isEditMode ? `/monitors/${monitorId}` : null ); + // Fetch notifications for the team + const { data: notifications } = useGet("/notifications/team"); + const { schema, defaults } = useMonitorForm({ data: existingMonitor ?? null }); const form = useForm({ @@ -313,6 +327,75 @@ const CreateMonitorPage = () => { } /> + {/* Notifications ConfigBox */} + { + // Map notifications to have 'name' property for v2 Autocomplete + const notificationOptions = (notifications ?? []).map((n) => ({ + ...n, + name: n.notificationName, + })); + const selectedNotifications = notificationOptions.filter((n) => + (field.value ?? []).includes(n.id) + ); + return ( + + option.name} + onChange={(_: unknown, newValue: typeof notificationOptions) => { + field.onChange(newValue.map((n) => n.id)); + }} + isOptionEqualToValue={(option, value) => option.id === value.id} + /> + {selectedNotifications.length > 0 && ( + + {selectedNotifications.map((notification, index) => ( + + + {notification.notificationName} + + { + field.onChange( + (field.value ?? []).filter( + (id: string) => id !== notification.id + ) + ); + }} + aria-label="Remove notification" + > + + + {index < selectedNotifications.length - 1 && } + + ))} + + )} + + ); + }} + /> + } + /> + {/* Submit Button */} Date: Thu, 29 Jan 2026 23:10:34 +0000 Subject: [PATCH 111/133] TLS/SSL --- client/src/Components/v2/inputs/Switch.tsx | 37 ++++++++++++++++++++++ client/src/Components/v2/inputs/index.tsx | 1 + client/src/Pages/CreateMonitor/index.tsx | 30 ++++++++++++++++++ client/src/locales/en.json | 9 ++++++ 4 files changed, 77 insertions(+) create mode 100644 client/src/Components/v2/inputs/Switch.tsx diff --git a/client/src/Components/v2/inputs/Switch.tsx b/client/src/Components/v2/inputs/Switch.tsx new file mode 100644 index 000000000..b8ae8c1ec --- /dev/null +++ b/client/src/Components/v2/inputs/Switch.tsx @@ -0,0 +1,37 @@ +import { forwardRef } from "react"; +import Switch from "@mui/material/Switch"; +import type { SwitchProps } from "@mui/material/Switch"; +import { useTheme } from "@mui/material/styles"; + +export const SwitchComponent = forwardRef( + function SwitchComponent({ sx, ...props }, ref) { + const theme = useTheme(); + const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : []; + + return ( + + ); + } +); diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx index a0646d394..4bfd62947 100644 --- a/client/src/Components/v2/inputs/index.tsx +++ b/client/src/Components/v2/inputs/index.tsx @@ -10,3 +10,4 @@ export { } from "./ToggleButton"; export { DialogInput as Dialog } from "./Dialog"; export * from "./Radio"; +export * from "./Switch"; diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index d5e10d97b..93539e10c 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -21,6 +21,7 @@ import { TextField, Select, Autocomplete, + SwitchComponent as Switch, } from "@/Components/v2/inputs"; import { useGet } from "@/Hooks/UseApi"; import { useMonitorForm } from "@/Hooks/useMonitorForm"; @@ -396,6 +397,35 @@ const CreateMonitorPage = () => { } /> + {/* TLS/SSL ConfigBox - only for HTTP monitors */} + {watchedType === "http" && ( + ( + + field.onChange(e.target.checked)} + /> + + {t("pages.createMonitor.form.ignoreTls.option.tls.label")} + + + )} + /> + } + /> + )} + {/* Submit Button */} Date: Thu, 29 Jan 2026 23:17:08 +0000 Subject: [PATCH 112/133] check frequency --- client/src/Pages/CreateMonitor/index.tsx | 31 ++++++++++++++++++++++++ client/src/locales/en.json | 9 +++++++ 2 files changed, 40 insertions(+) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 93539e10c..591f423fa 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -328,6 +328,37 @@ const CreateMonitorPage = () => { } /> + {/* Frequency ConfigBox */} + ( + + )} + /> + } + /> + {/* Notifications ConfigBox */} Date: Thu, 29 Jan 2026 23:39:14 +0000 Subject: [PATCH 113/133] add incidents config box --- client/src/Pages/CreateMonitor/index.tsx | 48 ++++++++++++++++++++++++ client/src/Validation/monitor.ts | 6 +-- client/src/locales/en.json | 12 ++++++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 591f423fa..c0740393b 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -359,6 +359,54 @@ const CreateMonitorPage = () => { } /> + {/* Incidents ConfigBox */} + + ( + { + const val = e.target.value; + field.onChange(val === "" ? undefined : Number(val)); + }} + onBlur={field.onBlur} + type="number" + fieldLabel={t("pages.createMonitor.form.incidents.option.checks.label")} + fullWidth + error={!!fieldState.error} + helperText={fieldState.error?.message ?? ""} + /> + )} + /> + ( + { + const val = e.target.value; + field.onChange(val === "" ? undefined : Number(val)); + }} + onBlur={field.onBlur} + type="number" + fieldLabel={t("pages.createMonitor.form.incidents.option.percentage.label")} + fullWidth + error={!!fieldState.error} + helperText={fieldState.error?.message ?? ""} + /> + )} + /> + + } + /> + {/* Notifications ConfigBox */} Date: Thu, 29 Jan 2026 23:52:21 +0000 Subject: [PATCH 114/133] sliders --- client/src/Components/v2/inputs/Slider.tsx | 41 +++++++++++++++ client/src/Components/v2/inputs/index.tsx | 1 + client/src/Pages/CreateMonitor/index.tsx | 61 +++++++++++----------- 3 files changed, 73 insertions(+), 30 deletions(-) create mode 100644 client/src/Components/v2/inputs/Slider.tsx diff --git a/client/src/Components/v2/inputs/Slider.tsx b/client/src/Components/v2/inputs/Slider.tsx new file mode 100644 index 000000000..11b3e8db8 --- /dev/null +++ b/client/src/Components/v2/inputs/Slider.tsx @@ -0,0 +1,41 @@ +import { forwardRef } from "react"; +import Slider from "@mui/material/Slider"; +import type { SliderProps } from "@mui/material/Slider"; +import { useTheme } from "@mui/material/styles"; + +export const SliderInput = forwardRef( + function SliderInput({ sx, ...props }, ref) { + const theme = useTheme(); + const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : []; + + return ( + + ); + } +); diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx index 4bfd62947..1618daf76 100644 --- a/client/src/Components/v2/inputs/index.tsx +++ b/client/src/Components/v2/inputs/index.tsx @@ -11,3 +11,4 @@ export { export { DialogInput as Dialog } from "./Dialog"; export * from "./Radio"; export * from "./Switch"; +export * from "./Slider"; diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index c0740393b..215cdab5f 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -22,6 +22,7 @@ import { Select, Autocomplete, SwitchComponent as Switch, + SliderInput as Slider, } from "@/Components/v2/inputs"; import { useGet } from "@/Hooks/UseApi"; import { useMonitorForm } from "@/Hooks/useMonitorForm"; @@ -340,7 +341,9 @@ const CreateMonitorPage = () => { + {t("matchMethodOptions.equal")} + {t("matchMethodOptions.include")} + {t("matchMethodOptions.regex")} + + )} + /> + ( + + )} + /> + ( + + )} + /> + + )} + + } + /> + )} + {/* Submit Button */} Date: Fri, 30 Jan 2026 20:21:31 +0000 Subject: [PATCH 120/133] useAdvncedMatching --- client/src/Hooks/useMonitorForm.ts | 2 + client/src/Pages/CreateMonitor/index.tsx | 63 ++++++++++++++----- client/src/Types/Monitor.ts | 1 + client/src/Validation/monitor.ts | 1 + client/src/locales/en.json | 4 +- server/src/db/models/Monitor.ts | 4 ++ .../monitors/MongoMonitorsRepository.ts | 2 + .../service/infrastructure/networkService.ts | 4 +- server/src/types/monitor.ts | 1 + server/src/validation/joi.ts | 2 + 10 files changed, 66 insertions(+), 18 deletions(-) diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts index 50dc7b920..153cf9f19 100644 --- a/client/src/Hooks/useMonitorForm.ts +++ b/client/src/Hooks/useMonitorForm.ts @@ -33,6 +33,7 @@ export const useMonitorForm = ({ type: "http", url: data?.url || "", ignoreTlsErrors: data?.ignoreTlsErrors || false, + useAdvancedMatching: data?.useAdvancedMatching || false, matchMethod: data?.matchMethod || "", expectedValue: data?.expectedValue || "", jsonPath: data?.jsonPath || "", @@ -95,6 +96,7 @@ export const useMonitorForm = ({ type: "http", url: "", ignoreTlsErrors: false, + useAdvancedMatching: false, matchMethod: "", expectedValue: "", jsonPath: "", diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 313b744cd..d7d9a645e 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useParams, useLocation, useNavigate } from "react-router"; import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -7,9 +7,10 @@ import { useTheme } from "@mui/material"; import Stack from "@mui/material/Stack"; import RadioGroup from "@mui/material/RadioGroup"; import FormControl from "@mui/material/FormControl"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import MenuItem from "@mui/material/MenuItem"; import Typography from "@mui/material/Typography"; +import Link from "@mui/material/Link"; import Divider from "@mui/material/Divider"; import IconButton from "@mui/material/IconButton"; import FormControlLabel from "@mui/material/FormControlLabel"; @@ -163,7 +164,7 @@ const CreateMonitorPage = () => { const watchedType = watch("type") as MonitorType; - const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); + const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean; useEffect(() => { clearErrors(); @@ -550,16 +551,22 @@ const CreateMonitorPage = () => { subtitle={t("pages.createMonitor.form.advanced.description")} rightContent={ - setShowAdvancedSettings(e.target.checked)} + ( + field.onChange(e.target.checked)} + /> + } + label={t("advancedMatching")} /> - } - label={t("advancedMatching")} + )} /> - {showAdvancedSettings && ( + {watchedUseAdvancedMatching && ( { )} @@ -583,7 +594,9 @@ const CreateMonitorPage = () => { { )} /> + + + ), + }} + /> + )} diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 94b3d83d7..ddf44e56f 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -35,6 +35,7 @@ export interface Monitor { statusWindowThreshold: number; type: MonitorType; ignoreTlsErrors: boolean; + useAdvancedMatching: boolean; jsonPath?: string; expectedValue?: string; matchMethod?: MonitorMatchMethod; diff --git a/client/src/Validation/monitor.ts b/client/src/Validation/monitor.ts index 39fccafbd..68a15fbec 100644 --- a/client/src/Validation/monitor.ts +++ b/client/src/Validation/monitor.ts @@ -27,6 +27,7 @@ const httpSchema = baseSchema.extend({ type: z.literal("http"), url: urlSchema, ignoreTlsErrors: z.boolean(), + useAdvancedMatching: z.boolean(), matchMethod: z.enum(["equal", "include", "regex", ""]).optional(), expectedValue: z.string().optional(), jsonPath: z.string().optional(), diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 80cb2e206..123a1f146 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -2,6 +2,8 @@ "common": { "appName": "Checkmate", "monitoringAgentName": "Capture", + "see": "See", + "forDocumentation": "for query language documentation.", "breadcrumbs": { "home": "Home", "details": "Details" @@ -322,7 +324,7 @@ }, "jsonPath": { "label": "JSONPath expression", - "description": "This expression will be evaluated against the response JSON data and the result will be used to match against the expected value." + "description": "This expression will be evaluated against the response JSON data and the result will be used to match against the expected value. See jmespath.org for query language documentation." } } } diff --git a/server/src/db/models/Monitor.ts b/server/src/db/models/Monitor.ts index 24c8ef0bf..d4cc9a872 100644 --- a/server/src/db/models/Monitor.ts +++ b/server/src/db/models/Monitor.ts @@ -92,6 +92,10 @@ const MonitorSchema = new Schema( type: Boolean, default: false, }, + useAdvancedMatching: { + type: Boolean, + default: false, + }, jsonPath: { type: String, }, diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 9cb519303..9b30e5ac2 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -290,6 +290,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { statusWindowThreshold: doc.statusWindowThreshold, type: doc.type, ignoreTlsErrors: doc.ignoreTlsErrors, + useAdvancedMatching: doc.useAdvancedMatching ?? false, jsonPath: doc.jsonPath ?? undefined, expectedValue: doc.expectedValue ?? undefined, matchMethod: doc.matchMethod ?? undefined, @@ -374,6 +375,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { statusWindowThreshold: doc.statusWindowThreshold, type: doc.type, ignoreTlsErrors: doc.ignoreTlsErrors, + useAdvancedMatching: doc.useAdvancedMatching ?? false, jsonPath: doc.jsonPath ?? undefined, expectedValue: doc.expectedValue ?? undefined, matchMethod: doc.matchMethod ?? undefined, diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index 7db8fc6e2..92d269808 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -255,7 +255,7 @@ class NetworkService implements INetworkService { } private async requestHttp(monitor: Monitor): Promise { - const { url, secret, id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; + const { url, secret, id, teamId, type, ignoreTlsErrors, useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor; const httpResponse = this.buildStatusResponse({ monitor, overrides: { @@ -305,7 +305,7 @@ class NetworkService implements INetworkService { timings: response.timings, }); - if (!expectedValue && !jsonPath) { + if (!useAdvancedMatching) { return httpResponse; } diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index 62ced091d..fe6ac8c8a 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -26,6 +26,7 @@ export interface Monitor { statusWindowThreshold: number; type: MonitorType; ignoreTlsErrors: boolean; + useAdvancedMatching: boolean; jsonPath?: string; expectedValue?: string; matchMethod?: MonitorMatchMethod; diff --git a/server/src/validation/joi.ts b/server/src/validation/joi.ts index 581d2afd2..926d3b0fe 100755 --- a/server/src/validation/joi.ts +++ b/server/src/validation/joi.ts @@ -158,6 +158,7 @@ const createMonitorBodyValidation = joi.object({ statusWindowThreshold: joi.number().min(1).max(100).default(60), url: joi.string().required(), ignoreTlsErrors: joi.boolean().default(false), + useAdvancedMatching: joi.boolean().default(false), port: joi.number(), isActive: joi.boolean(), interval: joi.number(), @@ -193,6 +194,7 @@ const editMonitorBodyValidation = joi.object({ notifications: joi.array().items(joi.string()), secret: joi.string(), ignoreTlsErrors: joi.boolean(), + useAdvancedMatching: joi.boolean(), jsonPath: joi.string().allow(""), expectedValue: joi.string().allow(""), matchMethod: joi.string().allow(null, ""), From 443ad46fa79bc202a9a2c05ee943a8d5fdae403c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 30 Jan 2026 20:38:05 +0000 Subject: [PATCH 121/133] update strings --- client/src/Pages/CreateMonitor/index.tsx | 22 +++++++++++----------- client/src/locales/en.json | 3 +++ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index d7d9a645e..c38f4eb05 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -13,7 +13,6 @@ import Typography from "@mui/material/Typography"; import Link from "@mui/material/Link"; import Divider from "@mui/material/Divider"; import IconButton from "@mui/material/IconButton"; -import FormControlLabel from "@mui/material/FormControlLabel"; import { Trash2 } from "lucide-react"; import { BasePage, ConfigBox } from "@/Components/v2/design-elements"; @@ -25,7 +24,6 @@ import { Autocomplete, SwitchComponent as Switch, SliderWithLabel, - Checkbox, } from "@/Components/v2/inputs"; import { useGet, usePost, usePatch } from "@/Hooks/UseApi"; import { useMonitorForm } from "@/Hooks/useMonitorForm"; @@ -555,15 +553,17 @@ const CreateMonitorPage = () => { name="useAdvancedMatching" control={control} render={({ field }) => ( - field.onChange(e.target.checked)} - /> - } - label={t("advancedMatching")} - /> + + field.onChange(e.target.checked)} + /> + {t("pages.createMonitor.form.advanced.option.advancedMatching.label")} + )} /> {watchedUseAdvancedMatching && ( diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 123a1f146..7cdfe215a 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -315,6 +315,9 @@ "title": "Advanced settings", "description": "Optional settings for advanced use cases", "option": { + "advancedMatching": { + "label": "Use advanced matching" + }, "matchMethod": { "label": "Match method" }, From 91bb60f2ff9b923cf214cbb8bd29f6764714825b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 30 Jan 2026 20:55:24 +0000 Subject: [PATCH 122/133] delte header --- .../common/controls/HeaderMonitorControls.tsx | 93 +- client/src/Pages/CreateMonitor/index.tsx | 14 +- .../Create/Components/CustomAlertsSection.jsx | 87 -- .../Components/CustomThreshold/index.jsx | 127 --- .../Create/Components/DiskSelection.jsx | 90 -- .../Components/MonitorActionButtons.jsx | 83 -- .../Create/Components/MonitorStatusHeader.jsx | 73 -- .../hooks/useInfrastructureMonitorForm.jsx | 99 --- .../Create/hooks/useInfrastructureSubmit.jsx | 84 -- .../hooks/useValidateInfrastructureForm.jsx | 37 - .../src/Pages/Infrastructure/Create/index.jsx | 409 --------- client/src/Pages/PageSpeed/Create/index.css | 0 client/src/Pages/PageSpeed/Create/index.jsx | 514 ----------- .../src/Pages/PageSpeed/Create/skeleton.jsx | 86 -- client/src/Pages/Uptime/Create/index.css | 0 client/src/Pages/Uptime/Create/index.jsx | 826 ------------------ client/src/Pages/Uptime/Create/skeleton.jsx | 90 -- client/src/Routes/index.jsx | 7 - client/src/locales/en.json | 3 +- 19 files changed, 97 insertions(+), 2625 deletions(-) delete mode 100644 client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx delete mode 100644 client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx delete mode 100644 client/src/Pages/Infrastructure/Create/Components/DiskSelection.jsx delete mode 100644 client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx delete mode 100644 client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx delete mode 100644 client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx delete mode 100644 client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx delete mode 100644 client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx delete mode 100644 client/src/Pages/Infrastructure/Create/index.jsx delete mode 100644 client/src/Pages/PageSpeed/Create/index.css delete mode 100644 client/src/Pages/PageSpeed/Create/index.jsx delete mode 100644 client/src/Pages/PageSpeed/Create/skeleton.jsx delete mode 100644 client/src/Pages/Uptime/Create/index.css delete mode 100644 client/src/Pages/Uptime/Create/index.jsx delete mode 100644 client/src/Pages/Uptime/Create/skeleton.jsx diff --git a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx index 17576a8b2..0db84940b 100644 --- a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx +++ b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx @@ -1,7 +1,7 @@ import Stack from "@mui/material/Stack"; import { Icon, MonitorStatus } from "@/Components/v2/design-elements"; import { Button } from "@/Components/v2/inputs"; -import { Settings, Pause, Play, Mail, Bug } from "lucide-react"; +import { Settings, Pause, Play, Mail, Bug, Trash } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; @@ -9,10 +9,28 @@ import { useTheme } from "@mui/material/styles"; import { usePost } from "@/Hooks/UseApi"; import type { Monitor } from "@/Types/Monitor.js"; +interface BaseHeaderProps { + monitor: Monitor; +} + +const BaseHeader = ({ monitor, children }: React.PropsWithChildren) => { + const theme = useTheme(); + return ( + + + {children} + + ); +}; interface HeaderMonitorControlsProps { path: string; - monitor?: Monitor; + monitor?: Monitor | null; isAdmin: boolean; refetch: Function; } @@ -36,13 +54,7 @@ export const HeaderMonitorControls = ({ return null; } return ( - - + )} - + + ); +}; + +interface HeaderDeleteControlsProps { + monitor?: Monitor | null; + isAdmin: boolean; + refetch: Function; +} + +export const HeaderDeleteControls = ({ + monitor, + isAdmin, + refetch, +}: HeaderDeleteControlsProps) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const theme = useTheme(); + const { + post, + loading: isPosting, + // error: postError, + } = usePost(); + + if (!monitor) { + return null; + } + return ( + + + {isAdmin && ( + + )} + {isAdmin && ( + + )} + + ); }; diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index c38f4eb05..37741746b 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -14,6 +14,7 @@ import Link from "@mui/material/Link"; import Divider from "@mui/material/Divider"; import IconButton from "@mui/material/IconButton"; import { Trash2 } from "lucide-react"; +import { HeaderDeleteControls } from "@/Components/v2/common"; import { BasePage, ConfigBox } from "@/Components/v2/design-elements"; import { @@ -138,7 +139,7 @@ const CreateMonitorPage = () => { ? "hardware" : "http"; - const { data: existingMonitor } = useGet( + const { data: existingMonitor, refetch: refetchMonitor } = useGet( isEditMode ? `/monitors/${monitorId}` : null ); @@ -206,6 +207,11 @@ const CreateMonitorPage = () => { component="form" onSubmit={handleSubmit(onSubmit, onError)} > + {/* Monitor Type Selection - only shown for uptime monitors */} {showTypeSelector && ( { checked={field.value ?? false} onChange={(e) => field.onChange(e.target.checked)} /> - {t("pages.createMonitor.form.advanced.option.advancedMatching.label")} + + {t( + "pages.createMonitor.form.advanced.option.advancedMatching.label" + )} + )} /> diff --git a/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx b/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx deleted file mode 100644 index 64f82cec2..000000000 --- a/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx +++ /dev/null @@ -1,87 +0,0 @@ -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import { Box, Stack, Typography } from "@mui/material"; -import { CustomThreshold } from "./CustomThreshold/index.jsx"; -import { capitalizeFirstLetter } from "../../../../Utils/stringUtils.js"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import PropTypes from "prop-types"; -const CustomAlertsSection = ({ - errors, - onChange, - infrastructureMonitor, - handleCheckboxChange, -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - - const METRICS = ["cpu", "memory", "disk", "temperature"]; - const METRIC_PREFIX = "usage_"; - const hasAlertError = (errors) => { - return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0; - }; - const getAlertError = (errors) => { - const errorKey = Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX)); - return errorKey ? errors[errorKey] : null; - }; - return ( - - - - {t("infrastructureCustomizeAlerts")} - - - {t("infrastructureAlertNotificationDescription")} - - - - {METRICS.map((metric) => { - return ( - - ); - })} - - {hasAlertError(errors) && ( - - {getAlertError(errors)} - - )} - - - ); -}; - -CustomAlertsSection.propTypes = { - errors: PropTypes.object.isRequired, - onChange: PropTypes.func.isRequired, - infrastructureMonitor: PropTypes.object.isRequired, - handleCheckboxChange: PropTypes.func.isRequired, -}; - -export default CustomAlertsSection; diff --git a/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx b/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx deleted file mode 100644 index d783f7985..000000000 --- a/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx +++ /dev/null @@ -1,127 +0,0 @@ -import { Box, Stack, Typography } from "@mui/material"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -import { useTheme } from "@emotion/react"; -import PropTypes from "prop-types"; - -/** - * CustomThreshold Component - * - * A reusable component that renders a checkbox with an associated numeric input field - * and an optional unit label. The input field can be enabled/disabled based on checkbox state. - * - * @component - * @param {Object} props - Component properties - * @param {string} [props.checkboxId] - Optional unique identifier for the checkbox - * @param {string} [props.checkboxName] - Optional name attribute for the checkbox - * @param {string} props.checkboxLabel - Label text for the checkbox - * @param {boolean} props.isChecked - Current checked state of the checkbox - * @param {Function} props.onCheckboxChange - Callback function when checkbox is toggled - * @param {string} props.fieldId - Unique identifier for the input field - * @param {string} [props.fieldName] - Optional name attribute for the input field - * @param {string} props.fieldValue - Current value of the input field - * @param {Function} props.onFieldChange - Callback function when input field value changes - * @param {Function} props.onFieldBlur - Callback function when input field loses focus - * @param {string} props.alertUnit - Unit label displayed next to the input field - * @param {Object} props.errors - Object containing validation errors for the field - * @param {Object} props.infrastructureMonitor - Infrastructure monitor configuration object - * - * @returns {React.ReactElement} Rendered CustomThreshold component - * - * @example - * - */ -export const CustomThreshold = ({ - checkboxId, - checkboxName, - checkboxLabel, - onCheckboxChange, - isChecked, - fieldId, - fieldName, - fieldValue, - onFieldChange, - onFieldBlur, - alertUnit, - errors, -}) => { - const theme = useTheme(); - return ( - - - - - - - - - {alertUnit} - - - - ); -}; - -CustomThreshold.propTypes = { - checkboxId: PropTypes.string, - checkboxName: PropTypes.string, - checkboxLabel: PropTypes.string.isRequired, - isChecked: PropTypes.bool.isRequired, - onCheckboxChange: PropTypes.func.isRequired, - fieldId: PropTypes.string.isRequired, - fieldName: PropTypes.string, - fieldValue: PropTypes.string.isRequired, - onFieldChange: PropTypes.func.isRequired, - onFieldBlur: PropTypes.func, - alertUnit: PropTypes.string.isRequired, - infrastructureMonitor: PropTypes.object.isRequired, - errors: PropTypes.object.isRequired, -}; diff --git a/client/src/Pages/Infrastructure/Create/Components/DiskSelection.jsx b/client/src/Pages/Infrastructure/Create/Components/DiskSelection.jsx deleted file mode 100644 index 50c4ed980..000000000 --- a/client/src/Pages/Infrastructure/Create/Components/DiskSelection.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from "react"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import { Box, Stack, Typography } from "@mui/material"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import PropTypes from "prop-types"; - -const DiskSelection = ({ availableDisks, selectedDisks, onChange }) => { - const theme = useTheme(); - const { t } = useTranslation(); - - const handleDiskChange = (event, mountpoint) => { - const isChecked = event.target.checked; - let newSelectedDisks = []; - - if (isChecked) { - newSelectedDisks = [...selectedDisks, mountpoint]; - } else { - newSelectedDisks = selectedDisks.filter((disk) => disk !== mountpoint); - } - - onChange(newSelectedDisks); - }; - - return ( - - - - {t("v1.infrastructure.disk_selection_title")} - - - {t("v1.infrastructure.disk_selection_description")} - - - - - {!availableDisks || availableDisks.length === 0 ? ( - - {t("v1.infrastructure.disk_selection_info")} - - ) : ( - availableDisks.map((disk) => { - const identifier = disk.mountpoint || disk.device; - return ( - - - handleDiskChange(e, identifier)} - /> - - - ); - }) - )} - - - ); -}; - -DiskSelection.propTypes = { - availableDisks: PropTypes.arrayOf( - PropTypes.shape({ - mountpoint: PropTypes.string.isRequired, - }) - ), - selectedDisks: PropTypes.arrayOf(PropTypes.string).isRequired, - onChange: PropTypes.func.isRequired, -}; - -export default DiskSelection; diff --git a/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx b/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx deleted file mode 100644 index 2bde5f321..000000000 --- a/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import { useState } from "react"; -import { Box, Button } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import Icon from "@/Components/v1/Icon"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; -import PropTypes from "prop-types"; - -const MonitorActionButtons = ({ monitor, isBusy, handlePause, handleRemove }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const [isOpen, setIsOpen] = useState(false); - - return ( - - - - setIsOpen(false)} - confirmationButtonLabel={t("delete")} - onConfirm={handleRemove} - /> - - ); -}; - -MonitorActionButtons.propTypes = { - monitor: PropTypes.object.isRequired, - isBusy: PropTypes.bool.isRequired, - handlePause: PropTypes.func.isRequired, - handleRemove: PropTypes.func.isRequired, -}; - -export default MonitorActionButtons; diff --git a/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx b/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx deleted file mode 100644 index 8fa6f4ff1..000000000 --- a/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Box, Stack, Tooltip, Typography } from "@mui/material"; -import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils.js"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import PulseDot from "@/Components/v1/Animated/PulseDot.jsx"; -import PropTypes from "prop-types"; -const MonitorStatusHeader = ({ monitor, infrastructureMonitor }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils(); - return ( - - - - - - - - {infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."} - - - {t("editing")} - - - ); -}; - -MonitorStatusHeader.propTypes = { - monitor: PropTypes.object.isRequired, - infrastructureMonitor: PropTypes.object.isRequired, -}; - -export default MonitorStatusHeader; diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx deleted file mode 100644 index a38ac9fc6..000000000 --- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useState, useCallback } from "react"; -const useInfrastructureMonitorForm = () => { - const [infrastructureMonitor, setInfrastructureMonitor] = useState({ - url: "", - name: "", - notifications: [], - notify_email: false, - interval: 0.25, - statusWindowSize: 5, - statusWindowThreshold: 60, - cpu: false, - usage_cpu: "", - memory: false, - usage_memory: "", - disk: false, - usage_disk: "", - temperature: false, - usage_temperature: "", - secret: "", - selectedDisks: [], - }); - - const onChangeForm = (name, value) => { - setInfrastructureMonitor({ - ...infrastructureMonitor, - [name]: value, - }); - }; - const handleCheckboxChange = (event) => { - setInfrastructureMonitor({ - ...infrastructureMonitor, - [event.target.name]: event.target.checked, - }); - }; - const initializeInfrastructureMonitorForCreate = useCallback((globalSettings) => { - const gt = globalSettings?.data?.settings?.globalThresholds || {}; - setInfrastructureMonitor((prev) => ({ - ...prev, - url: "", - name: "", - notifications: [], - interval: 0.25, - cpu: gt.cpu !== undefined, - usage_cpu: gt.cpu !== undefined ? gt.cpu.toString() : "", - memory: gt.memory !== undefined, - usage_memory: gt.memory !== undefined ? gt.memory.toString() : "", - disk: gt.disk !== undefined, - usage_disk: gt.disk !== undefined ? gt.disk.toString() : "", - temperature: gt.temperature !== undefined, - usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "", - secret: "", - selectedDisks: [], - })); - }, []); - - const initializeInfrastructureMonitorForUpdate = useCallback((monitor) => { - const MS_PER_MINUTE = 60000; - const { thresholds = {} } = monitor; - setInfrastructureMonitor((prev) => ({ - ...prev, - url: monitor.url.replace(/^https?:\/\//, ""), - name: monitor.name || "", - notifications: monitor.notifications || [], - interval: monitor.interval / MS_PER_MINUTE, - statusWindowSize: monitor.statusWindowSize, - statusWindowThreshold: monitor.statusWindowThreshold, - cpu: thresholds.usage_cpu !== undefined, - usage_cpu: - thresholds.usage_cpu !== undefined ? (thresholds.usage_cpu * 100).toString() : "", - memory: thresholds.usage_memory !== undefined, - usage_memory: - thresholds.usage_memory !== undefined - ? (thresholds.usage_memory * 100).toString() - : "", - disk: thresholds.usage_disk !== undefined, - usage_disk: - thresholds.usage_disk !== undefined - ? (thresholds.usage_disk * 100).toString() - : "", - temperature: thresholds.usage_temperature !== undefined, - usage_temperature: - thresholds.usage_temperature !== undefined - ? (thresholds.usage_temperature * 100).toString() - : "", - secret: monitor.secret || "", - selectedDisks: monitor.selectedDisks || [], - })); - }, []); - return { - infrastructureMonitor, - setInfrastructureMonitor, - onChangeForm, - handleCheckboxChange, - initializeInfrastructureMonitorForCreate, - initializeInfrastructureMonitorForUpdate, - }; -}; - -export default useInfrastructureMonitorForm; diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx deleted file mode 100644 index 04279cac4..000000000 --- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useCreateMonitor, useUpdateMonitor } from "../../../../Hooks/monitorHooks.js"; -const useInfrastructureSubmit = () => { - const [createMonitor, isCreating] = useCreateMonitor(); - const [updateMonitor, isUpdating] = useUpdateMonitor(); - const buildForm = (infrastructureMonitor, https) => { - const MS_PER_MINUTE = 60000; - - let form = { - url: `http${https ? "s" : ""}://` + infrastructureMonitor.url, - name: - infrastructureMonitor.name === "" - ? infrastructureMonitor.url - : infrastructureMonitor.name, - interval: infrastructureMonitor.interval * MS_PER_MINUTE, - statusWindowSize: infrastructureMonitor.statusWindowSize, - statusWindowThreshold: infrastructureMonitor.statusWindowThreshold, - cpu: infrastructureMonitor.cpu, - ...(infrastructureMonitor.cpu - ? { usage_cpu: infrastructureMonitor.usage_cpu } - : {}), - memory: infrastructureMonitor.memory, - ...(infrastructureMonitor.memory - ? { usage_memory: infrastructureMonitor.usage_memory } - : {}), - disk: infrastructureMonitor.disk, - ...(infrastructureMonitor.disk - ? { usage_disk: infrastructureMonitor.usage_disk } - : {}), - ...(infrastructureMonitor.temperature - ? { usage_temperature: infrastructureMonitor.usage_temperature } - : {}), - secret: infrastructureMonitor.secret, - selectedDisks: infrastructureMonitor.selectedDisks, - }; - return form; - }; - const submitInfrastructureForm = async ( - infrastructureMonitor, - form, - isCreate, - monitorId - ) => { - const { - cpu, - usage_cpu, - memory, - usage_memory, - disk, - usage_disk, - temperature, - usage_temperature, - selectedDisks, - ...rest - } = form; - - const thresholds = { - ...(cpu ? { usage_cpu: usage_cpu / 100 } : {}), - ...(memory ? { usage_memory: usage_memory / 100 } : {}), - ...(disk ? { usage_disk: usage_disk / 100 } : {}), - ...(temperature ? { usage_temperature: usage_temperature / 100 } : {}), - }; - - const finalForm = { - ...(isCreate ? {} : { id: monitorId }), - ...rest, - description: form.name, - type: "hardware", - notifications: infrastructureMonitor.notifications, - selectedDisks, - thresholds, - }; - // Handle create or update - isCreate - ? await createMonitor({ monitor: finalForm, redirect: "/infrastructure" }) - : await updateMonitor({ monitor: finalForm, redirect: "/infrastructure" }); - }; - return { - buildForm, - submitInfrastructureForm, - isCreating, - isUpdating, - }; -}; -export default useInfrastructureSubmit; diff --git a/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx b/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx deleted file mode 100644 index f7665e65f..000000000 --- a/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useState } from "react"; -import { infrastructureMonitorValidation } from "../../../../Validation/validation.js"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; -const useValidateInfrastructureForm = () => { - const [errors, setErrors] = useState({}); - - const validateField = (name, value) => { - const { error } = infrastructureMonitorValidation.validate( - { [name]: value }, - { abortEarly: false } - ); - setErrors((prev) => ({ - ...prev, - ...(error ? { [name]: error.details[0].message } : { [name]: undefined }), - })); - }; - - const validateForm = (form) => { - const { error } = infrastructureMonitorValidation.validate(form, { - abortEarly: false, - }); - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - console.log(newErrors); - setErrors(newErrors); - createToast({ body: "Please check the form for errors." }); - return error; - } - return null; - }; - return { errors, validateField, validateForm }; -}; -export default useValidateInfrastructureForm; diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx deleted file mode 100644 index 936d87ab0..000000000 --- a/client/src/Pages/Infrastructure/Create/index.jsx +++ /dev/null @@ -1,409 +0,0 @@ -//Components -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import FieldWrapper from "@/Components/v1/Inputs/FieldWrapper/index.jsx"; -import Link from "@/Components/v1/Link/index.jsx"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material"; -import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; -import MonitorStatusHeader from "./Components/MonitorStatusHeader.jsx"; -import MonitorActionButtons from "./Components/MonitorActionButtons.jsx"; -import CustomAlertsSection from "./Components/CustomAlertsSection.jsx"; -import DiskSelection from "./Components/DiskSelection.jsx"; -// Utils -import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx"; -import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications.js"; -import { networkService } from "../../../Utils/NetworkService.js"; -import { useParams } from "react-router-dom"; -import { useState, useEffect } from "react"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import { - useDeleteMonitor, - useFetchGlobalSettings, - useFetchMonitorById, - usePauseMonitor, -} from "@/Hooks/monitorHooks.js"; -import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm.jsx"; -import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm.jsx"; -import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit.jsx"; - -const CreateInfrastructureMonitor = () => { - const { monitorId } = useParams(); - const isCreate = typeof monitorId === "undefined"; - - const theme = useTheme(); - const { t } = useTranslation(); - - // State - const [monitor, setMonitor] = useState(null); - const [https, setHttps] = useState(false); - const [updateTrigger, setUpdateTrigger] = useState(false); - const [availableDisks, setAvailableDisks] = useState([]); - - // Fetch monitor details if editing - const [isLoading] = useFetchMonitorById({ - monitorId, - setMonitor, - updateTrigger: true, - }); - const [deleteMonitor, isDeleting] = useDeleteMonitor(); - const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings(); - const [notifications, notificationsAreLoading] = useGetNotificationsByTeamId(); - const [pauseMonitor, isPausing] = usePauseMonitor(); - const { - infrastructureMonitor, - setInfrastructureMonitor, - onChangeForm, - handleCheckboxChange, - initializeInfrastructureMonitorForCreate, - initializeInfrastructureMonitorForUpdate, - } = useInfrastructureMonitorForm(); - const { errors, validateField, validateForm } = useValidateInfrastructureForm(); - const { buildForm, submitInfrastructureForm, isCreating, isUpdating } = - useInfrastructureSubmit(); - - const FREQUENCIES = [ - { _id: 0.25, name: t("time.fifteenSeconds") }, - { _id: 0.5, name: t("time.thirtySeconds") }, - { _id: 1, name: t("time.oneMinute") }, - { _id: 2, name: t("time.twoMinutes") }, - { _id: 5, name: t("time.fiveMinutes") }, - { _id: 10, name: t("time.tenMinutes") }, - ]; - const CRUMBS = [ - { name: "Infrastructure monitors", path: "/infrastructure" }, - ...(isCreate - ? [{ name: "Create", path: "/infrastructure/create" }] - : [ - { name: "Details", path: `/infrastructure/${monitorId}` }, - { name: "Configure", path: `/infrastructure/configure/${monitorId}` }, - ]), - ]; - // Populate form fields if editing - useEffect(() => { - if (isCreate) { - if (globalSettingsLoading) return; - setHttps(false); - initializeInfrastructureMonitorForCreate(globalSettings); - } else if (monitor) { - setHttps(monitor.url.startsWith("https")); - initializeInfrastructureMonitorForUpdate(monitor); - const fetchLastCheck = async () => { - try { - let disks = monitor?.recentChecks?.[0]?.disk || []; - - if (disks.length > 0) { - setAvailableDisks(disks); - return; - } - - const response = await networkService.getChecksByMonitor({ - monitorId, - dateRange: "all", - rowsPerPage: 1, - sortOrder: "desc", - }); - disks = response?.data?.data?.checks?.[0]?.disk || []; - setAvailableDisks(disks); - } catch (error) { - setAvailableDisks([]); - } - }; - - fetchLastCheck(); - } - }, [ - isCreate, - monitor, - globalSettings, - globalSettingsLoading, - initializeInfrastructureMonitorForCreate, - initializeInfrastructureMonitorForUpdate, - ]); - - // Handlers - const onSubmit = async (event) => { - event.preventDefault(); - const form = buildForm(infrastructureMonitor, https); - // When editing, exclude URL from validation since it's disabled and can't be changed - const formToValidate = isCreate ? form : { ...form, url: monitor.url }; - const error = validateForm(formToValidate); - if (error) { - return; - } - submitInfrastructureForm(infrastructureMonitor, form, isCreate, monitorId); - }; - - const triggerUpdate = () => { - setUpdateTrigger(!updateTrigger); - }; - - const onChange = (event) => { - const { value, name } = event.target; - onChangeForm(name, value); - validateField(name, value); - }; - - const handlePause = async () => { - await pauseMonitor({ monitorId, triggerUpdate }); - }; - - const handleRemove = async (event) => { - event.preventDefault(); - await deleteMonitor({ monitor, redirect: "/infrastructure" }); - }; - - const isBusy = - isLoading || - isUpdating || - isCreating || - isDeleting || - isPausing || - notificationsAreLoading; - - return ( - - - - - - - - {!isCreate ? infrastructureMonitor.name : t("createYour") + " "} - - {isCreate ? ( - - {t("monitor")} - - ) : ( - <> - )} - - {!isCreate && monitor && ( - - )} - - {!isCreate && monitor && ( - - )} - - - - - {t("settingsGeneralSettings")} - - - {t("infrastructureCreateGeneralSettingsDescription")} - - - {t("infrastructureServerRequirement")}{" "} - - - - - } - placeholder={"localhost:59232/api/v1/metrics"} - label={t("infrastructureServerUrlLabel")} - https={https} - value={infrastructureMonitor.url} - onChange={onChange} - error={errors["url"] ? true : false} - helperText={errors["url"]} - disabled={!isCreate} - /> - {isCreate && ( - - - - - - - )} - - - - - - - - {t("notificationConfig.title")} - - {t("notificationConfig.description")} - - - - - - - {t("createMonitorPage.incidentConfigTitle")} - - - {t("createMonitorPage.incidentConfigDescription")} - - - - - - - - - - {monitorId && ( - - onChangeForm("selectedDisks", newSelectedDisks) - } - /> - )} - - - - - {t("distributedUptimeCreateAdvancedSettings")} - - - - - - - - - - - {!isCreate && ( - setIsOpen(false)} - confirmationButtonLabel={t("delete")} - onConfirm={handleRemove} - /> - )} - - ); -}; - -export default PageSpeedSetup; diff --git a/client/src/Pages/PageSpeed/Create/skeleton.jsx b/client/src/Pages/PageSpeed/Create/skeleton.jsx deleted file mode 100644 index 347fcaa65..000000000 --- a/client/src/Pages/PageSpeed/Create/skeleton.jsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Box, Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - <> - - - - - - - - - - - - - - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Uptime/Create/index.css b/client/src/Pages/Uptime/Create/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx deleted file mode 100644 index 01fc9eb7a..000000000 --- a/client/src/Pages/Uptime/Create/index.jsx +++ /dev/null @@ -1,826 +0,0 @@ -//Components -import { - Box, - Button, - ButtonGroup, - FormControlLabel, - Stack, - Switch, - Tooltip, - Typography, -} from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; -import Radio from "@/Components/v1/Inputs/Radio/index.jsx"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; -import PulseDot from "@/Components/v1/Animated/PulseDot.jsx"; -import SkeletonLayout from "./skeleton.jsx"; - -// Utils -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { monitorValidation } from "../../../Validation/validation.js"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import Icon from "@/Components/v1/Icon"; -import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js"; -import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications.js"; -import { useParams } from "react-router-dom"; -import { - useCreateMonitor, - useDeleteMonitor, - useUpdateMonitor, - usePauseMonitor, - useFetchMonitorById, - useFetchMonitorGames, -} from "../../../Hooks/monitorHooks.js"; - -/** - * Create page renders monitor creation or configuration views. - * @component - */ -const UptimeCreate = ({ isClone = false }) => { - const { monitorId } = useParams(); - const isCreate = typeof monitorId === "undefined" || isClone; - - // States - const [monitor, setMonitor] = useState({ - type: "http", - statusWindowSize: 5, - statusWindowThreshold: 60, - matchMethod: "equal", - expectedValue: "", - jsonPath: "", - notifications: [], - interval: 60000, - ignoreTlsErrors: false, - ...(isCreate ? { url: "", name: "" } : { port: undefined }), - }); - const [errors, setErrors] = useState({}); - const [https, setHttps] = useState(true); - const [isOpen, setIsOpen] = useState(false); - const [useAdvancedMatching, setUseAdvancedMatching] = useState(false); - const [updateTrigger, setUpdateTrigger] = useState(false); - const [games, setGames] = useState({}); - const triggerUpdate = () => { - setUpdateTrigger(!updateTrigger); - }; - - // Hooks - const [notifications, notificationsAreLoading, notificationsError] = - useGetNotificationsByTeamId(); - const { determineState, statusColor } = useMonitorUtils(); - // Fetch monitor details - const [isFetchingMonitor] = useFetchMonitorById({ - monitorId, - setMonitor, - updateTrigger, - }); - - // Fetch games - const [isFetchingGames] = useFetchMonitorGames({ - setGames, - updateTrigger, - }); - - // Combine the loading states - const isLoading = isFetchingMonitor || isFetchingGames; - - const [createMonitor, isCreating] = useCreateMonitor(); - const [pauseMonitor, isPausing] = usePauseMonitor({}); - const [deleteMonitor, isDeleting] = useDeleteMonitor(); - const [updateMonitor, isUpdating] = useUpdateMonitor(); - - // Setup - const theme = useTheme(); - const { t } = useTranslation(); - - // Constants - const MS_PER_MINUTE = 60000; - const FREQUENCIES = [ - { _id: 0.25, name: t("time.fifteenSeconds") }, - { _id: 0.5, name: t("time.thirtySeconds") }, - { _id: 1, name: t("time.oneMinute") }, - { _id: 2, name: t("time.twoMinutes") }, - { _id: 3, name: t("time.threeMinutes") }, - { _id: 4, name: t("time.fourMinutes") }, - { _id: 5, name: t("time.fiveMinutes") }, - { _id: 10, name: t("time.tenMinutes") }, - { _id: 15, name: t("time.fifteenMinutes") }, - { _id: 30, name: t("time.thirtyMinutes") }, - ]; - - const GAMELIST = Object.entries(games).map(([key, value]) => ({ - _id: key, - name: value.name, - })); - - const CRUMBS = [ - { name: "uptime", path: "/uptime" }, - ...(isCreate - ? [{ name: "create", path: `/uptime/create` }] - : [ - { name: "details", path: `/uptime/${monitorId}` }, - { name: "configure", path: `/uptime/configure/${monitorId}` }, - ]), - ]; - const matchMethodOptions = [ - { _id: "equal", name: t("matchMethodOptions.equal") }, - { _id: "include", name: t("matchMethodOptions.include") }, - { _id: "regex", name: t("matchMethodOptions.regex") }, - ]; - const expectedValuePlaceholders = { - regex: t("matchMethodOptions.regexPlaceholder"), - equal: t("matchMethodOptions.equalPlaceholder"), - include: t("matchMethodOptions.includePlaceholder"), - }; - const monitorTypeMaps = { - http: { - label: t("monitorType.http.label"), - placeholder: t("monitorType.http.placeholder"), - namePlaceholder: t("monitorType.http.namePlaceholder"), - }, - ping: { - label: t("monitorType.ping.label"), - placeholder: t("monitorType.ping.placeholder"), - namePlaceholder: t("monitorType.ping.namePlaceholder"), - }, - docker: { - label: t("monitorType.docker.label"), - placeholder: t("monitorType.docker.placeholder"), - namePlaceholder: t("monitorType.docker.namePlaceholder"), - }, - port: { - label: t("monitorType.port.label"), - placeholder: t("monitorType.port.placeholder"), - namePlaceholder: t("monitorType.port.namePlaceholder"), - }, - game: { - label: t("monitorType.game.label"), - placeholder: t("monitorType.game.placeholder"), - namePlaceholder: t("monitorType.game.namePlaceholder"), - }, - }; - - // Handlers - const onSubmit = async (event) => { - event.preventDefault(); - const { notifications, ...rest } = monitor; - - let form = {}; - if (isCreate) { - form = { - url: - monitor.type === "http" && !isClone - ? `http${https ? "s" : ""}://` + monitor.url - : monitor.url, - name: monitor.name || monitor.url.substring(0, 50), - statusWindowSize: monitor.statusWindowSize, - statusWindowThreshold: monitor.statusWindowThreshold, - type: monitor.type, - - port: - monitor.type === "port" || monitor.type === "game" ? monitor.port : undefined, - interval: monitor.interval, - matchMethod: monitor.matchMethod, - expectedValue: monitor.expectedValue, - jsonPath: monitor.jsonPath, - ignoreTlsErrors: monitor.ignoreTlsErrors, - gameId: monitor.gameId || undefined, - }; - } else { - form = { - id: monitor.id, - url: monitor.url, - name: monitor.name || monitor.url.substring(0, 50), - statusWindowSize: monitor.statusWindowSize, - statusWindowThreshold: monitor.statusWindowThreshold, - type: monitor.type, - matchMethod: monitor.matchMethod, - expectedValue: monitor.expectedValue, - jsonPath: monitor.jsonPath, - interval: monitor.interval, - teamId: monitor.teamId, - userId: monitor.userId, - port: - monitor.type === "port" || monitor.type === "game" ? monitor.port : undefined, - ignoreTlsErrors: monitor.ignoreTlsErrors, - gameId: monitor.gameId || undefined, - }; - } - if (!useAdvancedMatching) { - form.matchMethod = isCreate ? undefined : ""; - form.expectedValue = isCreate ? undefined : ""; - form.jsonPath = isCreate ? undefined : ""; - } - - const { error } = monitorValidation.validate(form, { - abortEarly: false, - }); - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - createToast({ body: t("checkFormError") }); - return; - } - - form = { - ...form, - description: monitor.name || monitor.url, - notifications: monitor.notifications, - }; - - if (isCreate) { - await createMonitor({ monitor: form, redirect: "/uptime" }); - } else { - await updateMonitor({ monitor: form, redirect: "/uptime" }); - } - }; - - const onChange = (event) => { - let { name, value, checked } = event.target; - - if (name === "ignoreTlsErrors") { - value = checked; - } - - if (name === "useAdvancedMatching") { - setUseAdvancedMatching(checked); - return; - } - - if (name === "interval") { - value = value * MS_PER_MINUTE; - } - - setMonitor((prev) => ({ ...prev, [name]: value })); - - if (name === "type") { - setErrors({}); - return; - } - - const { error } = monitorValidation.validate( - { type: monitor.type, [name]: value }, - { abortEarly: false } - ); - - setErrors((prev) => ({ - ...prev, - ...(error && error.details[0].path[0] === name - ? { [name]: error.details[0].message } - : { [name]: undefined }), - })); - }; - - const handleRemove = async (event) => { - event.preventDefault(); - const TEMP_MONITOR = { id: monitor.id }; - await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/uptime" }); - }; - - const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing; - const displayInterval = monitor?.interval / MS_PER_MINUTE || 1; - const parsedUrl = monitor?.url; - const protocol = parsedUrl?.protocol?.replace(":", "") || ""; - - useEffect(() => { - if (!isCreate || isClone) { - if (monitor.matchMethod) { - setUseAdvancedMatching(true); - } else { - setUseAdvancedMatching(false); - } - } - }, [monitor, isCreate]); - - if (Object.keys(monitor).length === 0) { - return ; - } - - return ( - - - - - - - - - {!isCreate ? monitor.name : t("createYour") + " "} - - {isCreate && ( - - {t("monitor")} - - )} - - {!isCreate && ( - - - - - - - - {monitor.url?.replace(/^https?:\/\//, "") || "..."} - - - {t("editing")} - - - )} - - {!isCreate && ( - - - - - )} - - {isCreate && ( - - - - {t("distributedUptimeCreateChecks")} - - - {t("distributedUptimeCreateChecksDescription")} - - - - - - {monitor.type === "http" ? ( - - - - - ) : ( - "" - )} - - - - - - {errors["type"] ? ( - - - {errors["type"]} - - - ) : ( - "" - )} - - - )} - - - - {t("settingsGeneralSettings")} - - - {isCreate - ? t(`uptimeGeneralInstructions.${monitor.type}`) - : t("distributedUptimeCreateSelectURL")} - - - - - ) - } - helperText={errors["url"]} - onChange={onChange} - disabled={!isCreate} - /> -