/** * MapPage — geographical overview of fail2ban bans. * * Shows a clickable SVG world map coloured by ban density, a time-range * selector, and a companion table filtered by the selected country (or all * bans when no country is selected). */ 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, 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 type { TimeRange } from "../types/map"; import type { BanOriginFilter } from "../types/ban"; // --------------------------------------------------------------------------- // Styles // --------------------------------------------------------------------------- const useStyles = makeStyles({ root: { display: "flex", flexDirection: "column", gap: tokens.spacingVerticalL, padding: tokens.spacingVerticalXXL, paddingLeft: tokens.spacingHorizontalXXL, paddingRight: tokens.spacingHorizontalXXL, }, header: { display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: tokens.spacingHorizontalM, }, tableWrapper: { overflow: "auto", maxHeight: "420px", borderRadius: tokens.borderRadiusMedium, border: `1px solid ${tokens.colorNeutralStroke1}`, }, filterBar: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: tokens.spacingHorizontalM, padding: tokens.spacingVerticalS, borderRadius: tokens.borderRadiusMedium, backgroundColor: tokens.colorNeutralBackground2, }, }); // --------------------------------------------------------------------------- // MapPage // --------------------------------------------------------------------------- export function MapPage(): React.JSX.Element { const styles = useStyles(); const [range, setRange] = useState("24h"); const [originFilter, setOriginFilter] = useState("all"); const [selectedCountry, setSelectedCountry] = useState(null); const { countries, countryNames, bans, total, loading, error, refresh } = useMapData(range, originFilter); const { thresholds: mapThresholds, error: mapThresholdError, } = useMapColorThresholds(); const thresholdLow = mapThresholds?.threshold_low ?? 20; const thresholdMedium = mapThresholds?.threshold_medium ?? 50; const thresholdHigh = mapThresholds?.threshold_high ?? 100; useEffect(() => { if (mapThresholdError) { // Silently fall back to defaults if fetch fails console.warn("Failed to load map color thresholds:", mapThresholdError); } }, [mapThresholdError]); /** Bans visible in the companion table (filtered by selected country). */ const visibleBans = useMemo(() => { if (!selectedCountry) return bans; return bans.filter((b) => b.country_code === selectedCountry); }, [bans, selectedCountry]); const selectedCountryName = selectedCountry ? (countryNames[selectedCountry] ?? selectedCountry) : null; return (
{/* ---------------------------------------------------------------- */} {/* Header row */} {/* ---------------------------------------------------------------- */}
World Map
{ setRange(value); setSelectedCountry(null); }} originFilter={originFilter} onOriginFilterChange={(value) => { setOriginFilter(value); setSelectedCountry(null); }} />
{/* ---------------------------------------------------------------- */} {/* Error / loading states */} {/* ---------------------------------------------------------------- */} {error && ( {error} )} {loading && !error && (
)} {/* ---------------------------------------------------------------- */} {/* World map */} {/* ---------------------------------------------------------------- */} {!loading && !error && ( )} {/* ---------------------------------------------------------------- */} {/* Active country filter info bar */} {/* ---------------------------------------------------------------- */} {selectedCountry && (
Showing {String(visibleBans.length)} bans from{" "} {selectedCountryName ?? selectedCountry} {" "}({String(countries[selectedCountry] ?? 0)} total in window)
)} {/* ---------------------------------------------------------------- */} {/* Summary line */} {/* ---------------------------------------------------------------- */} {!loading && !error && ( {String(total)} total ban{total !== 1 ? "s" : ""} in the selected period {" · "} {String(Object.keys(countries).length)} countr{Object.keys(countries).length !== 1 ? "ies" : "y"} affected )} {/* ---------------------------------------------------------------- */} {/* Companion bans table */} {/* ---------------------------------------------------------------- */} {!loading && !error && (
IP Address Jail Banned At Country Origin Times Banned {visibleBans.length === 0 ? ( No bans found. ) : ( visibleBans.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)} )) )}
)}
); }