From 2a45b4d46f0a08dfeb8a354dede94448e7147981 Mon Sep 17 00:00:00 2001 From: fabiangigler Date: Wed, 23 Sep 2026 12:21:26 +0200 Subject: [PATCH] Persist monitor list sorting --- client/src/Features/UI/uiSlice.ts | 18 +++++++++++ client/src/Hooks/useMonitorListController.ts | 31 +++++++++++++++---- .../monitors/monitor.repository.mongo.ts | 4 ++- .../integration/monitorRepository.test.ts | 28 +++++++++++++++++ 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/client/src/Features/UI/uiSlice.ts b/client/src/Features/UI/uiSlice.ts index 55e83333c..644d411a6 100644 --- a/client/src/Features/UI/uiSlice.ts +++ b/client/src/Features/UI/uiSlice.ts @@ -15,6 +15,8 @@ type TableName = interface TableState { rowsPerPage: number; + sortField?: string; + sortOrder?: "asc" | "desc"; } interface SidebarState { @@ -94,6 +96,20 @@ const uiSlice = createSlice({ const { table, value } = action.payload; state[table].rowsPerPage = value; }, + setTableSortField: ( + state, + action: PayloadAction<{ table: TableName; value: string }> + ) => { + const { table, value } = action.payload; + state[table].sortField = value; + }, + setTableSortOrder: ( + state, + action: PayloadAction<{ table: TableName; value: "asc" | "desc" }> + ) => { + const { table, value } = action.payload; + state[table].sortOrder = value; + }, toggleSidebar: (state) => { state.sidebar.collapsed = !state.sidebar.collapsed; }, @@ -131,6 +147,8 @@ export type { UIState, ThemeMode, ChartType, TableName }; export default uiSlice.reducer; export const { setRowsPerPage, + setTableSortField, + setTableSortOrder, toggleSidebar, setCollapsed, setMode, diff --git a/client/src/Hooks/useMonitorListController.ts b/client/src/Hooks/useMonitorListController.ts index cd6852594..245210b3c 100644 --- a/client/src/Hooks/useMonitorListController.ts +++ b/client/src/Hooks/useMonitorListController.ts @@ -1,4 +1,9 @@ -import { setRowsPerPage, type TableName } from "@/Features/UI/uiSlice"; +import { + setRowsPerPage, + setTableSortField, + setTableSortOrder, + type TableName, +} from "@/Features/UI/uiSlice"; import { useDelete, useGet } from "@/Hooks/UseApi"; import { useBulkMonitorActions } from "@/Hooks/useBulkMonitorActions"; import useDebounce from "@/Hooks/useDebounce"; @@ -24,10 +29,12 @@ interface MonitorListConfig { export const useMonitorListController = (config: MonitorListConfig) => { const dispatch = useDispatch(); - const rowsPerPage = useSelector( - (state: RootState) => - state.ui?.[config.rowsPerPageTable]?.rowsPerPage ?? config.rowsPerPageDefault + const tableState = useSelector( + (state: RootState) => state.ui?.[config.rowsPerPageTable] ); + const rowsPerPage = tableState?.rowsPerPage ?? config.rowsPerPageDefault; + const sortField = tableState?.sortField ?? config.initialSortField ?? ""; + const sortOrder = tableState?.sortOrder ?? "asc"; // Filter states const [selectedTypes, setSelectedTypes] = useState([]); @@ -36,8 +43,6 @@ export const useMonitorListController = (config: MonitorListConfig) => { const [selectedTags, setSelectedTags] = useState([]); const [page, setPage] = useState(0); const [search, setSearch] = useState(""); - const [sortField, setSortField] = useState(config.initialSortField ?? ""); - const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const [selectedMonitor, setSelectedMonitor] = useState(null); const debouncedSearch = useDebounce(search, 300); @@ -135,6 +140,20 @@ export const useMonitorListController = (config: MonitorListConfig) => { setPage(0); }; + const setSortField = useCallback( + (value: string) => { + dispatch(setTableSortField({ value, table: config.rowsPerPageTable })); + }, + [config.rowsPerPageTable, dispatch] + ); + + const setSortOrder = useCallback( + (value: "asc" | "desc") => { + dispatch(setTableSortOrder({ value, table: config.rowsPerPageTable })); + }, + [config.rowsPerPageTable, dispatch] + ); + // Check for active filters const hasActiveFilters = Boolean( selectedTypes.length > 0 || diff --git a/server/src/domain/monitors/monitor.repository.mongo.ts b/server/src/domain/monitors/monitor.repository.mongo.ts index 385dcebab..b5a7f651d 100644 --- a/server/src/domain/monitors/monitor.repository.mongo.ts +++ b/server/src/domain/monitors/monitor.repository.mongo.ts @@ -101,7 +101,9 @@ class MongoMonitorsRepository implements IMonitorsRepository { private pageOptions = (config: TeamQueryConfig) => { const { page = 0, rowsPerPage = 0, field = "createdAt", order = "desc" } = config ?? {}; - return { sort: { [field]: order === "asc" ? 1 : -1 } as const, skip: Math.max(page, 0) * rowsPerPage, limit: rowsPerPage }; + const direction: 1 | -1 = order === "asc" ? 1 : -1; + const sort: Record = field === "createdAt" ? { createdAt: direction } : { [field]: direction, createdAt: 1 }; + return { sort, skip: Math.max(page, 0) * rowsPerPage, limit: rowsPerPage }; }; findByTeamId = async (teamId: string, config: TeamQueryConfig, options?: { includeRecentChecks?: boolean }): Promise => { diff --git a/server/test/integration/monitorRepository.test.ts b/server/test/integration/monitorRepository.test.ts index 63ef4a105..446085a0e 100644 --- a/server/test/integration/monitorRepository.test.ts +++ b/server/test/integration/monitorRepository.test.ts @@ -69,6 +69,34 @@ describe("MongoMonitorsRepository", () => { }); }); + describe("sorting", () => { + it("uses creation time as the fallback for equal primary values", async () => { + const repo = new MongoMonitorsRepository(); + const teamId = new mongoose.Types.ObjectId(); + const createMonitor = (name: string, type: string, status: string, createdAt: Date) => + MonitorModel.create({ + userId: new mongoose.Types.ObjectId(), + teamId, + name, + type, + status, + url: "https://example.com", + createdAt, + }); + + await createMonitor("newer-up", "hardware", "up", new Date("2026-03-01T00:00:00.000Z")); + await createMonitor("down", "http", "down", new Date("2026-04-01T00:00:00.000Z")); + await createMonitor("older-up", "pagespeed", "up", new Date("2026-01-01T00:00:00.000Z")); + await createMonitor("middle-up", "docker", "up", new Date("2026-02-01T00:00:00.000Z")); + + const ascending = await repo.findByTeamIdWithStats(teamId.toString(), { field: "status", order: "asc" }); + const descending = await repo.findByTeamIdWithStats(teamId.toString(), { field: "status", order: "desc" }); + + expect(ascending.map((monitor) => monitor.name)).toEqual(["down", "older-up", "middle-up", "newer-up"]); + expect(descending.map((monitor) => monitor.name)).toEqual(["older-up", "middle-up", "newer-up", "down"]); + }); + }); + describe("findByIds recentChecks modes", () => { const seedFixtures = async () => { const teamId = new mongoose.Types.ObjectId();