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

@@ -0,0 +1,91 @@
/**
* React hook for loading action metadata used by the actions tab.
*/
import { useCallback, useEffect, useRef, useState } from "react";
import { fetchActions, removeActionFromJail, createAction, assignActionToJail } from "../api/config";
import { handleFetchError } from "../utils/fetchError";
import type { ActionConfig, ActionCreateRequest } from "../types/config";
export interface UseActionListResult {
actions: ActionConfig[];
loading: boolean;
error: string | null;
refresh: () => void;
removeActionFromJail: (jailName: string, actionName: string) => Promise<void>;
createAction: (payload: ActionCreateRequest) => Promise<ActionConfig>;
assignActionToJail: (jailName: string, payload: { action_name: string }, reload: boolean) => Promise<void>;
}
/**
* Load the action inventory and expose related action operations.
*/
export function useActionList(): UseActionListResult {
const [actions, setActions] = useState<ActionConfig[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const abortRef = useRef<AbortController | null>(null);
const refresh = useCallback((): void => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
setLoading(true);
setError(null);
fetchActions()
.then((resp) => {
if (!controller.signal.aborted) {
setActions(resp.actions);
}
})
.catch((err: unknown) => {
if (!controller.signal.aborted) {
handleFetchError(err, setError, "Failed to load actions");
}
})
.finally(() => {
if (!controller.signal.aborted) {
setLoading(false);
}
});
}, []);
useEffect(() => {
refresh();
return (): void => {
abortRef.current?.abort();
};
}, [refresh]);
const handleRemoveActionFromJail = useCallback(
async (jailName: string, actionName: string): Promise<void> => {
await removeActionFromJail(jailName, actionName);
},
[],
);
const handleCreateAction = useCallback(
async (payload: ActionCreateRequest): Promise<ActionConfig> => {
return await createAction(payload);
},
[],
);
const handleAssignActionToJail = useCallback(
async (jailName: string, payload: { action_name: string }, reload: boolean): Promise<void> => {
await assignActionToJail(jailName, payload, reload);
},
[],
);
return {
actions,
loading,
error,
refresh,
removeActionFromJail: handleRemoveActionFromJail,
createAction: handleCreateAction,
assignActionToJail: handleAssignActionToJail,
};
}

View File

@@ -0,0 +1,32 @@
/**
* React hook for loading and saving a single raw action file.
*/
import { useCallback } from "react";
import { fetchActionFile, updateActionFile } from "../api/file_config";
export interface UseActionRawFileResult {
fetchRawContent: () => Promise<string>;
saveRawContent: (content: string) => Promise<void>;
}
/**
* Return raw config file operations for an action file.
*/
export function useActionRawFile(name: string): UseActionRawFileResult {
const fetchRawContent = useCallback(async (): Promise<string> => {
const result = await fetchActionFile(name);
return result.content;
}, [name]);
const saveRawContent = useCallback(
async (content: string): Promise<void> => {
await updateActionFile(name, { content });
},
[name],
);
return {
fetchRawContent,
saveRawContent,
};
}

View File

@@ -0,0 +1,82 @@
/**
* React hook for loading filter config metadata used by the filter tab.
*/
import { useCallback, useEffect, useRef, useState } from "react";
import { fetchFilters, createFilter, assignFilterToJail } from "../api/config";
import { handleFetchError } from "../utils/fetchError";
import type { FilterConfig, FilterCreateRequest } from "../types/config";
export interface UseFilterListResult {
filters: FilterConfig[];
loading: boolean;
error: string | null;
refresh: () => void;
createFilter: (payload: FilterCreateRequest) => Promise<FilterConfig>;
assignFilterToJail: (jailName: string, payload: { filter_name: string }, reload: boolean) => Promise<void>;
}
/**
* Load the filter inventory and expose refresh semantics.
*/
export function useFilterList(): UseFilterListResult {
const [filters, setFilters] = useState<FilterConfig[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const abortRef = useRef<AbortController | null>(null);
const refresh = useCallback((): void => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
setLoading(true);
setError(null);
fetchFilters()
.then((resp) => {
if (!controller.signal.aborted) {
setFilters(resp.filters);
}
})
.catch((err: unknown) => {
if (!controller.signal.aborted) {
handleFetchError(err, setError, "Failed to load filters");
}
})
.finally(() => {
if (!controller.signal.aborted) {
setLoading(false);
}
});
}, []);
useEffect(() => {
refresh();
return (): void => {
abortRef.current?.abort();
};
}, [refresh]);
const handleCreateFilter = useCallback(
async (payload: FilterCreateRequest): Promise<FilterConfig> => {
return await createFilter(payload);
},
[],
);
const handleAssignFilterToJail = useCallback(
async (jailName: string, payload: { filter_name: string }, reload: boolean): Promise<void> => {
await assignFilterToJail(jailName, payload, reload);
},
[],
);
return {
filters,
loading,
error,
refresh,
createFilter: handleCreateFilter,
assignFilterToJail: handleAssignFilterToJail,
};
}

View File

@@ -0,0 +1,32 @@
/**
* React hook for loading and saving a single raw filter file.
*/
import { useCallback } from "react";
import { fetchFilterFile, updateFilterFile } from "../api/file_config";
export interface UseFilterRawFileResult {
fetchRawContent: () => Promise<string>;
saveRawContent: (content: string) => Promise<void>;
}
/**
* Return raw config file operations for a filter file.
*/
export function useFilterRawFile(name: string): UseFilterRawFileResult {
const fetchRawContent = useCallback(async (): Promise<string> => {
const result = await fetchFilterFile(name);
return result.content;
}, [name]);
const saveRawContent = useCallback(
async (content: string): Promise<void> => {
await updateFilterFile(name, { content });
},
[name],
);
return {
fetchRawContent,
saveRawContent,
};
}

View File

@@ -0,0 +1,123 @@
/**
* React hook for managing inactive jail operations and configuration actions.
*/
import { useCallback, useEffect, useRef, useState } from "react";
import {
activateJail,
deactivateJail,
deleteJailLocalOverride,
fetchInactiveJails,
validateJailConfig,
createJailConfigFile,
} from "../api/config";
import { handleFetchError } from "../utils/fetchError";
import type {
ActivateJailRequest,
ConfFileCreateRequest,
InactiveJail,
JailActivationResponse,
JailValidationResult,
} from "../types/config";
export interface UseJailAdminResult {
inactiveJails: InactiveJail[];
inactiveLoading: boolean;
inactiveError: string | null;
refreshInactiveJails: () => void;
deactivateJail: (name: string) => Promise<void>;
deleteJailLocalOverride: (name: string) => Promise<void>;
validateJailConfig: (name: string) => Promise<JailValidationResult>;
activateJail: (name: string, payload: ActivateJailRequest) => Promise<JailActivationResponse>;
createJailConfigFile: (payload: ConfFileCreateRequest) => Promise<void>;
}
/**
* Load inactive fail2ban jails and expose the admin actions used by the
* jail configuration tab.
*/
export function useJailAdmin(): UseJailAdminResult {
const [inactiveJails, setInactiveJails] = useState<InactiveJail[]>([]);
const [inactiveLoading, setInactiveLoading] = useState(false);
const [inactiveError, setInactiveError] = useState<string | null>(null);
const abortRef = useRef<AbortController | null>(null);
const refreshInactiveJails = useCallback((): void => {
abortRef.current?.abort();
const ctrl = new AbortController();
abortRef.current = ctrl;
setInactiveLoading(true);
setInactiveError(null);
fetchInactiveJails()
.then((resp) => {
if (!ctrl.signal.aborted) {
setInactiveJails(resp.jails);
}
})
.catch((err: unknown) => {
if (!ctrl.signal.aborted) {
handleFetchError(err, setInactiveError, "Failed to load inactive jails");
}
})
.finally(() => {
if (!ctrl.signal.aborted) {
setInactiveLoading(false);
}
});
}, []);
useEffect(() => {
refreshInactiveJails();
return (): void => {
abortRef.current?.abort();
};
}, [refreshInactiveJails]);
const handleDeactivateJail = useCallback(
async (name: string): Promise<void> => {
await deactivateJail(name);
},
[],
);
const handleDeleteLocalOverride = useCallback(
async (name: string): Promise<void> => {
await deleteJailLocalOverride(name);
},
[],
);
const handleValidateJailConfig = useCallback(
async (name: string): Promise<JailValidationResult> => {
return await validateJailConfig(name);
},
[],
);
const handleActivateJail = useCallback(
async (name: string, payload: ActivateJailRequest): Promise<JailActivationResponse> => {
return await activateJail(name, payload);
},
[],
);
const handleCreateJailConfigFile = useCallback(
async (payload: ConfFileCreateRequest): Promise<void> => {
await createJailConfigFile(payload);
},
[],
);
return {
inactiveJails,
inactiveLoading,
inactiveError,
refreshInactiveJails,
deactivateJail: handleDeactivateJail,
deleteJailLocalOverride: handleDeleteLocalOverride,
validateJailConfig: handleValidateJailConfig,
activateJail: handleActivateJail,
createJailConfigFile: handleCreateJailConfigFile,
};
}

View File

@@ -0,0 +1,57 @@
/**
* React hook for performing jail-specific configuration operations.
*/
import { useCallback } from "react";
import {
addLogPath,
deleteLogPath,
fetchJailConfigFileContent,
updateJailConfigFile,
} from "../api/config";
import type { AddLogPathRequest } from "../types/config";
export interface UseJailConfigOperationsResult {
addLogPath: (payload: AddLogPathRequest) => Promise<void>;
deleteLogPath: (path: string) => Promise<void>;
fetchRawContent: () => Promise<string>;
saveRawContent: (content: string) => Promise<void>;
}
/**
* Create callbacks for jail-specific config operations that are used by
* jail config detail components.
*/
export function useJailConfigOperations(jailName: string): UseJailConfigOperationsResult {
const addLog = useCallback(
async (payload: AddLogPathRequest): Promise<void> => {
await addLogPath(jailName, payload);
},
[jailName],
);
const deletePath = useCallback(
async (path: string): Promise<void> => {
await deleteLogPath(jailName, path);
},
[jailName],
);
const fetchRawContent = useCallback(async (): Promise<string> => {
const result = await fetchJailConfigFileContent(`${jailName}.conf`);
return result.content;
}, [jailName]);
const saveRawContent = useCallback(
async (content: string): Promise<void> => {
await updateJailConfigFile(`${jailName}.conf`, { content });
},
[jailName],
);
return {
addLogPath: addLog,
deleteLogPath: deletePath,
fetchRawContent,
saveRawContent,
};
}

View File

@@ -0,0 +1,53 @@
/**
* React hook for service health and log viewer data fetching.
*/
import { useCallback, useState } from "react";
import { fetchFail2BanLog, fetchServiceStatus } from "../api/config";
import type { Fail2BanLogResponse, ServiceStatusResponse } from "../types/config";
export interface UseServerHealthResult {
status: ServiceStatusResponse | null;
logData: Fail2BanLogResponse | null;
error: string | null;
refresh: () => Promise<void>;
}
/**
* Load service status and fail2ban log data for the server health panel.
*/
export function useServerHealth(
linesCount: number,
filterValue: string,
): UseServerHealthResult {
const [status, setStatus] = useState<ServiceStatusResponse | null>(null);
const [logData, setLogData] = useState<Fail2BanLogResponse | null>(null);
const [error, setError] = useState<string | null>(null);
const refresh = useCallback(async (): Promise<void> => {
try {
const [svcResult, logResult] = await Promise.allSettled([
fetchServiceStatus(),
fetchFail2BanLog(linesCount, filterValue || undefined),
]);
if (svcResult.status === "fulfilled") {
setStatus(svcResult.value);
} else {
setStatus(null);
}
if (logResult.status === "fulfilled") {
setLogData(logResult.value);
setError(null);
} else {
const reason: unknown = logResult.reason;
setError(reason instanceof Error ? reason.message : "Failed to load log data.");
}
} catch (err: unknown) {
const reason = err instanceof Error ? err.message : String(err);
setError(reason);
}
}, [filterValue, linesCount]);
return { status, logData, error, refresh };
}