Merge pull request #3985 from fabiangigler/feat/persist-monitor-sorting
Feat/persist monitor sorting
This commit is contained in:
14 files changed
+105
-70
No files matched your search
@@ -3,6 +3,7 @@ import {
|
||||
type DashboardCardKey,
|
||||
type DashboardSortOrder,
|
||||
} from "@/Types/Dashboard";
|
||||
import type { SortOrder } from "@/Types/Query";
|
||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
type ThemeMode = "light" | "dark";
|
||||
@@ -19,6 +20,8 @@ type TableName =
|
||||
|
||||
interface TableState {
|
||||
rowsPerPage: number;
|
||||
sortField?: string;
|
||||
sortOrder?: SortOrder;
|
||||
}
|
||||
|
||||
interface SidebarState {
|
||||
@@ -100,6 +103,14 @@ const uiSlice = createSlice({
|
||||
const { table, value } = action.payload;
|
||||
state[table].rowsPerPage = value;
|
||||
},
|
||||
setTableSort: (
|
||||
state,
|
||||
action: PayloadAction<{ table: TableName; field: string; order: SortOrder }>
|
||||
) => {
|
||||
const { table, field, order } = action.payload;
|
||||
state[table].sortField = field;
|
||||
state[table].sortOrder = order;
|
||||
},
|
||||
toggleSidebar: (state) => {
|
||||
state.sidebar.collapsed = !state.sidebar.collapsed;
|
||||
},
|
||||
@@ -140,6 +151,7 @@ export type { UIState, ThemeMode, ChartType, TableName };
|
||||
export default uiSlice.reducer;
|
||||
export const {
|
||||
setRowsPerPage,
|
||||
setTableSort,
|
||||
toggleSidebar,
|
||||
setCollapsed,
|
||||
setMode,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { setRowsPerPage, type TableName } from "@/Features/UI/uiSlice";
|
||||
import { setRowsPerPage, setTableSort, type TableName } from "@/Features/UI/uiSlice";
|
||||
import { useDelete, useGet } from "@/Hooks/UseApi";
|
||||
import { useBulkMonitorActions } from "@/Hooks/useBulkMonitorActions";
|
||||
import useDebounce from "@/Hooks/useDebounce";
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type MonitorType,
|
||||
} from "@/Types/Monitor";
|
||||
import type { Tag } from "@/Types/Tag";
|
||||
import type { SortOrder } from "@/Types/Query";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
@@ -24,10 +25,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<MonitorType[]>([]);
|
||||
@@ -36,8 +39,6 @@ export const useMonitorListController = (config: MonitorListConfig) => {
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [page, setPage] = useState<number>(0);
|
||||
const [search, setSearch] = useState<string>("");
|
||||
const [sortField, setSortField] = useState<string>(config.initialSortField ?? "");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
|
||||
const [selectedMonitor, setSelectedMonitor] = useState<Monitor | null>(null);
|
||||
const debouncedSearch = useDebounce<string>(search, 300);
|
||||
|
||||
@@ -135,6 +136,11 @@ export const useMonitorListController = (config: MonitorListConfig) => {
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const setSort = (field: string, order: SortOrder) => {
|
||||
dispatch(setTableSort({ field, order, table: config.rowsPerPageTable }));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
// Check for active filters
|
||||
const hasActiveFilters = Boolean(
|
||||
selectedTypes.length > 0 ||
|
||||
@@ -189,9 +195,8 @@ export const useMonitorListController = (config: MonitorListConfig) => {
|
||||
rowsPerPage,
|
||||
handleSetRowsPerPage,
|
||||
sortField,
|
||||
setSortField,
|
||||
sortOrder,
|
||||
setSortOrder,
|
||||
setSort,
|
||||
|
||||
// Delete
|
||||
selectedMonitor,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ColoredLabel,
|
||||
} from "@/Components/design-elements";
|
||||
import { SPACING } from "@/Utils/Theme/constants";
|
||||
import { getNextMonitorSort } from "@/Utils/MonitorUtils";
|
||||
import type { Header } from "@/Components/design-elements/Table";
|
||||
import { ActionsMenu, type ActionMenuItem } from "@/Components/actions-menu";
|
||||
import { ArrowUp, ArrowDown } from "lucide-react";
|
||||
@@ -20,6 +21,7 @@ import { usePost } from "@/Hooks/UseApi";
|
||||
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import type { Tag } from "@/Types/Tag";
|
||||
import type { SortOrder } from "@/Types/Query";
|
||||
import type { DockerContainerSummary } from "@/Types/Check";
|
||||
import { Checkbox } from "@/Components/inputs";
|
||||
|
||||
@@ -34,9 +36,8 @@ interface DockerMonitorsTableProps {
|
||||
refetch: () => void;
|
||||
setSelectedMonitor: (monitor: Monitor | null) => void;
|
||||
sortField: string;
|
||||
setSortField: (field: string) => void;
|
||||
sortOrder: "asc" | "desc";
|
||||
setSortOrder: (order: "asc" | "desc") => void;
|
||||
sortOrder: SortOrder;
|
||||
setSort: (field: string, order: SortOrder) => void;
|
||||
count: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
@@ -52,9 +53,8 @@ export const DockerMonitorsTable = ({
|
||||
refetch,
|
||||
setSelectedMonitor,
|
||||
sortField,
|
||||
setSortField,
|
||||
sortOrder,
|
||||
setSortOrder,
|
||||
setSort,
|
||||
count,
|
||||
page,
|
||||
setPage,
|
||||
@@ -105,14 +105,8 @@ export const DockerMonitorsTable = ({
|
||||
const handleSort = (e: React.MouseEvent, field: string) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (sortField === field) {
|
||||
const newOrder = sortOrder === "asc" ? "desc" : "asc";
|
||||
setSortOrder(newOrder);
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortOrder("asc");
|
||||
}
|
||||
refetch();
|
||||
const nextSort = getNextMonitorSort(field, sortField, sortOrder);
|
||||
setSort(nextSort.field, nextSort.order);
|
||||
};
|
||||
|
||||
const getActions = (monitor: Monitor): ActionMenuItem[] => {
|
||||
|
||||
@@ -31,9 +31,8 @@ const DockerMonitorsPage = () => {
|
||||
setPage={c.setPage}
|
||||
rowsPerPage={c.rowsPerPage}
|
||||
sortField={c.sortField}
|
||||
setSortField={c.setSortField}
|
||||
sortOrder={c.sortOrder}
|
||||
setSortOrder={c.setSortOrder}
|
||||
setSort={c.setSort}
|
||||
setRowsPerPage={c.handleSetRowsPerPage}
|
||||
selectedRows={c.selectedRows}
|
||||
onSelectionChange={c.setSelectedRows}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ColoredLabel,
|
||||
} from "@/Components/design-elements";
|
||||
import { SPACING } from "@/Utils/Theme/constants";
|
||||
import { getNextMonitorSort } from "@/Utils/MonitorUtils";
|
||||
import type { Header } from "@/Components/design-elements/Table";
|
||||
import { ActionsMenu, type ActionMenuItem } from "@/Components/actions-menu";
|
||||
import { ArrowUp, ArrowDown } from "lucide-react";
|
||||
@@ -21,6 +22,7 @@ import { usePost } from "@/Hooks/UseApi";
|
||||
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import type { Tag } from "@/Types/Tag";
|
||||
import type { SortOrder } from "@/Types/Query";
|
||||
import { Checkbox } from "@/Components/inputs";
|
||||
|
||||
interface InfraMonitorsTableProps {
|
||||
@@ -29,9 +31,8 @@ interface InfraMonitorsTableProps {
|
||||
refetch: () => void;
|
||||
setSelectedMonitor: (monitor: Monitor | null) => void;
|
||||
sortField: string;
|
||||
setSortField: (field: string) => void;
|
||||
sortOrder: "asc" | "desc";
|
||||
setSortOrder: (order: "asc" | "desc") => void;
|
||||
sortOrder: SortOrder;
|
||||
setSort: (field: string, order: SortOrder) => void;
|
||||
count: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
@@ -47,9 +48,8 @@ export const InfraMonitorsTable = ({
|
||||
refetch,
|
||||
setSelectedMonitor,
|
||||
sortField,
|
||||
setSortField,
|
||||
sortOrder,
|
||||
setSortOrder,
|
||||
setSort,
|
||||
count,
|
||||
page,
|
||||
setPage,
|
||||
@@ -100,14 +100,8 @@ export const InfraMonitorsTable = ({
|
||||
const handleSort = (e: React.MouseEvent, field: string) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (sortField === field) {
|
||||
const newOrder = sortOrder === "asc" ? "desc" : "asc";
|
||||
setSortOrder(newOrder);
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortOrder("asc");
|
||||
}
|
||||
refetch();
|
||||
const nextSort = getNextMonitorSort(field, sortField, sortOrder);
|
||||
setSort(nextSort.field, nextSort.order);
|
||||
};
|
||||
|
||||
const getActions = (monitor: Monitor): ActionMenuItem[] => {
|
||||
|
||||
@@ -31,9 +31,8 @@ const InfrastructureMonitors = () => {
|
||||
setPage={c.setPage}
|
||||
rowsPerPage={c.rowsPerPage}
|
||||
sortField={c.sortField}
|
||||
setSortField={c.setSortField}
|
||||
sortOrder={c.sortOrder}
|
||||
setSortOrder={c.setSortOrder}
|
||||
setSort={c.setSort}
|
||||
setRowsPerPage={c.handleSetRowsPerPage}
|
||||
selectedRows={c.selectedRows}
|
||||
onSelectionChange={c.setSelectedRows}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ColoredLabel,
|
||||
} from "@/Components/design-elements";
|
||||
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
|
||||
import { getNextMonitorSort } from "@/Utils/MonitorUtils";
|
||||
import { HistogramPageSpeed } from "@/Components/monitors";
|
||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||
import type { Header } from "@/Components/design-elements/Table";
|
||||
@@ -22,6 +23,7 @@ import { usePatch } from "@/Hooks/UseApi";
|
||||
|
||||
import type { Monitor, MonitorWithChecks } from "@/Types/Monitor";
|
||||
import type { Tag } from "@/Types/Tag";
|
||||
import type { SortOrder } from "@/Types/Query";
|
||||
import type { ActionMenuItem } from "@/Components/actions-menu";
|
||||
|
||||
export const PageSpeedMonitorsTable = ({
|
||||
@@ -30,9 +32,8 @@ export const PageSpeedMonitorsTable = ({
|
||||
refetch,
|
||||
setSelectedMonitor,
|
||||
sortField,
|
||||
setSortField,
|
||||
sortOrder,
|
||||
setSortOrder,
|
||||
setSort,
|
||||
count,
|
||||
page,
|
||||
setPage,
|
||||
@@ -44,9 +45,8 @@ export const PageSpeedMonitorsTable = ({
|
||||
refetch: Function;
|
||||
setSelectedMonitor: Function;
|
||||
sortField: string;
|
||||
setSortField: (field: string) => void;
|
||||
sortOrder: "asc" | "desc";
|
||||
setSortOrder: (order: "asc" | "desc") => void;
|
||||
sortOrder: SortOrder;
|
||||
setSort: (field: string, order: SortOrder) => void;
|
||||
count: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
@@ -81,14 +81,8 @@ export const PageSpeedMonitorsTable = ({
|
||||
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 nextSort = getNextMonitorSort(field, sortField, sortOrder);
|
||||
setSort(nextSort.field, nextSort.order);
|
||||
};
|
||||
|
||||
const getActions = (monitor: Monitor): ActionMenuItem[] => {
|
||||
|
||||
@@ -46,9 +46,8 @@ const PageSpeedMonitorsPage = () => {
|
||||
setPage={c.setPage}
|
||||
rowsPerPage={c.rowsPerPage}
|
||||
sortField={c.sortField}
|
||||
setSortField={c.setSortField}
|
||||
sortOrder={c.sortOrder}
|
||||
setSortOrder={c.setSortOrder}
|
||||
setSort={c.setSort}
|
||||
setRowsPerPage={c.handleSetRowsPerPage}
|
||||
/>
|
||||
</MonitorListPage>
|
||||
|
||||
@@ -24,8 +24,9 @@ import type { ActionMenuItem } from "@/Components/actions-menu";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { Checkbox } from "@/Components/inputs";
|
||||
import type { Tag } from "@/Types/Tag";
|
||||
import type { SortOrder } from "@/Types/Query";
|
||||
import { SPACING } from "@/Utils/Theme/constants";
|
||||
import { getUptimePercentageColor } from "@/Utils/MonitorUtils";
|
||||
import { getNextMonitorSort, getUptimePercentageColor } from "@/Utils/MonitorUtils";
|
||||
import { formatPercentage } from "@/Utils/FormatUtils";
|
||||
|
||||
interface MonitorTableProps {
|
||||
@@ -34,9 +35,8 @@ interface MonitorTableProps {
|
||||
refetch: () => void;
|
||||
setSelectedMonitor: (monitor: Monitor | null) => void;
|
||||
sortField: string;
|
||||
setSortField: (field: string) => void;
|
||||
sortOrder: "asc" | "desc";
|
||||
setSortOrder: (order: "asc" | "desc") => void;
|
||||
sortOrder: SortOrder;
|
||||
setSort: (field: string, order: SortOrder) => void;
|
||||
count: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
@@ -52,9 +52,8 @@ export const MonitorTable = ({
|
||||
refetch,
|
||||
setSelectedMonitor,
|
||||
sortField,
|
||||
setSortField,
|
||||
sortOrder,
|
||||
setSortOrder,
|
||||
setSort,
|
||||
count,
|
||||
page,
|
||||
setPage,
|
||||
@@ -91,14 +90,8 @@ export const MonitorTable = ({
|
||||
const handleSort = (e: React.MouseEvent, field: string) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (sortField === field) {
|
||||
const newOrder = sortOrder === "asc" ? "desc" : "asc";
|
||||
setSortOrder(newOrder);
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortOrder("asc");
|
||||
}
|
||||
refetch();
|
||||
const nextSort = getNextMonitorSort(field, sortField, sortOrder);
|
||||
setSort(nextSort.field, nextSort.order);
|
||||
};
|
||||
|
||||
const isBrowserOpenable = (monitor: Monitor): boolean => {
|
||||
|
||||
@@ -30,9 +30,8 @@ const UptimeMonitorsPage = () => {
|
||||
setPage={monitorListController.setPage}
|
||||
rowsPerPage={monitorListController.rowsPerPage}
|
||||
sortField={monitorListController.sortField}
|
||||
setSortField={monitorListController.setSortField}
|
||||
sortOrder={monitorListController.sortOrder}
|
||||
setSortOrder={monitorListController.setSortOrder}
|
||||
setSort={monitorListController.setSort}
|
||||
setRowsPerPage={monitorListController.handleSetRowsPerPage}
|
||||
selectedRows={monitorListController.selectedRows}
|
||||
onSelectionChange={monitorListController.setSelectedRows}
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
export const DateRanges = ["recent", "hour", "day", "week", "month", "all"] as const;
|
||||
export type DateRange = (typeof DateRanges)[number];
|
||||
|
||||
export const SortOrders = ["asc", "desc"] as const;
|
||||
export type SortOrder = (typeof SortOrders)[number];
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { MonitorStatus, MonitorType } from "@/Types/Monitor";
|
||||
import type { SortOrder } from "@/Types/Query";
|
||||
import type { PaletteKey } from "@/Utils/Theme/Theme";
|
||||
import type { ValueType } from "@/Components/design-elements/StatusLabel";
|
||||
import {
|
||||
@@ -10,6 +11,16 @@ import {
|
||||
type DockerLogLine,
|
||||
} from "@/Types/Check";
|
||||
|
||||
export const getNextMonitorSort = (
|
||||
field: string,
|
||||
sortField: string,
|
||||
sortOrder: SortOrder
|
||||
): { field: string; order: SortOrder } => {
|
||||
if (field !== sortField) return { field, order: "asc" };
|
||||
if (sortOrder === "asc") return { field, order: "desc" };
|
||||
return { field: "", order: "asc" };
|
||||
};
|
||||
|
||||
export const getMonitorPath = (type: MonitorType): string => {
|
||||
const pathMap: Record<MonitorType, string> = {
|
||||
http: "uptime",
|
||||
|
||||
@@ -101,7 +101,12 @@ 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<string, 1 | -1> = { [field]: direction };
|
||||
if (field !== "createdAt") {
|
||||
sort.createdAt = 1;
|
||||
}
|
||||
return { sort, skip: Math.max(page, 0) * rowsPerPage, limit: rowsPerPage };
|
||||
};
|
||||
|
||||
findByTeamId = async (teamId: string, config: TeamQueryConfig, options?: { includeRecentChecks?: boolean }): Promise<Monitor[]> => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user