/** * React hook for paginated jailed IPs within a specific jail. */ import { useCallback, useEffect, useRef, useState } from "react"; import { fetchJailBannedIps, unbanIp } from "../api/jails"; import { handleFetchError, createStringErrorAdapter } from "../utils/fetchError"; import type { ActiveBan } from "../types/jail"; export interface UseJailBannedIpsResult { items: ActiveBan[]; total: number; page: number; pageSize: number; search: string; loading: boolean; error: string | null; opError: string | null; refresh: () => Promise; setPage: (page: number) => void; setPageSize: (size: number) => void; setSearch: (term: string) => void; unban: (ip: string) => Promise; } export function useJailBannedIps(jailName: string): UseJailBannedIpsResult { const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(25); const [search, setSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [opError, setOpError] = useState(null); const debounceRef = useRef | null>(null); const abortRef = useRef(null); const load = useCallback(async (): Promise => { abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; const { signal } = controller; if (!jailName) { setItems([]); setTotal(0); setLoading(false); return; } setLoading(true); setError(null); try { const resp = await fetchJailBannedIps( jailName, page, pageSize, debouncedSearch || undefined, signal, ); if (signal.aborted) { return; } setItems(resp.items); setTotal(resp.total); } catch (err: unknown) { if (signal.aborted) { return; } handleFetchError(err, createStringErrorAdapter(setError), "Failed to fetch jailed IPs"); } finally { if (!signal.aborted) { setLoading(false); } } }, [jailName, page, pageSize, debouncedSearch]); useEffect(() => { if (debounceRef.current !== null) { clearTimeout(debounceRef.current); } debounceRef.current = setTimeout(() => { setDebouncedSearch(search); setPage(1); }, 300); return (): void => { if (debounceRef.current !== null) { clearTimeout(debounceRef.current); } }; }, [search]); useEffect(() => { void load(); return (): void => { abortRef.current?.abort(); }; }, [load]); const unban = useCallback(async (ip: string): Promise => { setOpError(null); try { await unbanIp(ip, jailName); await load(); } catch (err: unknown) { setOpError(err instanceof Error ? err.message : String(err)); } }, [jailName, load]); return { items, total, page, pageSize, search, loading, error, opError, refresh: load, setPage, setPageSize, setSearch, unban, }; }