/** * Jail detail page. * * Displays full configuration and state for a single fail2ban jail: * - Status badges and control buttons (start, stop, idle, reload) * - Log paths, fail-regex, ignore-regex patterns * - Date pattern, encoding, and actions * - Ignore list management (add / remove IPs) */ import { useState } from "react"; import { Badge, Button, Field, Input, MessageBar, MessageBarBody, Spinner, Switch, Text, Tooltip, makeStyles, tokens, } from "@fluentui/react-components"; import { useCommonSectionStyles } from "../theme/commonStyles"; import { ArrowClockwiseRegular, ArrowLeftRegular, ArrowSyncRegular, DismissRegular, PauseRegular, PlayRegular, StopRegular, } from "@fluentui/react-icons"; import { Link, useNavigate, useParams } from "react-router-dom"; import { useJailDetail, useJailBannedIps } from "../hooks/useJails"; import { formatSeconds } from "../utils/formatDate"; import type { Jail } from "../types/jail"; import { BannedIpsSection } from "../components/jail/BannedIpsSection"; // --------------------------------------------------------------------------- // Styles // --------------------------------------------------------------------------- const useStyles = makeStyles({ root: { display: "flex", flexDirection: "column", gap: tokens.spacingVerticalL, }, breadcrumb: { display: "flex", alignItems: "center", gap: tokens.spacingHorizontalS, }, headerRow: { display: "flex", alignItems: "center", gap: tokens.spacingHorizontalM, flexWrap: "wrap", }, controlRow: { display: "flex", flexWrap: "wrap", gap: tokens.spacingHorizontalS, }, grid: { display: "grid", gridTemplateColumns: "max-content 1fr", gap: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalM}`, alignItems: "baseline", }, label: { fontWeight: tokens.fontWeightSemibold, color: tokens.colorNeutralForeground2, }, mono: { fontFamily: "Consolas, 'Courier New', monospace", fontSize: tokens.fontSizeBase200, }, codeList: { display: "flex", flexDirection: "column", gap: tokens.spacingVerticalXXS, paddingTop: tokens.spacingVerticalXS, }, codeItem: { fontFamily: "Consolas, 'Courier New', monospace", fontSize: tokens.fontSizeBase200, padding: `2px ${tokens.spacingHorizontalS}`, backgroundColor: tokens.colorNeutralBackground2, borderRadius: tokens.borderRadiusSmall, wordBreak: "break-all", }, centred: { display: "flex", justifyContent: "center", alignItems: "center", padding: tokens.spacingVerticalXXL, }, ignoreRow: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: tokens.spacingHorizontalS, padding: `${tokens.spacingVerticalXXS} ${tokens.spacingHorizontalS}`, backgroundColor: tokens.colorNeutralBackground2, borderRadius: tokens.borderRadiusSmall, }, formRow: { display: "flex", gap: tokens.spacingHorizontalM, alignItems: "flex-end", flexWrap: "wrap", }, formField: { minWidth: "200px", flexGrow: 1 }, }); // --------------------------------------------------------------------------- // Components // --------------------------------------------------------------------------- function CodeList({ items, empty }: { items: string[]; empty: string }): React.JSX.Element { const styles = useStyles(); if (items.length === 0) { return {empty}; } return (
{items.map((item, i) => ( {item} ))}
); } // --------------------------------------------------------------------------- // Sub-component: Jail info card // --------------------------------------------------------------------------- interface JailInfoProps { jail: Jail; onRefresh: () => void; onStart: () => Promise; onStop: () => Promise; onSetIdle: (on: boolean) => Promise; onReload: () => Promise; } function JailInfoSection({ jail, onRefresh, onStart, onStop, onSetIdle, onReload }: JailInfoProps): React.JSX.Element { const styles = useStyles(); const sectionStyles = useCommonSectionStyles(); const navigate = useNavigate(); const [ctrlError, setCtrlError] = useState(null); const handle = (fn: () => Promise, postNavigate = false) => (): void => { setCtrlError(null); fn() .then(() => { if (postNavigate) { navigate("/jails"); } else { onRefresh(); } }) .catch((err: unknown) => { const msg = err instanceof Error ? err.message : String(err); setCtrlError(msg); }); }; return (
{jail.name} {jail.running ? ( jail.idle ? ( idle ) : ( running ) ) : ( stopped )}
{ctrlError && ( {ctrlError} )} {/* Control buttons */}
{jail.running ? ( ) : ( )}
{/* Stats grid */} {jail.status && (
Currently banned: {String(jail.status.currently_banned)} Total banned: {String(jail.status.total_banned)} Currently failed: {String(jail.status.currently_failed)} Total failed: {String(jail.status.total_failed)}
)} {/* Config grid */}
Backend: {jail.backend} Find time: {formatSeconds(jail.find_time)} Ban time: {formatSeconds(jail.ban_time)} Max retry: {String(jail.max_retry)} {jail.date_pattern && ( <> Date pattern: {jail.date_pattern} )} {jail.log_encoding && ( <> Log encoding: {jail.log_encoding} )}
); } // --------------------------------------------------------------------------- // Sub-component: Patterns section // --------------------------------------------------------------------------- function PatternsSection({ jail }: { jail: Jail }): React.JSX.Element { const sectionStyles = useCommonSectionStyles(); return (
Log Paths & Patterns
Log Paths Fail Regex Ignore Regex {jail.actions.length > 0 && ( <> Actions )}
); } // --------------------------------------------------------------------------- // Sub-component: Ban-time escalation section // --------------------------------------------------------------------------- function BantimeEscalationSection({ jail }: { jail: Jail }): React.JSX.Element | null { const styles = useStyles(); const sectionStyles = useCommonSectionStyles(); const esc = jail.bantime_escalation; if (!esc?.increment) return null; return (
Ban-time Escalation enabled
{esc.factor !== null && ( <> Factor: {String(esc.factor)} )} {esc.formula && ( <> Formula: {esc.formula} )} {esc.multipliers && ( <> Multipliers: {esc.multipliers} )} {esc.max_time !== null && ( <> Max time: {formatSeconds(esc.max_time)} )} {esc.rnd_time !== null && ( <> Random jitter: {formatSeconds(esc.rnd_time)} )} Count across all jails: {esc.overall_jails ? "yes" : "no"}
); } // --------------------------------------------------------------------------- // Sub-component: Ignore list section // --------------------------------------------------------------------------- interface IgnoreListSectionProps { jailName: string; ignoreList: string[]; ignoreSelf: boolean; onAdd: (ip: string) => Promise; onRemove: (ip: string) => Promise; onToggleIgnoreSelf: (on: boolean) => Promise; } function IgnoreListSection({ jailName: _jailName, ignoreList, ignoreSelf, onAdd, onRemove, onToggleIgnoreSelf, }: IgnoreListSectionProps): React.JSX.Element { const styles = useStyles(); const sectionStyles = useCommonSectionStyles(); const [inputVal, setInputVal] = useState(""); const [opError, setOpError] = useState(null); const handleAdd = (): void => { if (!inputVal.trim()) return; setOpError(null); onAdd(inputVal.trim()) .then(() => { setInputVal(""); }) .catch((err: unknown) => { const msg = err instanceof Error ? err.message : String(err); setOpError(msg); }); }; const handleRemove = (ip: string): void => { setOpError(null); onRemove(ip).catch((err: unknown) => { const msg = err instanceof Error ? err.message : String(err); setOpError(msg); }); }; return (
Ignore List (IP Whitelist)
{String(ignoreList.length)}
{/* Ignore-self toggle */} { onToggleIgnoreSelf(data.checked).catch((err: unknown) => { const msg = err instanceof Error ? err.message : String(err); setOpError(msg); }); }} /> {opError && ( {opError} )} {/* Add form */}
{ setInputVal(d.value); }} onKeyDown={(e) => { if (e.key === "Enter") handleAdd(); }} />
{/* List */} {ignoreList.length === 0 ? ( The ignore list is empty. ) : (
{ignoreList.map((ip) => (
{ip}
))}
)}
); } // --------------------------------------------------------------------------- // Page component // --------------------------------------------------------------------------- /** * Jail detail page. * * Fetches and displays the full configuration and state of a single jail * identified by the `:name` route parameter. */ export function JailDetailPage(): React.JSX.Element { const styles = useStyles(); const { name = "" } = useParams<{ name: string }>(); const { jail, ignoreList, ignoreSelf, loading, error, refresh, addIp, removeIp, toggleIgnoreSelf, start, stop, reload, setIdle } = useJailDetail(name); const { items, total, page, pageSize, search, loading: bannedLoading, error: bannedError, opError, refresh: refreshBanned, setPage, setPageSize, setSearch, unban, } = useJailBannedIps(name); if (loading && !jail) { return (
); } if (error) { return (
Failed to load jail {name}: {error}
); } if (!jail) return <>; return (
{/* Breadcrumb */}
/ {name}
); }