/**
* 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); }}
/>
}
size="small"
appearance="subtle"
onClick={() => { handleDelete(index); }}
aria-label="Remove entry"
/>
))}
}
size="small"
appearance="outline"
onClick={handleAdd}
style={{ marginTop: 4 }}
>
{addLabel}
);
}
// ---------------------------------------------------------------------------
// 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 (
);
}
// ---------------------------------------------------------------------------
// 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 */}
{/* 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 */}
{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 ;
}