Split multi-hook frontend modules into single-hook files
This commit is contained in:
@@ -198,6 +198,8 @@ Reference: `Docs/Refactoring.md` for full analysis of each issue.
|
|||||||
|
|
||||||
**Goal:** Split each multi-hook file so that every hook lives in its own file following the `hooks/<hookName>.ts` naming convention. Create: `useBlocklists.ts`, `useSchedule.ts`, `useImportLog.ts`, `useRunImport.ts`, `useJailConfigs.ts`, `useJailConfigDetail.ts`, `useGlobalConfig.ts`, `useServerSettings.ts`, `useRegexTester.ts`, `useLogPreview.ts`, and whatever hooks are in `useJails.ts`. If hooks share internal utilities or types, extract those to a helper module — do not inline them in each new file.
|
**Goal:** Split each multi-hook file so that every hook lives in its own file following the `hooks/<hookName>.ts` naming convention. Create: `useBlocklists.ts`, `useSchedule.ts`, `useImportLog.ts`, `useRunImport.ts`, `useJailConfigs.ts`, `useJailConfigDetail.ts`, `useGlobalConfig.ts`, `useServerSettings.ts`, `useRegexTester.ts`, `useLogPreview.ts`, and whatever hooks are in `useJails.ts`. If hooks share internal utilities or types, extract those to a helper module — do not inline them in each new file.
|
||||||
|
|
||||||
|
**Status:** Completed.
|
||||||
|
|
||||||
**Possible traps and issues:**
|
**Possible traps and issues:**
|
||||||
- All existing import sites must be updated. Each page and component imports specific hooks by name from these files; the import path changes but the imported name stays the same.
|
- All existing import sites must be updated. Each page and component imports specific hooks by name from these files; the import path changes but the imported name stays the same.
|
||||||
- Hooks within the same file may share local helper functions or state logic that is not currently exported. Those helpers must be extracted into a shared internal module (e.g., `hooks/_configHelpers.ts`) or duplicated if they are truly trivial.
|
- Hooks within the same file may share local helper functions or state logic that is not currently exported. Those helpers must be extracted into a shared internal module (e.g., `hooks/_configHelpers.ts`) or duplicated if they are truly trivial.
|
||||||
|
|||||||
90
frontend/src/hooks/useActiveBans.ts
Normal file
90
frontend/src/hooks/useActiveBans.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* React hook for live active ban list management.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { banIp, fetchActiveBans, unbanAllBans, unbanIp } from "../api/jails";
|
||||||
|
import { handleFetchError } from "../utils/fetchError";
|
||||||
|
import type { ActiveBan, UnbanAllResponse } from "../types/jail";
|
||||||
|
|
||||||
|
export interface UseActiveBansResult {
|
||||||
|
bans: ActiveBan[];
|
||||||
|
total: number;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refresh: () => void;
|
||||||
|
banIp: (jail: string, ip: string) => Promise<void>;
|
||||||
|
unbanIp: (ip: string, jail?: string) => Promise<void>;
|
||||||
|
unbanAll: () => Promise<UnbanAllResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and manage the currently-active ban list.
|
||||||
|
*/
|
||||||
|
export function useActiveBans(): UseActiveBansResult {
|
||||||
|
const [bans, setBans] = useState<ActiveBan[]>([]);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
|
const load = useCallback(() => {
|
||||||
|
abortRef.current?.abort();
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
abortRef.current = ctrl;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
fetchActiveBans()
|
||||||
|
.then((res) => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
setBans(res.bans);
|
||||||
|
setTotal(res.total);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
handleFetchError(err, setError, "Failed to fetch active bans");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
return (): void => {
|
||||||
|
abortRef.current?.abort();
|
||||||
|
};
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
const doBan = useCallback(async (jail: string, ip: string): Promise<void> => {
|
||||||
|
await banIp(jail, ip);
|
||||||
|
load();
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
const doUnban = useCallback(async (ip: string, jail?: string): Promise<void> => {
|
||||||
|
await unbanIp(ip, jail);
|
||||||
|
load();
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
const doUnbanAll = useCallback(async (): Promise<UnbanAllResponse> => {
|
||||||
|
const result = await unbanAllBans();
|
||||||
|
load();
|
||||||
|
return result;
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
bans,
|
||||||
|
total,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refresh: load,
|
||||||
|
banIp: doBan,
|
||||||
|
unbanIp: doUnban,
|
||||||
|
unbanAll: doUnbanAll,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,302 +1,5 @@
|
|||||||
/**
|
export { useBlocklists, type UseBlocklistsReturn } from "./useBlocklists";
|
||||||
* React hooks for blocklist management data fetching.
|
export { useSchedule, type UseScheduleReturn } from "./useSchedule";
|
||||||
*/
|
export { useImportLog, type UseImportLogReturn } from "./useImportLog";
|
||||||
|
export { useRunImport, type UseRunImportReturn } from "./useRunImport";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
export { useBlocklistStatus, type UseBlocklistStatusReturn } from "./useBlocklistStatus";
|
||||||
import {
|
|
||||||
createBlocklist,
|
|
||||||
deleteBlocklist,
|
|
||||||
fetchBlocklists,
|
|
||||||
fetchImportLog,
|
|
||||||
fetchSchedule,
|
|
||||||
previewBlocklist,
|
|
||||||
runImportNow,
|
|
||||||
updateBlocklist,
|
|
||||||
updateSchedule,
|
|
||||||
} from "../api/blocklist";
|
|
||||||
import { handleFetchError } from "../utils/fetchError";
|
|
||||||
import type {
|
|
||||||
BlocklistSource,
|
|
||||||
BlocklistSourceCreate,
|
|
||||||
BlocklistSourceUpdate,
|
|
||||||
ImportLogListResponse,
|
|
||||||
ImportRunResult,
|
|
||||||
PreviewResponse,
|
|
||||||
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>;
|
|
||||||
previewSource: (id: number) => Promise<PreviewResponse>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
handleFetchError(err, setError, "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));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const previewSource = useCallback(async (id: number): Promise<PreviewResponse> => {
|
|
||||||
return previewBlocklist(id);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
sources,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refresh: load,
|
|
||||||
createSource,
|
|
||||||
updateSource,
|
|
||||||
removeSource,
|
|
||||||
previewSource,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 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) => {
|
|
||||||
handleFetchError(err, setError, "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) {
|
|
||||||
handleFetchError(err, setError, "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) {
|
|
||||||
handleFetchError(err, setError, "Import failed");
|
|
||||||
} finally {
|
|
||||||
setRunning(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { running, lastResult, error, runNow };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// useBlocklistStatus
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/** How often to re-check the schedule endpoint for import errors (ms). */
|
|
||||||
const BLOCKLIST_POLL_INTERVAL_MS = 60_000;
|
|
||||||
|
|
||||||
export interface UseBlocklistStatusReturn {
|
|
||||||
/** `true` when the most recent import run completed with errors. */
|
|
||||||
hasErrors: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Poll `GET /api/blocklists/schedule` every 60 seconds to detect whether
|
|
||||||
* the most recent blocklist import had errors.
|
|
||||||
*
|
|
||||||
* Network failures during polling are silently ignored — the indicator
|
|
||||||
* simply retains its previous value until the next successful poll.
|
|
||||||
*/
|
|
||||||
export function useBlocklistStatus(): UseBlocklistStatusReturn {
|
|
||||||
const [hasErrors, setHasErrors] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
|
|
||||||
const poll = (): void => {
|
|
||||||
fetchSchedule()
|
|
||||||
.then((info) => {
|
|
||||||
if (!cancelled) {
|
|
||||||
setHasErrors(info.last_run_errors === true);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Silently swallow network errors — do not change indicator state.
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
poll();
|
|
||||||
const id = window.setInterval(poll, BLOCKLIST_POLL_INTERVAL_MS);
|
|
||||||
return (): void => {
|
|
||||||
cancelled = true;
|
|
||||||
window.clearInterval(id);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { hasErrors };
|
|
||||||
}
|
|
||||||
|
|||||||
45
frontend/src/hooks/useBlocklistStatus.ts
Normal file
45
frontend/src/hooks/useBlocklistStatus.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* React hook for polling blocklist schedule error state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { fetchSchedule } from "../api/blocklist";
|
||||||
|
|
||||||
|
const BLOCKLIST_POLL_INTERVAL_MS = 60_000;
|
||||||
|
|
||||||
|
export interface UseBlocklistStatusReturn {
|
||||||
|
hasErrors: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll `GET /api/blocklists/schedule` every 60 seconds to detect whether
|
||||||
|
* the most recent blocklist import had errors.
|
||||||
|
*/
|
||||||
|
export function useBlocklistStatus(): UseBlocklistStatusReturn {
|
||||||
|
const [hasErrors, setHasErrors] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const poll = (): void => {
|
||||||
|
fetchSchedule()
|
||||||
|
.then((info) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setHasErrors(info.last_run_errors === true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Silently swallow network errors — do not change indicator state.
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
poll();
|
||||||
|
const id = window.setInterval(poll, BLOCKLIST_POLL_INTERVAL_MS);
|
||||||
|
return (): void => {
|
||||||
|
cancelled = true;
|
||||||
|
window.clearInterval(id);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { hasErrors };
|
||||||
|
}
|
||||||
108
frontend/src/hooks/useBlocklists.ts
Normal file
108
frontend/src/hooks/useBlocklists.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* React hook for listing and mutating blocklist sources.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
createBlocklist,
|
||||||
|
deleteBlocklist,
|
||||||
|
fetchBlocklists,
|
||||||
|
previewBlocklist,
|
||||||
|
updateBlocklist,
|
||||||
|
} from "../api/blocklist";
|
||||||
|
import { handleFetchError } from "../utils/fetchError";
|
||||||
|
import type {
|
||||||
|
BlocklistSource,
|
||||||
|
BlocklistSourceCreate,
|
||||||
|
BlocklistSourceUpdate,
|
||||||
|
PreviewResponse,
|
||||||
|
} from "../types/blocklist";
|
||||||
|
|
||||||
|
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>;
|
||||||
|
previewSource: (id: number) => Promise<PreviewResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
handleFetchError(err, setError, "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));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const previewSource = useCallback(async (id: number): Promise<PreviewResponse> => {
|
||||||
|
return previewBlocklist(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sources,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refresh: load,
|
||||||
|
createSource,
|
||||||
|
updateSource,
|
||||||
|
removeSource,
|
||||||
|
previewSource,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,365 +1,6 @@
|
|||||||
/**
|
export { useJailConfigs, type UseJailConfigsResult } from "./useJailConfigs";
|
||||||
* React hooks for the configuration and server settings data.
|
export { useJailConfigDetail, type UseJailConfigDetailResult } from "./useJailConfigDetail";
|
||||||
*/
|
export { useGlobalConfig, type UseGlobalConfigResult } from "./useGlobalConfig";
|
||||||
|
export { useServerSettings, type UseServerSettingsResult } from "./useServerSettings";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
export { useRegexTester, type UseRegexTesterResult } from "./useRegexTester";
|
||||||
import {
|
export { useLogPreview, type UseLogPreviewResult } from "./useLogPreview";
|
||||||
addLogPath,
|
|
||||||
fetchGlobalConfig,
|
|
||||||
fetchJailConfig,
|
|
||||||
fetchJailConfigs,
|
|
||||||
previewLog,
|
|
||||||
reloadConfig,
|
|
||||||
restartFail2Ban,
|
|
||||||
testRegex,
|
|
||||||
updateGlobalConfig,
|
|
||||||
updateJailConfig,
|
|
||||||
} from "../api/config";
|
|
||||||
import {
|
|
||||||
fetchServerSettings,
|
|
||||||
flushLogs,
|
|
||||||
updateServerSettings,
|
|
||||||
} from "../api/server";
|
|
||||||
import { handleFetchError } from "../utils/fetchError";
|
|
||||||
import type {
|
|
||||||
AddLogPathRequest,
|
|
||||||
GlobalConfig,
|
|
||||||
GlobalConfigUpdate,
|
|
||||||
JailConfig,
|
|
||||||
JailConfigUpdate,
|
|
||||||
LogPreviewRequest,
|
|
||||||
LogPreviewResponse,
|
|
||||||
RegexTestRequest,
|
|
||||||
RegexTestResponse,
|
|
||||||
ServerSettings,
|
|
||||||
ServerSettingsUpdate,
|
|
||||||
} from "../types/config";
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// useJailConfigs — list all jail configs
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
interface UseJailConfigsResult {
|
|
||||||
jails: JailConfig[];
|
|
||||||
total: number;
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
refresh: () => void;
|
|
||||||
updateJail: (name: string, update: JailConfigUpdate) => Promise<void>;
|
|
||||||
reloadAll: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useJailConfigs(): UseJailConfigsResult {
|
|
||||||
const [jails, setJails] = useState<JailConfig[]>([]);
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
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);
|
|
||||||
|
|
||||||
fetchJailConfigs()
|
|
||||||
.then((resp) => {
|
|
||||||
setJails(resp.jails);
|
|
||||||
setTotal(resp.total);
|
|
||||||
})
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
handleFetchError(err, setError, "Failed to fetch jail configs");
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
load();
|
|
||||||
return (): void => {
|
|
||||||
abortRef.current?.abort();
|
|
||||||
};
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
const updateJail = useCallback(
|
|
||||||
async (name: string, update: JailConfigUpdate): Promise<void> => {
|
|
||||||
await updateJailConfig(name, update);
|
|
||||||
load();
|
|
||||||
},
|
|
||||||
[load],
|
|
||||||
);
|
|
||||||
|
|
||||||
const reloadAll = useCallback(async (): Promise<void> => {
|
|
||||||
await reloadConfig();
|
|
||||||
load();
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
return { jails, total, loading, error, refresh: load, updateJail, reloadAll };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// useJailConfigDetail — single jail config with mutation
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
interface UseJailConfigDetailResult {
|
|
||||||
jail: JailConfig | null;
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
refresh: () => void;
|
|
||||||
updateJail: (update: JailConfigUpdate) => Promise<void>;
|
|
||||||
addLog: (req: AddLogPathRequest) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useJailConfigDetail(name: string): UseJailConfigDetailResult {
|
|
||||||
const [jail, setJail] = useState<JailConfig | null>(null);
|
|
||||||
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);
|
|
||||||
|
|
||||||
fetchJailConfig(name)
|
|
||||||
.then((resp) => {
|
|
||||||
setJail(resp.jail);
|
|
||||||
})
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
handleFetchError(err, setError, "Failed to fetch jail config");
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}, [name]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
load();
|
|
||||||
return (): void => {
|
|
||||||
abortRef.current?.abort();
|
|
||||||
};
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
const updateJail = useCallback(
|
|
||||||
async (update: JailConfigUpdate): Promise<void> => {
|
|
||||||
await updateJailConfig(name, update);
|
|
||||||
load();
|
|
||||||
},
|
|
||||||
[name, load],
|
|
||||||
);
|
|
||||||
|
|
||||||
const addLog = useCallback(
|
|
||||||
async (req: AddLogPathRequest): Promise<void> => {
|
|
||||||
await addLogPath(name, req);
|
|
||||||
load();
|
|
||||||
},
|
|
||||||
[name, load],
|
|
||||||
);
|
|
||||||
|
|
||||||
return { jail, loading, error, refresh: load, updateJail, addLog };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// useGlobalConfig
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
interface UseGlobalConfigResult {
|
|
||||||
config: GlobalConfig | null;
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
refresh: () => void;
|
|
||||||
updateConfig: (update: GlobalConfigUpdate) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useGlobalConfig(): UseGlobalConfigResult {
|
|
||||||
const [config, setConfig] = useState<GlobalConfig | null>(null);
|
|
||||||
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);
|
|
||||||
|
|
||||||
fetchGlobalConfig()
|
|
||||||
.then(setConfig)
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
handleFetchError(err, setError, "Failed to fetch global config");
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
load();
|
|
||||||
return (): void => {
|
|
||||||
abortRef.current?.abort();
|
|
||||||
};
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
const updateConfig = useCallback(
|
|
||||||
async (update: GlobalConfigUpdate): Promise<void> => {
|
|
||||||
await updateGlobalConfig(update);
|
|
||||||
load();
|
|
||||||
},
|
|
||||||
[load],
|
|
||||||
);
|
|
||||||
|
|
||||||
return { config, loading, error, refresh: load, updateConfig };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// useServerSettings
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
interface UseServerSettingsResult {
|
|
||||||
settings: ServerSettings | null;
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
refresh: () => void;
|
|
||||||
updateSettings: (update: ServerSettingsUpdate) => Promise<void>;
|
|
||||||
flush: () => Promise<string>;
|
|
||||||
reload: () => Promise<void>;
|
|
||||||
restart: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useServerSettings(): UseServerSettingsResult {
|
|
||||||
const [settings, setSettings] = useState<ServerSettings | null>(null);
|
|
||||||
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);
|
|
||||||
|
|
||||||
fetchServerSettings()
|
|
||||||
.then((resp) => {
|
|
||||||
setSettings(resp.settings);
|
|
||||||
})
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
handleFetchError(err, setError, "Failed to fetch server settings");
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
load();
|
|
||||||
return (): void => {
|
|
||||||
abortRef.current?.abort();
|
|
||||||
};
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
const updateSettings_ = useCallback(
|
|
||||||
async (update: ServerSettingsUpdate): Promise<void> => {
|
|
||||||
await updateServerSettings(update);
|
|
||||||
load();
|
|
||||||
},
|
|
||||||
[load],
|
|
||||||
);
|
|
||||||
|
|
||||||
const reload = useCallback(async (): Promise<void> => {
|
|
||||||
await reloadConfig();
|
|
||||||
load();
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
const restart = useCallback(async (): Promise<void> => {
|
|
||||||
await restartFail2Ban();
|
|
||||||
load();
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
const flush = useCallback(async (): Promise<string> => {
|
|
||||||
return flushLogs();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
settings,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refresh: load,
|
|
||||||
updateSettings: updateSettings_,
|
|
||||||
flush,
|
|
||||||
reload,
|
|
||||||
restart,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// useRegexTester — lazy, triggered by test(req)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
interface UseRegexTesterResult {
|
|
||||||
result: RegexTestResponse | null;
|
|
||||||
testing: boolean;
|
|
||||||
test: (req: RegexTestRequest) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRegexTester(): UseRegexTesterResult {
|
|
||||||
const [result, setResult] = useState<RegexTestResponse | null>(null);
|
|
||||||
const [testing, setTesting] = useState(false);
|
|
||||||
|
|
||||||
const test_ = useCallback(async (req: RegexTestRequest): Promise<void> => {
|
|
||||||
setTesting(true);
|
|
||||||
try {
|
|
||||||
const resp = await testRegex(req);
|
|
||||||
setResult(resp);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (err instanceof Error) {
|
|
||||||
setResult({ matched: false, groups: [], error: err.message });
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setTesting(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { result, testing, test: test_ };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// useLogPreview — lazy, triggered by preview(req)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
interface UseLogPreviewResult {
|
|
||||||
preview: LogPreviewResponse | null;
|
|
||||||
loading: boolean;
|
|
||||||
run: (req: LogPreviewRequest) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useLogPreview(): UseLogPreviewResult {
|
|
||||||
const [preview, setPreview] = useState<LogPreviewResponse | null>(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const run_ = useCallback(async (req: LogPreviewRequest): Promise<void> => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const resp = await previewLog(req);
|
|
||||||
setPreview(resp);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (err instanceof Error) {
|
|
||||||
setPreview({
|
|
||||||
lines: [],
|
|
||||||
total_lines: 0,
|
|
||||||
matched_count: 0,
|
|
||||||
regex_error: err.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { preview, loading, run: run_ };
|
|
||||||
}
|
|
||||||
|
|||||||
68
frontend/src/hooks/useGlobalConfig.ts
Normal file
68
frontend/src/hooks/useGlobalConfig.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* React hook for loading and updating global configuration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { fetchGlobalConfig, updateGlobalConfig } from "../api/config";
|
||||||
|
import { handleFetchError } from "../utils/fetchError";
|
||||||
|
import type { GlobalConfig, GlobalConfigUpdate } from "../types/config";
|
||||||
|
|
||||||
|
export interface UseGlobalConfigResult {
|
||||||
|
config: GlobalConfig | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refresh: () => void;
|
||||||
|
updateConfig: (update: GlobalConfigUpdate) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load global configuration and expose update operations.
|
||||||
|
*/
|
||||||
|
export function useGlobalConfig(): UseGlobalConfigResult {
|
||||||
|
const [config, setConfig] = useState<GlobalConfig | null>(null);
|
||||||
|
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);
|
||||||
|
|
||||||
|
fetchGlobalConfig()
|
||||||
|
.then((resp) => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
setConfig(resp);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
handleFetchError(err, setError, "Failed to fetch global config");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!abortRef.current?.signal.aborted) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
return (): void => {
|
||||||
|
abortRef.current?.abort();
|
||||||
|
};
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
const updateConfig = useCallback(
|
||||||
|
async (update: GlobalConfigUpdate): Promise<void> => {
|
||||||
|
await updateGlobalConfig(update);
|
||||||
|
load();
|
||||||
|
},
|
||||||
|
[load],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { config, loading, error, refresh: load, updateConfig };
|
||||||
|
}
|
||||||
63
frontend/src/hooks/useImportLog.ts
Normal file
63
frontend/src/hooks/useImportLog.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* React hook for loading paginated blocklist import log entries.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { fetchImportLog } from "../api/blocklist";
|
||||||
|
import { handleFetchError } from "../utils/fetchError";
|
||||||
|
import type { ImportLogListResponse } from "../types/blocklist";
|
||||||
|
|
||||||
|
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) {
|
||||||
|
handleFetchError(err, setError, "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 };
|
||||||
|
}
|
||||||
49
frontend/src/hooks/useIpLookup.ts
Normal file
49
frontend/src/hooks/useIpLookup.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* React hook for looking up a single IP address.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { handleFetchError } from "../utils/fetchError";
|
||||||
|
import { lookupIp } from "../api/jails";
|
||||||
|
import type { IpLookupResponse } from "../types/jail";
|
||||||
|
|
||||||
|
export interface UseIpLookupResult {
|
||||||
|
result: IpLookupResponse | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
lookup: (ip: string) => void;
|
||||||
|
clear: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage IP lookup state and expose a lookup trigger.
|
||||||
|
*/
|
||||||
|
export function useIpLookup(): UseIpLookupResult {
|
||||||
|
const [result, setResult] = useState<IpLookupResponse | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const lookup = useCallback((ip: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setResult(null);
|
||||||
|
|
||||||
|
lookupIp(ip)
|
||||||
|
.then((res) => {
|
||||||
|
setResult(res);
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
handleFetchError(err, setError, "Failed to lookup IP");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clear = useCallback(() => {
|
||||||
|
setResult(null);
|
||||||
|
setError(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { result, loading, error, lookup, clear };
|
||||||
|
}
|
||||||
106
frontend/src/hooks/useJailBannedIps.ts
Normal file
106
frontend/src/hooks/useJailBannedIps.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* React hook for paginated jailed IPs within a specific jail.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { fetchJailBannedIps, unbanIp } from "../api/jails";
|
||||||
|
import { handleFetchError } from "../utils/fetchError";
|
||||||
|
import type { ActiveBan } from "../types/jail";
|
||||||
|
|
||||||
|
export interface UseJailBannedIpsResult {
|
||||||
|
items: ActiveBan[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
search: string;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
opError: string | null;
|
||||||
|
refresh: () => Promise<void>;
|
||||||
|
setPage: (page: number) => void;
|
||||||
|
setPageSize: (size: number) => void;
|
||||||
|
setSearch: (term: string) => void;
|
||||||
|
unban: (ip: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useJailBannedIps(jailName: string): UseJailBannedIpsResult {
|
||||||
|
const [items, setItems] = useState<ActiveBan[]>([]);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(25);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [opError, setOpError] = useState<string | null>(null);
|
||||||
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
const load = useCallback(async (): Promise<void> => {
|
||||||
|
if (!jailName) {
|
||||||
|
setItems([]);
|
||||||
|
setTotal(0);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetchJailBannedIps(jailName, page, pageSize, debouncedSearch || undefined);
|
||||||
|
setItems(resp.items);
|
||||||
|
setTotal(resp.total);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
handleFetchError(err, setError, "Failed to fetch jailed IPs");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [jailName, page, pageSize, debouncedSearch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (debounceRef.current !== null) {
|
||||||
|
clearTimeout(debounceRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
debounceRef.current = setTimeout(() => {
|
||||||
|
setDebouncedSearch(search);
|
||||||
|
setPage(1);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return (): void => {
|
||||||
|
if (debounceRef.current !== null) {
|
||||||
|
clearTimeout(debounceRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void load();
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
const unban = useCallback(async (ip: string): Promise<void> => {
|
||||||
|
setOpError(null);
|
||||||
|
try {
|
||||||
|
await unbanIp(ip, jailName);
|
||||||
|
await load();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setOpError(err instanceof Error ? err.message : String(err));
|
||||||
|
}
|
||||||
|
}, [jailName, load]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
search,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
opError,
|
||||||
|
refresh: load,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
setSearch,
|
||||||
|
unban,
|
||||||
|
};
|
||||||
|
}
|
||||||
77
frontend/src/hooks/useJailConfigDetail.ts
Normal file
77
frontend/src/hooks/useJailConfigDetail.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* React hook for loading and mutating a single jail configuration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { addLogPath, fetchJailConfig, updateJailConfig } from "../api/config";
|
||||||
|
import { handleFetchError } from "../utils/fetchError";
|
||||||
|
import type { AddLogPathRequest, JailConfig, JailConfigUpdate } from "../types/config";
|
||||||
|
|
||||||
|
export interface UseJailConfigDetailResult {
|
||||||
|
jail: JailConfig | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refresh: () => void;
|
||||||
|
updateJail: (update: JailConfigUpdate) => Promise<void>;
|
||||||
|
addLog: (req: AddLogPathRequest) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the detail view for a single jail config and expose edit actions.
|
||||||
|
*/
|
||||||
|
export function useJailConfigDetail(name: string): UseJailConfigDetailResult {
|
||||||
|
const [jail, setJail] = useState<JailConfig | null>(null);
|
||||||
|
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);
|
||||||
|
|
||||||
|
fetchJailConfig(name)
|
||||||
|
.then((resp) => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
setJail(resp.jail);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
handleFetchError(err, setError, "Failed to fetch jail config");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!abortRef.current?.signal.aborted) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [name]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
return (): void => {
|
||||||
|
abortRef.current?.abort();
|
||||||
|
};
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
const updateJail = useCallback(
|
||||||
|
async (update: JailConfigUpdate): Promise<void> => {
|
||||||
|
await updateJailConfig(name, update);
|
||||||
|
load();
|
||||||
|
},
|
||||||
|
[name, load],
|
||||||
|
);
|
||||||
|
|
||||||
|
const addLog = useCallback(
|
||||||
|
async (req: AddLogPathRequest): Promise<void> => {
|
||||||
|
await addLogPath(name, req);
|
||||||
|
load();
|
||||||
|
},
|
||||||
|
[name, load],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { jail, loading, error, refresh: load, updateJail, addLog };
|
||||||
|
}
|
||||||
77
frontend/src/hooks/useJailConfigs.ts
Normal file
77
frontend/src/hooks/useJailConfigs.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* React hook for loading the jail config inventory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { fetchJailConfigs, reloadConfig, updateJailConfig } from "../api/config";
|
||||||
|
import { handleFetchError } from "../utils/fetchError";
|
||||||
|
import type { JailConfig, JailConfigUpdate } from "../types/config";
|
||||||
|
|
||||||
|
export interface UseJailConfigsResult {
|
||||||
|
jails: JailConfig[];
|
||||||
|
total: number;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refresh: () => void;
|
||||||
|
updateJail: (name: string, update: JailConfigUpdate) => Promise<void>;
|
||||||
|
reloadAll: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all jail configs and expose update controls.
|
||||||
|
*/
|
||||||
|
export function useJailConfigs(): UseJailConfigsResult {
|
||||||
|
const [jails, setJails] = useState<JailConfig[]>([]);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
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);
|
||||||
|
|
||||||
|
fetchJailConfigs()
|
||||||
|
.then((resp) => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
setJails(resp.jails);
|
||||||
|
setTotal(resp.total);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
handleFetchError(err, setError, "Failed to fetch jail configs");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!abortRef.current?.signal.aborted) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
return (): void => {
|
||||||
|
abortRef.current?.abort();
|
||||||
|
};
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
const updateJail = useCallback(
|
||||||
|
async (name: string, update: JailConfigUpdate): Promise<void> => {
|
||||||
|
await updateJailConfig(name, update);
|
||||||
|
load();
|
||||||
|
},
|
||||||
|
[load],
|
||||||
|
);
|
||||||
|
|
||||||
|
const reloadAll = useCallback(async (): Promise<void> => {
|
||||||
|
await reloadConfig();
|
||||||
|
load();
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
return { jails, total, loading, error, refresh: load, updateJail, reloadAll };
|
||||||
|
}
|
||||||
121
frontend/src/hooks/useJailDetail.ts
Normal file
121
frontend/src/hooks/useJailDetail.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* React hook for fetching a single jail's detailed metadata.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { addIgnoreIp, delIgnoreIp, fetchJail, reloadJail, setJailIdle, startJail, stopJail, toggleIgnoreSelf as toggleIgnoreSelfApi } from "../api/jails";
|
||||||
|
import { handleFetchError } from "../utils/fetchError";
|
||||||
|
import type { Jail } from "../types/jail";
|
||||||
|
|
||||||
|
export interface UseJailDetailResult {
|
||||||
|
jail: Jail | null;
|
||||||
|
ignoreList: string[];
|
||||||
|
ignoreSelf: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refresh: () => void;
|
||||||
|
addIp: (ip: string) => Promise<void>;
|
||||||
|
removeIp: (ip: string) => Promise<void>;
|
||||||
|
toggleIgnoreSelf: (on: boolean) => Promise<void>;
|
||||||
|
start: () => Promise<void>;
|
||||||
|
stop: () => Promise<void>;
|
||||||
|
reload: () => Promise<void>;
|
||||||
|
setIdle: (on: boolean) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and manage the detail view for a single jail.
|
||||||
|
*/
|
||||||
|
export function useJailDetail(name: string): UseJailDetailResult {
|
||||||
|
const [jail, setJail] = useState<Jail | null>(null);
|
||||||
|
const [ignoreList, setIgnoreList] = useState<string[]>([]);
|
||||||
|
const [ignoreSelf, setIgnoreSelf] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
|
const load = useCallback(() => {
|
||||||
|
abortRef.current?.abort();
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
abortRef.current = ctrl;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
fetchJail(name)
|
||||||
|
.then((res) => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
setJail(res.jail);
|
||||||
|
setIgnoreList(res.ignore_list);
|
||||||
|
setIgnoreSelf(res.ignore_self);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
handleFetchError(err, setError, "Failed to fetch jail detail");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [name]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
return (): void => {
|
||||||
|
abortRef.current?.abort();
|
||||||
|
};
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
const addIp = useCallback(async (ip: string): Promise<void> => {
|
||||||
|
await addIgnoreIp(name, ip);
|
||||||
|
load();
|
||||||
|
}, [name, load]);
|
||||||
|
|
||||||
|
const removeIp = useCallback(async (ip: string): Promise<void> => {
|
||||||
|
await delIgnoreIp(name, ip);
|
||||||
|
load();
|
||||||
|
}, [name, load]);
|
||||||
|
|
||||||
|
const toggleIgnoreSelf = useCallback(async (on: boolean): Promise<void> => {
|
||||||
|
await toggleIgnoreSelfApi(name, on);
|
||||||
|
load();
|
||||||
|
}, [name, load]);
|
||||||
|
|
||||||
|
const start = useCallback(async (): Promise<void> => {
|
||||||
|
await startJail(name);
|
||||||
|
load();
|
||||||
|
}, [name, load]);
|
||||||
|
|
||||||
|
const stop = useCallback(async (): Promise<void> => {
|
||||||
|
await stopJail(name);
|
||||||
|
load();
|
||||||
|
}, [name, load]);
|
||||||
|
|
||||||
|
const reload = useCallback(async (): Promise<void> => {
|
||||||
|
await reloadJail(name);
|
||||||
|
load();
|
||||||
|
}, [name, load]);
|
||||||
|
|
||||||
|
const setIdle = useCallback(async (on: boolean): Promise<void> => {
|
||||||
|
await setJailIdle(name, on);
|
||||||
|
load();
|
||||||
|
}, [name, load]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
jail,
|
||||||
|
ignoreList,
|
||||||
|
ignoreSelf,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refresh: load,
|
||||||
|
addIp,
|
||||||
|
removeIp,
|
||||||
|
toggleIgnoreSelf,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
reload,
|
||||||
|
setIdle,
|
||||||
|
};
|
||||||
|
}
|
||||||
96
frontend/src/hooks/useJailList.ts
Normal file
96
frontend/src/hooks/useJailList.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* React hook for loading and controlling the jail overview list.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
fetchJails,
|
||||||
|
reloadAllJails,
|
||||||
|
reloadJail,
|
||||||
|
setJailIdle,
|
||||||
|
startJail,
|
||||||
|
stopJail,
|
||||||
|
} from "../api/jails";
|
||||||
|
import { handleFetchError } from "../utils/fetchError";
|
||||||
|
import type { JailSummary } from "../types/jail";
|
||||||
|
|
||||||
|
export interface UseJailsResult {
|
||||||
|
jails: JailSummary[];
|
||||||
|
total: number;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | 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 [jails, setJails] = useState<JailSummary[]>([]);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
|
const load = useCallback(() => {
|
||||||
|
abortRef.current?.abort();
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
abortRef.current = ctrl;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
fetchJails()
|
||||||
|
.then((res) => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
setJails(res.jails);
|
||||||
|
setTotal(res.total);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
handleFetchError(err, setError, "Failed to load jails");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
return (): void => {
|
||||||
|
abortRef.current?.abort();
|
||||||
|
};
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
const withRefresh =
|
||||||
|
(fn: (name: string) => Promise<unknown>) =>
|
||||||
|
async (name: string): Promise<void> => {
|
||||||
|
await fn(name);
|
||||||
|
load();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
jails,
|
||||||
|
total,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refresh: load,
|
||||||
|
startJail: withRefresh(startJail),
|
||||||
|
stopJail: withRefresh(stopJail),
|
||||||
|
setIdle: (name, on) => setJailIdle(name, on).then(() => {
|
||||||
|
load();
|
||||||
|
}),
|
||||||
|
reloadJail: withRefresh(reloadJail),
|
||||||
|
reloadAll: () => reloadAllJails().then(() => {
|
||||||
|
load();
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,513 +1,5 @@
|
|||||||
/**
|
export { useJails, type UseJailsResult } from "./useJailList";
|
||||||
* Jail management hooks.
|
export { useJailDetail, type UseJailDetailResult } from "./useJailDetail";
|
||||||
*
|
export { useJailBannedIps, type UseJailBannedIpsResult } from "./useJailBannedIps";
|
||||||
* Provides data-fetching and mutation hooks for all jail-related views,
|
export { useActiveBans, type UseActiveBansResult } from "./useActiveBans";
|
||||||
* following the same patterns established by `useBans.ts` and
|
export { useIpLookup, type UseIpLookupResult } from "./useIpLookup";
|
||||||
* `useServerStatus.ts`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
import { handleFetchError } from "../utils/fetchError";
|
|
||||||
import {
|
|
||||||
addIgnoreIp,
|
|
||||||
banIp,
|
|
||||||
delIgnoreIp,
|
|
||||||
fetchActiveBans,
|
|
||||||
fetchJail,
|
|
||||||
fetchJailBannedIps,
|
|
||||||
fetchJails,
|
|
||||||
lookupIp,
|
|
||||||
reloadAllJails,
|
|
||||||
reloadJail,
|
|
||||||
setJailIdle,
|
|
||||||
startJail,
|
|
||||||
stopJail,
|
|
||||||
toggleIgnoreSelf as toggleIgnoreSelfApi,
|
|
||||||
unbanAllBans,
|
|
||||||
unbanIp,
|
|
||||||
} from "../api/jails";
|
|
||||||
import type {
|
|
||||||
ActiveBan,
|
|
||||||
IpLookupResponse,
|
|
||||||
Jail,
|
|
||||||
JailSummary,
|
|
||||||
UnbanAllResponse,
|
|
||||||
} from "../types/jail";
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// useJails — overview list
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Return value for {@link useJails}. */
|
|
||||||
export interface UseJailsResult {
|
|
||||||
/** All known jails. */
|
|
||||||
jails: JailSummary[];
|
|
||||||
/** Total count returned by the backend. */
|
|
||||||
total: number;
|
|
||||||
/** `true` while a fetch is in progress. */
|
|
||||||
loading: boolean;
|
|
||||||
/** Error message from the last failed fetch, or `null`. */
|
|
||||||
error: string | null;
|
|
||||||
/** Re-fetch the jail list from the backend. */
|
|
||||||
refresh: () => void;
|
|
||||||
/** Start a specific jail (returns a promise for error handling). */
|
|
||||||
startJail: (name: string) => Promise<void>;
|
|
||||||
/** Stop a specific jail. */
|
|
||||||
stopJail: (name: string) => Promise<void>;
|
|
||||||
/** Toggle idle mode for a jail. */
|
|
||||||
setIdle: (name: string, on: boolean) => Promise<void>;
|
|
||||||
/** Reload a specific jail. */
|
|
||||||
reloadJail: (name: string) => Promise<void>;
|
|
||||||
/** Reload all jails at once. */
|
|
||||||
reloadAll: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch and manage the jail overview list.
|
|
||||||
*
|
|
||||||
* Automatically loads on mount and exposes control mutations that refresh
|
|
||||||
* the list after each operation.
|
|
||||||
*
|
|
||||||
* @returns Current jail list, loading/error state, and control callbacks.
|
|
||||||
*/
|
|
||||||
export function useJails(): UseJailsResult {
|
|
||||||
const [jails, setJails] = useState<JailSummary[]>([]);
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const abortRef = useRef<AbortController | null>(null);
|
|
||||||
|
|
||||||
const load = useCallback(() => {
|
|
||||||
abortRef.current?.abort();
|
|
||||||
const ctrl = new AbortController();
|
|
||||||
abortRef.current = ctrl;
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
fetchJails()
|
|
||||||
.then((res) => {
|
|
||||||
if (!ctrl.signal.aborted) {
|
|
||||||
setJails(res.jails);
|
|
||||||
setTotal(res.total);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
if (!ctrl.signal.aborted) {
|
|
||||||
handleFetchError(err, setError, "Failed to load jails");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (!ctrl.signal.aborted) {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
load();
|
|
||||||
return (): void => {
|
|
||||||
abortRef.current?.abort();
|
|
||||||
};
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
const withRefresh =
|
|
||||||
(fn: (name: string) => Promise<unknown>) =>
|
|
||||||
async (name: string): Promise<void> => {
|
|
||||||
await fn(name);
|
|
||||||
load();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
jails,
|
|
||||||
total,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refresh: load,
|
|
||||||
startJail: withRefresh(startJail),
|
|
||||||
stopJail: withRefresh(stopJail),
|
|
||||||
setIdle: (name, on) => setJailIdle(name, on).then((): void => { load(); }),
|
|
||||||
reloadJail: withRefresh(reloadJail),
|
|
||||||
reloadAll: () => reloadAllJails().then((): void => { load(); }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// useJailDetail — single jail
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Return value for {@link useJailDetail}. */
|
|
||||||
export interface UseJailDetailResult {
|
|
||||||
/** Full jail configuration, or `null` while loading. */
|
|
||||||
jail: Jail | null;
|
|
||||||
/** Current ignore list. */
|
|
||||||
ignoreList: string[];
|
|
||||||
/** Whether `ignoreself` is enabled. */
|
|
||||||
ignoreSelf: boolean;
|
|
||||||
/** `true` while a fetch is in progress. */
|
|
||||||
loading: boolean;
|
|
||||||
/** Error message or `null`. */
|
|
||||||
error: string | null;
|
|
||||||
/** Re-fetch from the backend. */
|
|
||||||
refresh: () => void;
|
|
||||||
/** Add an IP to the ignore list. */
|
|
||||||
addIp: (ip: string) => Promise<void>;
|
|
||||||
/** Remove an IP from the ignore list. */
|
|
||||||
removeIp: (ip: string) => Promise<void>;
|
|
||||||
/** Enable or disable the ignoreself option for this jail. */
|
|
||||||
toggleIgnoreSelf: (on: boolean) => Promise<void>;
|
|
||||||
/** Start the jail. */
|
|
||||||
start: () => Promise<void>;
|
|
||||||
/** Stop the jail. */
|
|
||||||
stop: () => Promise<void>;
|
|
||||||
/** Reload jail configuration. */
|
|
||||||
reload: () => Promise<void>;
|
|
||||||
/** Toggle idle mode on/off for the jail. */
|
|
||||||
setIdle: (on: boolean) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch and manage the detail view for a single jail.
|
|
||||||
*
|
|
||||||
* @param name - Jail name to load.
|
|
||||||
* @returns Jail detail, ignore list management helpers, and fetch state.
|
|
||||||
*/
|
|
||||||
export function useJailDetail(name: string): UseJailDetailResult {
|
|
||||||
const [jail, setJail] = useState<Jail | null>(null);
|
|
||||||
const [ignoreList, setIgnoreList] = useState<string[]>([]);
|
|
||||||
const [ignoreSelf, setIgnoreSelf] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const abortRef = useRef<AbortController | null>(null);
|
|
||||||
|
|
||||||
const load = useCallback(() => {
|
|
||||||
abortRef.current?.abort();
|
|
||||||
const ctrl = new AbortController();
|
|
||||||
abortRef.current = ctrl;
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
fetchJail(name)
|
|
||||||
.then((res) => {
|
|
||||||
if (!ctrl.signal.aborted) {
|
|
||||||
setJail(res.jail);
|
|
||||||
setIgnoreList(res.ignore_list);
|
|
||||||
setIgnoreSelf(res.ignore_self);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
if (!ctrl.signal.aborted) {
|
|
||||||
handleFetchError(err, setError, "Failed to fetch jail detail");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (!ctrl.signal.aborted) setLoading(false);
|
|
||||||
});
|
|
||||||
}, [name]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
load();
|
|
||||||
return (): void => {
|
|
||||||
abortRef.current?.abort();
|
|
||||||
};
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
const addIp = async (ip: string): Promise<void> => {
|
|
||||||
await addIgnoreIp(name, ip);
|
|
||||||
load();
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeIp = async (ip: string): Promise<void> => {
|
|
||||||
await delIgnoreIp(name, ip);
|
|
||||||
load();
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleIgnoreSelf = async (on: boolean): Promise<void> => {
|
|
||||||
await toggleIgnoreSelfApi(name, on);
|
|
||||||
load();
|
|
||||||
};
|
|
||||||
|
|
||||||
const doStart = async (): Promise<void> => {
|
|
||||||
await startJail(name);
|
|
||||||
load();
|
|
||||||
};
|
|
||||||
|
|
||||||
const doStop = async (): Promise<void> => {
|
|
||||||
await stopJail(name);
|
|
||||||
load();
|
|
||||||
};
|
|
||||||
|
|
||||||
const doReload = async (): Promise<void> => {
|
|
||||||
await reloadJail(name);
|
|
||||||
load();
|
|
||||||
};
|
|
||||||
|
|
||||||
const doSetIdle = async (on: boolean): Promise<void> => {
|
|
||||||
await setJailIdle(name, on);
|
|
||||||
load();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
jail,
|
|
||||||
ignoreList,
|
|
||||||
ignoreSelf,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refresh: load,
|
|
||||||
addIp,
|
|
||||||
removeIp,
|
|
||||||
toggleIgnoreSelf,
|
|
||||||
start: doStart,
|
|
||||||
stop: doStop,
|
|
||||||
reload: doReload,
|
|
||||||
setIdle: doSetIdle,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// useJailBannedIps
|
|
||||||
|
|
||||||
export interface UseJailBannedIpsResult {
|
|
||||||
items: ActiveBan[];
|
|
||||||
total: number;
|
|
||||||
page: number;
|
|
||||||
pageSize: number;
|
|
||||||
search: string;
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
opError: string | null;
|
|
||||||
refresh: () => Promise<void>;
|
|
||||||
setPage: (page: number) => void;
|
|
||||||
setPageSize: (size: number) => void;
|
|
||||||
setSearch: (term: string) => void;
|
|
||||||
unban: (ip: string) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useJailBannedIps(jailName: string): UseJailBannedIpsResult {
|
|
||||||
const [items, setItems] = useState<ActiveBan[]>([]);
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [pageSize, setPageSize] = useState(25);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [opError, setOpError] = useState<string | null>(null);
|
|
||||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
|
|
||||||
const load = useCallback(async (): Promise<void> => {
|
|
||||||
if (!jailName) {
|
|
||||||
setItems([]);
|
|
||||||
setTotal(0);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetchJailBannedIps(jailName, page, pageSize, debouncedSearch || undefined);
|
|
||||||
setItems(resp.items);
|
|
||||||
setTotal(resp.total);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
handleFetchError(err, setError, "Failed to fetch jailed IPs");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [jailName, page, pageSize, debouncedSearch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (debounceRef.current !== null) {
|
|
||||||
clearTimeout(debounceRef.current);
|
|
||||||
}
|
|
||||||
debounceRef.current = setTimeout(() => {
|
|
||||||
setDebouncedSearch(search);
|
|
||||||
setPage(1);
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
||||||
return () => {
|
|
||||||
if (debounceRef.current !== null) {
|
|
||||||
clearTimeout(debounceRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [search]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void load();
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
const unban = useCallback(async (ip: string): Promise<void> => {
|
|
||||||
setOpError(null);
|
|
||||||
try {
|
|
||||||
await unbanIp(ip, jailName);
|
|
||||||
await load();
|
|
||||||
} catch (err: unknown) {
|
|
||||||
setOpError(err instanceof Error ? err.message : String(err));
|
|
||||||
}
|
|
||||||
}, [jailName, load]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
items,
|
|
||||||
total,
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
search,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
opError,
|
|
||||||
refresh: load,
|
|
||||||
setPage,
|
|
||||||
setPageSize,
|
|
||||||
setSearch,
|
|
||||||
unban,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// useActiveBans — live ban list
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Return value for {@link useActiveBans}. */
|
|
||||||
export interface UseActiveBansResult {
|
|
||||||
/** All currently active bans. */
|
|
||||||
bans: ActiveBan[];
|
|
||||||
/** Total ban count. */
|
|
||||||
total: number;
|
|
||||||
/** `true` while fetching. */
|
|
||||||
loading: boolean;
|
|
||||||
/** Error message or `null`. */
|
|
||||||
error: string | null;
|
|
||||||
/** Re-fetch the active bans. */
|
|
||||||
refresh: () => void;
|
|
||||||
/** Ban an IP in a specific jail. */
|
|
||||||
banIp: (jail: string, ip: string) => Promise<void>;
|
|
||||||
/** Unban an IP from a jail (or all jails when `jail` is omitted). */
|
|
||||||
unbanIp: (ip: string, jail?: string) => Promise<void>;
|
|
||||||
/** Unban every currently banned IP across all jails. */
|
|
||||||
unbanAll: () => Promise<UnbanAllResponse>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch and manage the currently-active ban list.
|
|
||||||
*
|
|
||||||
* @returns Active ban list, mutation callbacks, and fetch state.
|
|
||||||
*/
|
|
||||||
export function useActiveBans(): UseActiveBansResult {
|
|
||||||
const [bans, setBans] = useState<ActiveBan[]>([]);
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const abortRef = useRef<AbortController | null>(null);
|
|
||||||
|
|
||||||
const load = useCallback(() => {
|
|
||||||
abortRef.current?.abort();
|
|
||||||
const ctrl = new AbortController();
|
|
||||||
abortRef.current = ctrl;
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
fetchActiveBans()
|
|
||||||
.then((res) => {
|
|
||||||
if (!ctrl.signal.aborted) {
|
|
||||||
setBans(res.bans);
|
|
||||||
setTotal(res.total);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
if (!ctrl.signal.aborted) {
|
|
||||||
handleFetchError(err, setError, "Failed to fetch active bans");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (!ctrl.signal.aborted) setLoading(false);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
load();
|
|
||||||
return (): void => {
|
|
||||||
abortRef.current?.abort();
|
|
||||||
};
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
const doBan = async (jail: string, ip: string): Promise<void> => {
|
|
||||||
await banIp(jail, ip);
|
|
||||||
load();
|
|
||||||
};
|
|
||||||
|
|
||||||
const doUnban = async (ip: string, jail?: string): Promise<void> => {
|
|
||||||
await unbanIp(ip, jail);
|
|
||||||
load();
|
|
||||||
};
|
|
||||||
|
|
||||||
const doUnbanAll = async (): Promise<UnbanAllResponse> => {
|
|
||||||
const result = await unbanAllBans();
|
|
||||||
load();
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
bans,
|
|
||||||
total,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refresh: load,
|
|
||||||
banIp: doBan,
|
|
||||||
unbanIp: doUnban,
|
|
||||||
unbanAll: doUnbanAll,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// useIpLookup — single IP lookup
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Return value for {@link useIpLookup}. */
|
|
||||||
export interface UseIpLookupResult {
|
|
||||||
/** Lookup result, or `null` when no lookup has been performed yet. */
|
|
||||||
result: IpLookupResponse | null;
|
|
||||||
/** `true` while a lookup is in progress. */
|
|
||||||
loading: boolean;
|
|
||||||
/** Error message or `null`. */
|
|
||||||
error: string | null;
|
|
||||||
/** Trigger an IP lookup. */
|
|
||||||
lookup: (ip: string) => void;
|
|
||||||
/** Clear the result and error state. */
|
|
||||||
clear: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manage IP lookup state (lazy — no fetch on mount).
|
|
||||||
*
|
|
||||||
* @returns Lookup result, state flags, and a `lookup` trigger callback.
|
|
||||||
*/
|
|
||||||
export function useIpLookup(): UseIpLookupResult {
|
|
||||||
const [result, setResult] = useState<IpLookupResponse | null>(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const lookup = useCallback((ip: string) => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
setResult(null);
|
|
||||||
|
|
||||||
lookupIp(ip)
|
|
||||||
.then((res) => {
|
|
||||||
setResult(res);
|
|
||||||
})
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
handleFetchError(err, setError, "Failed to lookup IP");
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const clear = useCallback(() => {
|
|
||||||
setResult(null);
|
|
||||||
setError(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { result, loading, error, lookup, clear };
|
|
||||||
}
|
|
||||||
|
|||||||
42
frontend/src/hooks/useLogPreview.ts
Normal file
42
frontend/src/hooks/useLogPreview.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* React hook for fetching a server log preview.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { previewLog } from "../api/config";
|
||||||
|
import type { LogPreviewRequest, LogPreviewResponse } from "../types/config";
|
||||||
|
|
||||||
|
export interface UseLogPreviewResult {
|
||||||
|
preview: LogPreviewResponse | null;
|
||||||
|
loading: boolean;
|
||||||
|
run: (req: LogPreviewRequest) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a log preview and expose the response state.
|
||||||
|
*/
|
||||||
|
export function useLogPreview(): UseLogPreviewResult {
|
||||||
|
const [preview, setPreview] = useState<LogPreviewResponse | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const run = useCallback(async (req: LogPreviewRequest): Promise<void> => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const resp = await previewLog(req);
|
||||||
|
setPreview(resp);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
setPreview({
|
||||||
|
lines: [],
|
||||||
|
total_lines: 0,
|
||||||
|
matched_count: 0,
|
||||||
|
regex_error: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { preview, loading, run };
|
||||||
|
}
|
||||||
37
frontend/src/hooks/useRegexTester.ts
Normal file
37
frontend/src/hooks/useRegexTester.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* React hook for running a regex test request.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { testRegex } from "../api/config";
|
||||||
|
import type { RegexTestRequest, RegexTestResponse } from "../types/config";
|
||||||
|
|
||||||
|
export interface UseRegexTesterResult {
|
||||||
|
result: RegexTestResponse | null;
|
||||||
|
testing: boolean;
|
||||||
|
test: (req: RegexTestRequest) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute regex tests and expose the current response.
|
||||||
|
*/
|
||||||
|
export function useRegexTester(): UseRegexTesterResult {
|
||||||
|
const [result, setResult] = useState<RegexTestResponse | null>(null);
|
||||||
|
const [testing, setTesting] = useState(false);
|
||||||
|
|
||||||
|
const test = useCallback(async (req: RegexTestRequest): Promise<void> => {
|
||||||
|
setTesting(true);
|
||||||
|
try {
|
||||||
|
const resp = await testRegex(req);
|
||||||
|
setResult(resp);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
setResult({ matched: false, groups: [], error: err.message });
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setTesting(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { result, testing, test };
|
||||||
|
}
|
||||||
39
frontend/src/hooks/useRunImport.ts
Normal file
39
frontend/src/hooks/useRunImport.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* React hook for triggering a manual blocklist import run.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { runImportNow } from "../api/blocklist";
|
||||||
|
import { handleFetchError } from "../utils/fetchError";
|
||||||
|
import type { ImportRunResult } from "../types/blocklist";
|
||||||
|
|
||||||
|
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) {
|
||||||
|
handleFetchError(err, setError, "Import failed");
|
||||||
|
} finally {
|
||||||
|
setRunning(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { running, lastResult, error, runNow };
|
||||||
|
}
|
||||||
44
frontend/src/hooks/useSchedule.ts
Normal file
44
frontend/src/hooks/useSchedule.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* React hook for fetching and updating the blocklist import schedule.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { fetchSchedule, updateSchedule } from "../api/blocklist";
|
||||||
|
import { handleFetchError } from "../utils/fetchError";
|
||||||
|
import type { ScheduleConfig, ScheduleInfo } from "../types/blocklist";
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
handleFetchError(err, setError, "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 };
|
||||||
|
}
|
||||||
95
frontend/src/hooks/useServerSettings.ts
Normal file
95
frontend/src/hooks/useServerSettings.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* React hook for loading and mutating server settings and controls.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { fetchServerSettings, flushLogs, updateServerSettings } from "../api/server";
|
||||||
|
import { reloadConfig, restartFail2Ban } from "../api/config";
|
||||||
|
import { handleFetchError } from "../utils/fetchError";
|
||||||
|
import type { ServerSettings, ServerSettingsUpdate } from "../types/config";
|
||||||
|
|
||||||
|
export interface UseServerSettingsResult {
|
||||||
|
settings: ServerSettings | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refresh: () => void;
|
||||||
|
updateSettings: (update: ServerSettingsUpdate) => Promise<void>;
|
||||||
|
flush: () => Promise<string>;
|
||||||
|
reload: () => Promise<void>;
|
||||||
|
restart: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load server settings and provide control actions.
|
||||||
|
*/
|
||||||
|
export function useServerSettings(): UseServerSettingsResult {
|
||||||
|
const [settings, setSettings] = useState<ServerSettings | null>(null);
|
||||||
|
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);
|
||||||
|
|
||||||
|
fetchServerSettings()
|
||||||
|
.then((resp) => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
setSettings(resp.settings);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (!ctrl.signal.aborted) {
|
||||||
|
handleFetchError(err, setError, "Failed to fetch server settings");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!abortRef.current?.signal.aborted) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
return (): void => {
|
||||||
|
abortRef.current?.abort();
|
||||||
|
};
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
const updateSettings = useCallback(
|
||||||
|
async (update: ServerSettingsUpdate): Promise<void> => {
|
||||||
|
await updateServerSettings(update);
|
||||||
|
load();
|
||||||
|
},
|
||||||
|
[load],
|
||||||
|
);
|
||||||
|
|
||||||
|
const reload = useCallback(async (): Promise<void> => {
|
||||||
|
await reloadConfig();
|
||||||
|
load();
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
const restart = useCallback(async (): Promise<void> => {
|
||||||
|
await restartFail2Ban();
|
||||||
|
load();
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
const flush = useCallback(async (): Promise<string> => {
|
||||||
|
return flushLogs();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
settings,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refresh: load,
|
||||||
|
updateSettings,
|
||||||
|
flush,
|
||||||
|
reload,
|
||||||
|
restart,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user