Merge Global tab into Server tab and remove Global tab

Global tab provided the same four editable fields as Server tab:
log_level, log_target, db_purge_age, db_max_matches. Server tab already
has these fields plus additional read-only info (db_path, syslog_socket)
and a Flush Logs button.

- Add hint text to DB Purge Age and DB Max Matches fields in ServerTab
- Remove GlobalTab component import from ConfigPage
- Remove 'global' from TabValue type
- Remove Global tab element from TabList
- Remove conditional render for GlobalTab
- Remove GlobalTab from barrel export (index.ts)
- Delete GlobalTab.tsx file
- Update ConfigPage test to remove Global tab test case

All 123 frontend tests pass.
This commit is contained in:
2026-03-14 21:52:44 +01:00
parent 2e1a4b3b2b
commit 037c18eb00
6 changed files with 150 additions and 236 deletions

View File

@@ -1,142 +0,0 @@
/**
* GlobalTab — global fail2ban settings editor.
*
* Provides form fields for log level, log target, database purge age,
* and database max matches.
*/
import { useEffect, useMemo, useState } from "react";
import {
Field,
Input,
MessageBar,
MessageBarBody,
Select,
Spinner,
} from "@fluentui/react-components";
import type { GlobalConfigUpdate } from "../../types/config";
import { useGlobalConfig } from "../../hooks/useConfig";
import { useAutoSave } from "../../hooks/useAutoSave";
import { AutoSaveIndicator } from "./AutoSaveIndicator";
import { useConfigStyles } from "./configStyles";
/** Available fail2ban log levels in descending severity order. */
const LOG_LEVELS = ["CRITICAL", "ERROR", "WARNING", "NOTICE", "INFO", "DEBUG"];
/**
* Tab component for editing global fail2ban configuration.
*
* @returns JSX element.
*/
export function GlobalTab(): React.JSX.Element {
const styles = useConfigStyles();
const { config, loading, error, updateConfig } = useGlobalConfig();
const [logLevel, setLogLevel] = useState("");
const [logTarget, setLogTarget] = useState("");
const [dbPurgeAge, setDbPurgeAge] = useState("");
const [dbMaxMatches, setDbMaxMatches] = useState("");
// Sync local state when config loads for the first time.
useEffect(() => {
if (config && logLevel === "") {
setLogLevel(config.log_level);
setLogTarget(config.log_target);
setDbPurgeAge(String(config.db_purge_age));
setDbMaxMatches(String(config.db_max_matches));
}
// Only run on first config load.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]);
const effectiveLogLevel = logLevel || config?.log_level || "";
const effectiveLogTarget = logTarget || config?.log_target || "";
const effectiveDbPurgeAge =
dbPurgeAge || (config ? String(config.db_purge_age) : "");
const effectiveDbMaxMatches =
dbMaxMatches || (config ? String(config.db_max_matches) : "");
const updatePayload = useMemo<GlobalConfigUpdate>(() => {
const update: GlobalConfigUpdate = {};
if (effectiveLogLevel) update.log_level = effectiveLogLevel;
if (effectiveLogTarget) update.log_target = effectiveLogTarget;
if (effectiveDbPurgeAge)
update.db_purge_age = Number(effectiveDbPurgeAge);
if (effectiveDbMaxMatches)
update.db_max_matches = Number(effectiveDbMaxMatches);
return update;
}, [effectiveLogLevel, effectiveLogTarget, effectiveDbPurgeAge, effectiveDbMaxMatches]);
const { status: saveStatus, errorText: saveErrorText, retry: retrySave } =
useAutoSave(updatePayload, updateConfig);
if (loading) return <Spinner label="Loading global config…" />;
if (error)
return (
<MessageBar intent="error">
<MessageBarBody>{error}</MessageBarBody>
</MessageBar>
);
return (
<div>
<div className={styles.sectionCard}>
<AutoSaveIndicator
status={saveStatus}
errorText={saveErrorText}
onRetry={retrySave}
/>
<div className={styles.fieldRow}>
<Field label="Log Level">
<Select
value={effectiveLogLevel}
onChange={(_e, d) => {
setLogLevel(d.value);
}}
>
{LOG_LEVELS.map((l) => (
<option key={l} value={l}>
{l}
</option>
))}
</Select>
</Field>
<Field label="Log Target">
<Input
value={effectiveLogTarget}
placeholder="STDOUT / /var/log/fail2ban.log"
onChange={(_e, d) => {
setLogTarget(d.value);
}}
/>
</Field>
</div>
<div className={styles.fieldRow}>
<Field
label="DB Purge Age (s)"
hint="Ban records older than this are removed from the fail2ban database."
>
<Input
type="number"
value={effectiveDbPurgeAge}
onChange={(_e, d) => {
setDbPurgeAge(d.value);
}}
/>
</Field>
<Field
label="DB Max Matches"
hint="Maximum number of log-line matches stored per ban record."
>
<Input
type="number"
value={effectiveDbMaxMatches}
onChange={(_e, d) => {
setDbMaxMatches(d.value);
}}
/>
</Field>
</div>
</div>
</div>
);
}

View File

@@ -154,7 +154,10 @@ export function ServerTab(): React.JSX.Element {
</Field>
</div>
<div className={styles.fieldRow}>
<Field label="DB Purge Age (s)">
<Field
label="DB Purge Age (s)"
hint="Ban records older than this are removed from the fail2ban database."
>
<Input
type="number"
value={effectiveDbPurgeAge}
@@ -163,7 +166,10 @@ export function ServerTab(): React.JSX.Element {
}}
/>
</Field>
<Field label="DB Max Matches">
<Field
label="DB Max Matches"
hint="Maximum number of log-line matches stored per ban record."
>
<Input
type="number"
value={effectiveDbMaxMatches}

View File

@@ -30,7 +30,6 @@ export { ExportTab } from "./ExportTab";
export { FilterForm } from "./FilterForm";
export type { FilterFormProps } from "./FilterForm";
export { FiltersTab } from "./FiltersTab";
export { GlobalTab } from "./GlobalTab";
export { JailFilesTab } from "./JailFilesTab";
export { JailFileForm } from "./JailFileForm";
export { JailsTab } from "./JailsTab";

View File

@@ -8,8 +8,7 @@
* Jails — per-jail config accordion with inline editing
* Filters — structured filter.d form editor
* Actions — structured action.d form editor
* Globalglobal fail2ban settings (log level, DB config)
* Server — server-level settings + flush logs
* Serverserver-level settings, logging, database config + flush logs
* Map — map color threshold configuration
* Regex Tester — live pattern tester
* Export — raw file editors for jail, filter, and action files
@@ -20,7 +19,6 @@ import { Tab, TabList, Text, makeStyles, tokens } from "@fluentui/react-componen
import {
ActionsTab,
FiltersTab,
GlobalTab,
JailsTab,
LogTab,
MapTab,
@@ -58,7 +56,6 @@ type TabValue =
| "jails"
| "filters"
| "actions"
| "global"
| "server"
| "map"
| "regex"
@@ -89,7 +86,6 @@ export function ConfigPage(): React.JSX.Element {
<Tab value="jails">Jails</Tab>
<Tab value="filters">Filters</Tab>
<Tab value="actions">Actions</Tab>
<Tab value="global">Global</Tab>
<Tab value="server">Server</Tab>
<Tab value="map">Map</Tab>
<Tab value="regex">Regex Tester</Tab>
@@ -100,7 +96,6 @@ export function ConfigPage(): React.JSX.Element {
{tab === "jails" && <JailsTab />}
{tab === "filters" && <FiltersTab />}
{tab === "actions" && <ActionsTab />}
{tab === "global" && <GlobalTab />}
{tab === "server" && <ServerTab />}
{tab === "map" && <MapTab />}
{tab === "regex" && <RegexTesterTab />}

View File

@@ -9,7 +9,6 @@ vi.mock("../../components/config", () => ({
JailsTab: () => <div data-testid="jails-tab">JailsTab</div>,
FiltersTab: () => <div data-testid="filters-tab">FiltersTab</div>,
ActionsTab: () => <div data-testid="actions-tab">ActionsTab</div>,
GlobalTab: () => <div data-testid="global-tab">GlobalTab</div>,
ServerTab: () => <div data-testid="server-tab">ServerTab</div>,
MapTab: () => <div data-testid="map-tab">MapTab</div>,
RegexTesterTab: () => <div data-testid="regex-tab">RegexTesterTab</div>,
@@ -45,12 +44,6 @@ describe("ConfigPage", () => {
expect(screen.getByTestId("actions-tab")).toBeInTheDocument();
});
it("switches to Global tab when Global tab is clicked", () => {
renderPage();
fireEvent.click(screen.getByRole("tab", { name: /global/i }));
expect(screen.getByTestId("global-tab")).toBeInTheDocument();
});
it("switches to Server tab when Server tab is clicked", () => {
renderPage();
fireEvent.click(screen.getByRole("tab", { name: /server/i }));