Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a30b92471a | |||
| 9e43282bbc | |||
| 2ea4a8304f |
@@ -68,6 +68,15 @@ FRONT_PKG="${SCRIPT_DIR}/../frontend/package.json"
|
|||||||
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"${FRONT_VERSION}\"/" "${FRONT_PKG}"
|
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"${FRONT_VERSION}\"/" "${FRONT_PKG}"
|
||||||
echo "frontend/package.json version updated → ${FRONT_VERSION}"
|
echo "frontend/package.json version updated → ${FRONT_VERSION}"
|
||||||
|
|
||||||
|
# Keep backend/pyproject.toml in sync so app.__version__ matches Docker/VERSION in the runtime container.
|
||||||
|
BACKEND_PYPROJECT="${SCRIPT_DIR}/../backend/pyproject.toml"
|
||||||
|
if [[ -f "${BACKEND_PYPROJECT}" ]]; then
|
||||||
|
sed -i "s/^version = \".*\"/version = \"${FRONT_VERSION}\"/" "${BACKEND_PYPROJECT}"
|
||||||
|
echo "backend/pyproject.toml version updated → ${FRONT_VERSION}"
|
||||||
|
else
|
||||||
|
echo "Warning: backend/pyproject.toml not found, skipping backend version sync" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Push containers
|
# Push containers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -7,3 +7,8 @@ Reference: `Docs/Refactoring.md` for full analysis of each issue.
|
|||||||
---
|
---
|
||||||
|
|
||||||
## Open Issues
|
## Open Issues
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
# Config File Service Extraction Summary
|
|
||||||
|
|
||||||
## ✓ Extraction Complete
|
|
||||||
|
|
||||||
Three new service modules have been created by extracting functions from `config_file_service.py`.
|
|
||||||
|
|
||||||
### Files Created
|
|
||||||
|
|
||||||
| File | Lines | Status |
|
|
||||||
|------|-------|--------|
|
|
||||||
| [jail_config_service.py](jail_config_service.py) | 991 | ✓ Created |
|
|
||||||
| [filter_config_service.py](filter_config_service.py) | 765 | ✓ Created |
|
|
||||||
| [action_config_service.py](action_config_service.py) | 988 | ✓ Created |
|
|
||||||
| **Total** | **2,744** | **✓ Verified** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. JAIL_CONFIG Service (`jail_config_service.py`)
|
|
||||||
|
|
||||||
### Public Functions (7)
|
|
||||||
- `list_inactive_jails(config_dir, socket_path)` → InactiveJailListResponse
|
|
||||||
- `activate_jail(config_dir, socket_path, name, req)` → JailActivationResponse
|
|
||||||
- `deactivate_jail(config_dir, socket_path, name)` → JailActivationResponse
|
|
||||||
- `delete_jail_local_override(config_dir, socket_path, name)` → None
|
|
||||||
- `validate_jail_config(config_dir, name)` → JailValidationResult
|
|
||||||
- `rollback_jail(config_dir, socket_path, name, start_cmd_parts)` → RollbackResponse
|
|
||||||
- `_rollback_activation_async(config_dir, name, socket_path, original_content)` → bool
|
|
||||||
|
|
||||||
### Helper Functions (5)
|
|
||||||
- `_write_local_override_sync()` - Atomic write of jail.d/{name}.local
|
|
||||||
- `_restore_local_file_sync()` - Restore or delete .local file during rollback
|
|
||||||
- `_validate_regex_patterns()` - Validate failregex/ignoreregex patterns
|
|
||||||
- `_set_jail_local_key_sync()` - Update single key in jail section
|
|
||||||
- `_validate_jail_config_sync()` - Synchronous validation (filter/action files, patterns, logpath)
|
|
||||||
|
|
||||||
### Custom Exceptions (3)
|
|
||||||
- `JailNotFoundInConfigError`
|
|
||||||
- `JailAlreadyActiveError`
|
|
||||||
- `JailAlreadyInactiveError`
|
|
||||||
|
|
||||||
### Shared Dependencies Imported
|
|
||||||
- `_safe_jail_name()` - From config_file_service
|
|
||||||
- `_parse_jails_sync()` - From config_file_service
|
|
||||||
- `_build_inactive_jail()` - From config_file_service
|
|
||||||
- `_get_active_jail_names()` - From config_file_service
|
|
||||||
- `_probe_fail2ban_running()` - From config_file_service
|
|
||||||
- `wait_for_fail2ban()` - From config_file_service
|
|
||||||
- `start_daemon()` - From config_file_service
|
|
||||||
- `_resolve_filter()` - From config_file_service
|
|
||||||
- `_parse_multiline()` - From config_file_service
|
|
||||||
- `_SOCKET_TIMEOUT`, `_META_SECTIONS` - Constants
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. FILTER_CONFIG Service (`filter_config_service.py`)
|
|
||||||
|
|
||||||
### Public Functions (6)
|
|
||||||
- `list_filters(config_dir, socket_path)` → FilterListResponse
|
|
||||||
- `get_filter(config_dir, socket_path, name)` → FilterConfig
|
|
||||||
- `update_filter(config_dir, socket_path, name, req, do_reload=False)` → FilterConfig
|
|
||||||
- `create_filter(config_dir, socket_path, req, do_reload=False)` → FilterConfig
|
|
||||||
- `delete_filter(config_dir, name)` → None
|
|
||||||
- `assign_filter_to_jail(config_dir, socket_path, jail_name, req, do_reload=False)` → None
|
|
||||||
|
|
||||||
### Helper Functions (4)
|
|
||||||
- `_extract_filter_base_name(filter_raw)` - Extract base name from filter string
|
|
||||||
- `_build_filter_to_jails_map()` - Map filters to jails using them
|
|
||||||
- `_parse_filters_sync()` - Scan filter.d/ and return tuples
|
|
||||||
- `_write_filter_local_sync()` - Atomic write of filter.d/{name}.local
|
|
||||||
- `_validate_regex_patterns()` - Validate regex patterns (shared with jail_config)
|
|
||||||
|
|
||||||
### Custom Exceptions (5)
|
|
||||||
- `FilterNotFoundError`
|
|
||||||
- `FilterAlreadyExistsError`
|
|
||||||
- `FilterReadonlyError`
|
|
||||||
- `FilterInvalidRegexError`
|
|
||||||
- `FilterNameError` (re-exported from config_file_service)
|
|
||||||
|
|
||||||
### Shared Dependencies Imported
|
|
||||||
- `_safe_filter_name()` - From config_file_service
|
|
||||||
- `_safe_jail_name()` - From config_file_service
|
|
||||||
- `_parse_jails_sync()` - From config_file_service
|
|
||||||
- `_get_active_jail_names()` - From config_file_service
|
|
||||||
- `_resolve_filter()` - From config_file_service
|
|
||||||
- `_parse_multiline()` - From config_file_service
|
|
||||||
- `_SAFE_FILTER_NAME_RE` - Constant pattern
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. ACTION_CONFIG Service (`action_config_service.py`)
|
|
||||||
|
|
||||||
### Public Functions (7)
|
|
||||||
- `list_actions(config_dir, socket_path)` → ActionListResponse
|
|
||||||
- `get_action(config_dir, socket_path, name)` → ActionConfig
|
|
||||||
- `update_action(config_dir, socket_path, name, req, do_reload=False)` → ActionConfig
|
|
||||||
- `create_action(config_dir, socket_path, req, do_reload=False)` → ActionConfig
|
|
||||||
- `delete_action(config_dir, name)` → None
|
|
||||||
- `assign_action_to_jail(config_dir, socket_path, jail_name, req, do_reload=False)` → None
|
|
||||||
- `remove_action_from_jail(config_dir, socket_path, jail_name, action_name, do_reload=False)` → None
|
|
||||||
|
|
||||||
### Helper Functions (5)
|
|
||||||
- `_safe_action_name(name)` - Validate action name
|
|
||||||
- `_extract_action_base_name()` - Extract base name from action string
|
|
||||||
- `_build_action_to_jails_map()` - Map actions to jails using them
|
|
||||||
- `_parse_actions_sync()` - Scan action.d/ and return tuples
|
|
||||||
- `_append_jail_action_sync()` - Append action to jail.d/{name}.local
|
|
||||||
- `_remove_jail_action_sync()` - Remove action from jail.d/{name}.local
|
|
||||||
- `_write_action_local_sync()` - Atomic write of action.d/{name}.local
|
|
||||||
|
|
||||||
### Custom Exceptions (4)
|
|
||||||
- `ActionNotFoundError`
|
|
||||||
- `ActionAlreadyExistsError`
|
|
||||||
- `ActionReadonlyError`
|
|
||||||
- `ActionNameError`
|
|
||||||
|
|
||||||
### Shared Dependencies Imported
|
|
||||||
- `_safe_jail_name()` - From config_file_service
|
|
||||||
- `_parse_jails_sync()` - From config_file_service
|
|
||||||
- `_get_active_jail_names()` - From config_file_service
|
|
||||||
- `_build_parser()` - From config_file_service
|
|
||||||
- `_SAFE_ACTION_NAME_RE` - Constant pattern
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. SHARED Utilities (remain in `config_file_service.py`)
|
|
||||||
|
|
||||||
### Utility Functions (14)
|
|
||||||
- `_safe_jail_name(name)` → str
|
|
||||||
- `_safe_filter_name(name)` → str
|
|
||||||
- `_ordered_config_files(config_dir)` → list[Path]
|
|
||||||
- `_build_parser()` → configparser.RawConfigParser
|
|
||||||
- `_is_truthy(value)` → bool
|
|
||||||
- `_parse_int_safe(value)` → int | None
|
|
||||||
- `_parse_time_to_seconds(value, default)` → int
|
|
||||||
- `_parse_multiline(raw)` → list[str]
|
|
||||||
- `_resolve_filter(raw_filter, jail_name, mode)` → str
|
|
||||||
- `_parse_jails_sync(config_dir)` → tuple
|
|
||||||
- `_build_inactive_jail(name, settings, source_file, config_dir=None)` → InactiveJail
|
|
||||||
- `_get_active_jail_names(socket_path)` → set[str]
|
|
||||||
- `_probe_fail2ban_running(socket_path)` → bool
|
|
||||||
- `wait_for_fail2ban(socket_path, max_wait_seconds, poll_interval)` → bool
|
|
||||||
- `start_daemon(start_cmd_parts)` → bool
|
|
||||||
|
|
||||||
### Shared Exceptions (3)
|
|
||||||
- `JailNameError`
|
|
||||||
- `FilterNameError`
|
|
||||||
- `ConfigWriteError`
|
|
||||||
|
|
||||||
### Constants (7)
|
|
||||||
- `_SOCKET_TIMEOUT`
|
|
||||||
- `_SAFE_JAIL_NAME_RE`
|
|
||||||
- `_META_SECTIONS`
|
|
||||||
- `_TRUE_VALUES`
|
|
||||||
- `_FALSE_VALUES`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Import Dependencies
|
|
||||||
|
|
||||||
### jail_config_service imports:
|
|
||||||
```python
|
|
||||||
config_file_service: (shared utilities + private functions)
|
|
||||||
jail_service.reload_all()
|
|
||||||
Fail2BanConnectionError
|
|
||||||
```
|
|
||||||
|
|
||||||
### filter_config_service imports:
|
|
||||||
```python
|
|
||||||
config_file_service: (shared utilities + _set_jail_local_key_sync)
|
|
||||||
jail_service.reload_all()
|
|
||||||
conffile_parser: (parse/merge/serialize filter functions)
|
|
||||||
jail_config_service: (JailNotFoundInConfigError - lazy import)
|
|
||||||
```
|
|
||||||
|
|
||||||
### action_config_service imports:
|
|
||||||
```python
|
|
||||||
config_file_service: (shared utilities + _build_parser)
|
|
||||||
jail_service.reload_all()
|
|
||||||
conffile_parser: (parse/merge/serialize action functions)
|
|
||||||
jail_config_service: (JailNotFoundInConfigError - lazy import)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cross-Service Dependencies
|
|
||||||
|
|
||||||
**Circular imports handled via lazy imports:**
|
|
||||||
- `filter_config_service` imports `JailNotFoundInConfigError` from `jail_config_service` inside function
|
|
||||||
- `action_config_service` imports `JailNotFoundInConfigError` from `jail_config_service` inside function
|
|
||||||
|
|
||||||
**Shared functions re-used:**
|
|
||||||
- `_set_jail_local_key_sync()` exported from `jail_config_service`, used by `filter_config_service`
|
|
||||||
- `_append_jail_action_sync()` and `_remove_jail_action_sync()` internal to `action_config_service`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification Results
|
|
||||||
|
|
||||||
✓ **Syntax Check:** All three files compile without errors
|
|
||||||
✓ **Import Verification:** All imports resolved correctly
|
|
||||||
✓ **Total Lines:** 2,744 lines across three new files
|
|
||||||
✓ **Function Coverage:** 100% of specified functions extracted
|
|
||||||
✓ **Type Hints:** Preserved throughout
|
|
||||||
✓ **Docstrings:** All preserved with full documentation
|
|
||||||
✓ **Comments:** All inline comments preserved
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps (if needed)
|
|
||||||
|
|
||||||
1. **Update router imports** - Point from config_file_service to specific service modules:
|
|
||||||
- `jail_config_service` for jail operations
|
|
||||||
- `filter_config_service` for filter operations
|
|
||||||
- `action_config_service` for action operations
|
|
||||||
|
|
||||||
2. **Update config_file_service.py** - Remove all extracted functions (optional cleanup)
|
|
||||||
- Optionally keep it as a facade/aggregator
|
|
||||||
- Or reduce it to only the shared utilities module
|
|
||||||
|
|
||||||
3. **Add __all__ exports** to each new module for cleaner public API
|
|
||||||
|
|
||||||
4. **Update type hints** in models if needed for cross-service usage
|
|
||||||
|
|
||||||
5. **Testing** - Run existing tests to ensure no regressions
|
|
||||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "bangui-backend"
|
name = "bangui-backend"
|
||||||
version = "0.9.8"
|
version = "0.9.14"
|
||||||
description = "BanGUI backend — fail2ban web management interface"
|
description = "BanGUI backend — fail2ban web management interface"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
import app
|
import app
|
||||||
|
|
||||||
@@ -13,3 +14,15 @@ def test_app_version_matches_docker_version() -> None:
|
|||||||
expected = version_file.read_text(encoding="utf-8").strip().lstrip("v")
|
expected = version_file.read_text(encoding="utf-8").strip().lstrip("v")
|
||||||
|
|
||||||
assert app.__version__ == expected
|
assert app.__version__ == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_backend_pyproject_version_matches_docker_version() -> None:
|
||||||
|
repo_root = Path(__file__).resolve().parents[2]
|
||||||
|
version_file = repo_root / "Docker" / "VERSION"
|
||||||
|
expected = version_file.read_text(encoding="utf-8").strip().lstrip("v")
|
||||||
|
|
||||||
|
pyproject_file = repo_root / "backend" / "pyproject.toml"
|
||||||
|
text = pyproject_file.read_text(encoding="utf-8")
|
||||||
|
match = re.search(r"^version\s*=\s*\"([^\"]+)\"", text, re.MULTILINE)
|
||||||
|
assert match is not None, "backend/pyproject.toml must contain a version entry"
|
||||||
|
assert match.group(1) == expected
|
||||||
|
|||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "bangui-frontend",
|
"name": "bangui-frontend",
|
||||||
"version": "0.9.10",
|
"version": "0.9.14",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bangui-frontend",
|
"name": "bangui-frontend",
|
||||||
"version": "0.9.10",
|
"version": "0.9.14",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/react-components": "^9.55.0",
|
"@fluentui/react-components": "^9.55.0",
|
||||||
"@fluentui/react-icons": "^2.0.257",
|
"@fluentui/react-icons": "^2.0.257",
|
||||||
|
|||||||
74
frontend/src/hooks/__tests__/useActionConfig.test.ts
Normal file
74
frontend/src/hooks/__tests__/useActionConfig.test.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { renderHook, act } from "@testing-library/react";
|
||||||
|
import * as configApi from "../../api/config";
|
||||||
|
import { useActionConfig } from "../useActionConfig";
|
||||||
|
|
||||||
|
vi.mock("../../api/config");
|
||||||
|
|
||||||
|
describe("useActionConfig", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(configApi.fetchAction).mockResolvedValue({
|
||||||
|
name: "iptables",
|
||||||
|
filename: "iptables.conf",
|
||||||
|
source_file: "/etc/fail2ban/action.d/iptables.conf",
|
||||||
|
active: false,
|
||||||
|
used_by_jails: [],
|
||||||
|
before: null,
|
||||||
|
after: null,
|
||||||
|
actionstart: "",
|
||||||
|
actionstop: "",
|
||||||
|
actioncheck: "",
|
||||||
|
actionban: "",
|
||||||
|
actionunban: "",
|
||||||
|
actionflush: "",
|
||||||
|
definition_vars: {},
|
||||||
|
init_vars: {},
|
||||||
|
has_local_override: false,
|
||||||
|
});
|
||||||
|
vi.mocked(configApi.updateAction).mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls fetchAction exactly once for stable name and rerenders", async () => {
|
||||||
|
const { rerender } = renderHook(
|
||||||
|
({ name }) => useActionConfig(name),
|
||||||
|
{ initialProps: { name: "iptables" } },
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(configApi.fetchAction).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Rerender with the same action name; fetch should not be called again.
|
||||||
|
rerender({ name: "iptables" });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(configApi.fetchAction).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls fetchAction again when name changes", async () => {
|
||||||
|
const { rerender } = renderHook(
|
||||||
|
({ name }) => useActionConfig(name),
|
||||||
|
{ initialProps: { name: "iptables" } },
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(configApi.fetchAction).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
rerender({ name: "ssh" });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(configApi.fetchAction).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
72
frontend/src/hooks/__tests__/useFilterConfig.test.ts
Normal file
72
frontend/src/hooks/__tests__/useFilterConfig.test.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { renderHook, act } from "@testing-library/react";
|
||||||
|
import * as configApi from "../../api/config";
|
||||||
|
import { useFilterConfig } from "../useFilterConfig";
|
||||||
|
|
||||||
|
vi.mock("../../api/config");
|
||||||
|
|
||||||
|
describe("useFilterConfig", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(configApi.fetchParsedFilter).mockResolvedValue({
|
||||||
|
name: "sshd",
|
||||||
|
filename: "sshd.conf",
|
||||||
|
source_file: "/etc/fail2ban/filter.d/sshd.conf",
|
||||||
|
active: false,
|
||||||
|
used_by_jails: [],
|
||||||
|
before: null,
|
||||||
|
after: null,
|
||||||
|
variables: {},
|
||||||
|
prefregex: null,
|
||||||
|
failregex: [],
|
||||||
|
ignoreregex: [],
|
||||||
|
maxlines: null,
|
||||||
|
datepattern: null,
|
||||||
|
journalmatch: null,
|
||||||
|
has_local_override: false,
|
||||||
|
});
|
||||||
|
vi.mocked(configApi.updateParsedFilter).mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls fetchParsedFilter only once for stable name", async () => {
|
||||||
|
const { rerender } = renderHook(
|
||||||
|
({ name }) => useFilterConfig(name),
|
||||||
|
{ initialProps: { name: "sshd" } },
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(configApi.fetchParsedFilter).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
rerender({ name: "sshd" });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(configApi.fetchParsedFilter).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls fetchParsedFilter again when name changes", async () => {
|
||||||
|
const { rerender } = renderHook(
|
||||||
|
({ name }) => useFilterConfig(name),
|
||||||
|
{ initialProps: { name: "sshd" } },
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(configApi.fetchParsedFilter).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
rerender({ name: "apache-auth" });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(configApi.fetchParsedFilter).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
* React hook for loading and updating a single parsed action config.
|
* React hook for loading and updating a single parsed action config.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useCallback } from "react";
|
||||||
import { useConfigItem } from "./useConfigItem";
|
import { useConfigItem } from "./useConfigItem";
|
||||||
import { fetchAction, updateAction } from "../api/config";
|
import { fetchAction, updateAction } from "../api/config";
|
||||||
import type { ActionConfig, ActionConfigUpdate } from "../types/config";
|
import type { ActionConfig, ActionConfigUpdate } from "../types/config";
|
||||||
@@ -23,12 +24,18 @@ export interface UseActionConfigResult {
|
|||||||
* @param name - Action base name (e.g. ``"iptables"``).
|
* @param name - Action base name (e.g. ``"iptables"``).
|
||||||
*/
|
*/
|
||||||
export function useActionConfig(name: string): UseActionConfigResult {
|
export function useActionConfig(name: string): UseActionConfigResult {
|
||||||
|
const fetchFn = useCallback(() => fetchAction(name), [name]);
|
||||||
|
const saveFn = useCallback(
|
||||||
|
(update: ActionConfigUpdate) => updateAction(name, update),
|
||||||
|
[name],
|
||||||
|
);
|
||||||
|
|
||||||
const { data, loading, error, saving, saveError, refresh, save } = useConfigItem<
|
const { data, loading, error, saving, saveError, refresh, save } = useConfigItem<
|
||||||
ActionConfig,
|
ActionConfig,
|
||||||
ActionConfigUpdate
|
ActionConfigUpdate
|
||||||
>({
|
>({
|
||||||
fetchFn: () => fetchAction(name),
|
fetchFn,
|
||||||
saveFn: (update) => updateAction(name, update),
|
saveFn,
|
||||||
mergeOnSave: (prev, update) =>
|
mergeOnSave: (prev, update) =>
|
||||||
prev
|
prev
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* React hook for loading and updating a single parsed filter config.
|
* React hook for loading and updating a single parsed filter config.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useCallback } from "react";
|
||||||
import { useConfigItem } from "./useConfigItem";
|
import { useConfigItem } from "./useConfigItem";
|
||||||
import { fetchParsedFilter, updateParsedFilter } from "../api/config";
|
import { fetchParsedFilter, updateParsedFilter } from "../api/config";
|
||||||
import type { FilterConfig, FilterConfigUpdate } from "../types/config";
|
import type { FilterConfig, FilterConfigUpdate } from "../types/config";
|
||||||
@@ -23,12 +24,18 @@ export interface UseFilterConfigResult {
|
|||||||
* @param name - Filter base name (e.g. ``"sshd"``).
|
* @param name - Filter base name (e.g. ``"sshd"``).
|
||||||
*/
|
*/
|
||||||
export function useFilterConfig(name: string): UseFilterConfigResult {
|
export function useFilterConfig(name: string): UseFilterConfigResult {
|
||||||
|
const fetchFn = useCallback(() => fetchParsedFilter(name), [name]);
|
||||||
|
const saveFn = useCallback(
|
||||||
|
(update: FilterConfigUpdate) => updateParsedFilter(name, update),
|
||||||
|
[name],
|
||||||
|
);
|
||||||
|
|
||||||
const { data, loading, error, saving, saveError, refresh, save } = useConfigItem<
|
const { data, loading, error, saving, saveError, refresh, save } = useConfigItem<
|
||||||
FilterConfig,
|
FilterConfig,
|
||||||
FilterConfigUpdate
|
FilterConfigUpdate
|
||||||
>({
|
>({
|
||||||
fetchFn: () => fetchParsedFilter(name),
|
fetchFn,
|
||||||
saveFn: (update) => updateParsedFilter(name, update),
|
saveFn,
|
||||||
mergeOnSave: (prev, update) =>
|
mergeOnSave: (prev, update) =>
|
||||||
prev
|
prev
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* React hook for loading and updating a single parsed jail.d config file.
|
* React hook for loading and updating a single parsed jail.d config file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useCallback } from "react";
|
||||||
import { useConfigItem } from "./useConfigItem";
|
import { useConfigItem } from "./useConfigItem";
|
||||||
import { fetchParsedJailFile, updateParsedJailFile } from "../api/config";
|
import { fetchParsedJailFile, updateParsedJailFile } from "../api/config";
|
||||||
import type { JailFileConfig, JailFileConfigUpdate } from "../types/config";
|
import type { JailFileConfig, JailFileConfigUpdate } from "../types/config";
|
||||||
@@ -21,12 +22,18 @@ export interface UseJailFileConfigResult {
|
|||||||
* @param filename - Filename including extension (e.g. ``"sshd.conf"``).
|
* @param filename - Filename including extension (e.g. ``"sshd.conf"``).
|
||||||
*/
|
*/
|
||||||
export function useJailFileConfig(filename: string): UseJailFileConfigResult {
|
export function useJailFileConfig(filename: string): UseJailFileConfigResult {
|
||||||
|
const fetchFn = useCallback(() => fetchParsedJailFile(filename), [filename]);
|
||||||
|
const saveFn = useCallback(
|
||||||
|
(update: JailFileConfigUpdate) => updateParsedJailFile(filename, update),
|
||||||
|
[filename],
|
||||||
|
);
|
||||||
|
|
||||||
const { data, loading, error, refresh, save } = useConfigItem<
|
const { data, loading, error, refresh, save } = useConfigItem<
|
||||||
JailFileConfig,
|
JailFileConfig,
|
||||||
JailFileConfigUpdate
|
JailFileConfigUpdate
|
||||||
>({
|
>({
|
||||||
fetchFn: () => fetchParsedJailFile(filename),
|
fetchFn,
|
||||||
saveFn: (update) => updateParsedJailFile(filename, update),
|
saveFn,
|
||||||
mergeOnSave: (prev, update) =>
|
mergeOnSave: (prev, update) =>
|
||||||
update.jails != null && prev
|
update.jails != null && prev
|
||||||
? { ...prev, jails: { ...prev.jails, ...update.jails } }
|
? { ...prev, jails: { ...prev.jails, ...update.jails } }
|
||||||
|
|||||||
Reference in New Issue
Block a user