This commit standardizes how API responses are wrapped, solving issue #24. Problem: - Inconsistent response envelopes (jails vs items vs bans vs no wrapper) - Frontend required multiple field name variants - Integration bugs from branching logic - No clear pattern for different response types Solution: - Created response.py with base classes: PaginatedListResponse, CollectionResponse, CommandResponse - Standardized all list/collection responses to use 'items' field - Domain-specific field names for detail and aggregation responses - Updated all backends routers and mappers - Updated frontend types and hooks to match Changes: Backend: - backend/app/models/response.py (new): Base response models - backend/app/models/ban.py: Updated responses to inherit from bases - backend/app/models/jail.py: Updated JailListResponse, JailCommandResponse - backend/app/models/config.py: Updated collection responses - backend/app/services/jail_service.py: Updated return statements - backend/app/mappers/ban_mappers.py: Updated 'bans' to 'items' - backend/tests/test_mappers/test_ban_mappers.py: Updated tests Frontend: - frontend/src/types/jail.ts: Updated response interfaces - frontend/src/types/config.ts: Updated response interfaces - frontend/src/hooks/useActiveBans.ts: Updated selector - frontend/src/hooks/useJailList.ts: Updated selector - frontend/src/hooks/useJailConfigs.ts: Updated selector - frontend/src/hooks/useConfigActiveStatus.ts: Updated field access - frontend/src/hooks/useJailAdmin.ts: Updated field access Documentation: - Docs/Backend-Development.md: Added § 4.1 API Response Envelope Policy The policy defines: 1. Paginated lists use PaginatedListResponse (items, total, page, page_size) 2. Non-paginated collections use CollectionResponse (items, total) 3. Detail responses use entity-specific field names (jail, status, settings) 4. Command responses use CommandResponse (message, success, optional target) 5. Aggregations use domain-specific fields (jails, countries, buckets, bans) All responses now follow one of these patterns, reducing frontend complexity. 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.items, []);
|
|
|
|
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,
|
|
};
|
|
}
|