Refactor ServerTab and ConfFilesTab to use reducers
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
* be reused for both filter and action files.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useReducer } from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionHeader,
|
||||
@@ -27,6 +27,88 @@ import { ApiError } from "../../api/client";
|
||||
import type { ConfFileEntry } from "../../types/config";
|
||||
import { useConfigStyles } from "./configStyles";
|
||||
|
||||
interface ConfFilesTabState {
|
||||
files: ConfFileEntry[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
openItems: string[];
|
||||
contents: Record<string, string>;
|
||||
editedContents: Record<string, string>;
|
||||
saving: string | null;
|
||||
msg: { text: string; ok: boolean } | null;
|
||||
newName: string;
|
||||
newContent: string;
|
||||
creating: boolean;
|
||||
}
|
||||
|
||||
type ConfFilesTabAction =
|
||||
| { type: "setFiles"; value: ConfFileEntry[] }
|
||||
| { type: "setLoading"; value: boolean }
|
||||
| { type: "setError"; value: string | null }
|
||||
| { type: "setOpenItems"; value: string[] }
|
||||
| { type: "setContent"; name: string; content: string }
|
||||
| { type: "setEditedContent"; name: string; content: string }
|
||||
| { type: "setSaving"; value: string | null }
|
||||
| { type: "setMsg"; value: { text: string; ok: boolean } | null }
|
||||
| { type: "setNewName"; value: string }
|
||||
| { type: "setNewContent"; value: string }
|
||||
| { type: "setCreating"; value: boolean };
|
||||
|
||||
export const initialConfFilesTabState: ConfFilesTabState = {
|
||||
files: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
openItems: [],
|
||||
contents: {},
|
||||
editedContents: {},
|
||||
saving: null,
|
||||
msg: null,
|
||||
newName: "",
|
||||
newContent: "",
|
||||
creating: false,
|
||||
};
|
||||
|
||||
export function confFilesTabReducer(
|
||||
state: ConfFilesTabState,
|
||||
action: ConfFilesTabAction,
|
||||
): ConfFilesTabState {
|
||||
switch (action.type) {
|
||||
case "setFiles":
|
||||
return { ...state, files: action.value };
|
||||
case "setLoading":
|
||||
return { ...state, loading: action.value };
|
||||
case "setError":
|
||||
return { ...state, error: action.value };
|
||||
case "setOpenItems":
|
||||
return { ...state, openItems: action.value };
|
||||
case "setContent":
|
||||
return {
|
||||
...state,
|
||||
contents: { ...state.contents, [action.name]: action.content },
|
||||
};
|
||||
case "setEditedContent":
|
||||
return {
|
||||
...state,
|
||||
editedContents: {
|
||||
...state.editedContents,
|
||||
[action.name]: action.content,
|
||||
},
|
||||
};
|
||||
case "setSaving":
|
||||
return { ...state, saving: action.value };
|
||||
case "setMsg":
|
||||
return { ...state, msg: action.value };
|
||||
case "setNewName":
|
||||
return { ...state, newName: action.value };
|
||||
case "setNewContent":
|
||||
return { ...state, newContent: action.value };
|
||||
case "setCreating":
|
||||
return { ...state, creating: action.value };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConfFilesTabProps {
|
||||
/** Human-readable label, e.g. "Filter" or "Action". */
|
||||
label: string;
|
||||
@@ -61,37 +143,43 @@ export function ConfFilesTab({
|
||||
createFile,
|
||||
}: ConfFilesTabProps): React.JSX.Element {
|
||||
const styles = useConfigStyles();
|
||||
const [files, setFiles] = useState<ConfFileEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [openItems, setOpenItems] = useState<string[]>([]);
|
||||
const [contents, setContents] = useState<Record<string, string>>({});
|
||||
const [editedContents, setEditedContents] = useState<Record<string, string>>(
|
||||
{},
|
||||
const [state, dispatch] = useReducer(
|
||||
confFilesTabReducer,
|
||||
initialConfFilesTabState,
|
||||
);
|
||||
const [saving, setSaving] = useState<string | null>(null);
|
||||
const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null);
|
||||
const [newName, setNewName] = useState("");
|
||||
const [newContent, setNewContent] = useState("");
|
||||
const [creating, setCreating] = useState(false);
|
||||
const {
|
||||
files,
|
||||
loading,
|
||||
error,
|
||||
openItems,
|
||||
contents,
|
||||
editedContents,
|
||||
saving,
|
||||
msg,
|
||||
newName,
|
||||
newContent,
|
||||
creating,
|
||||
} = state;
|
||||
|
||||
const loadFiles = useCallback(async (signal?: AbortSignal) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
dispatch({ type: "setLoading", value: true });
|
||||
dispatch({ type: "setError", value: null });
|
||||
try {
|
||||
const resp = await fetchList();
|
||||
if (signal?.aborted) return;
|
||||
setFiles(resp.files);
|
||||
dispatch({ type: "setFiles", value: resp.files });
|
||||
} catch (err: unknown) {
|
||||
if (signal?.aborted) return;
|
||||
setError(
|
||||
err instanceof ApiError
|
||||
? err.message
|
||||
: `Failed to load ${label.toLowerCase()} files.`,
|
||||
);
|
||||
dispatch({
|
||||
type: "setError",
|
||||
value:
|
||||
err instanceof ApiError
|
||||
? err.message
|
||||
: `Failed to load ${label.toLowerCase()} files.`,
|
||||
});
|
||||
} finally {
|
||||
if (!signal?.aborted) {
|
||||
setLoading(false);
|
||||
dispatch({ type: "setLoading", value: false });
|
||||
}
|
||||
}
|
||||
}, [fetchList, label]);
|
||||
@@ -111,23 +199,33 @@ export function ConfFilesTab({
|
||||
) => {
|
||||
const next = data.openItems as string[];
|
||||
const newlyOpened = next.filter((v) => !openItems.includes(v));
|
||||
setOpenItems(next);
|
||||
dispatch({ type: "setOpenItems", value: next });
|
||||
for (const name of newlyOpened) {
|
||||
if (!Object.prototype.hasOwnProperty.call(contents, name)) {
|
||||
void fetchFile(name)
|
||||
.then((c) => {
|
||||
setContents((prev) => ({ ...prev, [name]: c.content }));
|
||||
setEditedContents((prev) => ({ ...prev, [name]: c.content }));
|
||||
dispatch({
|
||||
type: "setContent",
|
||||
name,
|
||||
content: c.content,
|
||||
});
|
||||
dispatch({
|
||||
type: "setEditedContent",
|
||||
name,
|
||||
content: c.content,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setContents((prev) => ({
|
||||
...prev,
|
||||
[name]: "(failed to load)",
|
||||
}));
|
||||
setEditedContents((prev) => ({
|
||||
...prev,
|
||||
[name]: "(failed to load)",
|
||||
}));
|
||||
dispatch({
|
||||
type: "setContent",
|
||||
name,
|
||||
content: "(failed to load)",
|
||||
});
|
||||
dispatch({
|
||||
type: "setEditedContent",
|
||||
name,
|
||||
content: "(failed to load)",
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -137,20 +235,23 @@ export function ConfFilesTab({
|
||||
|
||||
const handleSave = useCallback(
|
||||
async (name: string) => {
|
||||
setSaving(name);
|
||||
setMsg(null);
|
||||
dispatch({ type: "setSaving", value: name });
|
||||
dispatch({ type: "setMsg", value: null });
|
||||
try {
|
||||
const content = editedContents[name] ?? contents[name] ?? "";
|
||||
await updateFile(name, { content });
|
||||
setContents((prev) => ({ ...prev, [name]: content }));
|
||||
setMsg({ text: `${name} saved.`, ok: true });
|
||||
dispatch({ type: "setContent", name, content });
|
||||
dispatch({ type: "setMsg", value: { text: `${name} saved.`, ok: true } });
|
||||
} catch (err: unknown) {
|
||||
setMsg({
|
||||
text: err instanceof ApiError ? err.message : "Save failed.",
|
||||
ok: false,
|
||||
dispatch({
|
||||
type: "setMsg",
|
||||
value: {
|
||||
text: err instanceof ApiError ? err.message : "Save failed.",
|
||||
ok: false,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
setSaving(null);
|
||||
dispatch({ type: "setSaving", value: null });
|
||||
}
|
||||
},
|
||||
[editedContents, contents, updateFile],
|
||||
@@ -159,31 +260,45 @@ export function ConfFilesTab({
|
||||
const handleCreate = useCallback(async () => {
|
||||
const name = newName.trim();
|
||||
if (!name) return;
|
||||
setCreating(true);
|
||||
setMsg(null);
|
||||
dispatch({ type: "setCreating", value: true });
|
||||
dispatch({ type: "setMsg", value: null });
|
||||
try {
|
||||
const created = await createFile({ name, content: newContent });
|
||||
setFiles((prev) => [
|
||||
...prev,
|
||||
{ name: created.name, filename: created.filename },
|
||||
]);
|
||||
setContents((prev) => ({ ...prev, [created.name]: created.content }));
|
||||
setEditedContents((prev) => ({
|
||||
...prev,
|
||||
[created.name]: created.content,
|
||||
}));
|
||||
setNewName("");
|
||||
setNewContent("");
|
||||
setMsg({ text: `${created.filename} created.`, ok: true });
|
||||
dispatch({
|
||||
type: "setFiles",
|
||||
value: [
|
||||
...files,
|
||||
{ name: created.name, filename: created.filename },
|
||||
],
|
||||
});
|
||||
dispatch({
|
||||
type: "setContent",
|
||||
name: created.name,
|
||||
content: created.content,
|
||||
});
|
||||
dispatch({
|
||||
type: "setEditedContent",
|
||||
name: created.name,
|
||||
content: created.content,
|
||||
});
|
||||
dispatch({ type: "setNewName", value: "" });
|
||||
dispatch({ type: "setNewContent", value: "" });
|
||||
dispatch({
|
||||
type: "setMsg",
|
||||
value: { text: `${created.filename} created.`, ok: true },
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
setMsg({
|
||||
text: err instanceof ApiError ? err.message : "Create failed.",
|
||||
ok: false,
|
||||
dispatch({
|
||||
type: "setMsg",
|
||||
value: {
|
||||
text: err instanceof ApiError ? err.message : "Create failed.",
|
||||
ok: false,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
setCreating(false);
|
||||
dispatch({ type: "setCreating", value: false });
|
||||
}
|
||||
}, [newName, newContent, createFile]);
|
||||
}, [createFile, files, newContent, newName]);
|
||||
|
||||
if (loading) return <Spinner label={`Loading ${label.toLowerCase()} files…`} />;
|
||||
if (error)
|
||||
@@ -247,10 +362,11 @@ export function ConfFilesTab({
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
onChange={(_e, d) => {
|
||||
setEditedContents((prev) => ({
|
||||
...prev,
|
||||
[file.name]: d.value,
|
||||
}));
|
||||
dispatch({
|
||||
type: "setEditedContent",
|
||||
name: file.name,
|
||||
content: d.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className={styles.buttonRow}>
|
||||
@@ -293,7 +409,7 @@ export function ConfFilesTab({
|
||||
placeholder={`e.g. my-${label.toLowerCase()}`}
|
||||
className={styles.codeFont}
|
||||
onChange={(_e, d) => {
|
||||
setNewName(d.value);
|
||||
dispatch({ type: "setNewName", value: d.value });
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
@@ -308,7 +424,7 @@ export function ConfFilesTab({
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
onChange={(_e, d) => {
|
||||
setNewContent(d.value);
|
||||
dispatch({ type: "setNewContent", value: d.value });
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* health + log viewer.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useReducer } from "react";
|
||||
import {
|
||||
Button,
|
||||
Field,
|
||||
@@ -36,6 +36,97 @@ import { useConfigStyles } from "./configStyles";
|
||||
/** Available fail2ban log levels in descending severity order. */
|
||||
const LOG_LEVELS = ["CRITICAL", "ERROR", "WARNING", "NOTICE", "INFO", "DEBUG"];
|
||||
|
||||
interface ServerTabMessage {
|
||||
text: string;
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
interface ServerTabState {
|
||||
logLevel: string;
|
||||
logTarget: string;
|
||||
dbPurgeAge: string;
|
||||
dbMaxMatches: string;
|
||||
flushing: boolean;
|
||||
msg: ServerTabMessage | null;
|
||||
isReloading: boolean;
|
||||
isRestarting: boolean;
|
||||
mapThresholdHigh: string;
|
||||
mapThresholdMedium: string;
|
||||
mapThresholdLow: string;
|
||||
mapValidPayload: MapColorThresholdsUpdate;
|
||||
}
|
||||
|
||||
type ServerTabAction =
|
||||
| { type: "setLogLevel"; value: string }
|
||||
| { type: "setLogTarget"; value: string }
|
||||
| { type: "setDbPurgeAge"; value: string }
|
||||
| { type: "setDbMaxMatches"; value: string }
|
||||
| { type: "setFlushing"; value: boolean }
|
||||
| { type: "setMsg"; value: ServerTabMessage | null }
|
||||
| { type: "setIsReloading"; value: boolean }
|
||||
| { type: "setIsRestarting"; value: boolean }
|
||||
| {
|
||||
type: "setMapThresholds";
|
||||
high: string;
|
||||
medium: string;
|
||||
low: string;
|
||||
}
|
||||
| { type: "setMapValidPayload"; payload: MapColorThresholdsUpdate };
|
||||
|
||||
export const initialServerTabState: ServerTabState = {
|
||||
logLevel: "",
|
||||
logTarget: "",
|
||||
dbPurgeAge: "",
|
||||
dbMaxMatches: "",
|
||||
flushing: false,
|
||||
msg: null,
|
||||
isReloading: false,
|
||||
isRestarting: false,
|
||||
mapThresholdHigh: "",
|
||||
mapThresholdMedium: "",
|
||||
mapThresholdLow: "",
|
||||
mapValidPayload: {
|
||||
threshold_high: 0,
|
||||
threshold_medium: 0,
|
||||
threshold_low: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export function serverTabReducer(
|
||||
state: ServerTabState,
|
||||
action: ServerTabAction,
|
||||
): ServerTabState {
|
||||
switch (action.type) {
|
||||
case "setLogLevel":
|
||||
return { ...state, logLevel: action.value };
|
||||
case "setLogTarget":
|
||||
return { ...state, logTarget: action.value };
|
||||
case "setDbPurgeAge":
|
||||
return { ...state, dbPurgeAge: action.value };
|
||||
case "setDbMaxMatches":
|
||||
return { ...state, dbMaxMatches: action.value };
|
||||
case "setFlushing":
|
||||
return { ...state, flushing: action.value };
|
||||
case "setMsg":
|
||||
return { ...state, msg: action.value };
|
||||
case "setIsReloading":
|
||||
return { ...state, isReloading: action.value };
|
||||
case "setIsRestarting":
|
||||
return { ...state, isRestarting: action.value };
|
||||
case "setMapThresholds":
|
||||
return {
|
||||
...state,
|
||||
mapThresholdHigh: action.high,
|
||||
mapThresholdMedium: action.medium,
|
||||
mapThresholdLow: action.low,
|
||||
};
|
||||
case "setMapValidPayload":
|
||||
return { ...state, mapValidPayload: action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab component for editing live fail2ban server settings.
|
||||
*
|
||||
@@ -45,27 +136,29 @@ export function ServerTab(): React.JSX.Element {
|
||||
const styles = useConfigStyles();
|
||||
const { settings, loading, error, updateSettings, flush, reload, restart } =
|
||||
useServerSettings();
|
||||
const [logLevel, setLogLevel] = useState("");
|
||||
const [logTarget, setLogTarget] = useState("");
|
||||
const [dbPurgeAge, setDbPurgeAge] = useState("");
|
||||
const [dbMaxMatches, setDbMaxMatches] = useState("");
|
||||
const [flushing, setFlushing] = useState(false);
|
||||
const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null);
|
||||
const [state, dispatch] = useReducer(serverTabReducer, initialServerTabState);
|
||||
|
||||
// Reload/Restart state
|
||||
const [isReloading, setIsReloading] = useState(false);
|
||||
const [isRestarting, setIsRestarting] = useState(false);
|
||||
const {
|
||||
logLevel,
|
||||
logTarget,
|
||||
dbPurgeAge,
|
||||
dbMaxMatches,
|
||||
flushing,
|
||||
msg,
|
||||
isReloading,
|
||||
isRestarting,
|
||||
mapThresholdHigh,
|
||||
mapThresholdMedium,
|
||||
mapThresholdLow,
|
||||
mapValidPayload,
|
||||
} = state;
|
||||
|
||||
// Map color thresholds
|
||||
const {
|
||||
thresholds: mapThresholds,
|
||||
error: mapThresholdsError,
|
||||
refresh: refreshMapThresholds,
|
||||
updateThresholds: updateMapThresholds,
|
||||
} = useMapColorThresholds();
|
||||
const [mapThresholdHigh, setMapThresholdHigh] = useState("");
|
||||
const [mapThresholdMedium, setMapThresholdMedium] = useState("");
|
||||
const [mapThresholdLow, setMapThresholdLow] = useState("");
|
||||
|
||||
const effectiveLogLevel = logLevel || settings?.log_level || "";
|
||||
const effectiveLogTarget = logTarget || settings?.log_target || "";
|
||||
@@ -78,8 +171,7 @@ export function ServerTab(): React.JSX.Element {
|
||||
const update: ServerSettingsUpdate = {};
|
||||
if (effectiveLogLevel) update.log_level = effectiveLogLevel;
|
||||
if (effectiveLogTarget) update.log_target = effectiveLogTarget;
|
||||
if (effectiveDbPurgeAge)
|
||||
update.db_purge_age = Number(effectiveDbPurgeAge);
|
||||
if (effectiveDbPurgeAge) update.db_purge_age = Number(effectiveDbPurgeAge);
|
||||
if (effectiveDbMaxMatches)
|
||||
update.db_max_matches = Number(effectiveDbMaxMatches);
|
||||
return update;
|
||||
@@ -89,62 +181,79 @@ export function ServerTab(): React.JSX.Element {
|
||||
useAutoSave(updatePayload, updateSettings);
|
||||
|
||||
const handleFlush = useCallback(async () => {
|
||||
setFlushing(true);
|
||||
setMsg(null);
|
||||
dispatch({ type: "setFlushing", value: true });
|
||||
dispatch({ type: "setMsg", value: null });
|
||||
try {
|
||||
const result = await flush();
|
||||
setMsg({ text: `Logs flushed: ${result}`, ok: true });
|
||||
dispatch({ type: "setMsg", value: { text: `Logs flushed: ${result}`, ok: true } });
|
||||
} catch (err: unknown) {
|
||||
setMsg({
|
||||
text: err instanceof ApiError ? err.message : "Flush failed.",
|
||||
ok: false,
|
||||
dispatch({
|
||||
type: "setMsg",
|
||||
value: {
|
||||
text: err instanceof ApiError ? err.message : "Flush failed.",
|
||||
ok: false,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
setFlushing(false);
|
||||
dispatch({ type: "setFlushing", value: false });
|
||||
}
|
||||
}, [flush]);
|
||||
|
||||
const handleReload = async (): Promise<void> => {
|
||||
setIsReloading(true);
|
||||
setMsg(null);
|
||||
const handleReload = useCallback(async (): Promise<void> => {
|
||||
dispatch({ type: "setIsReloading", value: true });
|
||||
dispatch({ type: "setMsg", value: null });
|
||||
try {
|
||||
await reload();
|
||||
setMsg({ text: "fail2ban reloaded successfully", ok: true });
|
||||
dispatch({
|
||||
type: "setMsg",
|
||||
value: { text: "fail2ban reloaded successfully", ok: true },
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
setMsg({
|
||||
text: err instanceof ApiError ? err.message : "Reload failed.",
|
||||
ok: false,
|
||||
dispatch({
|
||||
type: "setMsg",
|
||||
value: {
|
||||
text: err instanceof ApiError ? err.message : "Reload failed.",
|
||||
ok: false,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
setIsReloading(false);
|
||||
dispatch({ type: "setIsReloading", value: false });
|
||||
}
|
||||
};
|
||||
}, [reload]);
|
||||
|
||||
const handleRestart = async (): Promise<void> => {
|
||||
setIsRestarting(true);
|
||||
setMsg(null);
|
||||
const handleRestart = useCallback(async (): Promise<void> => {
|
||||
dispatch({ type: "setIsRestarting", value: true });
|
||||
dispatch({ type: "setMsg", value: null });
|
||||
try {
|
||||
await restart();
|
||||
setMsg({ text: "fail2ban restart initiated", ok: true });
|
||||
dispatch({
|
||||
type: "setMsg",
|
||||
value: { text: "fail2ban restart initiated", ok: true },
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
setMsg({
|
||||
text: err instanceof ApiError ? err.message : "Restart failed.",
|
||||
ok: false,
|
||||
dispatch({
|
||||
type: "setMsg",
|
||||
value: {
|
||||
text: err instanceof ApiError ? err.message : "Restart failed.",
|
||||
ok: false,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
setIsRestarting(false);
|
||||
dispatch({ type: "setIsRestarting", value: false });
|
||||
}
|
||||
};
|
||||
}, [restart]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapThresholds) return;
|
||||
|
||||
setMapThresholdHigh(String(mapThresholds.threshold_high));
|
||||
setMapThresholdMedium(String(mapThresholds.threshold_medium));
|
||||
setMapThresholdLow(String(mapThresholds.threshold_low));
|
||||
dispatch({
|
||||
type: "setMapThresholds",
|
||||
high: String(mapThresholds.threshold_high),
|
||||
medium: String(mapThresholds.threshold_medium),
|
||||
low: String(mapThresholds.threshold_low),
|
||||
});
|
||||
}, [mapThresholds]);
|
||||
|
||||
// Map threshold validation and auto-save.
|
||||
const mapHigh = Number(mapThresholdHigh);
|
||||
const mapMedium = Number(mapThresholdMedium);
|
||||
const mapLow = Number(mapThresholdLow);
|
||||
@@ -160,18 +269,15 @@ export function ServerTab(): React.JSX.Element {
|
||||
return null;
|
||||
}, [mapHigh, mapMedium, mapLow, mapThresholds]);
|
||||
|
||||
const [mapValidPayload, setMapValidPayload] = useState<MapColorThresholdsUpdate>({
|
||||
threshold_high: mapThresholds?.threshold_high ?? 0,
|
||||
threshold_medium: mapThresholds?.threshold_medium ?? 0,
|
||||
threshold_low: mapThresholds?.threshold_low ?? 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (mapValidationError !== null || !mapThresholds) return;
|
||||
setMapValidPayload({
|
||||
threshold_high: mapHigh,
|
||||
threshold_medium: mapMedium,
|
||||
threshold_low: mapLow,
|
||||
dispatch({
|
||||
type: "setMapValidPayload",
|
||||
payload: {
|
||||
threshold_high: mapHigh,
|
||||
threshold_medium: mapMedium,
|
||||
threshold_low: mapLow,
|
||||
},
|
||||
});
|
||||
}, [mapHigh, mapMedium, mapLow, mapValidationError, mapThresholds]);
|
||||
|
||||
@@ -224,7 +330,7 @@ export function ServerTab(): React.JSX.Element {
|
||||
<Select
|
||||
value={effectiveLogLevel}
|
||||
onChange={(_e, d) => {
|
||||
setLogLevel(d.value);
|
||||
dispatch({ type: "setLogLevel", value: d.value });
|
||||
}}
|
||||
>
|
||||
{LOG_LEVELS.map((l) => (
|
||||
@@ -239,7 +345,7 @@ export function ServerTab(): React.JSX.Element {
|
||||
value={effectiveLogTarget}
|
||||
placeholder="STDOUT / /var/log/fail2ban.log"
|
||||
onChange={(_e, d) => {
|
||||
setLogTarget(d.value);
|
||||
dispatch({ type: "setLogTarget", value: d.value });
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
@@ -269,7 +375,7 @@ export function ServerTab(): React.JSX.Element {
|
||||
type="number"
|
||||
value={effectiveDbPurgeAge}
|
||||
onChange={(_e, d) => {
|
||||
setDbPurgeAge(d.value);
|
||||
dispatch({ type: "setDbPurgeAge", value: d.value });
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
@@ -281,7 +387,7 @@ export function ServerTab(): React.JSX.Element {
|
||||
type="number"
|
||||
value={effectiveDbMaxMatches}
|
||||
onChange={(_e, d) => {
|
||||
setDbMaxMatches(d.value);
|
||||
dispatch({ type: "setDbMaxMatches", value: d.value });
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
@@ -363,7 +469,7 @@ export function ServerTab(): React.JSX.Element {
|
||||
type="number"
|
||||
value={mapThresholdLow}
|
||||
onChange={(_, d) => {
|
||||
setMapThresholdLow(d.value);
|
||||
dispatch({ type: "setMapThresholds", high: mapThresholdHigh, medium: mapThresholdMedium, low: d.value });
|
||||
}}
|
||||
min={1}
|
||||
/>
|
||||
@@ -373,7 +479,7 @@ export function ServerTab(): React.JSX.Element {
|
||||
type="number"
|
||||
value={mapThresholdMedium}
|
||||
onChange={(_, d) => {
|
||||
setMapThresholdMedium(d.value);
|
||||
dispatch({ type: "setMapThresholds", high: mapThresholdHigh, medium: d.value, low: mapThresholdLow });
|
||||
}}
|
||||
min={1}
|
||||
/>
|
||||
@@ -383,7 +489,7 @@ export function ServerTab(): React.JSX.Element {
|
||||
type="number"
|
||||
value={mapThresholdHigh}
|
||||
onChange={(_, d) => {
|
||||
setMapThresholdHigh(d.value);
|
||||
dispatch({ type: "setMapThresholds", high: d.value, medium: mapThresholdMedium, low: mapThresholdLow });
|
||||
}}
|
||||
min={1}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
confFilesTabReducer,
|
||||
initialConfFilesTabState,
|
||||
} from "../ConfFilesTab";
|
||||
|
||||
const sampleFile = { name: "example", filename: "example.conf" };
|
||||
|
||||
describe("confFilesTabReducer", () => {
|
||||
it("sets files and open items", () => {
|
||||
const state = confFilesTabReducer(initialConfFilesTabState, {
|
||||
type: "setFiles",
|
||||
value: [sampleFile],
|
||||
});
|
||||
|
||||
expect(state.files).toEqual([sampleFile]);
|
||||
|
||||
const nextState = confFilesTabReducer(state, {
|
||||
type: "setOpenItems",
|
||||
value: ["example"],
|
||||
});
|
||||
|
||||
expect(nextState.openItems).toEqual(["example"]);
|
||||
});
|
||||
|
||||
it("stores content edits independently from loaded content", () => {
|
||||
const state = confFilesTabReducer(initialConfFilesTabState, {
|
||||
type: "setContent",
|
||||
name: "example",
|
||||
content: "original",
|
||||
});
|
||||
|
||||
const nextState = confFilesTabReducer(state, {
|
||||
type: "setEditedContent",
|
||||
name: "example",
|
||||
content: "modified",
|
||||
});
|
||||
|
||||
expect(nextState.contents).toEqual({ example: "original" });
|
||||
expect(nextState.editedContents).toEqual({ example: "modified" });
|
||||
});
|
||||
|
||||
it("resets create form fields after creation", () => {
|
||||
const state = confFilesTabReducer(initialConfFilesTabState, {
|
||||
type: "setNewName",
|
||||
value: "test",
|
||||
});
|
||||
|
||||
const nextState = confFilesTabReducer(state, {
|
||||
type: "setNewContent",
|
||||
value: "content",
|
||||
});
|
||||
|
||||
expect(nextState.newName).toBe("test");
|
||||
expect(nextState.newContent).toBe("content");
|
||||
|
||||
const cleared = confFilesTabReducer(nextState, {
|
||||
type: "setNewName",
|
||||
value: "",
|
||||
});
|
||||
const clearedContent = confFilesTabReducer(cleared, {
|
||||
type: "setNewContent",
|
||||
value: "",
|
||||
});
|
||||
|
||||
expect(clearedContent.newName).toBe("");
|
||||
expect(clearedContent.newContent).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { initialServerTabState, serverTabReducer } from "../ServerTab";
|
||||
|
||||
describe("serverTabReducer", () => {
|
||||
it("updates the log level", () => {
|
||||
const nextState = serverTabReducer(initialServerTabState, {
|
||||
type: "setLogLevel",
|
||||
value: "DEBUG",
|
||||
});
|
||||
|
||||
expect(nextState.logLevel).toBe("DEBUG");
|
||||
});
|
||||
|
||||
it("toggles loading actions correctly", () => {
|
||||
const startState = serverTabReducer(initialServerTabState, {
|
||||
type: "setFlushing",
|
||||
value: true,
|
||||
});
|
||||
|
||||
expect(startState.flushing).toBe(true);
|
||||
|
||||
const endState = serverTabReducer(startState, {
|
||||
type: "setFlushing",
|
||||
value: false,
|
||||
});
|
||||
|
||||
expect(endState.flushing).toBe(false);
|
||||
});
|
||||
|
||||
it("updates map thresholds and valid payload together", () => {
|
||||
const thresholdState = serverTabReducer(initialServerTabState, {
|
||||
type: "setMapThresholds",
|
||||
high: "100",
|
||||
medium: "50",
|
||||
low: "10",
|
||||
});
|
||||
|
||||
expect(thresholdState.mapThresholdHigh).toBe("100");
|
||||
expect(thresholdState.mapThresholdMedium).toBe("50");
|
||||
expect(thresholdState.mapThresholdLow).toBe("10");
|
||||
|
||||
const payloadState = serverTabReducer(thresholdState, {
|
||||
type: "setMapValidPayload",
|
||||
payload: { threshold_high: 100, threshold_medium: 50, threshold_low: 10 },
|
||||
});
|
||||
|
||||
expect(payloadState.mapValidPayload).toEqual({
|
||||
threshold_high: 100,
|
||||
threshold_medium: 50,
|
||||
threshold_low: 10,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user