From 8dfedb8a52ff64661af358cc92f5142ddaa7673a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 21:26:27 +0000 Subject: [PATCH 01/20] docker fix --- docker/dist-arm/server.Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/dist-arm/server.Dockerfile b/docker/dist-arm/server.Dockerfile index 91ccd427e..c9835067f 100644 --- a/docker/dist-arm/server.Dockerfile +++ b/docker/dist-arm/server.Dockerfile @@ -1,7 +1,7 @@ # --------------------- # Frontend build stage # --------------------- -FROM node:24-slim AS frontend-build +FROM node:20-slim AS frontend-build @@ -20,7 +20,7 @@ RUN npm run build # --------------------- # Backend stage # --------------------- -FROM node:24-slim AS backend +FROM node:20-slim AS backend # Install ping RUN apt-get update \ From 6efc05879a904f1ce733996f779a29965fbed0e2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 21:54:20 +0000 Subject: [PATCH 02/20] cast to string --- server/src/controllers/authController.ts | 4 ++-- server/src/controllers/checkController.ts | 2 +- server/src/controllers/incidentController.ts | 2 +- server/src/controllers/maintenanceWindowController.ts | 8 ++++---- server/src/controllers/notificationController.ts | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index 23ea6e42e..165bec056 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -197,7 +197,7 @@ class AuthController { throw new AppError({ message: "Unauthorized", status: 403 }); } - const userId = req.params.userId; + const userId = req.params.userId as string; const user = { ...req.body }; await editUserByIdParamValidation.validateAsync(req.params); @@ -222,7 +222,7 @@ class AuthController { throw new AppError({ message: "Unauthorized", status: 403 }); } - const userId = req.params.userId; + const userId = req.params.userId as string; await editUserByIdParamValidation.validateAsync(req.params); await editUserPasswordByIdBodyValidation.validateAsync(req.body); const updatedPassword = req.body.password; diff --git a/server/src/controllers/checkController.ts b/server/src/controllers/checkController.ts index b13663d97..0c085f6d3 100755 --- a/server/src/controllers/checkController.ts +++ b/server/src/controllers/checkController.ts @@ -83,7 +83,7 @@ class CheckController { await deleteChecksParamValidation.validateAsync(req.params); const deletedCount = await this.checkService.deleteChecks({ - monitorId: req.params.monitorId, + monitorId: req.params.monitorId as string, teamId: req?.user?.teamId, }); diff --git a/server/src/controllers/incidentController.ts b/server/src/controllers/incidentController.ts index 72e6498f9..5f2b6d154 100644 --- a/server/src/controllers/incidentController.ts +++ b/server/src/controllers/incidentController.ts @@ -67,7 +67,7 @@ class IncidentController { getIncidentById = async (req: Request, res: Response, next: NextFunction) => { try { const teamId = requireTeamId(req.user?.teamId); - const incidentId = req.params.incidentId; + const incidentId = req.params.incidentId as string; if (!incidentId) { throw new AppError({ message: "Incident ID is required", service: SERVICE_NAME, status: 400 }); } diff --git a/server/src/controllers/maintenanceWindowController.ts b/server/src/controllers/maintenanceWindowController.ts index 0cc9610ea..1e8aba483 100644 --- a/server/src/controllers/maintenanceWindowController.ts +++ b/server/src/controllers/maintenanceWindowController.ts @@ -44,7 +44,7 @@ class MaintenanceWindowController { const teamId = requireTeamId(req.user?.teamId); - const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id, teamId }); + const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id as string, teamId }); return res.status(200).json({ success: true, @@ -80,7 +80,7 @@ class MaintenanceWindowController { const teamId = requireTeamId(req?.user?.teamId); - const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ monitorId: req.params.monitorId, teamId }); + const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ monitorId: req.params.monitorId as string, teamId }); return res.status(200).json({ success: true, @@ -97,7 +97,7 @@ class MaintenanceWindowController { const teamId = requireTeamId(req?.user?.teamId); - await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id, teamId }); + await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id as string, teamId }); return res.status(200).json({ success: true, @@ -115,7 +115,7 @@ class MaintenanceWindowController { const teamId = requireTeamId(req.user?.teamId); - const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ id: req.params.id, body: req.body, teamId }); + const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ id: req.params.id as string, body: req.body, teamId }); return res.status(200).json({ success: true, msg: "Maintenance window edited successfully", diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index 1aa10a82c..f32f80ddc 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -97,7 +97,7 @@ class NotificationController { throw new AppError({ message: "Team ID is required", status: 400 }); } - const notificationId = req.params.id; + const notificationId = req.params.id as string; if (!notificationId) { throw new AppError({ message: "Notification ID is required", status: 400 }); } @@ -115,7 +115,7 @@ class NotificationController { getNotificationById = async (req: Request, res: Response, next: NextFunction) => { try { const teamId = requireTeamId(req.user?.teamId); - const notificationId = req.params.id; + const notificationId = req.params.id as string; if (!notificationId) { throw new AppError({ message: "Notification ID is required", status: 400 }); } @@ -139,7 +139,7 @@ class NotificationController { }); const teamId = requireTeamId(req.user?.teamId); - const notificationId = req.params.id; + const notificationId = req.params.id as string; if (!notificationId) { throw new AppError({ message: "Notification ID is required", status: 400 }); } From 300b1b2238999fa2662ee5f085bb634bd725b19b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 21:57:09 +0000 Subject: [PATCH 03/20] 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 589825c1c500dd33bc702ee4f3a37c8471baa30e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 21:57:55 +0000 Subject: [PATCH 04/20] format --- server/src/controllers/maintenanceWindowController.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/server/src/controllers/maintenanceWindowController.ts b/server/src/controllers/maintenanceWindowController.ts index 1e8aba483..187e479cc 100644 --- a/server/src/controllers/maintenanceWindowController.ts +++ b/server/src/controllers/maintenanceWindowController.ts @@ -80,7 +80,10 @@ class MaintenanceWindowController { const teamId = requireTeamId(req?.user?.teamId); - const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ monitorId: req.params.monitorId as string, teamId }); + const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ + monitorId: req.params.monitorId as string, + teamId, + }); return res.status(200).json({ success: true, @@ -115,7 +118,11 @@ class MaintenanceWindowController { const teamId = requireTeamId(req.user?.teamId); - const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ id: req.params.id as string, body: req.body, teamId }); + const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ + id: req.params.id as string, + body: req.body, + teamId, + }); return res.status(200).json({ success: true, msg: "Maintenance window edited successfully", From f88e4ee6f44da54a776182020afde84e098bd450 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 22:24:16 +0000 Subject: [PATCH 05/20] 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 06/20] 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 07/20] 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 08/20] 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 6b4032ecfebc6d52b51ef60ee0c6b3a496339cf5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 28 Jan 2026 17:08:36 +0000 Subject: [PATCH 09/20] bump mongo for ARM to 6.0 --- docker/dist-arm/docker-compose.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/dist-arm/docker-compose.yaml b/docker/dist-arm/docker-compose.yaml index 9a87aa59b..2255de508 100644 --- a/docker/dist-arm/docker-compose.yaml +++ b/docker/dist-arm/docker-compose.yaml @@ -15,7 +15,7 @@ services: depends_on: - mongodb mongodb: - image: mongo:4.4.30 + image: mongo:6.0 restart: always command: ["mongod", "--quiet", "--bind_ip_all"] ports: @@ -23,7 +23,7 @@ services: volumes: - ./mongo/data:/data/db healthcheck: - test: ["CMD", "mongo", "--eval", "db.runCommand({ ping: 1 })", "--quiet"] + test: ["CMD", "mongosh", "--eval", "db.runCommand({ ping: 1 })", "--quiet"] interval: 5s timeout: 30s start_period: 0s From fcfad945a095912ceea6fc84cacab0dd25799890 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 28 Jan 2026 17:59:11 +0000 Subject: [PATCH 10/20] 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 11/20] 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 12/20] 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 13/20] 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 = () => {
- - {Object.keys(phases)?.map((phaseKey) => ( - - - - {`${phaseKeyFormattingMap[phaseKey] || phaseKey}:`} - - - - {`${phases[phaseKey]} ms`} - - - ))} - -
- - )} -
- ); -}; - -const IncidentTable = ({ - isLoading, - monitors, - selectedMonitor, - filter, - dateRange, - updateTrigger, -}) => { - //Redux state - const uiTimezone = useSelector((state) => state.ui.timezone); - - //Local state - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); - const selectedMonitorDetails = monitors?.[selectedMonitor]; - const selectedMonitorType = selectedMonitorDetails?.type; - - //Hooks - const [checksMonitor, checksCountMonitor, isLoadingMonitor, networkErrorMonitor] = - useFetchChecksByMonitor({ - monitorId: selectedMonitor === "0" ? undefined : selectedMonitor, - type: selectedMonitorType, - status: false, - sortOrder: "desc", - limit: null, - dateRange, - filter: filter === "resolved" ? "all" : filter, - ack: filter === "resolved" ? true : false, - page: page, - rowsPerPage: rowsPerPage, - enabled: selectedMonitor !== "0", - updateTrigger, - }); - - const [checksTeam, checksCountTeam, isLoadingTeam, networkErrorTeam] = - useFetchChecksTeam({ - status: false, - sortOrder: "desc", - limit: null, - dateRange, - filter: filter === "resolved" ? "all" : filter, - ack: filter === "resolved" ? true : false, - page: page, - rowsPerPage: rowsPerPage, - enabled: selectedMonitor === "0", - updateTrigger, - }); - - const checks = selectedMonitor === "0" ? checksTeam : checksMonitor; - const checksCount = selectedMonitor === "0" ? checksCountTeam : checksCountMonitor; - isLoading = isLoadingTeam || isLoadingMonitor; - const networkError = selectedMonitor === "0" ? networkErrorTeam : networkErrorMonitor; - - const { t } = useTranslation(); - - //Handlers - const handleChangePage = (_, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(event.target.value); - }; - - const headers = [ - { - id: "monitorName", - content: t("incidentsTableMonitorName"), - render: (row) => { - return monitors?.[row.metadata?.monitorId]?.name || "N/A"; - }, - }, - { - id: "status", - content: t("incidentsTableStatus"), - render: (row) => { - const status = row.status === true ? "up" : "down"; - return ( - - ); - }, - }, - { - id: "dateTime", - content: t("incidentsTableDateTime"), - render: (row) => { - const formattedDate = formatDateWithTz( - row.createdAt, - "YYYY-MM-DD HH:mm:ss A", - uiTimezone - ); - return formattedDate; - }, - }, - { - id: "statusCode", - content: t("incidentsTableStatusCode"), - render: (row) => , - }, - { id: "message", content: t("incidentsTableMessage"), render: (row) => row.message }, - ]; - - if (isLoading) return ; - - if (networkError) { - return ( - - - - ); - } - - if (!isLoading && typeof checksCount === "undefined") { - return {t("incidentsTableNoIncidents")}; - } - - return ( - <> - - - - ); -}; - -IncidentTable.propTypes = { - isLoading: PropTypes.bool, - monitors: PropTypes.object, - selectedMonitor: PropTypes.string, - filter: PropTypes.string, - dateRange: PropTypes.string, - updateTrigger: PropTypes.bool, - setUpdateTrigger: PropTypes.func, -}; -export default IncidentTable; diff --git a/client/src/Pages/Checks/Components/OptionsHeader/index.jsx b/client/src/Pages/Checks/Components/OptionsHeader/index.jsx deleted file mode 100644 index e04236a9b..000000000 --- a/client/src/Pages/Checks/Components/OptionsHeader/index.jsx +++ /dev/null @@ -1,139 +0,0 @@ -// Components -import { Stack, Typography, Button, ButtonGroup } from "@mui/material"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import PropTypes from "prop-types"; - -//Utils -import { useTheme } from "@emotion/react"; -import SkeletonLayout from "./skeleton.jsx"; -import { useTranslation } from "react-i18next"; - -const OptionsHeader = ({ - shouldRender, - selectedMonitor = 0, - setSelectedMonitor, - monitors, - filter = "all", - setFilter, - dateRange = "hour", - setDateRange, -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : []; - const filterOptions = [ - { id: "all", name: t("incidentsOptionsHeaderFilterAll") }, - { id: "down", name: t("incidentsOptionsHeaderFilterDown") }, - { id: "resolve", name: t("incidentsOptionsHeaderFilterCannotResolve") }, - { id: "resolved", name: t("incidentsOptionsHeaderFilterResolved") }, - ]; - - // The stacks below which are three in number have the same style so - const stackStyles = { - direction: "row", - alignItems: "center", - gap: theme.spacing(6), - }; - - if (!shouldRender) return ; - - return ( - - - - {t("incidentsOptionsHeader")} - - setFilter(e.target.value)} - items={filterOptions} - sx={{ - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastTextSecondary, - }} - /> - - - - {t("incidentsOptionsHeaderShow")} - - - - - - - - - - ); -}; - -OptionsHeader.propTypes = { - shouldRender: PropTypes.bool, - selectedMonitor: PropTypes.string, - setSelectedMonitor: PropTypes.func, - monitors: PropTypes.object, - filter: PropTypes.string, - setFilter: PropTypes.func, - dateRange: PropTypes.string, - setDateRange: PropTypes.func, -}; - -export default OptionsHeader; diff --git a/client/src/Pages/Checks/Components/OptionsHeader/skeleton.jsx b/client/src/Pages/Checks/Components/OptionsHeader/skeleton.jsx deleted file mode 100644 index 8a8990dc4..000000000 --- a/client/src/Pages/Checks/Components/OptionsHeader/skeleton.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Stack, Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Checks/Components/StatusBoxes/StatusBox.jsx b/client/src/Pages/Checks/Components/StatusBoxes/StatusBox.jsx deleted file mode 100644 index d7f02ada3..000000000 --- a/client/src/Pages/Checks/Components/StatusBoxes/StatusBox.jsx +++ /dev/null @@ -1,129 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Stack, Typography } from "@mui/material"; -import Background from "@/assets/Images/background-grid.svg?react"; -import Icon from "@/Components/v1/Icon"; - -const StatusBox = ({ title, value, status }) => { - const theme = useTheme(); - let sharedStyles = { - position: "absolute", - right: 8, - "& svg": { - width: 20, - height: 20, - opacity: 0.9, - "& path": { stroke: theme.palette.primary.contrastTextTertiary, strokeWidth: 1.7 }, - }, - }; - - let color; - let icon; - if (status === "up") { - color = theme.palette.success.lowContrast; - icon = ( - - - - ); - } else if (status === "down") { - color = theme.palette.error.lowContrast; - icon = ( - - - - ); - } else if (status === "paused") { - color = theme.palette.warning.lowContrast; - icon = ( - - - - ); - } else { - color = theme.palette.accent.main; - icon = ( - - - - ); - } - - return ( - - - - - - - - {title} - - {icon} - - - {value} - - - # - - - - - ); -}; - -StatusBox.propTypes = { - title: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - status: PropTypes.string, -}; - -export default StatusBox; diff --git a/client/src/Pages/Checks/Components/StatusBoxes/index.jsx b/client/src/Pages/Checks/Components/StatusBoxes/index.jsx deleted file mode 100644 index bb993111a..000000000 --- a/client/src/Pages/Checks/Components/StatusBoxes/index.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from "prop-types"; -import { Stack } from "@mui/material"; -import StatusBox from "./StatusBox.jsx"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import SkeletonLayout from "./skeleton.jsx"; - -const StatusBoxes = ({ isLoading, summary }) => { - const theme = useTheme(); - const { t } = useTranslation(); - if (isLoading) return ; - return ( - - - - - - - ); -}; - -StatusBoxes.propTypes = { - isLoading: PropTypes.bool, - summary: PropTypes.object, -}; - -export default StatusBoxes; diff --git a/client/src/Pages/Checks/Components/StatusBoxes/skeleton.jsx b/client/src/Pages/Checks/Components/StatusBoxes/skeleton.jsx deleted file mode 100644 index b9aa016b7..000000000 --- a/client/src/Pages/Checks/Components/StatusBoxes/skeleton.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -const SkeletonLayout = () => { - const theme = useTheme(); - return ( - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Checks/old.jsx b/client/src/Pages/Checks/old.jsx deleted file mode 100644 index bcd036a58..000000000 --- a/client/src/Pages/Checks/old.jsx +++ /dev/null @@ -1,100 +0,0 @@ -// Components -import { Stack } from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import IncidentTable from "./Components/IncidentTable/index.jsx"; -import OptionsHeader from "./Components/OptionsHeader/index.jsx"; -import StatusBoxes from "./Components/StatusBoxes/index.jsx"; - -//Utils -import { useTheme } from "@emotion/react"; -import { useFetchMonitorsByTeamId } from "@/Hooks/monitorHooks.js"; -import { useFetchChecksSummaryByTeamId } from "@/Hooks/checkHooks.js"; -import { useState, useEffect } from "react"; -import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; -import { useTranslation } from "react-i18next"; -import { useParams } from "react-router-dom"; - -//Constants -const Checks = () => { - // Redux state - const { t } = useTranslation(); - - const BREADCRUMBS = [{ name: t("checksPageTitle"), path: "/checks" }]; - - // Local state - const [selectedMonitor, setSelectedMonitor] = useState("0"); - const [filter, setFilter] = useState(undefined); - const [dateRange, setDateRange] = useState("hour"); - const [monitorLookup, setMonitorLookup] = useState(undefined); - const [updateTrigger, setUpdateTrigger] = useState(false); - - //Hooks - - //Utils - const theme = useTheme(); - const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({}); - const [summary, isLoadingSummary, networkErrorSummary] = useFetchChecksSummaryByTeamId({ - dateRange, - updateTrigger, - }); - const { monitorId } = useParams(); - - useEffect(() => { - if (monitorId) { - setSelectedMonitor(monitorId); - } - }, [monitorId]); - - useEffect(() => { - const monitorLookup = monitors?.reduce((acc, monitor) => { - acc[monitor.id] = { - id: monitor.id, - name: monitor.name, - type: monitor.type, - }; - return acc; - }, {}); - setMonitorLookup(monitorLookup); - }, [monitors]); - - if (networkError || networkErrorSummary) { - return ( - - - - ); - } - - return ( - - - - - - - - ); -}; - -export default Checks; diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 7916e2c06..e4fe01412 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -200,8 +200,28 @@ class MongoChecksRepository implements IChecksRepository { return documents.map((doc) => this.toEntity(doc)); }; + private toDocument = (check: Partial): CheckDocument => { + // Map id to _id for MongoDB storage + const { id, metadata, ...rest } = check; + return { + _id: id ? new mongoose.Types.ObjectId(id) : new mongoose.Types.ObjectId(), + metadata: metadata ? { + monitorId: new mongoose.Types.ObjectId(metadata.monitorId), + teamId: new mongoose.Types.ObjectId(metadata.teamId), + type: metadata.type, + } : { + monitorId: new mongoose.Types.ObjectId(), + teamId: new mongoose.Types.ObjectId(), + type: "http", + }, + ...rest, + } as unknown as CheckDocument; + }; + createChecks = async (checks: Check[]) => { - return await CheckModel.insertMany(checks); + const docs = checks.map((check) => this.toDocument(check)); + const inserted = await CheckModel.insertMany(docs); + return this.mapDocuments(inserted as unknown as CheckDocument[]); }; findByMonitorId = async ( From 396d5b369bd2202f3abbd510732fdf31e1bc0e25 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 28 Jan 2026 20:20:18 +0000 Subject: [PATCH 19/20] remove unused --- .../Details/Components/Charts/AreaChart.jsx | 330 ----------------- .../Components/Charts/AreaChartLegend.jsx | 62 ---- .../Details/Components/Charts/PieChart.jsx | 327 ----------------- .../Components/Charts/PieChartLegend.jsx | 101 ------ .../Components/PageSpeedAreaChart/index.jsx | 60 ---- .../PageSpeedAreaChart/skeleton.jsx | 17 - .../Components/PageSpeedStatusBoxes/index.jsx | 49 --- .../Components/PerformanceReport/index.jsx | 61 ---- .../Components/PerformanceReport/skeleton.jsx | 17 - client/src/Pages/PageSpeed/Details/old.jsx | 120 ------- .../Monitors/Components/Card/index.jsx | 331 ------------------ .../Monitors/Components/MonitorGrid/index.jsx | 22 -- client/src/Pages/PageSpeed/Monitors/old.jsx | 86 ----- .../Details/Hooks/useCertificateFetch.jsx | 44 --- client/src/Pages/Uptime/Details/old.jsx | 190 ---------- .../Monitors/Components/Filter/index.jsx | 162 --------- .../Monitors/Components/Skeleton/index.jsx | 61 ---- .../Monitors/Components/StatusBoxes/index.jsx | 42 --- .../Components/StatusBoxes/skeleton.jsx | 31 -- .../Components/StatusBoxes/statusBox.jsx | 115 ------ .../Components/UptimeDataTable/index.jsx | 241 ------------- client/src/Pages/Uptime/Monitors/old.jsx | 225 ------------ 22 files changed, 2694 deletions(-) delete mode 100644 client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx delete mode 100644 client/src/Pages/PageSpeed/Details/old.jsx delete mode 100644 client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx delete mode 100644 client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx delete mode 100644 client/src/Pages/PageSpeed/Monitors/old.jsx delete mode 100644 client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx delete mode 100644 client/src/Pages/Uptime/Details/old.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx delete mode 100644 client/src/Pages/Uptime/Monitors/old.jsx diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx deleted file mode 100644 index d8c118237..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx +++ /dev/null @@ -1,330 +0,0 @@ -import PropTypes from "prop-types"; -import { - AreaChart, - Area, - XAxis, - Tooltip, - CartesianGrid, - ResponsiveContainer, - Text, -} from "recharts"; -import { useTheme } from "@emotion/react"; -import { useMemo, useState } from "react"; -import { Box, Stack, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../../../Utils/timeUtilsLegacy.js"; -import { useSelector } from "react-redux"; - -const config = { - seo: { - id: "seo", - text: "SEO", - color: "secondary", - }, - performance: { - id: "performance", - text: "performance", - color: "success", - }, - bestPractices: { - id: "bestPractices", - text: "best practices", - color: "warning", - }, - accessibility: { - id: "accessibility", - text: "accessibility", - color: "accent", - }, -}; - -/** - * Custom tooltip for the area chart. - * @param {Object} props - * @param {boolean} props.active - Whether the tooltip is active. - * @param {Array} props.payload - The payload data for the tooltip. - * @param {string} props.label - The label for the tooltip. - * @returns {JSX.Element|null} The tooltip element or null if not active. - */ - -const CustomToolTip = ({ active, payload, label, config }) => { - const theme = useTheme(); - const uiTimezone = useSelector((state) => state.ui.timezone); - - if (active && payload && payload.length) { - return ( - - - {formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)} - - {Object.keys(config) - .reverse() - .map((key) => { - const { color } = config[key]; - const dotColor = theme.palette[color].main; - - return ( - - - - {config[key].text} - {" "} - {payload[0].payload[key]} - - ); - })} - - ); - } - return null; -}; - -CustomToolTip.propTypes = { - active: PropTypes.bool, - payload: PropTypes.array, - label: PropTypes.string, - config: PropTypes.object, -}; - -/** - * Processes data to insert gaps with null values based on the interval. - * @param {Array} data - * @param {number} interval - The interval in milliseconds for gaps. - * @returns {Array} The formatted data with gaps. - */ -const processDataWithGaps = (data, interval) => { - if (data.length === 0) return []; - let formattedData = []; - let last = new Date(data[0].createdAt).getTime(); - - // Helper function to add a null entry - const addNullEntry = (timestamp) => { - formattedData.push({ - accessibility: "N/A", - bestPractices: "N/A", - performance: "N/A", - seo: "N/A", - createdAt: timestamp, - }); - }; - - data.forEach((entry) => { - const current = new Date(entry.createdAt).getTime(); - - if (current - last > interval * 2) { - // Insert null entries for each interval - let temp = last + interval; - while (temp < current) { - addNullEntry(new Date(temp).toISOString()); - temp += interval; - } - } - - formattedData.push(entry); - last = current; - }); - - return formattedData; -}; - -/** - * Custom tick component to render ticks on the XAxis. - * - * @param {Object} props - * @param {number} props.x - The x coordinate for the tick. - * @param {number} props.y - The y coordinate for the tick. - * @param {Object} props.payload - The data object containing the tick value. - * @param {number} props.index - The index of the tick in the array of ticks. - * - * @returns {JSX.Element|null} The tick element or null if the tick should be hidden. - */ -const CustomTick = ({ x, y, payload, index }) => { - const theme = useTheme(); - const uiTimezone = useSelector((state) => state.ui.timezone); - - // Render nothing for the first tick - if (index === 0) return null; - - return ( - - {formatDateWithTz(payload?.value, "h:mm a", uiTimezone)} - - ); -}; -CustomTick.propTypes = { - x: PropTypes.number, - y: PropTypes.number, - payload: PropTypes.shape({ - value: PropTypes.string.isRequired, - }), - index: PropTypes.number, -}; - -/** - * A chart displaying pagespeed details over time. - * @param {Object} props - * @param {Array} props.data - The data to display in the chart. - * @param {number} props.interval - The interval in milliseconds for processing gaps. - * @returns {JSX.Element} The area chart component. - */ - -const PageSpeedAreaChart = ({ data, monitor, metrics }) => { - const theme = useTheme(); - const [isHovered, setIsHovered] = useState(false); - const memoizedData = useMemo( - () => processDataWithGaps(data, monitor.interval), - [data[0]] - ); - - const filteredConfig = Object.keys(config).reduce((result, key) => { - if (metrics[key]) { - result[key] = config[key]; - } - return result; - }, {}); - - return ( - - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - - } - axisLine={false} - tickLine={false} - height={18} - minTickGap={0} - interval="equidistantPreserveStart" - /> - } - /> - - {Object.values(filteredConfig).map(({ id, color }) => { - /* TODO not working? */ - const startColor = theme.palette[color].main; - const endColor = theme.palette[color].lowContrast; - - return ( - - - - - ); - })} - - {Object.keys(filteredConfig).map((key) => { - const { color } = filteredConfig[key]; - const strokeColor = theme.palette[color].main; - const bgColor = theme.palette.primary.main; - - return ( - - ); - })} - - - ); -}; - -PageSpeedAreaChart.propTypes = { - data: PropTypes.arrayOf( - PropTypes.shape({ - createdAt: PropTypes.string.isRequired, - accessibility: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - bestPractices: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - performance: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - seo: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - }) - ).isRequired, - monitor: PropTypes.object.isRequired, - metrics: PropTypes.object, -}; - -export default PageSpeedAreaChart; diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx deleted file mode 100644 index 525e4d0d2..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Box, Typography, Divider } from "@mui/material"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx"; -import { useTranslation } from "react-i18next"; -import { useTheme } from "@emotion/react"; - -const AreaChartLegend = ({ metrics, handleMetrics }) => { - const theme = useTheme(); - const { t } = useTranslation(); - return ( - - } - header="Metrics" - > - - - {t("shown")} - - - - handleMetrics("accessibility")} - /> - - handleMetrics("bestPractices")} - /> - - handleMetrics("performance")} - /> - - handleMetrics("seo")} - /> - - - ); -}; - -export default AreaChartLegend; diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx deleted file mode 100644 index eec075684..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx +++ /dev/null @@ -1,327 +0,0 @@ -import PropTypes from "prop-types"; -import { PieChart as MuiPieChart } from "@mui/x-charts/PieChart"; -import { useDrawingArea } from "@mui/x-charts"; -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { Box } from "@mui/material"; - -/** - * Renders a centered label within a pie chart. - * - * @param {Object} props - * @param {string | number} props.value - The value to display in the label. - * @param {string} props.color - The color of the text. - * @returns {JSX.Element} - */ -const PieCenterLabel = ({ value, color, setExpand }) => { - const { width, height } = useDrawingArea(); - return ( - setExpand(true)} - > - - - {value} - - - ); -}; - -PieCenterLabel.propTypes = { - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - color: PropTypes.string, - setExpand: PropTypes.func, -}; - -/** - * A component that renders a label on a pie chart slice. - * The label is positioned relative to the center of the pie chart and is optionally highlighted. - * - * @param {Object} props - * @param {number} props.value - The value to display inside the pie slice. - * @param {number} props.startAngle - The starting angle of the pie slice in degrees. - * @param {number} props.endAngle - The ending angle of the pie slice in degrees. - * @param {string} props.color - The color of the label text when highlighted. - * @param {boolean} props.highlighted - Determines if the label should be highlighted or not. - * @returns {JSX.Element} - */ -const PieValueLabel = ({ value, startAngle, endAngle, color, highlighted }) => { - const { width, height } = useDrawingArea(); - - // Compute the midpoint angle in radians - const angle = (((startAngle + endAngle) / 2) * Math.PI) / 180; - const radius = height / 4; // length from center of the circle to where the text is positioned - - // Calculate x and y positions - const x = Math.sin(angle) * radius; - const y = -Math.cos(angle) * radius; - - return ( - - - +{value} - - - ); -}; - -// Validate props using PropTypes -PieValueLabel.propTypes = { - value: PropTypes.number.isRequired, - startAngle: PropTypes.number.isRequired, - endAngle: PropTypes.number.isRequired, - color: PropTypes.string.isRequired, - highlighted: PropTypes.bool.isRequired, -}; - -/** - * Weight constants for different performance metrics. - * @type {Object} - */ -const weights = { - fcp: 10, - si: 10, - lcp: 25, - tbt: 30, - cls: 25, -}; - -const PieChart = ({ audits }) => { - const theme = useTheme(); - const [highlightedItem, setHighLightedItem] = useState(null); - const [expand, setExpand] = useState(false); - - /** - * Retrieves color properties based on the performance value. - * - * @param {number} value - The performance score used to determine the color properties. - * @returns {{stroke: string, strokeBg: string, text: string, bg: string}} The color properties for the given performance value. - */ - const getColors = (value) => { - if (value >= 90 && value <= 100) - return { - stroke: theme.palette.success.main, - strokeBg: theme.palette.success.lowContrast, - text: theme.palette.success.contrastText, - bg: theme.palette.success.lowContrast, - }; - else if (value >= 50 && value < 90) - return { - stroke: theme.palette.warning.main, - strokeBg: theme.palette.warning.lowContrast, - text: theme.palette.warning.contrastText, - bg: theme.palette.warning.lowContrast, - }; - else if (value >= 0 && value < 50) - return { - stroke: theme.palette.error.contrastText, - strokeBg: theme.palette.error.lowContrast, - text: theme.palette.error.contrastText, - bg: theme.palette.error.lowContrast, - }; - return { - stroke: theme.palette.tertiary.contrastText, - strokeBg: theme.palette.tertiary.contrastText, - text: theme.palette.tertiary.contrastText, - bg: theme.palette.tertiary.main, - }; - }; - - /** - * Calculates and formats the data needed for rendering a pie chart based on audit scores and weights. - * This function generates properties for each pie slice, including angles, radii, and colors. - * It also calculates performance based on the weighted values. - * - * @returns {Array} An array of objects, each representing the properties for a slice of the pie chart. - * @returns {number} performance - A variable updated with the rounded sum of weighted values. - */ - let performance = 0; - const getPieData = (audits) => { - if (typeof audits === "undefined") return undefined; - - let data = []; - let startAngle = 0; - const padding = 3; // padding between arcs - const max = 360 - padding * (Object.keys(audits).length - 1); // _id is a child of audits - - Object.keys(audits).forEach((key) => { - if (audits[key].score) { - let value = audits[key].score * weights[key]; - let endAngle = startAngle + (weights[key] * max) / 100; - - let theme = getColors(audits[key].score * 100); - data.push({ - id: key, - data: [ - { - value: value, - color: theme.stroke, - label: key.toUpperCase(), - }, - { - value: weights[key] - value, - color: theme.strokeBg, - label: "", - }, - ], - arcLabel: (item) => `${item.label}`, - arcLabelRadius: 95, - startAngle: startAngle, - endAngle: endAngle, - innerRadius: 73, - outerRadius: 80, - cornerRadius: 2, - highlightScope: { faded: "global", highlighted: "series" }, - faded: { - innerRadius: 73, - outerRadius: 80, - additionalRadius: -20, - arcLabelRadius: 5, - }, - cx: pieSize.width / 2, - }); - - performance += Math.floor(value); - startAngle = endAngle + padding; - } - }); - - return data; - }; - - const pieSize = { width: 230, height: 230 }; - const pieData = getPieData(audits); - const colorMap = getColors(performance); - - return ( - setExpand(false)} - sx={{ - display: "flex", - justifyContent: "center", - alignItems: "center", - width: "100%", - }} - > - {expand ? ( - - - {pieData?.map((pie) => ( - - ))} - - ) : ( - - - - )} - - ); -}; - -PieChart.propTypes = { - audits: PropTypes.object, -}; - -export default PieChart; diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx deleted file mode 100644 index 5cfdad608..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Stack, Box, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import PropTypes from "prop-types"; - -const PieChartLegend = ({ audits }) => { - const theme = useTheme(); - - return ( - - } - header="Performance metrics" - sx={{ flex: 1 }} - > - {typeof audits !== "undefined" && - Object.keys(audits).map((key) => { - if (key === "_id") return; - - let audit = audits[key]; - let score = audit.score * 100; - let bg = - score >= 90 - ? theme.palette.success.main - : score >= 50 - ? theme.palette.warning.main - : score >= 0 - ? theme.palette.error.main - : theme.palette.tertiary.main; - - // Find the position where the number ends and the unit begins - const match = audit?.displayValue?.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/); - let value; - let unit; - if (match) { - value = match[1]; - match[2] === "s" ? (unit = "seconds") : (unit = match[2]); - } else { - value = audit.displayValue; - } - - return ( - - - - {audit.title} - - - {value} - - {unit} - - - - - - ); - })} - - ); -}; - -PieChartLegend.propTypes = { - audits: PropTypes.object, -}; - -export default PieChartLegend; diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx deleted file mode 100644 index 9020ddd6d..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; -import AreaChart from "../Charts/AreaChart.jsx"; -import AreaChartLegend from "../Charts/AreaChartLegend.jsx"; -import SkeletonLayout from "./skeleton.jsx"; -import Icon from "@/Components/v1/Icon"; -import { Stack } from "@mui/material"; -import PropTypes from "prop-types"; - -import { useTheme } from "@emotion/react"; - -const PageSpeedAreaChart = ({ shouldRender, monitor, metrics, handleMetrics }) => { - const theme = useTheme(); - - if (!shouldRender) { - return ; - } - - const data = monitor?.checks ? [...monitor.checks].reverse() : []; - - return ( - - - } - header="Score history" - height="100%" - borderRadiusRight={16} - Legend={ - - } - > - - - - ); -}; - -PageSpeedAreaChart.propTypes = { - shouldRender: PropTypes.bool, - monitor: PropTypes.object, - metrics: PropTypes.object, - handleMetrics: PropTypes.func, -}; - -export default PageSpeedAreaChart; diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx deleted file mode 100644 index b793908d4..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Box, Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx deleted file mode 100644 index 6ff26c739..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; -import StatBox from "@/Components/v1/StatBox/index.jsx"; -import { Typography } from "@mui/material"; -import { getHumanReadableDuration } from "../../../../../Utils/timeUtilsLegacy.js"; -import { useTranslation } from "react-i18next"; - -const PageSpeedStatusBoxes = ({ shouldRender, monitorStats }) => { - const lastChecked = monitorStats?.lastCheckTimestamp - ? Date.now() - monitorStats.lastCheckTimestamp - : 0; - - // Determine time since last failure - const timeOfLastFailure = monitorStats?.timeOfLastFailure; - const timeSinceLastFailure = - timeOfLastFailure > 0 - ? Date.now() - timeOfLastFailure - : Date.now() - new Date(monitorStats?.createdAt); - - const streakTime = getHumanReadableDuration(timeSinceLastFailure); - - const time = getHumanReadableDuration(lastChecked); - - const { t } = useTranslation(); - - return ( - - - {streakTime} - {t("ago")} - - } - /> - - {time} - {t("ago")} - - } - /> - - ); -}; - -export default PageSpeedStatusBoxes; diff --git a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx deleted file mode 100644 index a2e1b5f96..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import PieChart from "../Charts/PieChart.jsx"; -import { Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import PieChartLegend from "../Charts/PieChartLegend.jsx"; -import SkeletonLayout from "./skeleton.jsx"; -import { useTranslation } from "react-i18next"; - -const PerformanceReport = ({ shouldRender, audits }) => { - const theme = useTheme(); - const { t } = useTranslation(); - - if (!shouldRender) { - return ; - } - - return ( - - } - header="Performance report" - Legend={} - borderRadiusRight={16} - > - - - {t("pageSpeedDetailsPerformanceReport")}{" "} - - {t("pageSpeedDetailsPerformanceReportCalculator")} - - - - ); -}; - -export default PerformanceReport; diff --git a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx b/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx deleted file mode 100644 index b793908d4..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Box, Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/PageSpeed/Details/old.jsx b/client/src/Pages/PageSpeed/Details/old.jsx deleted file mode 100644 index 3190787b3..000000000 --- a/client/src/Pages/PageSpeed/Details/old.jsx +++ /dev/null @@ -1,120 +0,0 @@ -// Components -import { Stack, Typography } from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx"; -import PageSpeedStatusBoxes from "./Components/PageSpeedStatusBoxes/index.jsx"; -import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx"; -import PageSpeedAreaChart from "./Components/PageSpeedAreaChart/index.jsx"; -import PerformanceReport from "./Components/PerformanceReport/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -// Utils -import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -import { useParams } from "react-router-dom"; -import { useFetchPageSpeedMonitorById } from "../../../Hooks/monitorHooks.js"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -// Constants -const BREADCRUMBS = [ - { name: "pagespeed", path: "/pagespeed" }, - { name: "details", path: `` }, -]; - -const PageSpeedDetails = () => { - const theme = useTheme(); - const { t } = useTranslation(); - const isAdmin = useIsAdmin(); - const { monitorId } = useParams(); - - // Local state - const [metrics, setMetrics] = useState({ - accessibility: true, - bestPractices: true, - performance: true, - seo: true, - }); - const [trigger, setTrigger] = useState(false); - // Network - const [monitor, monitorStats, isLoading, networkError] = useFetchPageSpeedMonitorById({ - monitorId, - dateRange: "day", - updateTrigger: trigger, - }); - - // Handlers - const handleMetrics = (id) => { - setMetrics((prev) => ({ ...prev, [id]: !prev[id] })); - }; - - const triggerUpdate = () => { - setTrigger(!trigger); - }; - if (networkError === true) { - return ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - // Empty view, displayed when loading is complete and there are no checks - if (!isLoading && monitor?.checks?.length === 0) { - return ( - - - - - {t("distributedUptimeDetailsNoMonitorHistory")} - - - ); - } - - return ( - - - - - - - - - - ); -}; - -export default PageSpeedDetails; diff --git a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx deleted file mode 100644 index 4a85863c4..000000000 --- a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx +++ /dev/null @@ -1,331 +0,0 @@ -import PropTypes from "prop-types"; -import Icon from "@/Components/v1/Icon"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import { Box, Grid, Stack, Typography } from "@mui/material"; -import { useNavigate } from "react-router-dom"; -import { useTheme } from "@emotion/react"; -import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; -import { useSelector } from "react-redux"; -import { formatDateWithTz, formatDurationSplit } from "@/Utils/timeUtilsLegacy.js"; -import { useMonitorUtils } from "@/Hooks/useMonitorUtils.js"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import IconBox from "@/Components/v1/IconBox/index.jsx"; -/** - * CustomToolTip displays a tooltip with formatted date and score information. - * @param {Object} props - * @param {Array} props.payload - Data to display in the tooltip - * @returns {JSX.Element} The rendered tooltip component - */ -const CustomToolTip = ({ payload }) => { - const theme = useTheme(); - const uiTimezone = useSelector((state) => state.ui.timezone); - - return ( - - - {formatDateWithTz( - payload[0]?.payload.createdAt, - "ddd, MMMM D, YYYY, h:mm A", - uiTimezone - )} - - - - - - {payload[0]?.name} - {" "} - {payload[0]?.payload.score} - - - ); -}; - -CustomToolTip.propTypes = { - payload: PropTypes.array, -}; - -/* TODO separate utils in folder*/ -/** - * Processes the raw data to include a score for each entry. - * @param {Array} data - The raw data array. - * @returns {Array} - The formatted data array with scores. - */ -const processData = (data) => { - if (data.length === 0) return []; - let formattedData = []; - - const calculateScore = (entry) => { - return ( - (entry.accessibility + entry.bestPractices + entry.performance + entry.seo) / 4 - ); - }; - - data.forEach((entry) => { - entry = { ...entry, score: calculateScore(entry) }; - formattedData.push(entry); - }); - - return formattedData; -}; - -/* TODO separate component*/ -/** - * Renders an area chart displaying page speed scores. - * @param {Object} props - * @param {Array} props.data - The raw data to be displayed in the chart. - * @param {string} props.status - The status of the page speed which determines the chart's color scheme. - * @returns {JSX.Element} - The rendered area chart. - */ -const PagespeedAreaChart = ({ data, status }) => { - const theme = useTheme(); - const [isHovered, setIsHovered] = useState(false); - const { statusToTheme } = useMonitorUtils(); - - const themeColor = statusToTheme[status]; - - const formattedData = processData(data); - - return ( - - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - - } - /> - - - - - - - - - - ); -}; - -PagespeedAreaChart.propTypes = { - data: PropTypes.arrayOf( - PropTypes.shape({ - accessibility: PropTypes.number.isRequired, - bestPractices: PropTypes.number.isRequired, - performance: PropTypes.number.isRequired, - seo: PropTypes.number.isRequired, - }) - ).isRequired, - status: PropTypes.string.isRequired, -}; - -/* TODO separate component */ -/** - * Renders a card displaying monitor details and an area chart. - * @param {Object} props - * @param {Object} props.monitor - The monitor data to be displayed in the card. - * @returns {JSX.Element} - The rendered card. - */ -const Card = ({ monitor }) => { - const { determineState, pagespeedStatusMsg } = useMonitorUtils(); - const theme = useTheme(); - const { t } = useTranslation(); - const navigate = useNavigate(); - const monitorState = determineState(monitor); - - return ( - - navigate(`/pagespeed/${monitor.id}`)} - border={1} - borderColor={theme.palette.primary.lowContrast} - borderRadius={theme.shape.borderRadius} - backgroundColor={theme.palette.primary.main} - sx={{ - display: "grid", - gridTemplateColumns: "34px 2fr 1fr", - columnGap: theme.spacing(5), - gridTemplateRows: "34px 1fr 3fr", - cursor: "pointer", - "&:hover": { - backgroundColor: theme.palette.tertiary.main, - }, - "& path": { - transition: "stroke-width 400ms ease", - }, - }} - > - - - - - {monitor.name} - - - - {monitor.url} - - - - - - - {t("checkingEvery")}{" "} - {(() => { - const { time, format } = formatDurationSplit(monitor?.interval); - return ( - <> - - {time}{" "} - - {format} - - ); - })()} - - - - - ); -}; - -Card.propTypes = { - monitor: PropTypes.object.isRequired, -}; - -export default Card; diff --git a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx deleted file mode 100644 index 9df5f7577..000000000 --- a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Grid } from "@mui/material"; -import Card from "../Card/index.jsx"; - -const MonitorGrid = ({ monitors, shouldRender = true }) => { - if (!shouldRender) return null; - - return ( - - {monitors?.map((monitor) => ( - - ))} - - ); -}; - -export default MonitorGrid; diff --git a/client/src/Pages/PageSpeed/Monitors/old.jsx b/client/src/Pages/PageSpeed/Monitors/old.jsx deleted file mode 100644 index f1bfcd0dd..000000000 --- a/client/src/Pages/PageSpeed/Monitors/old.jsx +++ /dev/null @@ -1,86 +0,0 @@ -// Components -import { useState } from "react"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import { Stack } from "@mui/material"; -import CreateMonitorHeader from "@/Components/v1/MonitorCreateHeader/index.jsx"; -import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx"; -import MonitorGrid from "./Components/MonitorGrid/index.jsx"; -import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; -import FallbackPageSpeedWarning from "@/Components/v1/Fallback/FallbackPageSpeedWarning.jsx"; - -// Utils -import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -import { useFetchMonitorsWithChecks } from "@/Hooks/monitorHooks.js"; -import { useFetchSettings } from "@/Hooks/settingsHooks.js"; -// Constants -const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }]; -const TYPES = ["pagespeed"]; -const PageSpeed = () => { - const theme = useTheme(); - const isAdmin = useIsAdmin(); - - const [ - summary, - monitorsWithChecks, - monitorsWithChecksCount, - monitorsWithChecksIsLoading, - monitorsWithChecksNetworkError, - ] = useFetchMonitorsWithChecks({ - types: TYPES, - limit: 10, - page: null, - rowsPerPage: null, - filter: null, - field: null, - order: null, - monitorUpdateTrigger: null, - }); - - const [settingsData, setSettingsData] = useState(undefined); - const [isSettingsLoading, settingsError] = useFetchSettings({ - setSettingsData, - setIsApiKeySet: () => {}, - setIsEmailPasswordSet: () => {}, - }); - - return ( - <> - - ) - } - > - - - - - - - - - ); -}; - -export default PageSpeed; diff --git a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx b/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx deleted file mode 100644 index 8912438d8..000000000 --- a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import { logger } from "../../../../Utils/Logger.js"; -import { useEffect, useState } from "react"; -import { networkService } from "../../../../main.jsx"; -import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; - -const useCertificateFetch = ({ - monitor, - monitorId, - certificateDateFormat, - uiTimezone, -}) => { - const [certificateExpiry, setCertificateExpiry] = useState(undefined); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - const fetchCertificate = async () => { - if (monitor?.type !== "http") { - return; - } - - try { - setIsLoading(true); - const res = await networkService.getCertificateExpiry({ - monitorId: monitorId, - }); - if (res?.data?.data?.certificateDate) { - const date = res.data.data.certificateDate; - setCertificateExpiry( - formatDateWithTz(date, certificateDateFormat, uiTimezone) ?? "N/A" - ); - } - } catch (error) { - setCertificateExpiry("N/A"); - logger.error(error); - } finally { - setIsLoading(false); - } - }; - fetchCertificate(); - }, [monitorId, certificateDateFormat, uiTimezone, monitor]); - return [certificateExpiry, isLoading]; -}; - -export default useCertificateFetch; diff --git a/client/src/Pages/Uptime/Details/old.jsx b/client/src/Pages/Uptime/Details/old.jsx deleted file mode 100644 index 144ceb4dc..000000000 --- a/client/src/Pages/Uptime/Details/old.jsx +++ /dev/null @@ -1,190 +0,0 @@ -// Components -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx"; -import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx"; -import ChartBoxes from "./Components/ChartBoxes/index.jsx"; -import ResponseTimeChart from "./Components/Charts/ResponseTimeChart.jsx"; -import ResponseTable from "./Components/ResponseTable/index.jsx"; -import UptimeStatusBoxes from "./Components/UptimeStatusBoxes/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; - -// Utils -import { useState } from "react"; -import { useParams } from "react-router-dom"; -import { useSelector } from "react-redux"; -import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -import { useFetchUptimeMonitorById } from "../../../Hooks/monitorHooks.js"; -import useCertificateFetch from "./Hooks/useCertificateFetch.jsx"; -import { useFetchChecksByMonitor } from "../../../Hooks/checkHooks.js"; -import { useTranslation } from "react-i18next"; - -// Constants -const BREADCRUMBS = [ - { name: "uptime", path: "/uptime" }, - { name: "details", path: "" }, - // { name: "details", path: `/uptime/${monitorId}` }, Is this needed? We can't click on this anywy -]; - -const certificateDateFormat = "MMM D, YYYY h A"; - -const UptimeDetails = () => { - // Redux state - const uiTimezone = useSelector((state) => state.ui.timezone); - - // Local state - const [dateRange, setDateRange] = useState("recent"); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(5); - const [trigger, setTrigger] = useState(false); - - // Utils - const dateFormat = - dateRange === "day" || dateRange === "recent" ? "MMM D, h A" : "MMM D"; - const { monitorId } = useParams(); - const theme = useTheme(); - const isAdmin = useIsAdmin(); - const { t } = useTranslation(); - - const [monitorData, monitorStats, monitorIsLoading, monitorNetworkError] = - useFetchUptimeMonitorById({ - monitorId, - dateRange, - trigger, - }); - - const monitor = monitorData?.monitor; - - const [certificateExpiry, certificateIsLoading] = useCertificateFetch({ - monitor, - monitorId, - certificateDateFormat, - uiTimezone, - }); - - const monitorType = monitor?.type; - - const [checks, checksCount, checksAreLoading, checksNetworkError] = - useFetchChecksByMonitor({ - monitorId, - type: monitorType, - sortOrder: "desc", - limit: null, - dateRange, - filter: null, - page, - rowsPerPage, - }); - - // Handlers - const triggerUpdate = () => { - setTrigger(!trigger); - }; - - const handlePageChange = (_, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(event.target.value); - }; - - if (monitorNetworkError || checksNetworkError) { - return ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - // Empty view, displayed when loading is complete and there are no checks - if (!monitorIsLoading && !checksAreLoading && checksCount === 0) { - return ( - - - - - - - - - {t("distributedUptimeDetailsNoMonitorHistory")} - - - ); - } - - return ( - - - - - - - - - - ); -}; - -export default UptimeDetails; diff --git a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx deleted file mode 100644 index 62ba0be13..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useTheme } from "@emotion/react"; -import PropTypes from "prop-types"; -import FilterHeader from "@/Components/v1/FilterHeader/index.jsx"; -import { useMemo } from "react"; -import { Box, Button } from "@mui/material"; -import Icon from "@/Components/v1/Icon"; -import { useTranslation } from "react-i18next"; - -/** - * Filter Component - * - * A high-level component that provides filtering options for type, status, and state. - * It allows users to select multiple options for each filter and reset the filters. - * - * @component - * @param {Object} props - The component props. - * @param {string[]} props.selectedTypes - An array of selected type values. - * @param {function} props.setSelectedTypes - A function to set the selected type values. - * @param {string[]} props.selectedStatus - An array of selected status values. - * @param {function} props.setSelectedStatus - A function to set the selected status values. - * @param {string[]} props.selectedState - An array of selected state values. - * @param {function} props.setSelectedState - A function to set the selected state values. - * @param {function} props.setToFilterStatus - A function to set the filter status based on selected status values. - * @param {function} props.setToFilterActive - A function to set the filter active state based on selected state values. - * @param {function} props.handleReset - A function to reset all filters. - * - * @returns {JSX.Element} The rendered Filter component. - */ - -const getTypeOptions = () => [ - { value: "http", label: "HTTP(S)" }, - { value: "ping", label: "Ping" }, - { value: "docker", label: "Docker" }, - { value: "port", label: "Port" }, - { value: "game", label: "Game" }, -]; - -// These functions were moved inline to ensure translations are applied correctly - -const Filter = ({ - selectedTypes, - setSelectedTypes, - selectedStatus, - setSelectedStatus, - selectedState, - setSelectedState, - setToFilterStatus, - setToFilterActive, - handleReset, -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - - const typeOptions = getTypeOptions(); - // Create status options with translations - const statusOptions = [ - { value: "Up", label: t("monitorStatus.up") }, - { value: "Down", label: t("monitorStatus.down") }, - ]; - // Create state options with translations - const stateOptions = [ - { value: "Active", label: t("monitorState.active") }, - { value: "Paused", label: t("monitorState.paused") }, - ]; - - const handleTypeChange = (event) => { - const selectedValues = event.target.value; - setSelectedTypes(selectedValues.length > 0 ? selectedValues : undefined); - }; - - const handleStatusChange = (event) => { - const selectedValues = event.target.value; - setSelectedStatus(selectedValues.length > 0 ? selectedValues : undefined); - - if (selectedValues.length === 0 || selectedValues.length === 2) { - setToFilterStatus(undefined); - } else { - setToFilterStatus(selectedValues[0] === "Up" ? "true" : "false"); - } - }; - - const handleStateChange = (event) => { - const selectedValues = event.target.value; - setSelectedState(selectedValues); - - if (selectedValues.length === 0 || selectedValues.length === 2) { - setToFilterActive(undefined); - } else { - setToFilterActive(selectedValues[0] === "Active" ? "true" : "false"); - } - }; - - const isFilterActive = useMemo(() => { - return ( - (selectedTypes?.length ?? 0) > 0 || - (selectedState?.length ?? 0) > 0 || - (selectedStatus?.length ?? 0) > 0 - ); - }, [selectedState, selectedTypes, selectedStatus]); - - return ( - - - - - - - ); -}; - -Filter.propTypes = { - selectedTypes: PropTypes.arrayOf(PropTypes.string), - setSelectedTypes: PropTypes.func.isRequired, - selectedStatus: PropTypes.arrayOf(PropTypes.string), - setSelectedStatus: PropTypes.func.isRequired, - selectedState: PropTypes.arrayOf(PropTypes.string), - setSelectedState: PropTypes.func.isRequired, - setToFilterStatus: PropTypes.func.isRequired, - setToFilterActive: PropTypes.func.isRequired, - handleReset: PropTypes.func.isRequired, -}; - -export default Filter; diff --git a/client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx b/client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx deleted file mode 100644 index 4e55be536..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - <> - - - - - - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx deleted file mode 100644 index 5cba9e3b1..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import PropTypes from "prop-types"; -import { Stack } from "@mui/material"; -import StatusBox from "./statusBox.jsx"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import SkeletonLayout from "./skeleton.jsx"; - -const StatusBoxes = ({ shouldRender, monitorsSummary }) => { - const theme = useTheme(); - const { t } = useTranslation(); - if (!shouldRender) return ; - return ( - - - - - - ); -}; - -StatusBoxes.propTypes = { - monitorsSummary: PropTypes.object, - shouldRender: PropTypes.bool, -}; - -export default StatusBoxes; diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx deleted file mode 100644 index df6e08ccd..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -const SkeletonLayout = () => { - const theme = useTheme(); - return ( - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx deleted file mode 100644 index 136c3575d..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx +++ /dev/null @@ -1,115 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Stack, Typography } from "@mui/material"; -import Icon from "@/Components/v1/Icon"; -import Background from "@/assets/Images/background-grid.svg?react"; - -const StatusBox = ({ title, value, status }) => { - const theme = useTheme(); - let sharedStyles = { - position: "absolute", - right: 8, - opacity: 0.5, - "& svg path": { stroke: theme.palette.primary.contrastTextTertiary }, - }; - - let color; - let icon; - if (status === "up") { - color = theme.palette.success.lowContrast; - icon = ( - - - - ); - } else if (status === "down") { - color = theme.palette.error.lowContrast; - icon = ( - - - - ); - } else if (status === "paused") { - color = theme.palette.warning.lowContrast; - icon = ( - - - - ); - } - - return ( - - - - - - - - {title} - - {icon} - - - {value} - - - # - - - - - ); -}; - -StatusBox.propTypes = { - title: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - status: PropTypes.string, -}; - -export default StatusBox; diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx deleted file mode 100644 index 8c3239dd3..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx +++ /dev/null @@ -1,241 +0,0 @@ -// Components -import { Box, Stack } from "@mui/material"; -import DataTable from "@/Components/v1/Table/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import Host from "@/Components/v1/Host/index.jsx"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import BarChart from "@/Components/v1/Charts/BarChart/index.jsx"; -import ActionsMenu from "@/Components/v1/ActionsMenu/index.jsx"; - -import LoadingSpinner from "../LoadingSpinner/index.jsx"; -import TableSkeleton from "@/Components/v1/Table/skeleton.jsx"; - -// Utils -import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; -import { useNavigate } from "react-router-dom"; -import PropTypes from "prop-types"; -import { useTranslation } from "react-i18next"; - -/** - * UptimeDataTable displays a table of uptime monitors with sorting, searching, and action capabilities - * @param {Object} props - Component props - * @param {boolean} props.isAdmin - Whether the current user has admin privileges - * @param {boolean} props.isLoading - Loading state of the table - * @param {Array<{ - * id: string, - * url: string, - * title: string, - * percentage: number, - * percentageColor: string, - * monitor: { - * id: string, - * type: string, - * checks: Array - * } - * }>} props.monitors - Array of monitor objects to display - * @param {number} props.monitorCount - Total count of monitors - * @param {Object} props.sort - Current sort configuration - * @param {string} props.sort.field - Field to sort by - * @param {'asc'|'desc'} props.sort.order - Sort direction - * @param {Function} props.setSort - Callback to update sort configuration - * @param {string} props.search - Current search query - * @param {Function} props.setSearch - Callback to update search query - * @param {boolean} props.isSearching - Whether a search is in progress - * @param {Function} props.setIsLoading - Callback to update loading state - * @param {Function} props.triggerUpdate - Callback to trigger a data refresh - * @returns {JSX.Element} Rendered component - */ -const UptimeDataTable = ({ - isAdmin, - isSearching, - filteredMonitors, - sort, - setSort, - triggerUpdate, - monitorsAreLoading, -}) => { - // Utils - const navigate = useNavigate(); - const { determineState } = useMonitorUtils(); - const theme = useTheme(); - const { t } = useTranslation(); - - // Local state - // Handlers - const handleSort = (field) => { - let order = ""; - if (sort?.field !== field) { - order = "desc"; - } else { - order = sort?.order === "asc" ? "desc" : "asc"; - } - setSort({ field, order }); - }; - - const headers = [ - { - id: "name", - content: ( - handleSort("name")} - > - {t("host")} - - {sort?.order === "asc" ? ( - - ) : ( - - )} - - - ), - render: (row) => ( - - ), - }, - { - id: "status", - content: ( - handleSort("status")} - > - {" "} - {t("status")} - - {sort?.order === "asc" ? ( - - ) : ( - - )} - - - ), - render: (row) => { - const status = determineState(row.monitor); - return ( - - ); - }, - }, - { - id: "responseTime", - content: t("responseTime"), - render: (row) => ( - - - - ), - }, - { - id: "type", - content: t("type"), - render: (row) => ( - - {row.monitor.type === "http" ? "HTTP(s)" : row.monitor.type} - - ), - }, - { - id: "actions", - content: t("actions"), - render: (row) => ( - - ), - }, - ]; - - if (monitorsAreLoading) { - return ; - } - - return ( - - - { - navigate(`/uptime/${row.id}`); - }, - emptyView: "No monitors found", - }} - /> - - ); -}; - -UptimeDataTable.propTypes = { - isSearching: PropTypes.bool, - setSort: PropTypes.func, - setSearch: PropTypes.func, - triggerUpdate: PropTypes.func, - debouncedSearch: PropTypes.string, - onSearchChange: PropTypes.func, - isAdmin: PropTypes.bool, - monitors: PropTypes.array, - filteredMonitors: PropTypes.array, - monitorCount: PropTypes.number, - monitorsAreLoading: PropTypes.bool, - sort: PropTypes.shape({ - field: PropTypes.string, - order: PropTypes.oneOf(["asc", "desc"]), - }), -}; - -export default UptimeDataTable; diff --git a/client/src/Pages/Uptime/Monitors/old.jsx b/client/src/Pages/Uptime/Monitors/old.jsx deleted file mode 100644 index 67019362f..000000000 --- a/client/src/Pages/Uptime/Monitors/old.jsx +++ /dev/null @@ -1,225 +0,0 @@ -// Required Data -// 1. Monitor summary -// 2. List of monitors filtered by search term with 25 checks each -// 2a.List of monitors must have the total number of monitors that match. - -// Components -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import Greeting from "../../../Utils/greeting.jsx"; -import StatusBoxes from "./Components/StatusBoxes/index.jsx"; -import UptimeDataTable from "./Components/UptimeDataTable/index.jsx"; -import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; -import CreateMonitorHeader from "@/Components/v1/MonitorCreateHeader/index.jsx"; -import SearchComponent from "./Components/SearchComponent/index.jsx"; -import Filter from "./Components/Filter/index.jsx"; -import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; - -import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx"; - -// MUI Components -import { Stack, Box, Button } from "@mui/material"; -// Utils -import { useState, useCallback, useEffect } from "react"; -import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { useSelector, useDispatch } from "react-redux"; -import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; -import PropTypes from "prop-types"; -import { - useFetchMonitorsWithChecks, - useFetchMonitorsByTeamId, -} from "@/Hooks/monitorHooks.js"; -import { useTranslation } from "react-i18next"; - -const TYPES = ["http", "ping", "docker", "port", "game"]; -const CreateMonitorButton = ({ shouldRender }) => { - // Utils - const navigate = useNavigate(); - const { t } = useTranslation(); - if (shouldRender === false) { - return; - } - - return ( - - - - ); -}; - -CreateMonitorButton.propTypes = { - shouldRender: PropTypes.bool, -}; - -const UptimeMonitors = () => { - // Redux state - const rowsPerPage = useSelector((state) => state.ui?.monitors?.rowsPerPage ?? 10); - - // Local state - const [search, setSearch] = useState(undefined); - const [page, setPage] = useState(undefined); - const [sort, setSort] = useState(undefined); - const [isSearching, setIsSearching] = useState(false); - const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false); - const [selectedTypes, setSelectedTypes] = useState(undefined); - const [selectedState, setSelectedState] = useState(undefined); - const [selectedStatus, setSelectedStatus] = useState(undefined); - const [toFilterStatus, setToFilterStatus] = useState(undefined); - const [toFilterActive, setToFilterActive] = useState(undefined); - - // Utils - const theme = useTheme(); - const isAdmin = useIsAdmin(); - const dispatch = useDispatch(); - const { t } = useTranslation(); - - const BREADCRUMBS = [{ name: t("menu.uptime"), path: "/uptime" }]; - - // Handlers - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - dispatch( - setRowsPerPage({ - value: parseInt(event.target.value, 10), - table: "monitors", - }) - ); - setPage(0); - }; - - const triggerUpdate = useCallback(() => { - setMonitorUpdateTrigger((prev) => !prev); - }, []); - - const handleReset = () => { - setSelectedState(undefined); - setSelectedTypes(undefined); - setSelectedStatus(undefined); - setToFilterStatus(undefined); - setToFilterActive(undefined); - }; - - const filterLookup = new Map([ - [toFilterStatus, "status"], - [toFilterActive, "isActive"], - ]); - - const activeFilter = [...filterLookup].find(([key]) => key !== undefined); - const field = activeFilter?.[1] || (search ? "name" : sort?.field); - const filter = activeFilter?.[0] || search; - - const effectiveTypes = selectedTypes?.length ? selectedTypes : TYPES; - - const [ - summary, - monitorsWithChecks, - monitorsWithChecksCount, - monitorsWithChecksIsLoading, - networkError, - ] = useFetchMonitorsWithChecks({ - types: effectiveTypes, - limit: 25, - page: page, - rowsPerPage: rowsPerPage, - filter: filter, - field: field, - order: sort?.order, - monitorUpdateTrigger, - }); - - const [monitors, listIsLoading, listNetworkError] = useFetchMonitorsByTeamId({ - type: ["http", "ping", "docker", "port", "game"], - }); - - useEffect(() => { - if (isSearching) { - setPage(undefined); - } - }, [isSearching]); - - const isLoading = monitorsWithChecksIsLoading; - - return ( - <> - - - - - - - - - - - - - - - - - - ); -}; - -export default UptimeMonitors; From 7f1c996632728bde3e270d3b3773fd8010ec4c04 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 28 Jan 2026 20:21:41 +0000 Subject: [PATCH 20/20] format --- .../checks/MongoChecksRepistory.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index e4fe01412..1f65673f3 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -205,15 +205,17 @@ class MongoChecksRepository implements IChecksRepository { const { id, metadata, ...rest } = check; return { _id: id ? new mongoose.Types.ObjectId(id) : new mongoose.Types.ObjectId(), - metadata: metadata ? { - monitorId: new mongoose.Types.ObjectId(metadata.monitorId), - teamId: new mongoose.Types.ObjectId(metadata.teamId), - type: metadata.type, - } : { - monitorId: new mongoose.Types.ObjectId(), - teamId: new mongoose.Types.ObjectId(), - type: "http", - }, + metadata: metadata + ? { + monitorId: new mongoose.Types.ObjectId(metadata.monitorId), + teamId: new mongoose.Types.ObjectId(metadata.teamId), + type: metadata.type, + } + : { + monitorId: new mongoose.Types.ObjectId(), + teamId: new mongoose.Types.ObjectId(), + type: "http", + }, ...rest, } as unknown as CheckDocument; };