/** * JailFileForm — structured form editor for a single ``jail.d/*.conf`` file. * * Renders each jail section in the file as an accordion panel with fields for * all common jail settings plus log paths, action references, and extra keys. * * All fields auto-save through ``useAutoSave``. */ import { useCallback, useEffect, useMemo, useState } from "react"; import { Accordion, AccordionHeader, AccordionItem, AccordionPanel, Badge, Button, Field, Input, MessageBar, MessageBarBody, Select, Skeleton, SkeletonItem, Switch, Text, tokens, } from "@fluentui/react-components"; import { Add24Regular, Delete24Regular, DocumentAdd24Regular } from "@fluentui/react-icons"; import type { JailFileConfig, JailFileConfigUpdate, JailSectionConfig } from "../../types/config"; import { useJailFileConfig } from "../../hooks/useJailFileConfig"; import { useAutoSave } from "../../hooks/useAutoSave"; import { AutoSaveIndicator } from "./AutoSaveIndicator"; import { useConfigStyles } from "./configStyles"; // --------------------------------------------------------------------------- // StringListEditor — add / remove lines in a list field // --------------------------------------------------------------------------- interface StringListEditorProps { items: string[]; onChange: (next: string[]) => void; placeholder?: string; addLabel?: string; } function StringListEditor({ items, onChange, placeholder, addLabel = "Add entry", }: StringListEditorProps): React.JSX.Element { const handleChange = (index: number, value: string): void => { const next = [...items]; next[index] = value; onChange(next); }; const handleDelete = (index: number): void => { onChange(items.filter((_, i) => i !== index)); }; const handleAdd = (): void => { onChange([...items, ""]); }; return (
{items.map((item, index) => (
{ handleChange(index, d.value); }} />
))}
); } // --------------------------------------------------------------------------- // KVEditor — key-value pair list (for extra settings) // --------------------------------------------------------------------------- interface KVEditorProps { entries: Record; onChange: (next: Record) => void; } function KVEditor({ entries, onChange }: KVEditorProps): React.JSX.Element { const styles = useConfigStyles(); const rows = Object.entries(entries); const handleKeyChange = (oldKey: string, newKey: string): void => { const next: Record = {}; for (const [k, v] of Object.entries(entries)) { next[k === oldKey ? newKey : k] = v; } onChange(next); }; const handleValueChange = (key: string, value: string): void => { onChange({ ...entries, [key]: value }); }; const handleDelete = (key: string): void => { const { [key]: _removed, ...rest } = entries; onChange(rest); }; const handleAdd = (): void => { let newKey = "key"; let n = 1; while (newKey in entries) { newKey = `key${String(n)}`; n++; } onChange({ ...entries, [newKey]: "" }); }; return (
{rows.map(([key, value]) => (
{ handleKeyChange(key, d.value); }} /> { handleValueChange(key, d.value); }} />
))}
); } // --------------------------------------------------------------------------- // JailSectionPanel — form fields for one [jailname] section // --------------------------------------------------------------------------- const BACKENDS = ["", "auto", "polling", "gamin", "pyinotify", "systemd"] as const; interface JailSectionPanelProps { jailName: string; section: JailSectionConfig; onChange: (next: JailSectionConfig) => void; } function JailSectionPanel({ jailName, section, onChange, }: JailSectionPanelProps): React.JSX.Element { const styles = useConfigStyles(); const update = useCallback( (patch: Partial): void => { onChange({ ...section, ...patch }); }, [onChange, section] ); return (
{/* Core fields grid */}
{ update({ enabled: d.checked }); }} aria-label={`Enable jail ${jailName}`} /> { update({ port: d.value || null }); }} /> { update({ filter: d.value || null }); }} /> { const n = parseInt(d.value, 10); update({ maxretry: d.value === "" ? null : isNaN(n) ? null : n }); }} /> { const n = parseInt(d.value, 10); update({ findtime: d.value === "" ? null : isNaN(n) ? null : n }); }} /> { const n = parseInt(d.value, 10); update({ bantime: d.value === "" ? null : isNaN(n) ? null : n }); }} />
{/* Log paths */} Log Paths ({section.logpath.length})
{ update({ logpath: next }); }} placeholder="e.g. /var/log/auth.log" addLabel="Add log path" />
{/* Actions */} Actions ({section.action.length})
{ update({ action: next }); }} placeholder="e.g. iptables-multiport[name=SSH, port=ssh]" addLabel="Add action" />
{/* Extra settings */} {(Object.keys(section.extra).length > 0) && ( Extra Settings ({Object.keys(section.extra).length})
{ update({ extra: next }); }} />
)}
); } // --------------------------------------------------------------------------- // JailFileFormInner — rendered once config is loaded // --------------------------------------------------------------------------- interface JailFileFormInnerProps { config: JailFileConfig; onSave: (update: JailFileConfigUpdate) => Promise; } function JailFileFormInner({ config, onSave, }: JailFileFormInnerProps): React.JSX.Element { const [jails, setJails] = useState>(config.jails); // Reset when a freshly-loaded config arrives. useEffect(() => { setJails(config.jails); }, [config]); const autoSavePayload = useMemo( () => ({ jails }), [jails] ); const { status: saveStatus, errorText: saveErrorText, retry: retrySave } = useAutoSave(autoSavePayload, onSave); const handleSectionChange = useCallback( (jailName: string, next: JailSectionConfig): void => { setJails((prev) => ({ ...prev, [jailName]: next })); }, [] ); const styles = useConfigStyles(); const jailNames = Object.keys(jails); return (
{/* Header row */}
{config.filename}
{jailNames.length === 0 ? (
No jail sections found in this file. Add a [jailname] section to the file to define jails.
) : ( {jailNames.map((jailName) => { const section = jails[jailName]; if (section === undefined) return null; return ( {jailName} {section.enabled === true ? "(enabled)" : section.enabled === false ? "(disabled)" : ""} { handleSectionChange(jailName, next); }} /> ); })} )}
); } // --------------------------------------------------------------------------- // JailFileForm — public component // --------------------------------------------------------------------------- interface JailFileFormProps { filename: string; } /** * Load and render a structured form editor for a ``jail.d/*.conf`` file. * * @param filename - Filename including extension (e.g. ``"sshd.conf"``). */ export function JailFileForm({ filename }: JailFileFormProps): React.JSX.Element { const { config, loading, error, save } = useJailFileConfig(filename); if (loading) { return (
); } if (error !== null) { return ( {error} ); } if (config === null) { return <>; } return ; }