From 459d78cbcb9306ef09f13f0f4d42f9cbc633ebf9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 19 Nov 2025 20:53:25 +0000 Subject: [PATCH] sorting --- client/package-lock.json | 17 +++++ client/package.json | 1 + .../pages/infrastructure/InfraMonitors.tsx | 19 ++++- .../infrastructure/InfraMonitorsTable.tsx | 59 ++++++++++++++- .../pages/pagespeed/PageSpeedMonitorTable.tsx | 61 ++++++++++++++- .../src/pages/pagespeed/PageSpeedMonitors.tsx | 19 ++++- client/src/pages/uptime/MonitorTable.tsx | 75 ++++++++++++++++++- client/src/pages/uptime/UptimeMonitors.tsx | 8 ++ server/src/controllers/MonitorController.ts | 4 + .../src/services/business/MonitorService.ts | 37 +++++++-- server/src/validation/index.ts | 2 + 11 files changed, 287 insertions(+), 15 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 74a67958c..2e0d8f9c3 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -46,6 +46,7 @@ "eslint-plugin-react-refresh": "^0.4.22", "globals": "^16.4.0", "i18n-unused": "0.19.0", + "prettier": "^3.6.2", "typescript": "~5.9.3", "typescript-eslint": "^8.45.0", "vite": "^7.1.7" @@ -4522,6 +4523,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-ms": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", diff --git a/client/package.json b/client/package.json index 4d02090eb..8e952ebff 100644 --- a/client/package.json +++ b/client/package.json @@ -49,6 +49,7 @@ "eslint-plugin-react-refresh": "^0.4.22", "globals": "^16.4.0", "i18n-unused": "0.19.0", + "prettier": "^3.6.2", "typescript": "~5.9.3", "typescript-eslint": "^8.45.0", "vite": "^7.1.7" diff --git a/client/src/pages/infrastructure/InfraMonitors.tsx b/client/src/pages/infrastructure/InfraMonitors.tsx index e12d14765..c59b0131e 100644 --- a/client/src/pages/infrastructure/InfraMonitors.tsx +++ b/client/src/pages/infrastructure/InfraMonitors.tsx @@ -27,13 +27,26 @@ const PageSpeedMonitorsPage = () => { const isSmall = useMediaQuery(theme.breakpoints.down("md")); const [selectedMonitor, setSelectedMonitor] = useState(null); const isDialogOpen = Boolean(selectedMonitor); + const [sortField, setSortField] = useState("name"); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); const { deleteFn } = useDelete(); + const requestParams = [ + "embedChecks=true", + "type=infrastructure", + `sortField=${sortField}`, + `sortOrder=${sortOrder}`, + `page=${page}`, + `rowsPerPage=${rowsPerPage}`, + ]; + + const monitorRequestUrl = `/monitors?${requestParams.join("&")}`; + const { response, loading, isValidating, error, refetch } = useGet< ApiResponse >( - `/monitors?embedChecks=true&type=infrastructure&page=${page}&rowsPerPage=${rowsPerPage}`, + monitorRequestUrl, {}, { refreshInterval: GLOBAL_REFRESH, @@ -82,6 +95,10 @@ const PageSpeedMonitorsPage = () => { monitors={monitors} refetch={refetch} setSelectedMonitor={setSelectedMonitor} + sortField={sortField} + setSortField={setSortField} + sortOrder={sortOrder} + setSortOrder={setSortOrder} count={count} page={page} setPage={setPage} diff --git a/client/src/pages/infrastructure/InfraMonitorsTable.tsx b/client/src/pages/infrastructure/InfraMonitorsTable.tsx index 64877f5b1..d15a3b378 100644 --- a/client/src/pages/infrastructure/InfraMonitorsTable.tsx +++ b/client/src/pages/infrastructure/InfraMonitorsTable.tsx @@ -4,6 +4,7 @@ import { Table, Pagination, StatusLabel } from "@/components/design-elements"; import type { Header } from "@/components/design-elements/Table"; import { ActionsMenu } from "@/components/actions-menu"; import { Gauge } from "@/components/design-elements"; +import { ArrowUp, ArrowDown } from "lucide-react"; import { useTranslation } from "react-i18next"; import useMediaQuery from "@mui/material/useMediaQuery"; @@ -18,6 +19,10 @@ export const InfraMonitorsTable = ({ monitors, refetch, setSelectedMonitor, + sortField, + setSortField, + sortOrder, + setSortOrder, count, page, setPage, @@ -27,6 +32,10 @@ export const InfraMonitorsTable = ({ monitors: IMonitor[]; refetch: Function; setSelectedMonitor: Function; + sortField: string; + setSortField: (field: string) => void; + sortOrder: "asc" | "desc"; + setSortOrder: (order: "asc" | "desc") => void; count: number; page: number; setPage: (page: number) => void; @@ -58,6 +67,19 @@ export const InfraMonitorsTable = ({ 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("desc"); + } + refetch(); + }; + const getActions = (monitor: IMonitor): ActionMenuItem[] => { return [ { @@ -117,17 +139,50 @@ export const InfraMonitorsTable = ({ }; const getHeaders = () => { + const renderSortIcon = (isActive: boolean) => ( + + {isActive ? ( + sortOrder === "asc" ? ( + + ) : ( + + ) + ) : null} + + ); const headers: Header[] = [ { id: "name", - content: t("host"), + content: ( + handleSort(e, "name")} + sx={{ cursor: "pointer" }} + > + {t("host")} + {renderSortIcon(sortField === "name")} + + ), render: (row) => { return row.name; }, }, { id: "status", - content: t("status"), + content: ( + handleSort(e, "status")} + sx={{ cursor: "pointer" }} + > + {t("status")} + {renderSortIcon(sortField === "status")} + + ), render: (row) => { return ; }, diff --git a/client/src/pages/pagespeed/PageSpeedMonitorTable.tsx b/client/src/pages/pagespeed/PageSpeedMonitorTable.tsx index bea7165b0..7a9d22973 100644 --- a/client/src/pages/pagespeed/PageSpeedMonitorTable.tsx +++ b/client/src/pages/pagespeed/PageSpeedMonitorTable.tsx @@ -3,6 +3,7 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import { Table, Pagination, StatusLabel } from "@/components/design-elements"; import { HistogramPageSpeed } from "@/components/monitors"; +import { ArrowDown, ArrowUp } from "lucide-react"; import type { Header } from "@/components/design-elements/Table"; import { ActionsMenu } from "@/components/actions-menu"; @@ -19,6 +20,10 @@ export const PageSpeedMonitorTable = ({ monitors, refetch, setSelectedMonitor, + sortField, + setSortField, + sortOrder, + setSortOrder, count, page, setPage, @@ -28,6 +33,10 @@ export const PageSpeedMonitorTable = ({ monitors: IMonitor[]; refetch: Function; setSelectedMonitor: Function; + sortField: string; + setSortField: (field: string) => void; + sortOrder: "asc" | "desc"; + setSortOrder: (order: "asc" | "desc") => void; count: number; page: number; setPage: (page: number) => void; @@ -59,6 +68,19 @@ export const PageSpeedMonitorTable = ({ 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("desc"); + } + refetch(); + }; + const getActions = (monitor: IMonitor): ActionMenuItem[] => { return [ { @@ -118,17 +140,52 @@ export const PageSpeedMonitorTable = ({ }; const getHeaders = () => { + const renderSortIcon = (isActive: boolean) => ( + + {isActive ? ( + sortOrder === "asc" ? ( + + ) : ( + + ) + ) : null} + + ); + const headers: Header[] = [ { id: "name", - content: t("host"), + content: ( + handleSort(e, "name")} + sx={{ cursor: "pointer" }} + > + {t("host")} + {renderSortIcon(sortField === "name")} + + ), render: (row) => { return row.name; }, }, { id: "status", - content: t("status"), + content: ( + handleSort(e, "status")} + sx={{ cursor: "pointer" }} + > + {t("status")} + {renderSortIcon(sortField === "status")} + + ), render: (row) => { return ; }, diff --git a/client/src/pages/pagespeed/PageSpeedMonitors.tsx b/client/src/pages/pagespeed/PageSpeedMonitors.tsx index 0e3aefd8c..4224ab4ba 100644 --- a/client/src/pages/pagespeed/PageSpeedMonitors.tsx +++ b/client/src/pages/pagespeed/PageSpeedMonitors.tsx @@ -27,13 +27,26 @@ const PageSpeedMonitorsPage = () => { const isSmall = useMediaQuery(theme.breakpoints.down("md")); 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 { deleteFn } = useDelete(); + const requestParams = [ + "embedChecks=true", + "type=pagespeed", + `sortField=${sortField}`, + `sortOrder=${sortOrder}`, + `page=${page}`, + `rowsPerPage=${rowsPerPage}`, + ]; + + const monitorRequestUrl = `/monitors?${requestParams.join("&")}`; + const { response, loading, isValidating, error, refetch } = useGet< ApiResponse >( - `/monitors?embedChecks=true&type=pagespeed&page=${page}&rowsPerPage=${rowsPerPage}`, + monitorRequestUrl, {}, { refreshInterval: GLOBAL_REFRESH, @@ -82,6 +95,10 @@ const PageSpeedMonitorsPage = () => { monitors={monitors} refetch={refetch} setSelectedMonitor={setSelectedMonitor} + sortField={sortField} + setSortField={setSortField} + sortOrder={sortOrder} + setSortOrder={setSortOrder} count={count} page={page} setPage={setPage} diff --git a/client/src/pages/uptime/MonitorTable.tsx b/client/src/pages/uptime/MonitorTable.tsx index b3d93140c..eeb667072 100644 --- a/client/src/pages/uptime/MonitorTable.tsx +++ b/client/src/pages/uptime/MonitorTable.tsx @@ -5,6 +5,7 @@ import { Table, Pagination, StatusLabel } from "@/components/design-elements"; import { HistogramResponseTime } from "@/components/monitors"; import type { Header } from "@/components/design-elements/Table"; import { ActionsMenu } from "@/components/actions-menu"; +import { ArrowDown, ArrowUp } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useTheme } from "@mui/material/styles"; @@ -18,6 +19,10 @@ export const MonitorTable = ({ monitors, refetch, setSelectedMonitor, + sortField, + setSortField, + sortOrder, + setSortOrder, count, page, setPage, @@ -27,6 +32,10 @@ export const MonitorTable = ({ monitors: IMonitor[]; refetch: Function; setSelectedMonitor: Function; + sortField: string; + setSortField: (field: string) => void; + sortOrder: "asc" | "desc"; + setSortOrder: (order: "asc" | "desc") => void; count: number; page: number; setPage: (page: number) => void; @@ -57,6 +66,19 @@ export const MonitorTable = ({ 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("desc"); + } + refetch(); + }; + const getActions = (monitor: IMonitor): ActionMenuItem[] => { return [ { @@ -116,17 +138,50 @@ export const MonitorTable = ({ }; const getHeaders = () => { + const renderSortIcon = (isActive: boolean) => ( + + ); const headers: Header[] = [ { id: "name", - content: t("host"), + content: ( + handleSort(e, "name")} + sx={{ cursor: "pointer" }} + > + {t("name")} + {renderSortIcon(sortField === "name")} + + ), + render: (row) => { - return row.name; + return row?.name; }, }, { id: "status", - content: t("status"), + content: ( + handleSort(e, "status")} + sx={{ cursor: "pointer" }} + > + {t("status")} + {renderSortIcon(sortField === "status")} + + ), render: (row) => { return ; }, @@ -144,7 +199,19 @@ export const MonitorTable = ({ }, { id: "type", - content: t("type"), + content: ( + handleSort(e, "type")} + sx={{ cursor: "pointer" }} + > + {t("type")} + {renderSortIcon(sortField === "type")} + + ), render: (row) => { return row.type; }, diff --git a/client/src/pages/uptime/UptimeMonitors.tsx b/client/src/pages/uptime/UptimeMonitors.tsx index 685c3f6fc..9250639d9 100644 --- a/client/src/pages/uptime/UptimeMonitors.tsx +++ b/client/src/pages/uptime/UptimeMonitors.tsx @@ -34,6 +34,8 @@ const UptimeMonitors = () => { const isSmall = useMediaQuery(theme.breakpoints.down("md")); 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 [selectedTypes, setSelectedTypes] = useState([ @@ -66,6 +68,8 @@ const UptimeMonitors = () => { "embedChecks=true", typeQuery, statusQuery, + `sortField=${sortField}`, + `sortOrder=${sortOrder}`, `page=${page}`, `rowsPerPage=${rowsPerPage}`, ].filter(Boolean); @@ -140,6 +144,10 @@ const UptimeMonitors = () => { monitors={monitors} refetch={refetch} setSelectedMonitor={setSelectedMonitor} + sortField={sortField} + setSortField={setSortField} + sortOrder={sortOrder} + setSortOrder={setSortOrder} count={count} page={page} setPage={setPage} diff --git a/server/src/controllers/MonitorController.ts b/server/src/controllers/MonitorController.ts index 3728fc509..e19d434fa 100644 --- a/server/src/controllers/MonitorController.ts +++ b/server/src/controllers/MonitorController.ts @@ -75,6 +75,8 @@ class MonitorController { let result; if (req.validatedQuery.embedChecks === true) { + const sortField = req.validatedQuery.sortField || "name"; + const sortOrder = req.validatedQuery.sortOrder || "desc"; const page = req.validatedQuery.page || 0; const rowsPerPage = req.validatedQuery.rowsPerPage || 10; const type = req.validatedQuery.type; @@ -82,6 +84,8 @@ class MonitorController { result = await this.monitorService.getAllEmbedChecks( teamId, + sortField, + sortOrder, page, rowsPerPage, type, diff --git a/server/src/services/business/MonitorService.ts b/server/src/services/business/MonitorService.ts index 0884a200c..f8688a658 100644 --- a/server/src/services/business/MonitorService.ts +++ b/server/src/services/business/MonitorService.ts @@ -18,6 +18,8 @@ export interface IMonitorService { getAll: (teamId: string) => Promise; getAllEmbedChecks: ( teamId: string, + sortField: string, + sortOrder: string, page: number, limit: number, type: MonitorType[], @@ -87,6 +89,8 @@ class MonitorService implements IMonitorService { getAllEmbedChecks = async ( teamId: string, + sortField: string, + sortOrder: string, page: number, rowsPerPage: number, type: MonitorType[] = [], @@ -134,7 +138,7 @@ class MonitorService implements IMonitorService { const monitors = await Monitor.find(matchConditions) .lean() - .sort({ name: 1 }) + .sort({ [sortField]: sortOrder === "asc" ? 1 : -1 }) .skip(skip) .limit(rowsPerPage); @@ -157,7 +161,11 @@ class MonitorService implements IMonitorService { latestChecks: { $push: "$$ROOT" }, }, }, - { $project: { latestChecks: { $slice: [{ $ifNull: ["$latestChecks", []] }, 25] } } }, + { + $project: { + latestChecks: { $slice: [{ $ifNull: ["$latestChecks", []] }, 25] }, + }, + }, ]); const checksMap = new Map( @@ -336,7 +344,14 @@ class MonitorService implements IMonitorService { temperature: { $map: { input: { - $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$tempsArrays", 0] }, []] } }], + $range: [ + 0, + { + $size: { + $ifNull: [{ $arrayElemAt: ["$tempsArrays", 0] }, []], + }, + }, + ], }, as: "idx", in: { @@ -362,7 +377,14 @@ class MonitorService implements IMonitorService { disk: { $map: { input: { - $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$disksArray", 0] }, []] } }], + $range: [ + 0, + { + $size: { + $ifNull: [{ $arrayElemAt: ["$disksArray", 0] }, []], + }, + }, + ], }, as: "idx", in: { @@ -406,7 +428,12 @@ class MonitorService implements IMonitorService { net: { $map: { input: { - $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$netsArray", 0] }, []] } }], + $range: [ + 0, + { + $size: { $ifNull: [{ $arrayElemAt: ["$netsArray", 0] }, []] }, + }, + ], }, as: "idx", in: { diff --git a/server/src/validation/index.ts b/server/src/validation/index.ts index 6e95ac334..b0d229205 100644 --- a/server/src/validation/index.ts +++ b/server/src/validation/index.ts @@ -135,6 +135,8 @@ export const monitorAllEmbedChecksQuerySchema = z.object({ if (!val) return []; return Array.isArray(val) ? val : [val]; }), + sortField: z.string().optional(), + sortOrder: z.string().optional(), page: z .string() .optional()