/** * React hook for loading and controlling the jail overview list. */ import { useCallback, useEffect, useRef, useState } from "react"; import { fetchJails, reloadAllJails, reloadJail, setJailIdle, startJail, stopJail, } from "../api/jails"; import { handleFetchError } from "../utils/fetchError"; import type { JailSummary } from "../types/jail"; export interface UseJailsResult { jails: JailSummary[]; total: number; loading: boolean; error: string | null; refresh: () => void; startJail: (name: string) => Promise; stopJail: (name: string) => Promise; setIdle: (name: string, on: boolean) => Promise; reloadJail: (name: string) => Promise; reloadAll: () => Promise; } /** * Fetch and manage the jail overview list. */ export function useJails(): UseJailsResult { const [jails, setJails] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const abortRef = useRef(null); const load = useCallback(() => { abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; setLoading(true); setError(null); fetchJails() .then((res) => { if (!ctrl.signal.aborted) { setJails(res.jails); setTotal(res.total); } }) .catch((err: unknown) => { if (!ctrl.signal.aborted) { handleFetchError(err, setError, "Failed to load jails"); } }) .finally(() => { if (!ctrl.signal.aborted) { setLoading(false); } }); }, []); useEffect(() => { load(); return (): void => { abortRef.current?.abort(); }; }, [load]); const withRefresh = (fn: (name: string) => Promise) => async (name: string): Promise => { await fn(name); load(); }; return { jails, total, loading, error, refresh: load, startJail: withRefresh(startJail), stopJail: withRefresh(stopJail), setIdle: (name, on) => setJailIdle(name, on).then(() => { load(); }), reloadJail: withRefresh(reloadJail), reloadAll: () => reloadAllJails().then(() => { load(); }), }; }