feat(hooks): consolidate data-fetching patterns with useListData and usePolledData
- Refactor useJails (useJailList.ts) to use useListData with onSuccess for total - Refactor useBanTrend to use useListData with onSuccess for bucket_size - Refactor useDashboardCountryData to use useListData with onSuccess for aggregated data - Refactor useHistory to use useListData with proper abort guard in finally() - Create usePolledData for single-item endpoints with polling and window focus refetch - Refactor useServerStatus to use usePolledData for 30s polling + window focus refetch - Keep useIpHistory with manual pattern (single-item, no list semantics) - Document deferred refactoring of useJailDetail (depends on T-13 for data/command split) All data-fetching hooks now follow one of two consistent patterns: 1. useListData: for paginated/list endpoints with refresh semantics 2. usePolledData: for single-item endpoints with polling and focus-refetch This eliminates code duplication, centralizes abort-guard logic, and enables consistent fixes across all data-fetching hooks. Resolves T-12. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -5,13 +5,10 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { fetchHistory, fetchIpHistory } from "../api/history";
|
||||
import { handleFetchError } from "../utils/fetchError";
|
||||
import type { HistoryBanItem, IpDetailResponse } from "../types/history";
|
||||
import { useListData } from "./useListData";
|
||||
import type { HistoryBanItem, IpDetailResponse, HistoryListResponse } from "../types/history";
|
||||
import type { BanOriginFilter, TimeRange } from "../types/ban";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// useHistory — paginated list
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface UseHistoryResult {
|
||||
items: HistoryBanItem[];
|
||||
total: number;
|
||||
@@ -43,50 +40,38 @@ export function useHistory(
|
||||
ip?: string,
|
||||
source: "fail2ban" | "archive" = "archive",
|
||||
): UseHistoryResult {
|
||||
const [items, setItems] = useState<HistoryBanItem[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(page);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const load = useCallback((): void => {
|
||||
abortRef.current?.abort();
|
||||
abortRef.current = new AbortController();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const fetcher = useCallback(
|
||||
(signal: AbortSignal) =>
|
||||
fetchHistory(
|
||||
{
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
range,
|
||||
origin,
|
||||
jail,
|
||||
ip,
|
||||
source,
|
||||
},
|
||||
signal,
|
||||
),
|
||||
[currentPage, pageSize, range, origin, jail, ip, source],
|
||||
);
|
||||
|
||||
fetchHistory(
|
||||
{
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
range,
|
||||
origin,
|
||||
jail,
|
||||
ip,
|
||||
source,
|
||||
},
|
||||
abortRef.current.signal,
|
||||
)
|
||||
.then((resp) => {
|
||||
setItems(resp.items);
|
||||
setTotal(resp.total);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
handleFetchError(err, setError, "Failed to fetch history");
|
||||
})
|
||||
.finally((): void => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [currentPage, pageSize, range, origin, jail, ip, source]);
|
||||
const selector = useCallback((response: HistoryListResponse) => response.items, []);
|
||||
|
||||
const onSuccess = useCallback((response: HistoryListResponse) => {
|
||||
setTotal(response.total);
|
||||
}, []);
|
||||
|
||||
useEffect((): (() => void) => {
|
||||
load();
|
||||
return (): void => {
|
||||
abortRef.current?.abort();
|
||||
};
|
||||
}, [load]);
|
||||
const { items, loading, error, refresh } = useListData<HistoryListResponse, HistoryBanItem>({
|
||||
fetcher,
|
||||
selector,
|
||||
errorMessage: "Failed to fetch history",
|
||||
onSuccess,
|
||||
});
|
||||
|
||||
return {
|
||||
items,
|
||||
@@ -95,7 +80,7 @@ export function useHistory(
|
||||
loading,
|
||||
error,
|
||||
setPage: setCurrentPage,
|
||||
refresh: load,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,6 +95,12 @@ export interface UseIpHistoryResult {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and manage IP detail history.
|
||||
*
|
||||
* @param ip - IP address to fetch history for
|
||||
* @returns IP detail response, loading state, error, and refresh callback
|
||||
*/
|
||||
export function useIpHistory(ip: string): UseIpHistoryResult {
|
||||
const [detail, setDetail] = useState<IpDetailResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -124,13 +115,19 @@ export function useIpHistory(ip: string): UseIpHistoryResult {
|
||||
|
||||
fetchIpHistory(ip, abortRef.current.signal)
|
||||
.then((resp) => {
|
||||
setDetail(resp);
|
||||
if (!abortRef.current?.signal.aborted) {
|
||||
setDetail(resp);
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
handleFetchError(err, setError, "Failed to fetch IP history");
|
||||
if (!abortRef.current?.signal.aborted) {
|
||||
handleFetchError(err, setError, "Failed to fetch IP history");
|
||||
}
|
||||
})
|
||||
.finally((): void => {
|
||||
setLoading(false);
|
||||
if (!abortRef.current?.signal.aborted) {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}, [ip]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user