Task 11: Remove direct API calls from components

This commit is contained in:
2026-04-18 21:20:45 +02:00
parent 3f197b1ad7
commit 2105f8b435
21 changed files with 712 additions and 266 deletions

View File

@@ -32,19 +32,8 @@ import {
Play24Regular,
} from "@fluentui/react-icons";
import { ApiError } from "../../api/client";
import {
addLogPath,
deactivateJail,
deleteJailLocalOverride,
deleteLogPath,
fetchInactiveJails,
fetchJailConfigFileContent,
updateJailConfigFile,
validateJailConfig,
} from "../../api/config";
import type {
AddLogPathRequest,
ConfFileUpdateRequest,
InactiveJail,
JailConfig,
JailConfigUpdate,
@@ -54,6 +43,8 @@ import type {
import { useAutoSave } from "../../hooks/useAutoSave";
import { useConfigActiveStatus } from "../../hooks/useConfigActiveStatus";
import { useJailConfigs } from "../../hooks/useConfig";
import { useJailAdmin } from "../../hooks/useJailAdmin";
import { useJailConfigOperations } from "../../hooks/useJailConfigOperations";
import { ActivateJailDialog } from "./ActivateJailDialog";
import { AutoSaveIndicator } from "./AutoSaveIndicator";
import { ConfigListDetail } from "./ConfigListDetail";
@@ -166,12 +157,19 @@ function JailConfigDetail({
esc0?.overall_jails ?? false,
);
const {
addLogPath: addLogPathToJail,
deleteLogPath: deleteJailLogPath,
fetchRawContent,
saveRawContent,
} = useJailConfigOperations(jail.name);
const handleDeleteLogPath = useCallback(
async (path: string) => {
setDeletingPath(path);
setMsg(null);
try {
await deleteLogPath(jail.name, path);
await deleteJailLogPath(path);
setLogPaths((prev) => prev.filter((p) => p !== path));
setMsg({ text: `Removed log path: ${path}`, ok: true });
} catch (err: unknown) {
@@ -183,7 +181,7 @@ function JailConfigDetail({
setDeletingPath(null);
}
},
[jail.name],
[deleteJailLogPath],
);
const handleAddLogPath = useCallback(async () => {
@@ -193,7 +191,7 @@ function JailConfigDetail({
setMsg(null);
try {
const req: AddLogPathRequest = { log_path: trimmed, tail: newLogPathTail };
await addLogPath(jail.name, req);
await addLogPathToJail(req);
setLogPaths((prev) => [...prev, trimmed]);
setNewLogPath("");
setMsg({ text: `Added log path: ${trimmed}`, ok: true });
@@ -205,7 +203,7 @@ function JailConfigDetail({
} finally {
setAddingLogPath(false);
}
}, [jail.name, newLogPath, newLogPathTail]);
}, [addLogPathToJail, newLogPath, newLogPathTail]);
const autoSavePayload = useMemo<JailConfigUpdate>(
() => ({
@@ -249,16 +247,14 @@ function JailConfigDetail({
// Raw config file fetch/save helpers — uses jail.d/<name>.conf convention.
const fetchRaw = useCallback(async (): Promise<string> => {
const result = await fetchJailConfigFileContent(`${jail.name}.conf`);
return result.content;
}, [jail.name]);
return await fetchRawContent();
}, [fetchRawContent]);
const saveRaw = useCallback(
async (content: string): Promise<void> => {
const req: ConfFileUpdateRequest = { content };
await updateJailConfigFile(`${jail.name}.conf`, req);
await saveRawContent(content);
},
[jail.name],
[saveRawContent],
);
return (
@@ -629,6 +625,7 @@ interface InactiveJailDetailProps {
onActivate: () => void;
/** Called when the user requests removal of the .local override file. */
onDeactivate?: () => void;
onValidate: () => Promise<JailValidationResult>;
}
/**
@@ -646,6 +643,7 @@ function InactiveJailDetail({
jail,
onActivate,
onDeactivate,
onValidate,
}: InactiveJailDetailProps): React.JSX.Element {
const styles = useConfigStyles();
const [validating, setValidating] = useState(false);
@@ -654,11 +652,11 @@ function InactiveJailDetail({
const handleValidate = useCallback((): void => {
setValidating(true);
setValidationResult(null);
validateJailConfig(jail.name)
onValidate()
.then((result) => { setValidationResult(result); })
.catch(() => { /* validation call failed — ignore */ })
.finally(() => { setValidating(false); });
}, [jail.name]);
}, [onValidate]);
const blockingIssues: JailValidationIssue[] =
validationResult?.issues.filter((i) => i.field !== "logpath") ?? [];
@@ -767,51 +765,51 @@ export function JailsTab({ initialJail }: JailsTabProps): React.JSX.Element {
const { jails, loading, error, refresh, updateJail } =
useJailConfigs();
const { activeJails } = useConfigActiveStatus();
const {
inactiveJails,
inactiveLoading,
refreshInactiveJails,
deactivateJail,
deleteJailLocalOverride,
validateJailConfig,
activateJail,
createJailConfigFile,
} = useJailAdmin();
const [selectedName, setSelectedName] = useState<string | null>(null);
const [createDialogOpen, setCreateDialogOpen] = useState(false);
// Inactive jails
const [inactiveJails, setInactiveJails] = useState<InactiveJail[]>([]);
const [inactiveLoading, setInactiveLoading] = useState(false);
const [activateTarget, setActivateTarget] = useState<InactiveJail | null>(null);
const loadInactive = useCallback((): void => {
setInactiveLoading(true);
fetchInactiveJails()
.then((res) => { setInactiveJails(res.jails); })
.catch(() => { /* non-critical — active-only view still works */ })
.finally(() => { setInactiveLoading(false); });
}, []);
useEffect(() => {
loadInactive();
}, [loadInactive]);
const handleDeactivate = useCallback((name: string): void => {
deactivateJail(name)
.then(() => {
const handleDeactivate = useCallback(
async (name: string): Promise<void> => {
try {
await deactivateJail(name);
setSelectedName(null);
refresh();
loadInactive();
})
.catch(() => { /* non-critical — list refreshes on next load */ });
}, [refresh, loadInactive]);
refreshInactiveJails();
} catch {
/* non-critical — list refreshes on next load */ }
},
[deactivateJail, refresh, refreshInactiveJails],
);
const handleDeactivateInactive = useCallback((name: string): void => {
deleteJailLocalOverride(name)
.then(() => {
const handleDeactivateInactive = useCallback(
async (name: string): Promise<void> => {
try {
await deleteJailLocalOverride(name);
setSelectedName(null);
loadInactive();
})
.catch(() => { /* non-critical — list refreshes on next load */ });
}, [loadInactive]);
refreshInactiveJails();
} catch {
/* non-critical — list refreshes on next load */ }
},
[deleteJailLocalOverride, refreshInactiveJails],
);
const handleActivated = useCallback((): void => {
setActivateTarget(null);
setSelectedName(null);
refresh();
loadInactive();
}, [refresh, loadInactive]);
refreshInactiveJails();
}, [refresh, refreshInactiveJails]);
/** Unified list items: active jails first (from useJailConfigs), then inactive. */
const listItems = useMemo<Array<{ name: string; kind: "active" | "inactive" }>>(() => {
@@ -924,6 +922,7 @@ export function JailsTab({ initialJail }: JailsTabProps): React.JSX.Element {
? (): void => { handleDeactivateInactive(selectedInactiveJail.name); }
: undefined
}
onValidate={() => validateJailConfig(selectedInactiveJail.name)}
/>
) : null}
</ConfigListDetail>
@@ -934,15 +933,36 @@ export function JailsTab({ initialJail }: JailsTabProps): React.JSX.Element {
open={activateTarget !== null}
onClose={() => { setActivateTarget(null); }}
onActivated={handleActivated}
onValidate={async () => {
if (!activateTarget) {
return { jail_name: "", valid: false, issues: [] };
}
return await validateJailConfig(activateTarget.name);
}}
onActivate={async (payload) => {
if (!activateTarget) {
return {
name: "",
active: false,
message: "No jail selected.",
fail2ban_running: false,
validation_warnings: [],
};
}
return await activateJail(activateTarget.name, payload);
}}
/>
<CreateJailDialog
open={createDialogOpen}
onClose={() => { setCreateDialogOpen(false); }}
onCreateJail={async (payload) => {
await createJailConfigFile(payload);
}}
onCreated={() => {
setCreateDialogOpen(false);
refresh();
loadInactive();
refreshInactiveJails();
}}
/>
</div>