feat(frontend): add ConfigListDetail, RawConfigSection components and useConfigActiveStatus hook
- ConfigListDetail: reusable two-pane master/detail layout (list + detail) with active/inactive badges, sorted active-first, keyboard navigation, and responsive collapse to Dropdown below 900 px - RawConfigSection: collapsible raw-text editor with save/feedback for any config file, backed by configurable fetch/save callbacks - useConfigActiveStatus: hook that derives active jail, filter, and action sets from the live jails list and jail config data
This commit is contained in:
137
frontend/src/hooks/useConfigActiveStatus.ts
Normal file
137
frontend/src/hooks/useConfigActiveStatus.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* useConfigActiveStatus — compute active status for jails, filters, and
|
||||
* actions by correlating runtime jail data with their config files.
|
||||
*
|
||||
* Active determination rules:
|
||||
* - Jail: `enabled === true` in the live JailSummary list.
|
||||
* - Filter: referenced by at least one enabled jail (jail name = filter name
|
||||
* by fail2ban convention, unless a filter field is provided).
|
||||
* - Action: referenced in the `actions` array of at least one enabled jail's
|
||||
* JailConfig.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { fetchJails } from "../api/jails";
|
||||
import {
|
||||
fetchActionFiles,
|
||||
fetchFilterFiles,
|
||||
fetchJailConfigs,
|
||||
} from "../api/config";
|
||||
import type { JailConfig } from "../types/config";
|
||||
import type { JailSummary } from "../types/jail";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public interface
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface UseConfigActiveStatusResult {
|
||||
/** Names of jails that are currently enabled. */
|
||||
activeJails: Set<string>;
|
||||
/** Names of filter files referenced by at least one enabled jail. */
|
||||
activeFilters: Set<string>;
|
||||
/** Names of action files referenced by at least one enabled jail. */
|
||||
activeActions: Set<string>;
|
||||
/** True while any parallel fetch is in progress. */
|
||||
loading: boolean;
|
||||
/** Error message from any failed fetch, or null. */
|
||||
error: string | null;
|
||||
/** Re-fetch all data sources. */
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Fetch jails, jail configs, filter files, and action files in parallel and
|
||||
* derive active-status sets for each config type.
|
||||
*
|
||||
* @returns Active-status sets, loading flag, error, and refresh function.
|
||||
*/
|
||||
export function useConfigActiveStatus(): UseConfigActiveStatusResult {
|
||||
const [activeJails, setActiveJails] = useState<Set<string>>(new Set());
|
||||
const [activeFilters, setActiveFilters] = useState<Set<string>>(new Set());
|
||||
const [activeActions, setActiveActions] = useState<Set<string>>(new Set());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const load = useCallback((): void => {
|
||||
abortRef.current?.abort();
|
||||
const ctrl = new AbortController();
|
||||
abortRef.current = ctrl;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
Promise.all([
|
||||
fetchJails(),
|
||||
fetchJailConfigs(),
|
||||
fetchFilterFiles(),
|
||||
fetchActionFiles(),
|
||||
])
|
||||
.then(([jailsResp, configsResp, _filterResp, _actionResp]) => {
|
||||
if (ctrl.signal.aborted) return;
|
||||
|
||||
const summaries: JailSummary[] = jailsResp.jails;
|
||||
const configs: JailConfig[] = configsResp.jails;
|
||||
|
||||
// Active jails: enabled in the runtime summary list.
|
||||
const jailSet = new Set<string>(
|
||||
summaries.filter((j) => j.enabled).map((j) => j.name),
|
||||
);
|
||||
|
||||
// Active filters: referenced by any enabled jail.
|
||||
// By fail2ban convention the filter name equals the jail name unless
|
||||
// the config explicitly declares a [filter] section (not surfaced in
|
||||
// JailConfig), so we use the jail name as the filter name.
|
||||
const filterSet = new Set<string>();
|
||||
for (const cfg of configs) {
|
||||
if (jailSet.has(cfg.name)) {
|
||||
filterSet.add(cfg.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Active actions: referenced in the actions array of any enabled jail.
|
||||
const actionSet = new Set<string>();
|
||||
for (const cfg of configs) {
|
||||
if (jailSet.has(cfg.name)) {
|
||||
for (const action of cfg.actions) {
|
||||
// Actions can be stored as "name[param=val]" — extract base name.
|
||||
const bracketIdx = action.indexOf("[");
|
||||
const baseName = (bracketIdx >= 0 ? action.slice(0, bracketIdx) : action).trim();
|
||||
actionSet.add(baseName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setActiveJails(jailSet);
|
||||
setActiveFilters(filterSet);
|
||||
setActiveActions(actionSet);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (ctrl.signal.aborted) return;
|
||||
setError(err instanceof Error ? err.message : "Failed to load status.");
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
return (): void => {
|
||||
abortRef.current?.abort();
|
||||
};
|
||||
}, [load]);
|
||||
|
||||
return {
|
||||
activeJails,
|
||||
activeFilters,
|
||||
activeActions,
|
||||
loading,
|
||||
error,
|
||||
refresh: load,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user