From c7aa45cd773a13fe72bc2419a1d2cee826db8266 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 18:24:58 +0000 Subject: [PATCH 1/9] initial commit --- .../v2/monitors/charts/HistogramPageSpeed.tsx | 106 ++++++++ .../charts/HistogramPageSpeedTooltip.tsx | 40 +++ client/src/Components/v2/monitors/index.tsx | 2 + .../Components/PageSpeedMonitorsTable.tsx | 253 ++++++++++++++++++ client/src/Pages/PageSpeed/Monitors/index.tsx | 86 ++++++ .../PageSpeed/Monitors/{index.jsx => old.jsx} | 0 .../Components/UptimeMonitorsTable.tsx | 2 +- client/src/Routes/index.jsx | 15 +- client/src/Types/Settings.ts | 34 +++ client/src/locales/en.json | 12 +- 10 files changed, 544 insertions(+), 6 deletions(-) create mode 100644 client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx create mode 100644 client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx create mode 100644 client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx create mode 100644 client/src/Pages/PageSpeed/Monitors/index.tsx rename client/src/Pages/PageSpeed/Monitors/{index.jsx => old.jsx} (100%) create mode 100644 client/src/Types/Settings.ts diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx new file mode 100644 index 000000000..509886434 --- /dev/null +++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx @@ -0,0 +1,106 @@ +import Stack from "@mui/material/Stack"; +import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; +import { HistogramPageSpeedTooltip } from "@/Components/v2/monitors/charts/HistogramPageSpeedTooltip"; + +import { useTheme } from "@mui/material/styles"; +import type { Check } from "@/Types/Check"; +import type { MonitorStatus } from "@/Types/Monitor"; + +import { getStatusColor } from "@/Utils/MonitorUtils"; + +const processData = (checks: Check[]) => { + if (checks.length === 0) return []; + const formattedData: { score: number; date: string }[] = []; + + const calculateScore = (entry: Check) => { + const accessibility = entry?.accessibility || 0; + const bestPractices = entry?.bestPractices || 0; + const performance = entry?.performance || 0; + const seo = entry?.seo || 0; + return Math.floor(((accessibility + bestPractices + performance + seo) / 4) * 100); + }; + + checks.forEach((entry) => { + const score = calculateScore(entry); + const date = entry.createdAt; + formattedData.push({ score, date }); + }); + + return formattedData; +}; + +export const HistogramPageSpeed = ({ + checks, + status, +}: { + checks: Check[]; + status: MonitorStatus; +}) => { + const theme = useTheme(); + const scores = processData(checks ?? []); + const color = getStatusColor(status, theme); + return ( + event.stopPropagation()} + overflow={"hidden"} + sx={{ + cursor: "default", + position: "relative", + }} + > + + + + } + /> + + + + + + + + + + + ); +}; diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx new file mode 100644 index 000000000..6fd18251c --- /dev/null +++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx @@ -0,0 +1,40 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { formatDateWithTz } from "@/Utils/TimeUtils"; +import type { TooltipProps } from "recharts"; +import { useTheme } from "@mui/material/styles"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; + +export const HistogramPageSpeedTooltip = ({ + payload, +}: TooltipProps) => { + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + const theme = useTheme(); + return ( + + + {formatDateWithTz( + payload?.[0]?.payload?.date ?? 0, + "MMMM D, YYYY, HH:mm A", + uiTimezone + )} + + + Score: {payload?.[0]?.payload?.score} + + + ); +}; diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx index de38d2cab..ca0aaa15e 100644 --- a/client/src/Components/v2/monitors/index.tsx +++ b/client/src/Components/v2/monitors/index.tsx @@ -3,3 +3,5 @@ export * from "./MonitorStatBoxes"; export * from "./charts/HistogramStatus"; export * from "./charts/RadialAvgResponse"; export * from "./charts/HistogramDetails"; +export * from "./charts/HistogramPageSpeed"; +export * from "./charts/HistogramPageSpeedTooltip"; diff --git a/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx b/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx new file mode 100644 index 000000000..0322948c3 --- /dev/null +++ b/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx @@ -0,0 +1,253 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { Table, Pagination, StatusLabel } from "@/Components/v2/design-elements"; +import { HistogramPageSpeed } from "@/Components/v2/monitors"; +import { ArrowDown, ArrowUp } from "lucide-react"; +import type { Header } from "@/Components/v2/design-elements/Table"; +import { ActionsMenu } from "@/Components/v2/actions-menu"; + +import { useTranslation } from "react-i18next"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router-dom"; +import { usePatch } from "@/Hooks/UseApi"; + +import type { Check } from "@/Types/Check"; +import type { Monitor, MonitorWithChecks } from "@/Types/Monitor"; +import type { ActionMenuItem } from "@/Components/v2/actions-menu"; + +export const PageSpeedMonitorsTable = ({ + monitors, + refetch, + sortField, + setSortField, + sortOrder, + setSortOrder, + count, + page, + setPage, + rowsPerPage, + setRowsPerPage, +}: { + monitors: MonitorWithChecks[]; + refetch: Function; + sortField: string; + setSortField: (field: string) => void; + sortOrder: "asc" | "desc"; + setSortOrder: (order: "asc" | "desc") => void; + count: number; + page: number; + setPage: (page: number) => void; + rowsPerPage: number; + setRowsPerPage: (rowsPerPage: number) => void; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const navigate = useNavigate(); + const { + patch, + // loading: isPatching, + // error: postError, + } = usePatch(); + + 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); + }; + + const handleSort = (e: any, field: string) => { + e.preventDefault(); + e.stopPropagation(); + if (sortField === field) { + const newOrder = sortOrder === "asc" ? "desc" : "asc"; + setSortOrder(newOrder); + } else { + setSortField(field); + setSortOrder("asc"); + } + refetch(); + }; + + const getActions = (monitor: Monitor): ActionMenuItem[] => { + return [ + { + id: 1, + label: t("pages.common.monitors.actions.openSite"), + action: () => { + window.open(monitor.url, "_blank", "noreferrer"); + }, + closeMenu: true, + }, + { + id: 2, + label: t("pages.common.monitors.actions.details"), + action: () => { + navigate(`${monitor.id}`); + }, + }, + { + id: 3, + label: t("pages.common.monitors.actions.incidents"), + action: () => { + navigate(`/incidents?monitorId=${monitor.id}`); + }, + }, + { + id: 4, + label: t("pages.common.monitors.actions.configure"), + action: () => { + navigate(`/pagespeed/configure/${monitor.id}`); + }, + }, + // { + // id: 5, + // label: "Clone", + // action: () => { + + // }, + // }, + { + id: 6, + label: + monitor.isActive === false + ? t("common.buttons.resume") + : t("common.buttons.pause"), + action: async () => { + await patch(`/monitors/${monitor.id}/active`); + refetch(); + }, + closeMenu: true, + }, + { + id: 7, + label: ( + + {t("common.buttons.delete")} + + ), + action: () => { + // setSelectedMonitor(monitor); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = () => { + const renderSortIcon = (isActive: boolean) => ( + + {isActive ? ( + sortOrder === "asc" ? ( + + ) : ( + + ) + ) : null} + + ); + + const headers: Header[] = [ + { + id: "name", + content: ( + handleSort(e, "name")} + sx={{ cursor: "pointer" }} + > + {t("common.table.headers.name")} + {renderSortIcon(sortField === "name")} + + ), + render: (row) => { + return row.name; + }, + }, + { + id: "status", + content: ( + handleSort(e, "status")} + sx={{ cursor: "pointer" }} + > + {t("common.table.headers.status")} + {renderSortIcon(sortField === "status")} + + ), + render: (row) => { + return ; + }, + }, + { + id: "histogram", + content: t("pages.pageSpeed.table.headers.pageSpeedScore"), + render: (row) => { + return ( + + + + ); + }, + }, + + { + id: "actions", + content: t("common.table.headers.actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + let headers = getHeaders(); + + if (isSmall) { + headers = headers.filter((h) => h.id !== "histogram"); + } + return ( + + { + navigate(`/pagespeed/${row.id}`); + }} + /> + + + ); +}; diff --git a/client/src/Pages/PageSpeed/Monitors/index.tsx b/client/src/Pages/PageSpeed/Monitors/index.tsx new file mode 100644 index 000000000..158260456 --- /dev/null +++ b/client/src/Pages/PageSpeed/Monitors/index.tsx @@ -0,0 +1,86 @@ +import { + MonitorBasePageWithStates, + // UpStatusBox, + // DownStatusBox, + // PausedStatusBox, +} from "@/Components/v2/design-elements"; +import { HeaderCreate } from "@/Components/v2/common"; +// import Stack from "@mui/material/Stack"; +import { PageSpeedMonitorsTable } from "@/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable"; + +import { useState } from "react"; +import { useTheme } from "@mui/material"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; +import { useGet } from "@/Hooks/UseApi"; +import type { MonitorsWithChecksResponse } from "@/Types/Monitor"; +import type { AppSettingsResponse } from "@/Types/Settings"; + +const PageSpeedMonitorsPage = () => { + const theme = useTheme(); + const isAdmin = useIsAdmin(); + const monitorsUrl = "/monitors/team/with-checks?type=pagespeed&limit=10"; + const { + data: monitorsData, + isLoading: monitorsIsLoading, + error: monitorsError, + refetch, + } = useGet(monitorsUrl); + + const settingsUrl = "/settings"; + const { + data: settingsData, + isLoading: settingsIsLoading, + error: settingsError, + } = useGet(settingsUrl); + + const monitors = monitorsData?.monitors; + const monitorsCount = monitorsData?.count; + // const summary = monitorsData?.summary; + const settings = settingsData; + + const isLoading = monitorsIsLoading || settingsIsLoading; + + const [sortField, setSortField] = useState("name"); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + + return ( + + + {/* + + + + */} + + + ); +}; + +export default PageSpeedMonitorsPage; diff --git a/client/src/Pages/PageSpeed/Monitors/index.jsx b/client/src/Pages/PageSpeed/Monitors/old.jsx similarity index 100% rename from client/src/Pages/PageSpeed/Monitors/index.jsx rename to client/src/Pages/PageSpeed/Monitors/old.jsx diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx index 21e014688..59daf1702 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx @@ -97,7 +97,7 @@ export const MonitorTable = ({ id: 4, label: t("pages.common.monitors.actions.configure"), action: () => { - navigate(`/uptime/create/${monitor.id}`); + navigate(`/uptime/configure/${monitor.id}`); }, }, // { diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 715d4374e..c9a1084d6 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -20,7 +20,7 @@ import UptimeDetails from "../Pages/Uptime/Details"; import UptimeCreate from "../Pages/Uptime/Create/index.jsx"; // PageSpeed -import PageSpeed from "../Pages/PageSpeed/Monitors/index.jsx"; +import PageSpeed from "../Pages/PageSpeed/Monitors/index"; import PageSpeedDetails from "../Pages/PageSpeed/Details/index.jsx"; import PageSpeedCreate from "../Pages/PageSpeed/Create/index.jsx"; @@ -63,6 +63,7 @@ import Logs from "../Pages/Logs/index.jsx"; const Routes = () => { const mode = useSelector((state) => state.ui.mode); const AdminCheckedRegister = withAdminCheck(AuthRegister); + const v2theme = mode === "light" ? lightTheme : darkTheme; return ( { path="/uptime" element={ <> - + @@ -105,7 +106,7 @@ const Routes = () => { path="/uptime/:monitorId/" element={ <> - + @@ -118,7 +119,13 @@ const Routes = () => { } + element={ + <> + + + + + } /> Date: Mon, 26 Jan 2026 18:27:12 +0000 Subject: [PATCH 2/9] fix: add overflowX hidden to table cells to prevent horizontal scrollbar --- client/src/Components/v2/design-elements/Table.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/Components/v2/design-elements/Table.tsx b/client/src/Components/v2/design-elements/Table.tsx index c87fa41b8..7270ac519 100644 --- a/client/src/Components/v2/design-elements/Table.tsx +++ b/client/src/Components/v2/design-elements/Table.tsx @@ -173,6 +173,7 @@ export function DataTable< color: theme.palette.text.secondary, padding: `${theme.spacing(7)} ${theme.spacing(8)}`, fontSize: theme.typography.fontSize, + overflowX: "hidden", }, "& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": { borderBottom: "none", From 67f74dd7de8b8fb61e213603d54c13347ab8584d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 18:28:55 +0000 Subject: [PATCH 3/9] fix: prevent horizontal scrollbar in table by adding overflowX hidden and making histogram responsive --- client/src/Components/v2/design-elements/Table.tsx | 2 +- .../src/Components/v2/monitors/charts/HistogramPageSpeed.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/Components/v2/design-elements/Table.tsx b/client/src/Components/v2/design-elements/Table.tsx index 7270ac519..0516f1ddd 100644 --- a/client/src/Components/v2/design-elements/Table.tsx +++ b/client/src/Components/v2/design-elements/Table.tsx @@ -147,7 +147,7 @@ export function DataTable<
event.stopPropagation()} overflow={"hidden"} sx={{ From 6cb45ea6deb2245d783aac5b76bc71b75ce609a2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 19:36:29 +0000 Subject: [PATCH 4/9] delete pagepseed --- client/src/Components/v2/inputs/Dialog.tsx | 56 +++++++++++++++++++ client/src/Components/v2/inputs/index.tsx | 1 + client/src/Hooks/UseApi.ts | 37 +++++++++++- .../Components/PageSpeedMonitorsTable.tsx | 5 +- client/src/Pages/PageSpeed/Monitors/index.tsx | 45 +++++++++++---- client/src/Pages/Uptime/Monitors/index.tsx | 1 - client/src/locales/en.json | 10 +++- 7 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 client/src/Components/v2/inputs/Dialog.tsx diff --git a/client/src/Components/v2/inputs/Dialog.tsx b/client/src/Components/v2/inputs/Dialog.tsx new file mode 100644 index 000000000..373d95023 --- /dev/null +++ b/client/src/Components/v2/inputs/Dialog.tsx @@ -0,0 +1,56 @@ +import Dialog from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +import DialogContentText from "@mui/material/DialogContentText"; +import DialogTitle from "@mui/material/DialogTitle"; +import { Button } from "@/Components/v2/inputs"; +import { typographyLevels } from "@/Utils/Theme/v2Palette"; +import { useTranslation } from "react-i18next"; + +export const DialogInput = ({ + open, + title, + content, + onConfirm, + onCancel, + confirmColor = "primary", + cancelColor = "error", + loading = false, +}: { + open: boolean; + title?: string; + content?: string; + onConfirm?(item: any): any; + onCancel?(item: any): any; + confirmColor?: "error" | "primary"; + cancelColor?: "error" | "primary"; + loading?: boolean; +}) => { + const { t } = useTranslation(); + return ( + + {title} + + {content} + + + + + + + ); +}; diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx index 87698a4f0..70e740dbd 100644 --- a/client/src/Components/v2/inputs/index.tsx +++ b/client/src/Components/v2/inputs/index.tsx @@ -8,3 +8,4 @@ export { ToggleButtonInput as ToggleButton, ToggleButtonGroupInput as ToggleButtonGroup, } from "./ToggleButton"; +export { DialogInput as Dialog } from "./Dialog"; diff --git a/client/src/Hooks/UseApi.ts b/client/src/Hooks/UseApi.ts index 441c2b77b..aa71f01af 100644 --- a/client/src/Hooks/UseApi.ts +++ b/client/src/Hooks/UseApi.ts @@ -1,7 +1,7 @@ import useSWR from "swr"; import type { SWRConfiguration } from "swr"; import type { AxiosRequestConfig } from "axios"; -import { get, patch, post } from "@/Utils/ApiClient"; +import { get, patch, post, deleteOp } from "@/Utils/ApiClient"; import { useState } from "react"; import { useToast } from "@/Hooks/UseToast"; @@ -107,3 +107,38 @@ export const usePatch = () => { return { patch: patchFn, loading, error }; }; + +export const useDelete = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { toastError, toastSuccess } = useToast(); + + const deleteFn = async ( + endpoint: string, + config?: AxiosRequestConfig + ): Promise | null> => { + setLoading(true); + setError(null); + + try { + const res = await deleteOp>(endpoint, { + ...config, + headers: { + ...config?.headers, + }, + }); + toastSuccess(res.data?.msg || "Operation successful"); + return res.data; + } catch (err: any) { + console.error(err); + const errMsg = err?.response?.data?.message || err.message || "An error occurred"; + toastError(errMsg); + setError(errMsg); + return null; + } finally { + setLoading(false); + } + }; + + return { deleteFn, loading, error }; +}; diff --git a/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx b/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx index 0322948c3..f38e1c706 100644 --- a/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx +++ b/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx @@ -13,13 +13,13 @@ import { useTheme } from "@mui/material/styles"; import { useNavigate } from "react-router-dom"; import { usePatch } from "@/Hooks/UseApi"; -import type { Check } from "@/Types/Check"; import type { Monitor, MonitorWithChecks } from "@/Types/Monitor"; import type { ActionMenuItem } from "@/Components/v2/actions-menu"; export const PageSpeedMonitorsTable = ({ monitors, refetch, + setSelectedMonitor, sortField, setSortField, sortOrder, @@ -32,6 +32,7 @@ export const PageSpeedMonitorsTable = ({ }: { monitors: MonitorWithChecks[]; refetch: Function; + setSelectedMonitor: Function; sortField: string; setSortField: (field: string) => void; sortOrder: "asc" | "desc"; @@ -138,7 +139,7 @@ export const PageSpeedMonitorsTable = ({ ), action: () => { - // setSelectedMonitor(monitor); + setSelectedMonitor(monitor); }, closeMenu: true, }, diff --git a/client/src/Pages/PageSpeed/Monitors/index.tsx b/client/src/Pages/PageSpeed/Monitors/index.tsx index 158260456..64b39dbce 100644 --- a/client/src/Pages/PageSpeed/Monitors/index.tsx +++ b/client/src/Pages/PageSpeed/Monitors/index.tsx @@ -4,20 +4,24 @@ import { // DownStatusBox, // PausedStatusBox, } from "@/Components/v2/design-elements"; +import { Dialog } from "@/Components/v2/inputs"; import { HeaderCreate } from "@/Components/v2/common"; // import Stack from "@mui/material/Stack"; import { PageSpeedMonitorsTable } from "@/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable"; +import type { Monitor } from "@/Types/Monitor"; +import { useTranslation } from "react-i18next"; import { useState } from "react"; -import { useTheme } from "@mui/material"; import { useIsAdmin } from "@/Hooks/useIsAdmin"; -import { useGet } from "@/Hooks/UseApi"; +import { useGet, useDelete } from "@/Hooks/UseApi"; import type { MonitorsWithChecksResponse } from "@/Types/Monitor"; import type { AppSettingsResponse } from "@/Types/Settings"; const PageSpeedMonitorsPage = () => { - const theme = useTheme(); + const { t } = useTranslation(); const isAdmin = useIsAdmin(); + const { deleteFn, loading: isDeleting } = useDelete(); + const monitorsUrl = "/monitors/team/with-checks?type=pagespeed&limit=10"; const { data: monitorsData, @@ -33,18 +37,30 @@ const PageSpeedMonitorsPage = () => { error: settingsError, } = useGet(settingsUrl); - const monitors = monitorsData?.monitors; - const monitorsCount = monitorsData?.count; - // const summary = monitorsData?.summary; - const settings = settingsData; - - const isLoading = monitorsIsLoading || settingsIsLoading; - + const [selectedMonitor, setSelectedMonitor] = useState(null); + const isDialogOpen = Boolean(selectedMonitor); const [sortField, setSortField] = useState("name"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); + const monitors = monitorsData?.monitors; + const monitorsCount = monitorsData?.count; + // const summary = monitorsData?.summary; + + const isLoading = monitorsIsLoading || settingsIsLoading; + + const handleConfirm = async () => { + if (!selectedMonitor) return; + await deleteFn(`/monitors/${selectedMonitor.id}`); + setSelectedMonitor(null); + refetch(); + }; + + const handleCancel = () => { + setSelectedMonitor(null); + }; + return ( { { rowsPerPage={rowsPerPage} setRowsPerPage={setRowsPerPage} /> + ); }; diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index e3e90429a..061fb9454 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -173,7 +173,6 @@ const UptimeMonitorsPage = () => { sortOrder={sortOrder} setSortOrder={setSortOrder} setRowsPerPage={(rowsPerPage: number) => { - console.log("wtf"); dispatch( setRowsPerPage({ value: rowsPerPage, diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 8a44872ff..a630b68e2 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -9,7 +9,15 @@ "incidents": "Incidents", "pause": "Pause", "resume": "Resume", - "delete": "Delete" + "delete": "Delete", + "cancel": "Cancel", + "confirm": "Confirm" + }, + "dialogs": { + "delete": { + "description": "This action cannot be undone.", + "title": "Are you sure you want to delete this?" + } }, "toasts": { "networkError": "Network error", From 6380dcc6badd90f135f2b722f9929a8a3511d38c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 20:05:11 +0000 Subject: [PATCH 5/9] summary --- client/src/Pages/PageSpeed/Monitors/index.tsx | 16 +++++----- .../Components/UptimeMonitorsTable.tsx | 6 ++-- client/src/Pages/Uptime/Monitors/index.tsx | 31 +++++++++++++++++-- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/client/src/Pages/PageSpeed/Monitors/index.tsx b/client/src/Pages/PageSpeed/Monitors/index.tsx index 64b39dbce..33a04bb6b 100644 --- a/client/src/Pages/PageSpeed/Monitors/index.tsx +++ b/client/src/Pages/PageSpeed/Monitors/index.tsx @@ -1,15 +1,16 @@ import { MonitorBasePageWithStates, - // UpStatusBox, - // DownStatusBox, - // PausedStatusBox, + UpStatusBox, + DownStatusBox, + PausedStatusBox, } from "@/Components/v2/design-elements"; import { Dialog } from "@/Components/v2/inputs"; import { HeaderCreate } from "@/Components/v2/common"; -// import Stack from "@mui/material/Stack"; +import Stack from "@mui/material/Stack"; import { PageSpeedMonitorsTable } from "@/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable"; import type { Monitor } from "@/Types/Monitor"; +import { useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useState } from "react"; import { useIsAdmin } from "@/Hooks/useIsAdmin"; @@ -19,6 +20,7 @@ import type { AppSettingsResponse } from "@/Types/Settings"; const PageSpeedMonitorsPage = () => { const { t } = useTranslation(); + const theme = useTheme(); const isAdmin = useIsAdmin(); const { deleteFn, loading: isDeleting } = useDelete(); @@ -46,7 +48,7 @@ const PageSpeedMonitorsPage = () => { const monitors = monitorsData?.monitors; const monitorsCount = monitorsData?.count; - // const summary = monitorsData?.summary; + const summary = monitorsData?.summary; const isLoading = monitorsIsLoading || settingsIsLoading; @@ -74,14 +76,14 @@ const PageSpeedMonitorsPage = () => { isLoading={isLoading} isAdmin={isAdmin} /> - {/* - */} + void; sortField: string; setSortField: (field: string) => void; sortOrder: "asc" | "desc"; @@ -126,9 +128,7 @@ export const MonitorTable = ({ {t("pages.common.monitors.actions.delete")} ), - action: () => { - // setSelectedMonitor(monitor); - }, + action: () => setSelectedMonitor(monitor), closeMenu: true, }, ]; diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 061fb9454..5c3828d07 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -5,14 +5,14 @@ import { DownStatusBox, PausedStatusBox, } from "@/Components/v2/design-elements"; -import { TextField } from "@/Components/v2/inputs"; +import { TextField, Dialog } from "@/Components/v2/inputs"; import Stack from "@mui/material/Stack"; import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable"; import { HeaderCreate } from "@/Components/v2/common"; import { useTranslation } from "react-i18next"; import useMediaQuery from "@mui/material/useMediaQuery"; -import { useGet } from "@/Hooks/UseApi"; +import { useGet, useDelete } from "@/Hooks/UseApi"; import type { Monitor, MonitorType, MonitorsWithChecksResponse } from "@/Types/Monitor"; import { useState, useMemo, useCallback } from "react"; import { useSelector, useDispatch } from "react-redux"; @@ -40,6 +40,8 @@ const UptimeMonitorsPage = () => { const [search, setSearch] = useState(""); const [sortField, setSortField] = useState(""); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); + const [selectedMonitor, setSelectedMonitor] = useState(null); + const isDialogOpen = Boolean(selectedMonitor); // Convert filter selections to API filter values // Status: "up" -> true, "down" -> false @@ -88,6 +90,7 @@ const UptimeMonitorsPage = () => { data: monitors, isLoading: monitorsLoading, error, + refetch: refetchMonitorsAndSummary, } = useGet( "/monitors/team?type=http&type=ping&type=port&type=docker", {}, @@ -107,6 +110,9 @@ const UptimeMonitorsPage = () => { const { monitors: monitorsWithChecks, summary, count } = monitorsWithChecksData ?? {}; + // Delete hook + const { deleteFn, loading: isDeleting } = useDelete(); + // Handlers const handleClearFilters = useCallback(() => { setSelectedTypes([]); @@ -115,6 +121,18 @@ const UptimeMonitorsPage = () => { setSearch(""); }, []); + const handleConfirm = async () => { + if (!selectedMonitor) return; + await deleteFn(`/monitors/${selectedMonitor.id}`); + setSelectedMonitor(null); + refetch(); + refetchMonitorsAndSummary(); + }; + + const handleCancel = () => { + setSelectedMonitor(null); + }; + const isLoading = monitorsLoading || monitorsWithChecksLoading; return ( @@ -164,6 +182,7 @@ const UptimeMonitorsPage = () => { { setPage(0); }} /> + ); }; From 1819df5117af50da5ed8b7c9b0604d87296544a4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 20:27:11 +0000 Subject: [PATCH 6/9] priority fallback --- .../v2/design-elements/BasePage.tsx | 52 +++++++++++++++++++ client/src/Pages/PageSpeed/Monitors/index.tsx | 2 +- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/client/src/Components/v2/design-elements/BasePage.tsx b/client/src/Components/v2/design-elements/BasePage.tsx index c8618fa67..bbe19be07 100644 --- a/client/src/Components/v2/design-elements/BasePage.tsx +++ b/client/src/Components/v2/design-elements/BasePage.tsx @@ -1,8 +1,11 @@ import Stack from "@mui/material/Stack"; +import Alert from "@mui/material/Alert"; +import Link from "@mui/material/Link"; import { ErrorFallback, EmptyFallback, EmptyMonitorFallback, + BaseFallback, } from "@/Components/v2/design-elements/Fallback"; import { Breadcrumb } from "@/Components/v2/design-elements/Breadcrumb"; import CircularProgress from "@mui/material/CircularProgress"; @@ -10,6 +13,41 @@ import CircularProgress from "@mui/material/CircularProgress"; import type { StackProps } from "@mui/material/Stack"; import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; + +export const PageSpeedKeyPriorityFallback = () => { + const theme = useTheme(); + const { t } = useTranslation(); + + return ( + + + {t("pageSpeedWarning")}{" "} + + {t("pageSpeedLearnMoreLink")} + {" "} + {t("pageSpeedAddApiKey")} + + + ); +}; + interface BasePageProps extends StackProps { loading?: boolean; error?: boolean; @@ -114,6 +152,7 @@ interface MonitorBasePageWithStatesProps extends StackProps { page: string; actionLink?: string; children: React.ReactNode; + priorityFallback?: React.ReactNode; } const isEmpty = (items: any[]) => { @@ -129,12 +168,25 @@ export const MonitorBasePageWithStates = ({ page, actionLink, children, + priorityFallback, ...props }: MonitorBasePageWithStatesProps) => { const { t } = useTranslation(); const showLoading = loading && (!items || items.length === 0); + if (priorityFallback) { + return ( + + {priorityFallback} + + ); + } + if (!loading && isEmpty(items)) { return ( { isLoading: monitorsIsLoading, error: monitorsError, refetch, - } = useGet(monitorsUrl); + } = useGet(monitorsUrl, {}, { refreshInterval: 30000 }); const settingsUrl = "/settings"; const { From 90e9b41973f4dc6f650b639558c42b8292f80236 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 20:47:10 +0000 Subject: [PATCH 7/9] pagespeed key alert --- .../v2/design-elements/BasePage.tsx | 40 +++++++++---------- client/src/Pages/PageSpeed/Monitors/index.tsx | 5 +++ client/src/locales/en.json | 5 +++ 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/client/src/Components/v2/design-elements/BasePage.tsx b/client/src/Components/v2/design-elements/BasePage.tsx index bbe19be07..de20fc005 100644 --- a/client/src/Components/v2/design-elements/BasePage.tsx +++ b/client/src/Components/v2/design-elements/BasePage.tsx @@ -12,13 +12,11 @@ import CircularProgress from "@mui/material/CircularProgress"; import type { StackProps } from "@mui/material/Stack"; import { useTheme } from "@mui/material/styles"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { Link as RouterLink } from "react-router-dom"; +import { Typography } from "@mui/material"; export const PageSpeedKeyPriorityFallback = () => { - const theme = useTheme(); - const { t } = useTranslation(); - return ( { sx={{ width: "100%", maxWidth: 600, - backgroundColor: theme.palette.warning.light, - color: theme.palette.warning.contrastText, - "& .MuiAlert-icon": { - color: theme.palette.warning.contrastText, - }, }} > - {t("pageSpeedWarning")}{" "} - - {t("pageSpeedLearnMoreLink")} - {" "} - {t("pageSpeedAddApiKey")} + + + ), + }} + /> + ); @@ -178,8 +176,8 @@ export const MonitorBasePageWithStates = ({ if (priorityFallback) { return ( {priorityFallback} diff --git a/client/src/Pages/PageSpeed/Monitors/index.tsx b/client/src/Pages/PageSpeed/Monitors/index.tsx index 811859d41..d15e09e70 100644 --- a/client/src/Pages/PageSpeed/Monitors/index.tsx +++ b/client/src/Pages/PageSpeed/Monitors/index.tsx @@ -3,6 +3,7 @@ import { UpStatusBox, DownStatusBox, PausedStatusBox, + PageSpeedKeyPriorityFallback, } from "@/Components/v2/design-elements"; import { Dialog } from "@/Components/v2/inputs"; import { HeaderCreate } from "@/Components/v2/common"; @@ -52,6 +53,9 @@ const PageSpeedMonitorsPage = () => { const isLoading = monitorsIsLoading || settingsIsLoading; + const showApiKeyWarning = + isAdmin && settingsData && !settingsData.pagespeedKeySet; + const handleConfirm = async () => { if (!selectedMonitor) return; await deleteFn(`/monitors/${selectedMonitor.id}`); @@ -70,6 +74,7 @@ const PageSpeedMonitorsPage = () => { items={monitors || []} page="pageSpeed" actionLink="/pagespeed/create" + priorityFallback={showApiKeyWarning ? : undefined} > Settings to add one. Without it, the PageSpeed monitor won't function." + } + }, "dialogs": { "delete": { "description": "This action cannot be undone.", From a07e41d8f15b800c63ebc6c6ec876685b759eeb2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 20:47:24 +0000 Subject: [PATCH 8/9] format --- client/src/Pages/PageSpeed/Monitors/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/Pages/PageSpeed/Monitors/index.tsx b/client/src/Pages/PageSpeed/Monitors/index.tsx index d15e09e70..887ec02c0 100644 --- a/client/src/Pages/PageSpeed/Monitors/index.tsx +++ b/client/src/Pages/PageSpeed/Monitors/index.tsx @@ -53,8 +53,7 @@ const PageSpeedMonitorsPage = () => { const isLoading = monitorsIsLoading || settingsIsLoading; - const showApiKeyWarning = - isAdmin && settingsData && !settingsData.pagespeedKeySet; + const showApiKeyWarning = isAdmin && settingsData && !settingsData.pagespeedKeySet; const handleConfirm = async () => { if (!selectedMonitor) return; From 451e3524589304db873087c5c44a30b50bf8047d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 20:47:29 +0000 Subject: [PATCH 9/9] types --- server/src/service/business/monitorService.ts | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 513fc2a82..38fcaef78 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -342,7 +342,7 @@ export class MonitorService implements IMonitorService { order?: "asc" | "desc"; explain?: boolean; }): Promise => { - const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId); + const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId, { type }); const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter }); const monitors = await this.monitorsRepository.findByTeamId(teamId, { limit, @@ -402,9 +402,27 @@ export class MonitorService implements IMonitorService { deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { const monitor = await this.monitorsRepository.deleteById(monitorId, teamId); - await this.monitorStatsRepository.deleteByMonitorId(monitor.id); - await this.checksRepository.deleteByMonitorId(monitor.id); - await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id); + await this.monitorStatsRepository.deleteByMonitorId(monitor.id).catch((err: any) => { + this.logger.warn({ + message: `Error deleting monitor stats for monitor ${monitor.id} with name ${monitor.name}`, + service: SERVICE_NAME, + stack: err.stack, + }); + }); + await this.checksRepository.deleteByMonitorId(monitor.id).catch((err: any) => { + this.logger.warn({ + message: `Error deleting checks for monitor ${monitor.id} with name ${monitor.name}`, + service: SERVICE_NAME, + stack: err.stack, + }); + }); + await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id).catch((err: any) => { + this.logger.warn({ + message: `Error removing monitor ${monitor.id} with name ${monitor.name} from status pages`, + service: SERVICE_NAME, + stack: err.stack, + }); + }); await this.jobQueue.deleteJob(monitor); return monitor; };