Files
BanGUI/frontend/src/hooks/useConfig.ts
2026-03-22 14:24:25 +01:00

370 lines
9.5 KiB
TypeScript

/**
* React hooks for the configuration and server settings data.
*/
import { useCallback, useEffect, useRef, useState } from "react";
import {
addLogPath,
fetchGlobalConfig,
fetchJailConfig,
fetchJailConfigs,
fetchServerSettings,
flushLogs,
previewLog,
reloadConfig,
testRegex,
updateGlobalConfig,
updateJailConfig,
updateServerSettings,
} from "../api/config";
import type {
AddLogPathRequest,
GlobalConfig,
GlobalConfigUpdate,
JailConfig,
JailConfigUpdate,
LogPreviewRequest,
LogPreviewResponse,
RegexTestRequest,
RegexTestResponse,
ServerSettings,
ServerSettingsUpdate,
} from "../types/config";
// ---------------------------------------------------------------------------
// useJailConfigs — list all jail configs
// ---------------------------------------------------------------------------
interface UseJailConfigsResult {
jails: JailConfig[];
total: number;
loading: boolean;
error: string | null;
refresh: () => void;
updateJail: (name: string, update: JailConfigUpdate) => Promise<void>;
reloadAll: () => Promise<void>;
}
export function useJailConfigs(): UseJailConfigsResult {
const [jails, setJails] = useState<JailConfig[]>([]);
const [total, setTotal] = useState(0);
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);
fetchJailConfigs()
.then((resp) => {
setJails(resp.jails);
setTotal(resp.total);
})
.catch((err: unknown) => {
if (err instanceof Error && err.name !== "AbortError") {
setError(err.message);
}
})
.finally(() => {
setLoading(false);
});
}, []);
useEffect(() => {
load();
return (): void => {
abortRef.current?.abort();
};
}, [load]);
const updateJail = useCallback(
async (name: string, update: JailConfigUpdate): Promise<void> => {
await updateJailConfig(name, update);
load();
},
[load],
);
const reloadAll = useCallback(async (): Promise<void> => {
await reloadConfig();
load();
}, [load]);
return { jails, total, loading, error, refresh: load, updateJail, reloadAll };
}
// ---------------------------------------------------------------------------
// useJailConfigDetail — single jail config with mutation
// ---------------------------------------------------------------------------
interface UseJailConfigDetailResult {
jail: JailConfig | null;
loading: boolean;
error: string | null;
refresh: () => void;
updateJail: (update: JailConfigUpdate) => Promise<void>;
addLog: (req: AddLogPathRequest) => Promise<void>;
}
export function useJailConfigDetail(name: string): UseJailConfigDetailResult {
const [jail, setJail] = useState<JailConfig | null>(null);
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);
fetchJailConfig(name)
.then((resp) => {
setJail(resp.jail);
})
.catch((err: unknown) => {
if (err instanceof Error && err.name !== "AbortError") {
setError(err.message);
}
})
.finally(() => {
setLoading(false);
});
}, [name]);
useEffect(() => {
load();
return (): void => {
abortRef.current?.abort();
};
}, [load]);
const updateJail = useCallback(
async (update: JailConfigUpdate): Promise<void> => {
await updateJailConfig(name, update);
load();
},
[name, load],
);
const addLog = useCallback(
async (req: AddLogPathRequest): Promise<void> => {
await addLogPath(name, req);
load();
},
[name, load],
);
return { jail, loading, error, refresh: load, updateJail, addLog };
}
// ---------------------------------------------------------------------------
// useGlobalConfig
// ---------------------------------------------------------------------------
interface UseGlobalConfigResult {
config: GlobalConfig | null;
loading: boolean;
error: string | null;
refresh: () => void;
updateConfig: (update: GlobalConfigUpdate) => Promise<void>;
}
export function useGlobalConfig(): UseGlobalConfigResult {
const [config, setConfig] = useState<GlobalConfig | null>(null);
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);
fetchGlobalConfig()
.then(setConfig)
.catch((err: unknown) => {
if (err instanceof Error && err.name !== "AbortError") {
setError(err.message);
}
})
.finally(() => {
setLoading(false);
});
}, []);
useEffect(() => {
load();
return (): void => {
abortRef.current?.abort();
};
}, [load]);
const updateConfig = useCallback(
async (update: GlobalConfigUpdate): Promise<void> => {
await updateGlobalConfig(update);
load();
},
[load],
);
return { config, loading, error, refresh: load, updateConfig };
}
// ---------------------------------------------------------------------------
// useServerSettings
// ---------------------------------------------------------------------------
interface UseServerSettingsResult {
settings: ServerSettings | null;
loading: boolean;
error: string | null;
refresh: () => void;
updateSettings: (update: ServerSettingsUpdate) => Promise<void>;
flush: () => Promise<string>;
reload: () => Promise<void>;
restart: () => Promise<void>;
}
export function useServerSettings(): UseServerSettingsResult {
const [settings, setSettings] = useState<ServerSettings | null>(null);
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);
fetchServerSettings()
.then((resp) => {
setSettings(resp.settings);
})
.catch((err: unknown) => {
if (err instanceof Error && err.name !== "AbortError") {
setError(err.message);
}
})
.finally(() => {
setLoading(false);
});
}, []);
useEffect(() => {
load();
return (): void => {
abortRef.current?.abort();
};
}, [load]);
const updateSettings_ = useCallback(
async (update: ServerSettingsUpdate): Promise<void> => {
await updateServerSettings(update);
load();
},
[load],
);
const reload = useCallback(async (): Promise<void> => {
await reloadConfig();
load();
}, [load]);
const restart = useCallback(async (): Promise<void> => {
await restartFail2Ban();
load();
}, [load]);
const flush = useCallback(async (): Promise<string> => {
return flushLogs();
}, []);
return {
settings,
loading,
error,
refresh: load,
updateSettings: updateSettings_,
flush,
reload,
restart,
};
}
// ---------------------------------------------------------------------------
// useRegexTester — lazy, triggered by test(req)
// ---------------------------------------------------------------------------
interface UseRegexTesterResult {
result: RegexTestResponse | null;
testing: boolean;
test: (req: RegexTestRequest) => Promise<void>;
}
export function useRegexTester(): UseRegexTesterResult {
const [result, setResult] = useState<RegexTestResponse | null>(null);
const [testing, setTesting] = useState(false);
const test_ = useCallback(async (req: RegexTestRequest): Promise<void> => {
setTesting(true);
try {
const resp = await testRegex(req);
setResult(resp);
} catch (err: unknown) {
if (err instanceof Error) {
setResult({ matched: false, groups: [], error: err.message });
}
} finally {
setTesting(false);
}
}, []);
return { result, testing, test: test_ };
}
// ---------------------------------------------------------------------------
// useLogPreview — lazy, triggered by preview(req)
// ---------------------------------------------------------------------------
interface UseLogPreviewResult {
preview: LogPreviewResponse | null;
loading: boolean;
run: (req: LogPreviewRequest) => Promise<void>;
}
export function useLogPreview(): UseLogPreviewResult {
const [preview, setPreview] = useState<LogPreviewResponse | null>(null);
const [loading, setLoading] = useState(false);
const run_ = useCallback(async (req: LogPreviewRequest): Promise<void> => {
setLoading(true);
try {
const resp = await previewLog(req);
setPreview(resp);
} catch (err: unknown) {
if (err instanceof Error) {
setPreview({
lines: [],
total_lines: 0,
matched_count: 0,
regex_error: err.message,
});
}
} finally {
setLoading(false);
}
}, []);
return { preview, loading, run: run_ };
}