/** * `BannedIpsSection` component. * * Displays a paginated table of IPs currently banned in a specific fail2ban * jail. Supports server-side search filtering (debounced), page navigation, * page-size selection, and per-row unban actions. * * Only the current page is geo-enriched by the backend, so the component * remains fast even when a jail contains thousands of banned IPs. */ import { Badge, Button, DataGrid, DataGridBody, DataGridCell, DataGridHeader, DataGridHeaderCell, DataGridRow, Dropdown, Field, Input, MessageBar, MessageBarBody, Option, Spinner, Text, Tooltip, makeStyles, tokens, type TableColumnDefinition, createTableColumn, } from "@fluentui/react-components"; import { useCommonSectionStyles } from "../../theme/commonStyles"; import { formatTimestamp } from "../../utils/formatDate"; import { ArrowClockwiseRegular, ChevronLeftRegular, ChevronRightRegular, DismissRegular, SearchRegular, } from "@fluentui/react-icons"; import type { ActiveBan } from "../../types/jail"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- /** Available page-size options. */ const PAGE_SIZE_OPTIONS = [10, 25, 50, 100] as const; // --------------------------------------------------------------------------- // Styles // --------------------------------------------------------------------------- const useStyles = makeStyles({ header: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: tokens.spacingHorizontalM, paddingBottom: tokens.spacingVerticalS, borderBottomWidth: "1px", borderBottomStyle: "solid", borderBottomColor: tokens.colorNeutralStroke2, }, headerLeft: { display: "flex", alignItems: "center", gap: tokens.spacingHorizontalM, }, toolbar: { display: "flex", alignItems: "flex-end", gap: tokens.spacingHorizontalS, flexWrap: "wrap", }, searchField: { minWidth: "200px", flexGrow: 1, }, tableWrapper: { overflowX: "auto", }, centred: { display: "flex", justifyContent: "center", alignItems: "center", padding: tokens.spacingVerticalXXL, }, pagination: { display: "flex", alignItems: "center", justifyContent: "flex-end", gap: tokens.spacingHorizontalS, paddingTop: tokens.spacingVerticalS, flexWrap: "wrap", }, pageSizeWrapper: { display: "flex", alignItems: "center", gap: tokens.spacingHorizontalXS, }, mono: { fontFamily: "Consolas, 'Courier New', monospace", fontSize: tokens.fontSizeBase200, }, }); // --------------------------------------------------------------------------- // Column definitions // --------------------------------------------------------------------------- /** A row item augmented with an `onUnban` callback for the row action. */ interface BanRow { ban: ActiveBan; onUnban: (ip: string) => void; } const columns: TableColumnDefinition[] = [ createTableColumn({ columnId: "ip", renderHeaderCell: () => "IP Address", renderCell: ({ ban }) => ( {ban.ip} ), }), createTableColumn({ columnId: "country", renderHeaderCell: () => "Country", renderCell: ({ ban }) => ban.country ? ( {ban.country} ) : ( ), }), createTableColumn({ columnId: "banned_at", renderHeaderCell: () => "Banned At", renderCell: ({ ban }) => ( {ban.banned_at ? formatTimestamp(ban.banned_at) : "—"} ), }), createTableColumn({ columnId: "expires_at", renderHeaderCell: () => "Expires At", renderCell: ({ ban }) => ( {ban.expires_at ? formatTimestamp(ban.expires_at) : "—"} ), }), createTableColumn({ columnId: "actions", renderHeaderCell: () => "", renderCell: ({ ban, onUnban }) => (