From 794018a6abbf95d3028ce1c7331eb184217b6ab6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sat, 24 Jan 2026 20:41:21 +0000 Subject: [PATCH 01/15] 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 02/15] 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 03/15] 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 04/15] 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 05/15] 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 06/15] 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 07/15] 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 08/15] 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 09/15] 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 10/15] 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 = ({