diff --git a/frontend/src/pages/HistoryPage.tsx b/frontend/src/pages/HistoryPage.tsx index 37d289b..e37da6a 100644 --- a/frontend/src/pages/HistoryPage.tsx +++ b/frontend/src/pages/HistoryPage.tsx @@ -16,33 +16,22 @@ import { DataGridHeader, DataGridHeaderCell, DataGridRow, - MessageBar, - MessageBarBody, - Spinner, - Table, - TableBody, - TableCell, - TableCellLayout, - TableColumnDefinition, - TableHeader, - TableHeaderCell, - TableRow, Text, Toolbar, ToolbarButton, + TableColumnDefinition, createTableColumn, makeStyles, tokens, } from "@fluentui/react-components"; -import { useCardStyles } from "../theme/commonStyles"; import { ArrowCounterclockwiseRegular, - ArrowLeftRegular, ChevronLeftRegular, ChevronRightRegular, } from "@fluentui/react-icons"; import { DashboardFilterBar } from "../components/DashboardFilterBar"; -import { useHistory, useIpHistory } from "../hooks/useHistory"; +import { useHistory } from "../hooks/useHistory"; +import { IpDetailView } from "./history/IpDetailView"; import type { HistoryBanItem, HistoryQuery, TimeRange } from "../types/history"; import type { BanOriginFilter } from "../types/ban"; @@ -216,169 +205,6 @@ const HISTORY_COLUMNS = ( }), ] as ReturnType>[]; -// --------------------------------------------------------------------------- -// IpDetailView — per-IP detail view -// --------------------------------------------------------------------------- - -interface IpDetailViewProps { - ip: string; - onBack: () => void; -} - -function IpDetailView({ ip, onBack }: IpDetailViewProps): React.JSX.Element { - const styles = useStyles(); - const cardStyles = useCardStyles(); - const { detail, loading, error, refresh } = useIpHistory(ip); - - if (loading) { - return ( -
- -
- ); - } - - if (error) { - return ( - - {error} - - ); - } - - if (!detail) { - return ( - - No history found for {ip}. - - ); - } - - return ( -
- {/* Back button + heading */} -
-
- - - {ip} - -
- -
- - {/* Summary grid */} -
-
- Total Bans - {String(detail.total_bans)} -
-
- Total Failures - {String(detail.total_failures)} -
-
- Last Banned - - {detail.last_ban_at - ? new Date(detail.last_ban_at).toLocaleString() - : "—"} - -
-
- Country - - {detail.country_name ?? detail.country_code ?? "—"} - -
-
- ASN - {detail.asn ?? "—"} -
-
- Organisation - {detail.org ?? "—"} -
-
- - {/* Timeline table */} - - Ban Timeline ({String(detail.timeline.length)} events) - - -
- - - - Banned At - Jail - Failures - Times Banned - Matched Lines - - - - {detail.timeline.map((event) => ( - - - - {new Date(event.banned_at).toLocaleString()} - - - - {event.jail} - - - {String(event.failures)} - - - {String(event.ban_count)} - - - - {event.matches.length === 0 ? ( - - — - - ) : ( - - {event.matches.join("\n")} - - )} - - - - ))} - -
-
-
- ); -} - // --------------------------------------------------------------------------- // HistoryPage — main component // --------------------------------------------------------------------------- diff --git a/frontend/src/pages/MapPage.tsx b/frontend/src/pages/MapPage.tsx index b243a11..46362cf 100644 --- a/frontend/src/pages/MapPage.tsx +++ b/frontend/src/pages/MapPage.tsx @@ -8,33 +8,23 @@ import { useState, useMemo, useEffect } from "react"; import { - Badge, Button, MessageBar, MessageBarBody, Spinner, - Table, - TableBody, - TableCell, - TableCellLayout, - TableHeader, - TableHeaderCell, - TableRow, Text, - Tooltip, makeStyles, tokens, } from "@fluentui/react-components"; import { ArrowCounterclockwiseRegular, - ChevronLeftRegular, - ChevronRightRegular, DismissRegular, } from "@fluentui/react-icons"; import { DashboardFilterBar } from "../components/DashboardFilterBar"; import { WorldMap } from "../components/WorldMap"; import { useMapData } from "../hooks/useMapData"; import { useMapColorThresholds } from "../hooks/useMapColorThresholds"; +import { MapBansTable } from "./map/MapBansTable"; import type { TimeRange } from "../types/map"; import type { BanOriginFilter } from "../types/ban"; @@ -106,8 +96,6 @@ export function MapPage(): React.JSX.Element { const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(100); - const PAGE_SIZE_OPTIONS = [25, 50, 100] as const; - const source = range === "24h" ? "fail2ban" : "archive"; const { countries, countryNames, bans, total, loading, error, refresh } = @@ -268,123 +256,17 @@ export function MapPage(): React.JSX.Element { {/* ---------------------------------------------------------------- */} {!error && hasLoadedOnce && (
- - - - IP Address - Jail - Banned At - Country - Origin - Times Banned - - - - {visibleBans.length === 0 ? ( - - - - - No bans found. - - - - - ) : ( - pageBans.map((ban) => ( - - - {ban.ip} - - - {ban.jail} - - - - {new Date(ban.banned_at).toLocaleString()} - - - - - {ban.country_name ?? ban.country_code ? ( - ban.country_name ?? ban.country_code - ) : ( - - - — - - - )} - - - - - - {ban.origin === "blocklist" ? "Blocklist" : "Selfblock"} - - - - - {String(ban.ban_count)} - - - )) - )} - -
-
-
- - Showing {pageBans.length} of {visibleBans.length} filtered ban{visibleBans.length !== 1 ? "s" : ""} - {" · "}Page {page} of {totalPages} - - -
- - Page size - - -
-
-
-
-
+
)} diff --git a/frontend/src/pages/history/IpDetailView.tsx b/frontend/src/pages/history/IpDetailView.tsx new file mode 100644 index 0000000..8faab34 --- /dev/null +++ b/frontend/src/pages/history/IpDetailView.tsx @@ -0,0 +1,203 @@ +import { + Button, + MessageBar, + MessageBarBody, + Spinner, + Table, + TableBody, + TableCell, + TableCellLayout, + TableHeader, + TableHeaderCell, + TableRow, + Text, + makeStyles, + tokens, +} from "@fluentui/react-components"; +import { ArrowCounterclockwiseRegular, ArrowLeftRegular } from "@fluentui/react-icons"; +import { useCardStyles } from "../../theme/commonStyles"; +import { useIpHistory } from "../../hooks/useHistory"; + +interface IpDetailViewProps { + ip: string; + onBack: () => void; +} + +const useStyles = makeStyles({ + root: { + display: "flex", + flexDirection: "column", + gap: tokens.spacingVerticalL, + }, + header: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + gap: tokens.spacingHorizontalM, + flexWrap: "wrap", + }, + monoText: { + fontFamily: "Consolas, 'Courier New', monospace", + fontSize: "0.85rem", + }, + detailGrid: { + display: "grid", + gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))", + gap: tokens.spacingVerticalM, + padding: tokens.spacingVerticalM, + marginBottom: tokens.spacingVerticalM, + }, + detailField: { + display: "flex", + flexDirection: "column", + gap: tokens.spacingVerticalXS, + }, + detailLabel: { + color: tokens.colorNeutralForeground3, + fontSize: "0.75rem", + textTransform: "uppercase", + letterSpacing: "0.05em", + }, + detailValue: { + fontSize: "0.9rem", + fontWeight: "600", + }, + tableWrapper: { + overflow: "auto", + borderRadius: tokens.borderRadiusMedium, + border: `1px solid ${tokens.colorNeutralStroke1}`, + }, +}); + +export function IpDetailView({ ip, onBack }: IpDetailViewProps): React.JSX.Element { + const styles = useStyles(); + const cardStyles = useCardStyles(); + const { detail, loading, error, refresh } = useIpHistory(ip); + + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( + + {error} + + ); + } + + if (!detail) { + return ( + + No history found for {ip}. + + ); + } + + return ( +
+
+
+ + + {ip} + +
+ +
+ +
+
+ Total Bans + {String(detail.total_bans)} +
+
+ Total Failures + {String(detail.total_failures)} +
+
+ Last Banned + + {detail.last_ban_at ? new Date(detail.last_ban_at).toLocaleString() : "—"} + +
+
+ Country + {detail.country_name ?? detail.country_code ?? "—"} +
+
+ ASN + {detail.asn ?? "—"} +
+
+ Organisation + {detail.org ?? "—"} +
+
+ + + Ban Timeline ({String(detail.timeline.length)} events) + + +
+ + + + Banned At + Jail + Failures + Times Banned + Matched Lines + + + + {detail.timeline.map((event) => ( + + + {new Date(event.banned_at).toLocaleString()} + + + {event.jail} + + + {String(event.failures)} + + + {String(event.ban_count)} + + + + {event.matches.length === 0 ? ( + + — + + ) : ( + + {event.matches.join("\n")} + + )} + + + + ))} + +
+
+
+ ); +} diff --git a/frontend/src/pages/map/MapBansTable.tsx b/frontend/src/pages/map/MapBansTable.tsx new file mode 100644 index 0000000..86c3259 --- /dev/null +++ b/frontend/src/pages/map/MapBansTable.tsx @@ -0,0 +1,157 @@ +import { + Badge, + Button, + Table, + TableBody, + TableCell, + TableCellLayout, + TableHeader, + TableHeaderCell, + TableRow, + Text, + Tooltip, + tokens, +} from "@fluentui/react-components"; +import { ChevronLeftRegular, ChevronRightRegular } from "@fluentui/react-icons"; +import type { MapBanItem } from "../../types/map"; + +interface MapBansTableProps { + pageBans: MapBanItem[]; + visibleCount: number; + page: number; + pageSize: number; + totalPages: number; + hasPrev: boolean; + hasNext: boolean; + onPageChange: (nextPage: number) => void; + onPageSizeChange: (pageSize: number) => void; +} + +export function MapBansTable({ + pageBans, + visibleCount, + page, + pageSize, + totalPages, + hasPrev, + hasNext, + onPageChange, + onPageSizeChange, +}: MapBansTableProps): React.JSX.Element { + return ( + <> + + + + IP Address + Jail + Banned At + Country + Origin + Times Banned + + + + {visibleCount === 0 ? ( + + + + + No bans found. + + + + + ) : ( + pageBans.map((ban) => ( + + + {ban.ip} + + + {ban.jail} + + + {new Date(ban.banned_at).toLocaleString()} + + + + {ban.country_name ?? ban.country_code ? ( + ban.country_name ?? ban.country_code + ) : ( + + + + )} + + + + + + {ban.origin === "blocklist" ? "Blocklist" : "Selfblock"} + + + + + {String(ban.ban_count)} + + + )) + )} + +
+ + {visibleCount > 0 && ( +
+
+ + Showing {pageBans.length} of {visibleCount} filtered ban{visibleCount !== 1 ? "s" : ""} + {" · "}Page {page} of {totalPages} + +
+ + Page size + + +
+
+ +
+
+
+ )} + + ); +}