From 300b1b2238999fa2662ee5f085bb634bd725b19b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 21:57:09 +0000 Subject: [PATCH 01/13] initial commit --- client/src/Pages/Checks/index.tsx | 53 +++++++++++++++++++ .../src/Pages/Checks/{index.jsx => old.jsx} | 0 client/src/Routes/index.jsx | 2 +- 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 client/src/Pages/Checks/index.tsx rename client/src/Pages/Checks/{index.jsx => old.jsx} (100%) diff --git a/client/src/Pages/Checks/index.tsx b/client/src/Pages/Checks/index.tsx new file mode 100644 index 000000000..5cb38ff5f --- /dev/null +++ b/client/src/Pages/Checks/index.tsx @@ -0,0 +1,53 @@ +import { BasePage, Breadcrumb } from "@/Components/v2/design-elements"; +import { useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useState, useEffect, useMemo } from "react"; +import { useParams } from "react-router-dom"; +import { useGet } from "@/Hooks/UseApi"; + +interface MonitorLookupItem { + id: string; + name: string; + type: string; +} + +interface MonitorLookup { + [key: string]: MonitorLookupItem; +} + +const Checks = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const { monitorId } = useParams<{ monitorId?: string }>(); + + // Local state + const [selectedMonitor, setSelectedMonitor] = useState("0"); + const [filter, setFilter] = useState(undefined); + const [dateRange, setDateRange] = useState("hour"); + + // Data fetching with SWR + const monitorsUrl = "/monitors/team"; + const summaryUrl = `/checks/team/summary?dateRange=${dateRange}`; + + const { + data: monitorsResponse, + isLoading: isLoadingMonitors, + error: monitorsError, + } = useGet(monitorsUrl); + + const { + data: summaryResponse, + isLoading: isLoadingSummary, + error: summaryError, + } = useGet(summaryUrl); + + console.log(monitorsResponse, summaryResponse); + + return ( + + + + ); +}; + +export default Checks; diff --git a/client/src/Pages/Checks/index.jsx b/client/src/Pages/Checks/old.jsx similarity index 100% rename from client/src/Pages/Checks/index.jsx rename to client/src/Pages/Checks/old.jsx diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 45715a488..3abcfc16d 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -33,7 +33,7 @@ import InfrastructureDetails from "../Pages/Infrastructure/Details/index.jsx"; import ServerUnreachable from "../Pages/ServerUnreachable.jsx"; // Checks -import Checks from "../Pages/Checks/index.jsx"; +import Checks from "../Pages/Checks/index"; // Incidents import Incidents from "../Pages/Incidents/index.jsx"; From f88e4ee6f44da54a776182020afde84e098bd450 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 22:24:16 +0000 Subject: [PATCH 02/13] refactor --- .../v2/design-elements/StatusBox.tsx | 22 +++++++++++++++++ client/src/Pages/Checks/index.tsx | 24 +++++++++---------- client/src/Routes/index.jsx | 8 ++++++- client/src/Types/Check.ts | 2 -- .../checks/MongoChecksRepistory.ts | 4 +--- server/src/types/check.ts | 2 -- 6 files changed, 42 insertions(+), 20 deletions(-) diff --git a/client/src/Components/v2/design-elements/StatusBox.tsx b/client/src/Components/v2/design-elements/StatusBox.tsx index 6b1c1920c..23cef3b37 100644 --- a/client/src/Components/v2/design-elements/StatusBox.tsx +++ b/client/src/Components/v2/design-elements/StatusBox.tsx @@ -98,6 +98,28 @@ export const PausedStatusBox = ({ n }: { n: number }) => { /> ); }; +export const TotalChecksBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; +export const DownChecksBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; export const InitializingStatusBox = ({ n }: { n: number }) => { const theme = useTheme(); diff --git a/client/src/Pages/Checks/index.tsx b/client/src/Pages/Checks/index.tsx index 5cb38ff5f..f692044e2 100644 --- a/client/src/Pages/Checks/index.tsx +++ b/client/src/Pages/Checks/index.tsx @@ -1,19 +1,13 @@ -import { BasePage, Breadcrumb } from "@/Components/v2/design-elements"; +import Stack from "@mui/material/Stack"; +import { BasePage, TotalChecksBox, DownChecksBox } from "@/Components/v2/design-elements"; + import { useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useState, useEffect, useMemo } from "react"; import { useParams } from "react-router-dom"; import { useGet } from "@/Hooks/UseApi"; - -interface MonitorLookupItem { - id: string; - name: string; - type: string; -} - -interface MonitorLookup { - [key: string]: MonitorLookupItem; -} +import type { Monitor } from "@/Types/Monitor"; +import type { ChecksSummary } from "@/Types/Check"; const Checks = () => { const { t } = useTranslation(); @@ -45,7 +39,13 @@ const Checks = () => { return ( - + + + + ); }; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 3abcfc16d..5ee16a49e 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -163,7 +163,13 @@ const Routes = () => { /> } + element={ + <> + + + + + } /> => { diff --git a/server/src/types/check.ts b/server/src/types/check.ts index ce932d79d..8e223d7c8 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -172,9 +172,7 @@ export interface UptimeChecksResult { export interface ChecksSummary { totalChecks: number; - resolvedChecks: number; downChecks: number; - cannotResolveChecks: number; } export interface HasResponseTime { From f3ba8c98152ad7e81a7caa16538e04d0ac8a3d6b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 23:05:00 +0000 Subject: [PATCH 03/13] monitor select --- .../v2/design-elements/StatusBox.tsx | 19 ++- .../Pages/Checks/Components/ChecksTable.tsx | 132 ++++++++++++++++++ client/src/Pages/Checks/index.tsx | 43 +++++- .../checks/MongoChecksRepistory.ts | 23 ++- 4 files changed, 197 insertions(+), 20 deletions(-) create mode 100644 client/src/Pages/Checks/Components/ChecksTable.tsx diff --git a/client/src/Components/v2/design-elements/StatusBox.tsx b/client/src/Components/v2/design-elements/StatusBox.tsx index 23cef3b37..9e322cf73 100644 --- a/client/src/Components/v2/design-elements/StatusBox.tsx +++ b/client/src/Components/v2/design-elements/StatusBox.tsx @@ -4,12 +4,16 @@ import Box from "@mui/material/Box"; import { BaseBox } from "@/Components/v2/design-elements"; import Background from "@/assets/Images/background-grid.svg?react"; import { useTranslation } from "react-i18next"; +import type { SxProps } from "@mui/material"; import { useTheme } from "@mui/material/styles"; -type StatusBoxProps = React.PropsWithChildren<{ children: React.ReactNode }>; +type StatusBoxProps = React.PropsWithChildren<{ + children: React.ReactNode; + sx?: SxProps; +}>; -export const BGBox = ({ children }: StatusBoxProps) => { +export const BGBox = ({ children, sx }: StatusBoxProps) => { const theme = useTheme(); return ( { position: "relative", flex: 1, padding: theme.spacing(4), + ...sx, }} > { const theme = useTheme(); return ( - + { label={t("pages.common.monitors.status.total")} n={n} color={theme.palette.primary.light} + sx={{ + maxWidth: "300px", + }} /> ); }; @@ -117,6 +127,9 @@ export const DownChecksBox = ({ n }: { n: number }) => { label={t("pages.common.monitors.status.down")} n={n} color={theme.palette.error.light} + sx={{ + maxWidth: "300px", + }} /> ); }; diff --git a/client/src/Pages/Checks/Components/ChecksTable.tsx b/client/src/Pages/Checks/Components/ChecksTable.tsx new file mode 100644 index 000000000..269e74035 --- /dev/null +++ b/client/src/Pages/Checks/Components/ChecksTable.tsx @@ -0,0 +1,132 @@ +import { + Table, + Pagination, + ValueLabel, + StatusLabel, +} from "@/Components/v2/design-elements"; +import Box from "@mui/material/Box"; +import type { Header } from "@/Components/v2/design-elements/Table"; +import type { Monitor, MonitorStatus } from "@/Types/Monitor"; + +import { useTranslation } from "react-i18next"; +import { formatDateWithTz } from "@/Utils/TimeUtils"; +import { useNavigate } from "react-router"; +import type { Check } from "@/Types/Check"; +import type { RootState } from "@/Types/state"; +import { useSelector } from "react-redux"; + +export const ChecksTable = ({ + monitors, + selectedMonitorId, + page, + setPage, + rowsPerPage, + setRowsPerPage, +}: { + monitors: Monitor[] | null; + selectedMonitorId: string; + page: number; + setPage: (page: number) => void; + rowsPerPage: number; + setRowsPerPage: (rowsPerPage: number) => void; +}) => { + const { t } = useTranslation(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + const navigate = useNavigate(); + + // DATA FETCHING HERE + + const getHeaders = (t: Function, uiTimezone: string) => { + const headers: Header[] = [ + { + id: "monitorName", + content: t("common.table.headers.monitor"), + render: (row) => { + return ( + monitors?.find((monitor) => monitor.id === row.metadata.monitorId)?.name || + "N/A" + ); + }, + }, + { + id: "status", + content: "Status", + render: (row) => { + return ; + }, + }, + { + id: "date", + content: t("checks.table.headers.dateTime"), + render: (row) => { + return formatDateWithTz( + row.createdAt, + "ddd, MMMM D, YYYY, HH:mm A", + uiTimezone + ); + }, + }, + { + id: "statusCode", + content: t("checks.table.headers.statusCode"), + render: (row) => { + const code = row.statusCode; + if (!code) return "N/A"; + const value = code < 300 ? "positive" : code < 400 ? "neutral" : "negative"; + return ( + + ); + }, + }, + { + id: "message", + content: t("common.table.headers.message"), + render: (row) => { + return row.message || "N/A"; + }, + }, + ]; + return headers; + }; + + const headers = getHeaders(t, uiTimezone); + + const handlePageChange = ( + _e: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + const value = Number(e.target.value); + setPage(0); + setRowsPerPage(value); + }; + + return ( + + { + navigate(`/checks/${row.id}`); + }} + emptyViewText={t("checks.table.empty")} + /> + + + ); +}; diff --git a/client/src/Pages/Checks/index.tsx b/client/src/Pages/Checks/index.tsx index f692044e2..eeb555668 100644 --- a/client/src/Pages/Checks/index.tsx +++ b/client/src/Pages/Checks/index.tsx @@ -1,7 +1,10 @@ import Stack from "@mui/material/Stack"; import { BasePage, TotalChecksBox, DownChecksBox } from "@/Components/v2/design-elements"; +import { HeaderTimeRange } from "@/Components/v2/common"; +import { Select } from "@/Components/v2/inputs"; +import IncidentTable from "./Components/IncidentTable/index.jsx"; -import { useTheme } from "@mui/material"; +import { MenuItem, useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useState, useEffect, useMemo } from "react"; import { useParams } from "react-router-dom"; @@ -9,6 +12,12 @@ import { useGet } from "@/Hooks/UseApi"; import type { Monitor } from "@/Types/Monitor"; import type { ChecksSummary } from "@/Types/Check"; +interface MonitorLookupEntry { + id: string; + name: string; + type: string; +} + const Checks = () => { const { t } = useTranslation(); const theme = useTheme(); @@ -16,8 +25,9 @@ const Checks = () => { // Local state const [selectedMonitor, setSelectedMonitor] = useState("0"); - const [filter, setFilter] = useState(undefined); + const [filter, setFilter] = useState(undefined); const [dateRange, setDateRange] = useState("hour"); + const [updateTrigger, setUpdateTrigger] = useState(false); // Data fetching with SWR const monitorsUrl = "/monitors/team"; @@ -35,8 +45,6 @@ const Checks = () => { error: summaryError, } = useGet(summaryUrl); - console.log(monitorsResponse, summaryResponse); - return ( { + + + + + ); }; diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index e584cd661..708cf4ffe 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -366,27 +366,24 @@ class MongoChecksRepository implements IChecksRepository { }; findSummaryByTeamId = async (teamId: string, dateRange: string) => { - const matchStage = { + const baseMatch = { "metadata.teamId": new mongoose.Types.ObjectId(teamId), - status: false, ...(dateRangeLookup[dateRange] && { createdAt: { $gte: dateRangeLookup[dateRange], }, }), }; - const checks = await CheckModel.aggregate([ - { $match: matchStage }, - { - $group: { - _id: null, - totalChecks: { $sum: 1 }, - downChecks: { $sum: { $cond: [{ $eq: ["$ack", false] }, 1, 0] } }, - }, - }, - { $project: { _id: 0 } }, + + const [totalResult, downResult] = await Promise.all([ + CheckModel.countDocuments(baseMatch), + CheckModel.countDocuments({ ...baseMatch, status: false }), ]); - return checks[0] ?? { totalChecks: 0, downChecks: 0 }; + + return { + totalChecks: totalResult, + downChecks: downResult, + }; }; deleteByMonitorId = async (monitorId: string): Promise => { From c48049e0afc9c918d2521511ed3e270f7db3cbfd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 23:17:40 +0000 Subject: [PATCH 04/13] checks table --- .../Pages/Checks/Components/ChecksTable.tsx | 50 +++++++++++++++++-- client/src/Pages/Checks/index.tsx | 26 ++++++---- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/client/src/Pages/Checks/Components/ChecksTable.tsx b/client/src/Pages/Checks/Components/ChecksTable.tsx index 269e74035..9088c2cef 100644 --- a/client/src/Pages/Checks/Components/ChecksTable.tsx +++ b/client/src/Pages/Checks/Components/ChecksTable.tsx @@ -9,15 +9,18 @@ import type { Header } from "@/Components/v2/design-elements/Table"; import type { Monitor, MonitorStatus } from "@/Types/Monitor"; import { useTranslation } from "react-i18next"; +import { useMemo } from "react"; import { formatDateWithTz } from "@/Utils/TimeUtils"; import { useNavigate } from "react-router"; -import type { Check } from "@/Types/Check"; +import type { Check, ChecksResponse } from "@/Types/Check"; import type { RootState } from "@/Types/state"; import { useSelector } from "react-redux"; +import { useGet } from "@/Hooks/UseApi"; export const ChecksTable = ({ monitors, selectedMonitorId, + dateRange, page, setPage, rowsPerPage, @@ -25,6 +28,7 @@ export const ChecksTable = ({ }: { monitors: Monitor[] | null; selectedMonitorId: string; + dateRange: string; page: number; setPage: (page: number) => void; rowsPerPage: number; @@ -34,7 +38,47 @@ export const ChecksTable = ({ const uiTimezone = useSelector((state: RootState) => state.ui.timezone); const navigate = useNavigate(); - // DATA FETCHING HERE + // Get selected monitor type + const selectedMonitorType = monitors?.find((m) => m.id === selectedMonitorId)?.type; + + // Team checks URL (when selectedMonitorId === "0") + const teamChecksUrl = useMemo(() => { + if (selectedMonitorId !== "0") return null; + const params = new URLSearchParams(); + params.append("sortOrder", "desc"); + if (dateRange) params.append("dateRange", dateRange); + params.append("page", String(page)); + params.append("rowsPerPage", String(rowsPerPage)); + return `/checks/team?${params.toString()}`; + }, [selectedMonitorId, dateRange, page, rowsPerPage]); + + // Monitor checks URL (when specific monitor selected) + const monitorChecksUrl = useMemo(() => { + if (selectedMonitorId === "0" || !selectedMonitorType) return null; + const params = new URLSearchParams(); + params.append("type", selectedMonitorType); + params.append("sortOrder", "desc"); + params.append("status", "false"); + if (dateRange) params.append("dateRange", dateRange); + params.append("page", String(page)); + params.append("rowsPerPage", String(rowsPerPage)); + return `/checks/${selectedMonitorId}?${params.toString()}`; + }, [selectedMonitorId, selectedMonitorType, dateRange, page, rowsPerPage]); + + // Fetch data - null key skips the request + const { data: teamData, isLoading: isLoadingTeam } = + useGet(teamChecksUrl); + const { data: monitorData, isLoading: isLoadingMonitor } = + useGet(monitorChecksUrl); + + // Select correct data based on selection + const checks = + selectedMonitorId === "0" ? (teamData?.checks ?? []) : (monitorData?.checks ?? []); + const checksCount = + selectedMonitorId === "0" + ? (teamData?.checksCount ?? 0) + : (monitorData?.checksCount ?? 0); + const isLoading = isLoadingTeam || isLoadingMonitor; const getHeaders = (t: Function, uiTimezone: string) => { const headers: Header[] = [ @@ -121,7 +165,7 @@ export const ChecksTable = ({ /> { const { t } = useTranslation(); const theme = useTheme(); const { monitorId } = useParams<{ monitorId?: string }>(); // Local state - const [selectedMonitor, setSelectedMonitor] = useState("0"); - const [filter, setFilter] = useState(undefined); + const [selectedMonitor, setSelectedMonitor] = useState(monitorId || "0"); const [dateRange, setDateRange] = useState("hour"); - const [updateTrigger, setUpdateTrigger] = useState(false); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); // Data fetching with SWR const monitorsUrl = "/monitors/team"; @@ -81,6 +75,16 @@ const Checks = () => { setDateRange={setDateRange} /> + + ); }; From 07705e9823572c05525575edea517de41c4ba396 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 23:21:04 +0000 Subject: [PATCH 05/13] fix recent --- client/src/Pages/Checks/index.tsx | 2 +- server/src/repositories/checks/MongoChecksRepistory.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/Pages/Checks/index.tsx b/client/src/Pages/Checks/index.tsx index 3da94da59..a861ec5bf 100644 --- a/client/src/Pages/Checks/index.tsx +++ b/client/src/Pages/Checks/index.tsx @@ -19,7 +19,7 @@ const Checks = () => { // Local state const [selectedMonitor, setSelectedMonitor] = useState(monitorId || "0"); - const [dateRange, setDateRange] = useState("hour"); + const [dateRange, setDateRange] = useState("recent"); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 708cf4ffe..1c44978f4 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -19,7 +19,7 @@ import mongoose from "mongoose"; const SERVICE_NAME = "StatusService"; const dateRangeLookup: Record = { - recent: new Date(new Date().setDate(new Date().getDate() - 2)), + recent: new Date(new Date().setHours(new Date().getHours() - 2)), hour: new Date(new Date().setHours(new Date().getHours() - 1)), day: new Date(new Date().setDate(new Date().getDate() - 1)), week: new Date(new Date().setDate(new Date().getDate() - 7)), From fcfad945a095912ceea6fc84cacab0dd25799890 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 28 Jan 2026 17:59:11 +0000 Subject: [PATCH 06/13] selects --- .../v2/common/controls/HeaderTimeRange.tsx | 5 ++ .../v2/design-elements/StatusBox.tsx | 14 ++++ .../Pages/Checks/Components/ChecksTable.tsx | 20 ++--- client/src/Pages/Checks/index.tsx | 83 ++++++++++++------- client/src/locales/en.json | 15 +++- .../checks/MongoChecksRepistory.ts | 11 ++- server/src/validation/joi.ts | 8 +- 7 files changed, 110 insertions(+), 46 deletions(-) diff --git a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx index 57b61473a..9b8b9dcec 100644 --- a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx +++ b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx @@ -2,6 +2,8 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import { ToggleButtonGroup, ToggleButton } from "@/Components/v2/inputs"; import { useTheme } from "@mui/material/styles"; +import CircularProgress from "@mui/material/CircularProgress"; +import Box from "@mui/material/Box"; interface MonitorTimeFrameHeaderProps { isLoading?: boolean; @@ -72,6 +74,9 @@ export const HeaderTimeRange = ({ alignItems="center" gap={theme.spacing(4)} > + + {isLoading && } + Showing statistics for past{" "} {dateRange === "recent" diff --git a/client/src/Components/v2/design-elements/StatusBox.tsx b/client/src/Components/v2/design-elements/StatusBox.tsx index 9e322cf73..d48871a2a 100644 --- a/client/src/Components/v2/design-elements/StatusBox.tsx +++ b/client/src/Components/v2/design-elements/StatusBox.tsx @@ -133,6 +133,20 @@ export const DownChecksBox = ({ n }: { n: number }) => { /> ); }; +export const UpChecksBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; export const InitializingStatusBox = ({ n }: { n: number }) => { const theme = useTheme(); diff --git a/client/src/Pages/Checks/Components/ChecksTable.tsx b/client/src/Pages/Checks/Components/ChecksTable.tsx index 9088c2cef..e48af37b8 100644 --- a/client/src/Pages/Checks/Components/ChecksTable.tsx +++ b/client/src/Pages/Checks/Components/ChecksTable.tsx @@ -20,6 +20,7 @@ import { useGet } from "@/Hooks/UseApi"; export const ChecksTable = ({ monitors, selectedMonitorId, + statusFilter, dateRange, page, setPage, @@ -28,6 +29,7 @@ export const ChecksTable = ({ }: { monitors: Monitor[] | null; selectedMonitorId: string; + statusFilter: string; dateRange: string; page: number; setPage: (page: number) => void; @@ -38,40 +40,36 @@ export const ChecksTable = ({ const uiTimezone = useSelector((state: RootState) => state.ui.timezone); const navigate = useNavigate(); - // Get selected monitor type const selectedMonitorType = monitors?.find((m) => m.id === selectedMonitorId)?.type; - // Team checks URL (when selectedMonitorId === "0") const teamChecksUrl = useMemo(() => { if (selectedMonitorId !== "0") return null; const params = new URLSearchParams(); params.append("sortOrder", "desc"); if (dateRange) params.append("dateRange", dateRange); + if (statusFilter) params.append("filter", statusFilter); params.append("page", String(page)); params.append("rowsPerPage", String(rowsPerPage)); return `/checks/team?${params.toString()}`; - }, [selectedMonitorId, dateRange, page, rowsPerPage]); + }, [selectedMonitorId, dateRange, statusFilter, page, rowsPerPage]); - // Monitor checks URL (when specific monitor selected) const monitorChecksUrl = useMemo(() => { if (selectedMonitorId === "0" || !selectedMonitorType) return null; const params = new URLSearchParams(); params.append("type", selectedMonitorType); params.append("sortOrder", "desc"); - params.append("status", "false"); + if (statusFilter) params.append("filter", statusFilter); if (dateRange) params.append("dateRange", dateRange); params.append("page", String(page)); params.append("rowsPerPage", String(rowsPerPage)); return `/checks/${selectedMonitorId}?${params.toString()}`; - }, [selectedMonitorId, selectedMonitorType, dateRange, page, rowsPerPage]); + }, [selectedMonitorId, selectedMonitorType, dateRange, statusFilter, page, rowsPerPage]); - // Fetch data - null key skips the request const { data: teamData, isLoading: isLoadingTeam } = useGet(teamChecksUrl); const { data: monitorData, isLoading: isLoadingMonitor } = useGet(monitorChecksUrl); - // Select correct data based on selection const checks = selectedMonitorId === "0" ? (teamData?.checks ?? []) : (monitorData?.checks ?? []); const checksCount = @@ -101,7 +99,7 @@ export const ChecksTable = ({ }, { id: "date", - content: t("checks.table.headers.dateTime"), + content: t("common.table.headers.dateTime"), render: (row) => { return formatDateWithTz( row.createdAt, @@ -112,7 +110,7 @@ export const ChecksTable = ({ }, { id: "statusCode", - content: t("checks.table.headers.statusCode"), + content: t("pages.checks.table.headers.statusCode"), render: (row) => { const code = row.statusCode; if (!code) return "N/A"; @@ -161,7 +159,7 @@ export const ChecksTable = ({ onRowClick={(row) => { navigate(`/checks/${row.id}`); }} - emptyViewText={t("checks.table.empty")} + emptyViewText={t("pages.checks.table.empty")} /> { const { t } = useTranslation(); - const theme = useTheme(); const { monitorId } = useParams<{ monitorId?: string }>(); // Local state const [selectedMonitor, setSelectedMonitor] = useState(monitorId || "0"); const [dateRange, setDateRange] = useState("recent"); + const [statusFilter, setStatusFilter] = useState("down"); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); @@ -27,17 +32,16 @@ const Checks = () => { const monitorsUrl = "/monitors/team"; const summaryUrl = `/checks/team/summary?dateRange=${dateRange}`; - const { - data: monitorsResponse, - isLoading: isLoadingMonitors, - error: monitorsError, - } = useGet(monitorsUrl); + const { data: monitorsResponse, isLoading: isLoadingMonitors } = + useGet(monitorsUrl); - const { - data: summaryResponse, - isLoading: isLoadingSummary, - error: summaryError, - } = useGet(summaryUrl); + const { data: summaryResponse, isLoading: isLoadingSummary } = + useGet(summaryUrl); + + const isLoading = isLoadingMonitors || isLoadingSummary; + const totalChecks = summaryResponse?.totalChecks || 0; + const downChecks = summaryResponse?.downChecks || 0; + const upChecks = totalChecks - (summaryResponse?.downChecks || 0); return ( @@ -45,32 +49,52 @@ const Checks = () => { direction={{ xs: "column", md: "row" }} gap={4} > - - + + + - + + + @@ -79,6 +103,7 @@ const Checks = () => { { const matchStage: Record = { "metadata.teamId": new mongoose.Types.ObjectId(teamId), - status: false, ...(dateRangeLookup[dateRange] && { createdAt: { $gte: dateRangeLookup[dateRange], @@ -283,9 +287,14 @@ class MongoChecksRepository implements IChecksRepository { switch (filter) { case "all": break; + case "up": + matchStage.status = true; + break; case "down": + matchStage.status = false; break; case "resolve": + matchStage.status = false; matchStage.statusCode = 5000; break; default: diff --git a/server/src/validation/joi.ts b/server/src/validation/joi.ts index 8871c29e2..1d5fcbbf0 100755 --- a/server/src/validation/joi.ts +++ b/server/src/validation/joi.ts @@ -111,7 +111,7 @@ const getMonitorByIdQueryValidation = joi.object({ status: joi.boolean(), sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), - dateRange: joi.string().valid("hour", "day", "week", "month", "all"), + dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"), numToDisplay: joi.number(), normalize: joi.boolean(), }); @@ -307,7 +307,7 @@ const getChecksQueryValidation = joi.object({ sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"), - filter: joi.string().valid("all", "down", "resolve"), + filter: joi.string().valid("all", "up", "down", "resolve"), ack: joi.boolean(), page: joi.number(), rowsPerPage: joi.number(), @@ -317,8 +317,8 @@ const getChecksQueryValidation = joi.object({ const getTeamChecksQueryValidation = joi.object({ sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), - dateRange: joi.string().valid("hour", "day", "week", "month", "all"), - filter: joi.string().valid("all", "down", "resolve"), + dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"), + filter: joi.string().valid("all", "up", "down", "resolve"), ack: joi.boolean(), page: joi.number(), rowsPerPage: joi.number(), From 7a66b5310b850acf808d1c551689254acdabcd96 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 28 Jan 2026 18:03:33 +0000 Subject: [PATCH 07/13] expose isValidating --- client/src/Hooks/UseApi.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/Hooks/UseApi.ts b/client/src/Hooks/UseApi.ts index aa71f01af..08ef9a9a5 100644 --- a/client/src/Hooks/UseApi.ts +++ b/client/src/Hooks/UseApi.ts @@ -21,7 +21,7 @@ export const useGet = ( axiosConfig?: AxiosRequestConfig, swrConfig?: SWRConfiguration ) => { - const { data, error, isLoading, mutate } = useSWR>( + const { data, error, isLoading, isValidating, mutate } = useSWR>( url, (url: string) => fetcher(url, axiosConfig), swrConfig @@ -30,6 +30,7 @@ export const useGet = ( return { data: data?.data ?? null, isLoading, + isValidating, error, refetch: mutate, }; From 975d134dd88254126fc95fd952b080a892bf6e6c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 28 Jan 2026 18:45:11 +0000 Subject: [PATCH 08/13] extract strings --- .../v2/common/controls/HeaderTimeRange.tsx | 45 +++-------- .../v2/design-elements/StatusBox.tsx | 9 --- .../Pages/Checks/Components/ChecksTable.tsx | 52 ++----------- client/src/Pages/Checks/index.tsx | 78 ++++++++++++++++--- client/src/locales/en.json | 16 ++++ 5 files changed, 101 insertions(+), 99 deletions(-) diff --git a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx index 9b8b9dcec..799cbc9a3 100644 --- a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx +++ b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx @@ -3,6 +3,7 @@ import Typography from "@mui/material/Typography"; import { ToggleButtonGroup, ToggleButton } from "@/Components/v2/inputs"; import { useTheme } from "@mui/material/styles"; import CircularProgress from "@mui/material/CircularProgress"; +import { useTranslation } from "react-i18next"; import Box from "@mui/material/Box"; interface MonitorTimeFrameHeaderProps { @@ -19,7 +20,7 @@ export const HeaderTimeRange = ({ setDateRange, }: MonitorTimeFrameHeaderProps) => { const theme = useTheme(); - + const { t } = useTranslation(); const handleChange = ( _event: React.MouseEvent, newValue: string | null @@ -39,29 +40,17 @@ export const HeaderTimeRange = ({ onChange={handleChange} size="small" > - - Recent + + {t("components.headerTimeRange.labels.recent")} - - Day + + {t("components.headerTimeRange.labels.day")} - - Week + + {t("components.headerTimeRange.labels.week")} - - Month + + {t("components.headerTimeRange.labels.month")} ); @@ -74,19 +63,9 @@ export const HeaderTimeRange = ({ alignItems="center" gap={theme.spacing(4)} > - - {isLoading && } - + {isLoading && } - Showing statistics for past{" "} - {dateRange === "recent" - ? "2 hours" - : dateRange === "day" - ? "24 hours" - : dateRange === "week" - ? "7 days" - : "30 days"} - . + {t(`components.headerTimeRange.description.${dateRange}`)} {timeFramePicker} diff --git a/client/src/Components/v2/design-elements/StatusBox.tsx b/client/src/Components/v2/design-elements/StatusBox.tsx index d48871a2a..bf7979cca 100644 --- a/client/src/Components/v2/design-elements/StatusBox.tsx +++ b/client/src/Components/v2/design-elements/StatusBox.tsx @@ -113,9 +113,6 @@ export const TotalChecksBox = ({ n }: { n: number }) => { label={t("pages.common.monitors.status.total")} n={n} color={theme.palette.primary.light} - sx={{ - maxWidth: "300px", - }} /> ); }; @@ -127,9 +124,6 @@ export const DownChecksBox = ({ n }: { n: number }) => { label={t("pages.common.monitors.status.down")} n={n} color={theme.palette.error.light} - sx={{ - maxWidth: "300px", - }} /> ); }; @@ -141,9 +135,6 @@ export const UpChecksBox = ({ n }: { n: number }) => { label={t("pages.common.monitors.status.up")} n={n} color={theme.palette.success.light} - sx={{ - maxWidth: "300px", - }} /> ); }; diff --git a/client/src/Pages/Checks/Components/ChecksTable.tsx b/client/src/Pages/Checks/Components/ChecksTable.tsx index e48af37b8..6464be25c 100644 --- a/client/src/Pages/Checks/Components/ChecksTable.tsx +++ b/client/src/Pages/Checks/Components/ChecksTable.tsx @@ -9,28 +9,24 @@ import type { Header } from "@/Components/v2/design-elements/Table"; import type { Monitor, MonitorStatus } from "@/Types/Monitor"; import { useTranslation } from "react-i18next"; -import { useMemo } from "react"; import { formatDateWithTz } from "@/Utils/TimeUtils"; import { useNavigate } from "react-router"; -import type { Check, ChecksResponse } from "@/Types/Check"; +import type { Check } from "@/Types/Check"; import type { RootState } from "@/Types/state"; import { useSelector } from "react-redux"; -import { useGet } from "@/Hooks/UseApi"; export const ChecksTable = ({ monitors, - selectedMonitorId, - statusFilter, - dateRange, + checks, + checksCount, page, setPage, rowsPerPage, setRowsPerPage, }: { monitors: Monitor[] | null; - selectedMonitorId: string; - statusFilter: string; - dateRange: string; + checks: Check[]; + checksCount: number; page: number; setPage: (page: number) => void; rowsPerPage: number; @@ -40,44 +36,6 @@ export const ChecksTable = ({ const uiTimezone = useSelector((state: RootState) => state.ui.timezone); const navigate = useNavigate(); - const selectedMonitorType = monitors?.find((m) => m.id === selectedMonitorId)?.type; - - const teamChecksUrl = useMemo(() => { - if (selectedMonitorId !== "0") return null; - const params = new URLSearchParams(); - params.append("sortOrder", "desc"); - if (dateRange) params.append("dateRange", dateRange); - if (statusFilter) params.append("filter", statusFilter); - params.append("page", String(page)); - params.append("rowsPerPage", String(rowsPerPage)); - return `/checks/team?${params.toString()}`; - }, [selectedMonitorId, dateRange, statusFilter, page, rowsPerPage]); - - const monitorChecksUrl = useMemo(() => { - if (selectedMonitorId === "0" || !selectedMonitorType) return null; - const params = new URLSearchParams(); - params.append("type", selectedMonitorType); - params.append("sortOrder", "desc"); - if (statusFilter) params.append("filter", statusFilter); - if (dateRange) params.append("dateRange", dateRange); - params.append("page", String(page)); - params.append("rowsPerPage", String(rowsPerPage)); - return `/checks/${selectedMonitorId}?${params.toString()}`; - }, [selectedMonitorId, selectedMonitorType, dateRange, statusFilter, page, rowsPerPage]); - - const { data: teamData, isLoading: isLoadingTeam } = - useGet(teamChecksUrl); - const { data: monitorData, isLoading: isLoadingMonitor } = - useGet(monitorChecksUrl); - - const checks = - selectedMonitorId === "0" ? (teamData?.checks ?? []) : (monitorData?.checks ?? []); - const checksCount = - selectedMonitorId === "0" - ? (teamData?.checksCount ?? 0) - : (monitorData?.checksCount ?? 0); - const isLoading = isLoadingTeam || isLoadingMonitor; - const getHeaders = (t: Function, uiTimezone: string) => { const headers: Header[] = [ { diff --git a/client/src/Pages/Checks/index.tsx b/client/src/Pages/Checks/index.tsx index e69146f42..150656a88 100644 --- a/client/src/Pages/Checks/index.tsx +++ b/client/src/Pages/Checks/index.tsx @@ -11,24 +11,22 @@ import { ChecksTable } from "./Components/ChecksTable"; import { MenuItem, useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { useState } from "react"; +import { useState, useMemo } from "react"; import { useParams } from "react-router-dom"; import { useGet } from "@/Hooks/UseApi"; import type { Monitor } from "@/Types/Monitor"; -import type { ChecksSummary } from "@/Types/Check"; +import type { ChecksSummary, ChecksResponse } from "@/Types/Check"; const Checks = () => { const { t } = useTranslation(); const { monitorId } = useParams<{ monitorId?: string }>(); - // Local state const [selectedMonitor, setSelectedMonitor] = useState(monitorId || "0"); const [dateRange, setDateRange] = useState("recent"); const [statusFilter, setStatusFilter] = useState("down"); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); - // Data fetching with SWR const monitorsUrl = "/monitors/team"; const summaryUrl = `/checks/team/summary?dateRange=${dateRange}`; @@ -38,7 +36,68 @@ const Checks = () => { const { data: summaryResponse, isLoading: isLoadingSummary } = useGet(summaryUrl); - const isLoading = isLoadingMonitors || isLoadingSummary; + const selectedMonitorType = monitorsResponse?.find( + (m) => m.id === selectedMonitor + )?.type; + + const teamChecksUrl = useMemo(() => { + if (selectedMonitor !== "0") return null; + const params = new URLSearchParams(); + params.append("sortOrder", "desc"); + if (dateRange) params.append("dateRange", dateRange); + if (statusFilter) params.append("filter", statusFilter); + params.append("page", String(page)); + params.append("rowsPerPage", String(rowsPerPage)); + return `/checks/team?${params.toString()}`; + }, [selectedMonitor, dateRange, statusFilter, page, rowsPerPage]); + + const monitorChecksUrl = useMemo(() => { + if (selectedMonitor === "0" || !selectedMonitorType) return null; + const params = new URLSearchParams(); + params.append("type", selectedMonitorType); + params.append("sortOrder", "desc"); + if (statusFilter) params.append("filter", statusFilter); + if (dateRange) params.append("dateRange", dateRange); + params.append("page", String(page)); + params.append("rowsPerPage", String(rowsPerPage)); + return `/checks/${selectedMonitor}?${params.toString()}`; + }, [selectedMonitor, selectedMonitorType, dateRange, statusFilter, page, rowsPerPage]); + + const { + data: teamChecksData, + isLoading: isLoadingTeamChecks, + isValidating: isValidatingTeamChecks, + } = useGet( + teamChecksUrl, + {}, + { keepPreviousData: true, refreshInterval: 30000 } + ); + const { + data: monitorChecksData, + isLoading: isLoadingMonitorChecks, + isValidating: isValidatingMonitorChecks, + } = useGet( + monitorChecksUrl, + {}, + { keepPreviousData: true, refreshInterval: 30000 } + ); + + const checks = + selectedMonitor === "0" + ? (teamChecksData?.checks ?? []) + : (monitorChecksData?.checks ?? []); + const checksCount = + selectedMonitor === "0" + ? (teamChecksData?.checksCount ?? 0) + : (monitorChecksData?.checksCount ?? 0); + + const isLoadingChecks = + isLoadingTeamChecks || + isLoadingMonitorChecks || + isValidatingTeamChecks || + isValidatingMonitorChecks; + + const isLoading = isLoadingMonitors || isLoadingSummary || isLoadingChecks; const totalChecks = summaryResponse?.totalChecks || 0; const downChecks = summaryResponse?.downChecks || 0; const upChecks = totalChecks - (summaryResponse?.downChecks || 0); @@ -61,7 +120,7 @@ const Checks = () => { gap={2} > @@ -102,9 +161,8 @@ const Checks = () => { Date: Wed, 28 Jan 2026 19:00:34 +0000 Subject: [PATCH 09/13] Responsive --- .../v2/common/controls/HeaderTimeRange.tsx | 15 ++++++++++++--- client/src/Pages/Checks/index.tsx | 10 ++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx index 799cbc9a3..e80327b64 100644 --- a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx +++ b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx @@ -1,10 +1,11 @@ import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import { ToggleButtonGroup, ToggleButton } from "@/Components/v2/inputs"; import { useTheme } from "@mui/material/styles"; import CircularProgress from "@mui/material/CircularProgress"; import { useTranslation } from "react-i18next"; -import Box from "@mui/material/Box"; +import { useMediaQuery } from "@mui/material"; interface MonitorTimeFrameHeaderProps { isLoading?: boolean; @@ -21,6 +22,7 @@ export const HeaderTimeRange = ({ }: MonitorTimeFrameHeaderProps) => { const theme = useTheme(); const { t } = useTranslation(); + const isSmallScreen = useMediaQuery(theme.breakpoints.down("md")); const handleChange = ( _event: React.MouseEvent, newValue: string | null @@ -35,10 +37,12 @@ export const HeaderTimeRange = ({ if (hasDateRange) { timeFramePicker = ( {t("components.headerTimeRange.labels.recent")} @@ -58,12 +62,17 @@ export const HeaderTimeRange = ({ return ( - {isLoading && } + + {isLoading && } + {t(`components.headerTimeRange.description.${dateRange}`)} diff --git a/client/src/Pages/Checks/index.tsx b/client/src/Pages/Checks/index.tsx index 150656a88..1d8fc0553 100644 --- a/client/src/Pages/Checks/index.tsx +++ b/client/src/Pages/Checks/index.tsx @@ -1,4 +1,5 @@ import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; import { BasePage, TotalChecksBox, @@ -9,7 +10,7 @@ import { HeaderTimeRange } from "@/Components/v2/common"; import { Select } from "@/Components/v2/inputs"; import { ChecksTable } from "./Components/ChecksTable"; -import { MenuItem, useTheme } from "@mui/material"; +import { MenuItem } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useState, useMemo } from "react"; import { useParams } from "react-router-dom"; @@ -115,15 +116,16 @@ const Checks = () => {