/** * JailFilesTab — manage jail.d config files with a structured form editor. * * Displays all jail.d config files in a collapsible accordion. Each file * panel renders a ``JailFileForm`` with per-section auto-save editing. The * file-level enable/disable toggle remains in the accordion header. * * A "Create new jail file" section at the bottom allows adding new files. */ import { useCallback, useEffect, useState } from "react"; import { Accordion, AccordionHeader, AccordionItem, AccordionPanel, Button, Field, Input, MessageBar, MessageBarBody, Skeleton, SkeletonItem, Switch, Text, tokens, } from "@fluentui/react-components"; import { Add24Regular, ArrowClockwise24Regular, DocumentAdd24Regular } from "@fluentui/react-icons"; import { ApiError } from "../../api/client"; import { createJailConfigFile, fetchJailConfigFiles, setJailConfigFileEnabled, } from "../../api/config"; import type { JailConfigFile } from "../../types/config"; import { JailFileForm } from "./JailFileForm"; import { useConfigStyles } from "./configStyles"; /** * Tab component for managing jail.d configuration files with structured forms. * * @returns JSX element. */ export function JailFilesTab(): React.JSX.Element { const styles = useConfigStyles(); const [files, setFiles] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [toggling, setToggling] = useState(null); const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null); // Create new file form state const [newFilename, setNewFilename] = useState(""); const [creating, setCreating] = useState(false); const [createError, setCreateError] = useState(null); const loadFiles = useCallback(async (): Promise => { setLoading(true); setError(null); try { const resp = await fetchJailConfigFiles(); setFiles(resp.files); } catch (err: unknown) { setError( err instanceof ApiError ? err.message : "Failed to load jail config files.", ); } finally { setLoading(false); } }, []); useEffect(() => { void loadFiles(); }, [loadFiles]); const handleToggleEnabled = useCallback( async (filename: string, enabled: boolean): Promise => { setToggling(filename); setMsg(null); try { await setJailConfigFileEnabled(filename, { enabled }); setFiles((prev) => prev.map((f) => (f.filename === filename ? { ...f, enabled } : f)), ); setMsg({ text: `${filename} ${enabled ? "enabled" : "disabled"}.`, ok: true }); } catch (err: unknown) { setMsg({ text: err instanceof ApiError ? err.message : "Toggle failed.", ok: false, }); } finally { setToggling(null); } }, [], ); const handleCreate = useCallback(async (): Promise => { const name = newFilename.trim(); if (!name) return; setCreating(true); setCreateError(null); try { await createJailConfigFile({ name, content: `# ${name}\n` }); setNewFilename(""); await loadFiles(); } catch (err: unknown) { setCreateError(err instanceof ApiError ? err.message : "Failed to create file."); } finally { setCreating(false); } }, [newFilename, loadFiles]); if (loading) { return ( {[0, 1, 2].map((i) => ( ))} ); } if (error) { return ( {error} ); } return (
Files in jail.d/. Toggle the switch to enable or disable a jail config file. Changes take effect on the next fail2ban reload. {msg !== null && ( {msg.text} )}
{files.length === 0 && (
No files found in jail.d/. Use the form below to create your first jail config file.
)} {files.map((file) => ( {file.filename} { void handleToggleEnabled(file.filename, d.checked); }} onClick={(e) => { e.stopPropagation(); }} /> ))} {/* Create new jail file */}
Create New Jail File {createError !== null && ( {createError} )}
{ setNewFilename(d.value); }} onKeyDown={(e) => { if (e.key === "Enter") { void handleCreate(); } }} />
); }