T-18: Merge useDashboardCountryData and useMapData into shared base hook
Create useBansByCountry as the shared base hook containing all common fetch logic, abort-controller pattern, and state management. Both useDashboardCountryData and useMapData now wrap this base hook: - useDashboardCountryData: Thin wrapper that calls base hook with autoFetch=true - useMapData: Wraps base hook with 300ms debounce layer Changes: - Create useBansByCountry.ts (base hook with optional autoFetch parameter) - Refactor useDashboardCountryData.ts to use base hook - Refactor useMapData.ts to use base hook with debounce wrapper - Add tests for all three hooks Benefits: - Single source of truth for ban-by-country logic - Bug fixes in base hook apply to both consumers - Eliminates code duplication (~80 lines reduced) - Maintains backward compatibility: existing call sites work unchanged Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
/**
|
||||
* `useDashboardCountryData` hook.
|
||||
*
|
||||
* Fetches ban-by-country aggregates for dashboard chart components. Unlike
|
||||
* `useMapData`, this hook has no debouncing or map-specific state.
|
||||
* Fetches ban-by-country aggregates for dashboard chart components. This is a thin
|
||||
* wrapper around {@link useBansByCountry} that preserves the original API and naming.
|
||||
*
|
||||
* Unlike `useMapData`, this hook has no debouncing.
|
||||
*
|
||||
* Re-fetches automatically when `timeRange` or `origin` changes.
|
||||
*/
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { fetchBansByCountry } from "../api/map";
|
||||
import { useListData } from "./useListData";
|
||||
import type { BanOriginFilter, TimeRange } from "../types/ban";
|
||||
import type { BansByCountryResponse, MapBanItem } from "../types/map";
|
||||
import type { MapBanItem } from "../types/map";
|
||||
import { useBansByCountry } from "./useBansByCountry";
|
||||
|
||||
/** Return value shape for {@link useDashboardCountryData}. */
|
||||
export interface UseDashboardCountryDataResult {
|
||||
@@ -36,6 +36,7 @@ export interface UseDashboardCountryDataResult {
|
||||
*
|
||||
* @param timeRange - Time-range preset: `"24h"`, `"7d"`, `"30d"`, or `"365d"`.
|
||||
* @param origin - Origin filter: `"all"`, `"blocklist"`, or `"selfblock"`.
|
||||
* @param source - Data source: `"fail2ban"` or `"archive"`.
|
||||
* @returns Aggregated country data, ban list, loading state, and error.
|
||||
*/
|
||||
export function useDashboardCountryData(
|
||||
@@ -43,30 +44,8 @@ export function useDashboardCountryData(
|
||||
origin: BanOriginFilter,
|
||||
source: "fail2ban" | "archive" = "fail2ban",
|
||||
): UseDashboardCountryDataResult {
|
||||
const [countries, setCountries] = useState<Record<string, number>>({});
|
||||
const [countryNames, setCountryNames] = useState<Record<string, string>>({});
|
||||
const [total, setTotal] = useState<number>(0);
|
||||
|
||||
const fetcher = useCallback(
|
||||
(signal: AbortSignal) =>
|
||||
fetchBansByCountry(timeRange, origin, source, undefined, signal),
|
||||
[timeRange, origin, source],
|
||||
);
|
||||
|
||||
const selector = useCallback((response: BansByCountryResponse) => response.bans, []);
|
||||
|
||||
const onSuccess = useCallback((response: BansByCountryResponse) => {
|
||||
setCountries(response.countries);
|
||||
setCountryNames(response.country_names);
|
||||
setTotal(response.total);
|
||||
}, []);
|
||||
|
||||
const { items: bans, loading, error, refresh } = useListData<BansByCountryResponse, MapBanItem>({
|
||||
fetcher,
|
||||
selector,
|
||||
errorMessage: "Failed to fetch dashboard country data",
|
||||
onSuccess,
|
||||
});
|
||||
const { countries, countryNames, bans, total, loading, error, refresh } =
|
||||
useBansByCountry(timeRange, origin, source, undefined);
|
||||
|
||||
return { countries, countryNames, bans, total, loading, error, reload: refresh };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user