Stage 10: external blocklist importer — backend + frontend
- blocklist_repo.py: CRUD for blocklist_sources table - import_log_repo.py: add/list/get-last log entries - blocklist_service.py: source CRUD, preview, import (download/validate/ban), import_all, schedule get/set/info - blocklist_import.py: APScheduler task (hourly/daily/weekly schedule triggers) - blocklist.py router: 9 endpoints (list/create/update/delete/preview/import/ schedule-get+put/log) - blocklist.py models: ScheduleFrequency (StrEnum), ScheduleConfig, ScheduleInfo, ImportSourceResult, ImportRunResult, PreviewResponse - 59 new tests (18 repo + 19 service + 22 router); 374 total pass - ruff clean, mypy clean for Stage 10 files - types/blocklist.ts, api/blocklist.ts, hooks/useBlocklist.ts - BlocklistsPage.tsx: source management, schedule picker, import log table - Frontend tsc + ESLint clean
This commit is contained in:
237
frontend/src/hooks/useBlocklist.ts
Normal file
237
frontend/src/hooks/useBlocklist.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* React hooks for blocklist management data fetching.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
createBlocklist,
|
||||
deleteBlocklist,
|
||||
fetchBlocklists,
|
||||
fetchImportLog,
|
||||
fetchSchedule,
|
||||
runImportNow,
|
||||
updateBlocklist,
|
||||
updateSchedule,
|
||||
} from "../api/blocklist";
|
||||
import type {
|
||||
BlocklistSource,
|
||||
BlocklistSourceCreate,
|
||||
BlocklistSourceUpdate,
|
||||
ImportLogListResponse,
|
||||
ImportRunResult,
|
||||
ScheduleConfig,
|
||||
ScheduleInfo,
|
||||
} from "../types/blocklist";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// useBlocklists
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface UseBlocklistsReturn {
|
||||
sources: BlocklistSource[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => void;
|
||||
createSource: (payload: BlocklistSourceCreate) => Promise<BlocklistSource>;
|
||||
updateSource: (id: number, payload: BlocklistSourceUpdate) => Promise<BlocklistSource>;
|
||||
removeSource: (id: number) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all blocklist sources and expose CRUD operations.
|
||||
*/
|
||||
export function useBlocklists(): UseBlocklistsReturn {
|
||||
const [sources, setSources] = useState<BlocklistSource[]>([]);
|
||||
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();
|
||||
const ctrl = new AbortController();
|
||||
abortRef.current = ctrl;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
fetchBlocklists()
|
||||
.then((data) => {
|
||||
if (!ctrl.signal.aborted) {
|
||||
setSources(data.sources);
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (!ctrl.signal.aborted) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load blocklists");
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
return (): void => {
|
||||
abortRef.current?.abort();
|
||||
};
|
||||
}, [load]);
|
||||
|
||||
const createSource = useCallback(
|
||||
async (payload: BlocklistSourceCreate): Promise<BlocklistSource> => {
|
||||
const created = await createBlocklist(payload);
|
||||
setSources((prev) => [...prev, created]);
|
||||
return created;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const updateSource = useCallback(
|
||||
async (id: number, payload: BlocklistSourceUpdate): Promise<BlocklistSource> => {
|
||||
const updated = await updateBlocklist(id, payload);
|
||||
setSources((prev) => prev.map((s) => (s.id === id ? updated : s)));
|
||||
return updated;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const removeSource = useCallback(async (id: number): Promise<void> => {
|
||||
await deleteBlocklist(id);
|
||||
setSources((prev) => prev.filter((s) => s.id !== id));
|
||||
}, []);
|
||||
|
||||
return { sources, loading, error, refresh: load, createSource, updateSource, removeSource };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// useSchedule
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface UseScheduleReturn {
|
||||
info: ScheduleInfo | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
saveSchedule: (config: ScheduleConfig) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and update the blocklist import schedule.
|
||||
*/
|
||||
export function useSchedule(): UseScheduleReturn {
|
||||
const [info, setInfo] = useState<ScheduleInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetchSchedule()
|
||||
.then((data) => {
|
||||
setInfo(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
setError(err instanceof Error ? err.message : "Failed to load schedule");
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const saveSchedule = useCallback(async (config: ScheduleConfig): Promise<void> => {
|
||||
const updated = await updateSchedule(config);
|
||||
setInfo(updated);
|
||||
}, []);
|
||||
|
||||
return { info, loading, error, saveSchedule };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// useImportLog
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface UseImportLogReturn {
|
||||
data: ImportLogListResponse | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
page: number;
|
||||
setPage: (n: number) => void;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the paginated import log with optional source filter.
|
||||
*/
|
||||
export function useImportLog(
|
||||
sourceId?: number,
|
||||
pageSize = 50,
|
||||
): UseImportLogReturn {
|
||||
const [data, setData] = useState<ImportLogListResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const load = useCallback((): void => {
|
||||
abortRef.current?.abort();
|
||||
const ctrl = new AbortController();
|
||||
abortRef.current = ctrl;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
fetchImportLog(page, pageSize, sourceId)
|
||||
.then((result) => {
|
||||
if (!ctrl.signal.aborted) {
|
||||
setData(result);
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (!ctrl.signal.aborted) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load import log");
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}, [page, pageSize, sourceId]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
return (): void => {
|
||||
abortRef.current?.abort();
|
||||
};
|
||||
}, [load]);
|
||||
|
||||
return { data, loading, error, page, setPage, refresh: load };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// useRunImport
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface UseRunImportReturn {
|
||||
running: boolean;
|
||||
lastResult: ImportRunResult | null;
|
||||
error: string | null;
|
||||
runNow: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger and track a manual blocklist import run.
|
||||
*/
|
||||
export function useRunImport(): UseRunImportReturn {
|
||||
const [running, setRunning] = useState(false);
|
||||
const [lastResult, setLastResult] = useState<ImportRunResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const runNow = useCallback(async (): Promise<void> => {
|
||||
setRunning(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await runImportNow();
|
||||
setLastResult(result);
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : "Import failed");
|
||||
} finally {
|
||||
setRunning(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { running, lastResult, error, runNow };
|
||||
}
|
||||
Reference in New Issue
Block a user