Files
BanGUI/frontend/src/hooks/useJailList.ts
Lukas 5166789b68 feat: Implement typed error contracts in generic hooks
Introduce discriminated FetchError union type to replace weak string error
handling in API calls and hooks. Enables actionable error diagnostics.

Changes:
- Create types/api.ts with FetchError discriminated union (api_error,
  network_error, abort_error)
- Export type guards: isAuthError, isAbortError, isNetworkError, isApiError
- Update useListData and usePolledData to expose typed FetchError instead of
  string
- Add getErrorMessage() helper to extract displayable messages from FetchError
- Add createStringErrorAdapter() for backward compatibility with string error
  state
- Update handleFetchError() to work with both FetchError and string setters
- Update all consumer hooks to expose typed errors
- Update components to use getErrorMessage() when displaying errors
- Update tests to mock FetchError instead of strings
- Add comprehensive typed error model documentation to Web-Development.md

This enables better error handling patterns:
- Check error.type to distinguish between API, network, and abort errors
- Extract status codes for specific handling (401/403 auth, 50x server errors)
- Maintain backward compatibility with existing string-based error states

All TypeScript compilation passes with no errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-28 09:13:47 +02:00

108 lines
2.4 KiB
TypeScript

/**
* React hook for loading and controlling the jail overview list.
*/
import { useCallback, useState } from "react";
import {
fetchJails,
reloadAllJails,
reloadJail,
setJailIdle,
startJail,
stopJail,
} from "../api/jails";
import { useListData } from "./useListData";
import type { JailSummary, JailListResponse } from "../types/jail";
import type { FetchError } from "../types/api";
export interface UseJailsResult {
jails: JailSummary[];
total: number;
loading: boolean;
error: FetchError | null;
refresh: () => void;
startJail: (name: string) => Promise<void>;
stopJail: (name: string) => Promise<void>;
setIdle: (name: string, on: boolean) => Promise<void>;
reloadJail: (name: string) => Promise<void>;
reloadAll: () => Promise<void>;
}
/**
* Fetch and manage the jail overview list.
*/
export function useJails(): UseJailsResult {
const [total, setTotal] = useState(0);
const fetcher = useCallback(
(signal: AbortSignal) => fetchJails(signal),
[],
);
const selector = useCallback((response: JailListResponse) => response.jails, []);
const onSuccess = useCallback((response: JailListResponse) => {
setTotal(response.total);
}, []);
const { items: jails, loading, error, refresh } = useListData<JailListResponse, JailSummary>({
fetcher,
selector,
errorMessage: "Failed to load jails",
onSuccess,
});
const startJailMemo = useCallback(
async (name: string): Promise<void> => {
await startJail(name);
refresh();
},
[refresh],
);
const stopJailMemo = useCallback(
async (name: string): Promise<void> => {
await stopJail(name);
refresh();
},
[refresh],
);
const reloadJailMemo = useCallback(
async (name: string): Promise<void> => {
await reloadJail(name);
refresh();
},
[refresh],
);
const setIdleMemo = useCallback(
(name: string, on: boolean): Promise<void> =>
setJailIdle(name, on).then(() => {
refresh();
}),
[refresh],
);
const reloadAllMemo = useCallback(
(): Promise<void> =>
reloadAllJails().then(() => {
refresh();
}),
[refresh],
);
return {
jails,
total,
loading,
error,
refresh,
startJail: startJailMemo,
stopJail: stopJailMemo,
setIdle: setIdleMemo,
reloadJail: reloadJailMemo,
reloadAll: reloadAllMemo,
};
}