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>
108 lines
2.4 KiB
TypeScript
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,
|
|
};
|
|
}
|