feat: implement dashboard ban overview (Stage 5)
- Add ban_service reading fail2ban SQLite DB via read-only aiosqlite - Add geo_service resolving IPs via ip-api.com with 10k in-memory cache - Add GET /api/dashboard/bans and GET /api/dashboard/accesses endpoints - Add TimeRange, DashboardBanItem, DashboardBanListResponse, AccessListItem, AccessListResponse models in models/ban.py - Build BanTable component (Fluent UI DataGrid) with bans/accesses modes, pagination, loading/error/empty states, and ban-count badges - Build useBans hook managing time-range and pagination state - Update DashboardPage: status bar + time-range toolbar + tab switcher - Add 37 new backend tests (ban service, geo service, dashboard router) - All 141 tests pass; ruff/mypy --strict/tsc --noEmit clean
This commit is contained in:
107
frontend/src/hooks/useBans.ts
Normal file
107
frontend/src/hooks/useBans.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* `useBans` hook.
|
||||
*
|
||||
* Fetches and manages paginated ban-list or access-list data from the
|
||||
* dashboard endpoints. Re-fetches automatically when `timeRange` or `page`
|
||||
* changes.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { fetchAccesses, fetchBans } from "../api/dashboard";
|
||||
import type { AccessListItem, DashboardBanItem, TimeRange } from "../types/ban";
|
||||
|
||||
/** The dashboard view mode: aggregate bans or individual access events. */
|
||||
export type BanTableMode = "bans" | "accesses";
|
||||
|
||||
/** Items per page for the ban/access tables. */
|
||||
const PAGE_SIZE = 100;
|
||||
|
||||
/** Return value shape for {@link useBans}. */
|
||||
export interface UseBansResult {
|
||||
/** Ban items — populated when `mode === "bans"`, otherwise empty. */
|
||||
banItems: DashboardBanItem[];
|
||||
/** Access items — populated when `mode === "accesses"`, otherwise empty. */
|
||||
accessItems: AccessListItem[];
|
||||
/** Total records in the selected time window (for pagination). */
|
||||
total: number;
|
||||
/** Current 1-based page number. */
|
||||
page: number;
|
||||
/** Navigate to a specific page. */
|
||||
setPage: (p: number) => void;
|
||||
/** Whether a fetch is currently in flight. */
|
||||
loading: boolean;
|
||||
/** Error message if the last fetch failed, otherwise `null`. */
|
||||
error: string | null;
|
||||
/** Imperatively re-fetch the current page. */
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and manage dashboard ban-list or access-list data.
|
||||
*
|
||||
* Automatically re-fetches when `mode`, `timeRange`, or `page` changes.
|
||||
*
|
||||
* @param mode - `"bans"` for the ban-list view; `"accesses"` for the
|
||||
* access-list view.
|
||||
* @param timeRange - Time-range preset that controls how far back to look.
|
||||
* @returns Current data, pagination state, loading flag, and a `refresh`
|
||||
* callback.
|
||||
*/
|
||||
export function useBans(mode: BanTableMode, timeRange: TimeRange): UseBansResult {
|
||||
const [banItems, setBanItems] = useState<DashboardBanItem[]>([]);
|
||||
const [accessItems, setAccessItems] = useState<AccessListItem[]>([]);
|
||||
const [total, setTotal] = useState<number>(0);
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Reset page when mode or time range changes.
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [mode, timeRange]);
|
||||
|
||||
const doFetch = useCallback(async (): Promise<void> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
if (mode === "bans") {
|
||||
const data = await fetchBans(timeRange, page, PAGE_SIZE);
|
||||
setBanItems(data.items);
|
||||
setAccessItems([]);
|
||||
setTotal(data.total);
|
||||
} else {
|
||||
const data = await fetchAccesses(timeRange, page, PAGE_SIZE);
|
||||
setAccessItems(data.items);
|
||||
setBanItems([]);
|
||||
setTotal(data.total);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : "Failed to fetch data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [mode, timeRange, page]);
|
||||
|
||||
// Stable ref to the latest doFetch so the refresh callback is always current.
|
||||
const doFetchRef = useRef(doFetch);
|
||||
doFetchRef.current = doFetch;
|
||||
|
||||
useEffect(() => {
|
||||
void doFetch();
|
||||
}, [doFetch]);
|
||||
|
||||
const refresh = useCallback((): void => {
|
||||
void doFetchRef.current();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
banItems,
|
||||
accessItems,
|
||||
total,
|
||||
page,
|
||||
setPage,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user