Refactor frontend API calls into hooks and complete task states
This commit is contained in:
@@ -9,7 +9,6 @@
|
||||
* remains fast even when a jail contains thousands of banned IPs.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -40,17 +39,12 @@ import {
|
||||
DismissRegular,
|
||||
SearchRegular,
|
||||
} from "@fluentui/react-icons";
|
||||
import { fetchJailBannedIps, unbanIp } from "../../api/jails";
|
||||
import type { ActiveBan } from "../../types/jail";
|
||||
import { ApiError } from "../../api/client";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Debounce delay in milliseconds for the search input. */
|
||||
const SEARCH_DEBOUNCE_MS = 300;
|
||||
|
||||
/** Available page-size options. */
|
||||
const PAGE_SIZE_OPTIONS = [10, 25, 50, 100] as const;
|
||||
|
||||
@@ -229,8 +223,19 @@ const columns: TableColumnDefinition<BanRow>[] = [
|
||||
|
||||
/** Props for {@link BannedIpsSection}. */
|
||||
export interface BannedIpsSectionProps {
|
||||
/** The jail name whose banned IPs are displayed. */
|
||||
jailName: string;
|
||||
items: ActiveBan[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
search: string;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
opError: string | null;
|
||||
onSearch: (term: string) => void;
|
||||
onPageChange: (page: number) => void;
|
||||
onPageSizeChange: (size: number) => void;
|
||||
onRefresh: () => void;
|
||||
onUnban: (ip: string) => void;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -242,81 +247,26 @@ export interface BannedIpsSectionProps {
|
||||
*
|
||||
* @param props - {@link BannedIpsSectionProps}
|
||||
*/
|
||||
export function BannedIpsSection({ jailName }: BannedIpsSectionProps): React.JSX.Element {
|
||||
export function BannedIpsSection({
|
||||
items,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
loading,
|
||||
error,
|
||||
opError,
|
||||
onSearch,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
onRefresh,
|
||||
onUnban,
|
||||
}: BannedIpsSectionProps): React.JSX.Element {
|
||||
const styles = useStyles();
|
||||
|
||||
const [items, setItems] = useState<ActiveBan[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState<number>(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);
|
||||
|
||||
// Debounce the search input so we don't spam the backend on every keystroke.
|
||||
useEffect(() => {
|
||||
if (debounceRef.current !== null) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
debounceRef.current = setTimeout((): void => {
|
||||
setDebouncedSearch(search);
|
||||
setPage(1);
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
return (): void => {
|
||||
if (debounceRef.current !== null) clearTimeout(debounceRef.current);
|
||||
};
|
||||
}, [search]);
|
||||
|
||||
const load = useCallback(() => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
fetchJailBannedIps(jailName, page, pageSize, debouncedSearch || undefined)
|
||||
.then((resp) => {
|
||||
setItems(resp.items);
|
||||
setTotal(resp.total);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
const msg =
|
||||
err instanceof ApiError
|
||||
? `${String(err.status)}: ${err.body}`
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: String(err);
|
||||
setError(msg);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [jailName, page, pageSize, debouncedSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const handleUnban = (ip: string): void => {
|
||||
setOpError(null);
|
||||
unbanIp(ip, jailName)
|
||||
.then(() => {
|
||||
load();
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
const msg =
|
||||
err instanceof ApiError
|
||||
? `${String(err.status)}: ${err.body}`
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: String(err);
|
||||
setOpError(msg);
|
||||
});
|
||||
};
|
||||
|
||||
const rows: BanRow[] = items.map((ban) => ({
|
||||
ban,
|
||||
onUnban: handleUnban,
|
||||
onUnban,
|
||||
}));
|
||||
|
||||
const totalPages = pageSize > 0 ? Math.ceil(total / pageSize) : 1;
|
||||
@@ -335,7 +285,7 @@ export function BannedIpsSection({ jailName }: BannedIpsSectionProps): React.JSX
|
||||
size="small"
|
||||
appearance="subtle"
|
||||
icon={<ArrowClockwiseRegular />}
|
||||
onClick={load}
|
||||
onClick={onRefresh}
|
||||
aria-label="Refresh banned IPs"
|
||||
/>
|
||||
</div>
|
||||
@@ -350,7 +300,7 @@ export function BannedIpsSection({ jailName }: BannedIpsSectionProps): React.JSX
|
||||
placeholder="e.g. 192.168"
|
||||
value={search}
|
||||
onChange={(_, d) => {
|
||||
setSearch(d.value);
|
||||
onSearch(d.value);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
@@ -420,8 +370,8 @@ export function BannedIpsSection({ jailName }: BannedIpsSectionProps): React.JSX
|
||||
onOptionSelect={(_, d) => {
|
||||
const newSize = Number(d.optionValue);
|
||||
if (!Number.isNaN(newSize)) {
|
||||
setPageSize(newSize);
|
||||
setPage(1);
|
||||
onPageSizeChange(newSize);
|
||||
onPageChange(1);
|
||||
}
|
||||
}}
|
||||
style={{ minWidth: "80px" }}
|
||||
@@ -445,7 +395,7 @@ export function BannedIpsSection({ jailName }: BannedIpsSectionProps): React.JSX
|
||||
icon={<ChevronLeftRegular />}
|
||||
disabled={page <= 1}
|
||||
onClick={() => {
|
||||
setPage((p) => Math.max(1, p - 1));
|
||||
onPageChange(Math.max(1, page - 1));
|
||||
}}
|
||||
aria-label="Previous page"
|
||||
/>
|
||||
@@ -455,7 +405,7 @@ export function BannedIpsSection({ jailName }: BannedIpsSectionProps): React.JSX
|
||||
icon={<ChevronRightRegular />}
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => {
|
||||
setPage((p) => p + 1);
|
||||
onPageChange(page + 1);
|
||||
}}
|
||||
aria-label="Next page"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user