From 7fb0e9dc702afaea81f72e7643ccf48cafdf7791 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 27 Aug 2026 13:18:02 -0700 Subject: [PATCH] new monitor list controller hook --- client/src/Hooks/useMonitorListController.ts | 207 +++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 client/src/Hooks/useMonitorListController.ts diff --git a/client/src/Hooks/useMonitorListController.ts b/client/src/Hooks/useMonitorListController.ts new file mode 100644 index 000000000..63cc4ee91 --- /dev/null +++ b/client/src/Hooks/useMonitorListController.ts @@ -0,0 +1,207 @@ +import { setRowsPerPage, type TableName } from "@/Features/UI/uiSlice"; +import { useDelete, useGet } from "@/Hooks/UseApi"; +import { useBulkMonitorActions } from "@/Hooks/useBulkMonitorActions"; +import useDebounce from "@/Hooks/useDebounce"; +import type { RootState } from "@/store"; +import { + type MonitorsWithChecksResponse, + SelectableMonitorTypes, + type Monitor, + type MonitorType, +} from "@/Types/Monitor"; +import type { Tag } from "@/Types/Tag"; +import { useCallback, useMemo, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; + +interface MonitorListConfig { + types: MonitorType[] | "selectable"; + checksLimit: number; + refreshInterval: number; + rowsPerPageTable: TableName; + rowsPerPageDefault: number; + initialSortField?: string; // default "" +} + +export const useMonitorListController = (config: MonitorListConfig) => { + const dispatch = useDispatch(); + const rowsPerPage = useSelector( + (state: RootState) => + state.ui?.[config.rowsPerPageTable]?.rowsPerPage ?? config.rowsPerPageDefault + ); + + // Filter states + const [selectedTypes, setSelectedTypes] = useState([]); + const [selectedStatus, setSelectedStatus] = useState(""); + const [selectedState, setSelectedState] = useState(""); + 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); + + // Convert filter selections to API filter values + // Status: pass "up"/"down" directly to the API + // State: "active" -> true, "paused" -> false + const toFilterStatus = useMemo(() => { + if (selectedStatus === "up") return "up"; + if (selectedStatus === "down") return "down"; + return undefined; + }, [selectedStatus]); + + const toFilterActive = useMemo(() => { + if (selectedState === "active") return "true"; + if (selectedState === "paused") return "false"; + return undefined; + }, [selectedState]); + + // Determine field and filter for the API request + // Priority: status > isActive > search > sort + const filterLookup = new Map([ + [toFilterStatus, "status"], + [toFilterActive, "isActive"], + ]); + const activeFilter = [...filterLookup].find(([key]) => key !== undefined); + const field = activeFilter?.[1] || (debouncedSearch ? "name" : sortField || undefined); + const filter = activeFilter?.[0] || debouncedSearch || undefined; + + const effectiveTypes = useMemo(() => { + return config.types === "selectable" + ? selectedTypes.length > 0 + ? selectedTypes + : [...SelectableMonitorTypes] + : config.types; + }, [config.types, selectedTypes]); + + // Fetching + const monitorsWithChecksUrl = useMemo(() => { + const params = new URLSearchParams(); + effectiveTypes.forEach((type) => params.append("type", type)); + selectedTags.forEach((tag) => params.append("tags", tag)); + params.append("limit", String(config.checksLimit)); + if (page !== undefined) params.append("page", String(page)); + if (rowsPerPage) params.append("rowsPerPage", String(rowsPerPage)); + if (filter) params.append("filter", filter); + if (field) params.append("field", field); + if (sortOrder) params.append("order", sortOrder); + return `/monitors/team/with-checks?${params.toString()}`; + }, [ + effectiveTypes, + selectedTags, + page, + rowsPerPage, + filter, + field, + sortOrder, + config.checksLimit, + ]); + + const { + data: monitorsWithChecksData, + isLoading, + error, + refetch, + } = useGet( + monitorsWithChecksUrl, + {}, + { refreshInterval: config.refreshInterval, keepPreviousData: true } + ); + const { data: tags } = useGet("/tags/team"); + + const bulk = useBulkMonitorActions(refetch, page); + + // Delete ops + const { deleteFn, loading: isDeleting } = useDelete(); + const handleConfirmDelete = async () => { + if (!selectedMonitor) return; + await deleteFn(`/monitors/${selectedMonitor.id}`); + setSelectedMonitor(null); + refetch(); + }; + const handleCancelDelete = () => setSelectedMonitor(null); + + // Handle filters + const handleClearFilters = useCallback(() => { + setSelectedTypes([]); + setSelectedStatus(""); + setSelectedState(""); + setSelectedTags([]); + setSearch(""); + }, []); + + const handleSetRowsPerPage = (value: number) => { + dispatch(setRowsPerPage({ value, table: config.rowsPerPageTable })); + setPage(0); + }; + + // Check for active filters + const hasActiveFilters = Boolean( + selectedTypes.length > 0 || + selectedStatus || + selectedState || + selectedTags.length > 0 || + search + ); + + // Show empty state only when there are truly no monitors (not just filtered out) + // If filters are active and count is 0, pass 1 to prevent empty state fallback + + const { monitors, summary, count } = monitorsWithChecksData ?? { + monitors: null, + summary: null, + count: 0, + }; + + const effectiveTotalCount = + hasActiveFilters && (summary?.totalMonitors ?? 0) === 0 + ? 1 + : (summary?.totalMonitors ?? 0); + + return { + // Data + monitors, + summary, + count, + tags, + isLoading, + error, + refetch, + effectiveTotalCount, + hasActiveFilters, + + // Filter state and setters + selectedTypes, + setSelectedTypes, + selectedStatus, + setSelectedStatus, + selectedState, + setSelectedState, + selectedTags, + setSelectedTags, + search, + setSearch, + handleClearFilters, + + // Paging and Sorting + page, + setPage, + rowsPerPage, + handleSetRowsPerPage, + sortField, + setSortField, + sortOrder, + setSortOrder, + + // Delete + selectedMonitor, + setSelectedMonitor, + isDialogOpen: Boolean(selectedMonitor), + isDeleting, + handleConfirmDelete, + handleCancelDelete, + + // Rendered only when config.bulkActions + ...bulk, + }; +};