Refactor frontend API calls into hooks and complete task states
This commit is contained in:
29
frontend/src/hooks/__tests__/useJailBannedIps.test.ts
Normal file
29
frontend/src/hooks/__tests__/useJailBannedIps.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderHook, act, waitFor } from "@testing-library/react";
|
||||
import { useJailBannedIps } from "../useJails";
|
||||
import * as api from "../../api/jails";
|
||||
|
||||
vi.mock("../../api/jails");
|
||||
|
||||
describe("useJailBannedIps", () => {
|
||||
it("loads bans and allows unban", async () => {
|
||||
const fetchMock = vi.mocked(api.fetchJailBannedIps);
|
||||
const unbanMock = vi.mocked(api.unbanIp);
|
||||
|
||||
fetchMock.mockResolvedValue({ items: [{ ip: "1.2.3.4", jail: "sshd", banned_at: "2025-01-01T10:00:00+00:00", expires_at: "2025-01-01T10:10:00+00:00", ban_count: 1, country: "US" }], total: 1, page: 1, page_size: 25 });
|
||||
unbanMock.mockResolvedValue({ message: "ok", jail: "sshd" });
|
||||
|
||||
const { result } = renderHook(() => useJailBannedIps("sshd"));
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
expect(result.current.items.length).toBe(1);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.unban("1.2.3.4");
|
||||
});
|
||||
|
||||
expect(unbanMock).toHaveBeenCalledWith("1.2.3.4", "sshd");
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
41
frontend/src/hooks/__tests__/useMapColorThresholds.test.ts
Normal file
41
frontend/src/hooks/__tests__/useMapColorThresholds.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderHook, act, waitFor } from "@testing-library/react";
|
||||
import { useMapColorThresholds } from "../useMapColorThresholds";
|
||||
import * as api from "../../api/config";
|
||||
|
||||
vi.mock("../../api/config");
|
||||
|
||||
describe("useMapColorThresholds", () => {
|
||||
it("loads thresholds and exposes values", async () => {
|
||||
const mocked = vi.mocked(api.fetchMapColorThresholds);
|
||||
mocked.mockResolvedValue({ threshold_low: 10, threshold_medium: 20, threshold_high: 50 });
|
||||
|
||||
const { result } = renderHook(() => useMapColorThresholds());
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
expect(result.current.thresholds).toEqual({ threshold_low: 10, threshold_medium: 20, threshold_high: 50 });
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
it("updates thresholds via callback", async () => {
|
||||
const fetchMock = vi.mocked(api.fetchMapColorThresholds);
|
||||
const updateMock = vi.mocked(api.updateMapColorThresholds);
|
||||
|
||||
fetchMock.mockResolvedValue({ threshold_low: 10, threshold_medium: 20, threshold_high: 50 });
|
||||
updateMock.mockResolvedValue({ threshold_low: 15, threshold_medium: 25, threshold_high: 75 });
|
||||
|
||||
const { result } = renderHook(() => useMapColorThresholds());
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.updateThresholds({ threshold_low: 15, threshold_medium: 25, threshold_high: 75 });
|
||||
});
|
||||
|
||||
expect(result.current.thresholds).toEqual({ threshold_low: 15, threshold_medium: 25, threshold_high: 75 });
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
fetchBlocklists,
|
||||
fetchImportLog,
|
||||
fetchSchedule,
|
||||
previewBlocklist,
|
||||
runImportNow,
|
||||
updateBlocklist,
|
||||
updateSchedule,
|
||||
@@ -35,6 +36,7 @@ export interface UseBlocklistsReturn {
|
||||
createSource: (payload: BlocklistSourceCreate) => Promise<BlocklistSource>;
|
||||
updateSource: (id: number, payload: BlocklistSourceUpdate) => Promise<BlocklistSource>;
|
||||
removeSource: (id: number) => Promise<void>;
|
||||
previewSource: (id: number) => Promise<PreviewResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,7 +101,20 @@ export function useBlocklists(): UseBlocklistsReturn {
|
||||
setSources((prev) => prev.filter((s) => s.id !== id));
|
||||
}, []);
|
||||
|
||||
return { sources, loading, error, refresh: load, createSource, updateSource, removeSource };
|
||||
const previewSource = useCallback(async (id: number): Promise<PreviewResponse> => {
|
||||
return previewBlocklist(id);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
sources,
|
||||
loading,
|
||||
error,
|
||||
refresh: load,
|
||||
createSource,
|
||||
updateSource,
|
||||
removeSource,
|
||||
previewSource,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
delIgnoreIp,
|
||||
fetchActiveBans,
|
||||
fetchJail,
|
||||
fetchJailBannedIps,
|
||||
fetchJails,
|
||||
lookupIp,
|
||||
reloadAllJails,
|
||||
@@ -261,6 +262,107 @@ export function useJailDetail(name: string): UseJailDetailResult {
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} 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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
55
frontend/src/hooks/useMapColorThresholds.ts
Normal file
55
frontend/src/hooks/useMapColorThresholds.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { fetchMapColorThresholds, updateMapColorThresholds } from "../api/config";
|
||||
import type {
|
||||
MapColorThresholdsResponse,
|
||||
MapColorThresholdsUpdate,
|
||||
} from "../types/config";
|
||||
|
||||
export interface UseMapColorThresholdsResult {
|
||||
thresholds: MapColorThresholdsResponse | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
updateThresholds: (payload: MapColorThresholdsUpdate) => Promise<MapColorThresholdsResponse>;
|
||||
}
|
||||
|
||||
export function useMapColorThresholds(): UseMapColorThresholdsResult {
|
||||
const [thresholds, setThresholds] = useState<MapColorThresholdsResponse | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async (): Promise<void> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const data = await fetchMapColorThresholds();
|
||||
setThresholds(data);
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : "Failed to fetch map color thresholds");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
const updateThresholds = useCallback(
|
||||
async (payload: MapColorThresholdsUpdate): Promise<MapColorThresholdsResponse> => {
|
||||
const updated = await updateMapColorThresholds(payload);
|
||||
setThresholds(updated);
|
||||
return updated;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
thresholds,
|
||||
loading,
|
||||
error,
|
||||
refresh: load,
|
||||
updateThresholds,
|
||||
};
|
||||
}
|
||||
41
frontend/src/hooks/useTimezoneData.ts
Normal file
41
frontend/src/hooks/useTimezoneData.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { fetchTimezone } from "../api/setup";
|
||||
|
||||
export interface UseTimezoneDataResult {
|
||||
timezone: string;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useTimezoneData(): UseTimezoneDataResult {
|
||||
const [timezone, setTimezone] = useState<string>("UTC");
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async (): Promise<void> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const resp = await fetchTimezone();
|
||||
setTimezone(resp.timezone);
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : "Failed to fetch timezone");
|
||||
setTimezone("UTC");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
return {
|
||||
timezone,
|
||||
loading,
|
||||
error,
|
||||
refresh: load,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user