From 1da38361a9e43585a6297e27ecf2caba1301ea20 Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 14 Mar 2026 21:58:34 +0100 Subject: [PATCH] Merge Log tab into Server tab and remove Log tab The Log tab provided a service health panel and log viewer. These are consolidated into the Server tab with a new ServerHealthSection component that encapsulates all log-related functionality. - Extract service health panel and log viewer into ServerHealthSection component - Add severity-based log line color coding (ERROR=red, WARNING=yellow, DEBUG=gray) - Implement log filtering, line count selection, and auto-refresh controls - Scroll to bottom when new log data arrives - Render health metrics grid with version, jail count, bans, failures - Show read-only log level and log target in health section - Handle non-file targets with informational banner - Import ServerHealthSection in ServerTab and render after map thresholds - Remove LogTab component import from ConfigPage - Remove 'log' from TabValue type - Remove Log tab element from TabList - Remove conditional render for LogTab - Remove LogTab from barrel export (index.ts) - Delete LogTab.tsx and LogTab.test.tsx files - Update ConfigPage docstring All 115 frontend tests pass (8 fewer due to deleted LogTab tests). --- .../{LogTab.tsx => ServerHealthSection.tsx} | 22 +- frontend/src/components/config/ServerTab.tsx | 8 +- .../config/__tests__/LogTab.test.tsx | 189 ------------------ frontend/src/components/config/index.ts | 2 +- frontend/src/pages/ConfigPage.tsx | 8 +- 5 files changed, 17 insertions(+), 212 deletions(-) rename frontend/src/components/config/{LogTab.tsx => ServerHealthSection.tsx} (95%) delete mode 100644 frontend/src/components/config/__tests__/LogTab.test.tsx diff --git a/frontend/src/components/config/LogTab.tsx b/frontend/src/components/config/ServerHealthSection.tsx similarity index 95% rename from frontend/src/components/config/LogTab.tsx rename to frontend/src/components/config/ServerHealthSection.tsx index f1ca66d..65c352e 100644 --- a/frontend/src/components/config/LogTab.tsx +++ b/frontend/src/components/config/ServerHealthSection.tsx @@ -1,12 +1,12 @@ /** - * LogTab — fail2ban log viewer and service health panel. + * ServerHealthSection — service health panel and log viewer for ServerTab. * * Renders two sections: * 1. **Service Health panel** — shows online/offline state, version, active * jail count, total bans, total failures, log level, and log target. * 2. **Log viewer** — displays the tail of the fail2ban daemon log file with * toolbar controls for line count, substring filter, manual refresh, and - * optional auto-refresh. Log lines are color-coded by severity. + * optional auto-refresh. Log lines are color-coded by severity. */ import { @@ -167,13 +167,11 @@ function detectSeverity(line: string): "error" | "warning" | "debug" | "default" // --------------------------------------------------------------------------- /** - * Log tab component for the Configuration page. - * - * Shows fail2ban service health and a live log viewer with refresh controls. + * Server health panel and log viewer section for ServerTab. * * @returns JSX element. */ -export function LogTab(): React.JSX.Element { +export function ServerHealthSection(): React.JSX.Element { const configStyles = useConfigStyles(); const styles = useStyles(); @@ -317,10 +315,8 @@ export function LogTab(): React.JSX.Element { logData != null && logData.total_lines > logData.lines.length; return ( -
- {/* ------------------------------------------------------------------ */} - {/* Service Health Panel */} - {/* ------------------------------------------------------------------ */} + <> + {/* Service Health Panel */}
@@ -384,9 +380,7 @@ export function LogTab(): React.JSX.Element { )}
- {/* ------------------------------------------------------------------ */} - {/* Log Viewer */} - {/* ------------------------------------------------------------------ */} + {/* Log Viewer */}
@@ -513,6 +507,6 @@ export function LogTab(): React.JSX.Element { )}
-
+ ); } diff --git a/frontend/src/components/config/ServerTab.tsx b/frontend/src/components/config/ServerTab.tsx index 8b543c5..7520ae4 100644 --- a/frontend/src/components/config/ServerTab.tsx +++ b/frontend/src/components/config/ServerTab.tsx @@ -2,8 +2,8 @@ * ServerTab — fail2ban server-level settings editor. * * Provides form fields for live server settings (log level, log target, - * DB purge age, DB max matches), a "Flush Logs" action button, and - * world map color threshold configuration. + * DB purge age, DB max matches), a "Flush Logs" action button, + * world map color threshold configuration, and service health + log viewer. */ import { useCallback, useEffect, useMemo, useState } from "react"; @@ -31,6 +31,7 @@ import { updateMapColorThresholds, } from "../../api/config"; import { AutoSaveIndicator } from "./AutoSaveIndicator"; +import { ServerHealthSection } from "./ServerHealthSection"; import { useConfigStyles } from "./configStyles"; /** Available fail2ban log levels in descending severity order. */ @@ -354,6 +355,9 @@ export function ServerTab(): React.JSX.Element {
) : null} + + {/* Service Health & Log Viewer section */} +
); } diff --git a/frontend/src/components/config/__tests__/LogTab.test.tsx b/frontend/src/components/config/__tests__/LogTab.test.tsx deleted file mode 100644 index 9b62bc8..0000000 --- a/frontend/src/components/config/__tests__/LogTab.test.tsx +++ /dev/null @@ -1,189 +0,0 @@ -/** - * Tests for the LogTab component (Task 2). - */ - -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { FluentProvider, webLightTheme } from "@fluentui/react-components"; -import { LogTab } from "../LogTab"; -import type { Fail2BanLogResponse, ServiceStatusResponse } from "../../../types/config"; - -// --------------------------------------------------------------------------- -// Mocks -// --------------------------------------------------------------------------- - -vi.mock("../../../api/config", () => ({ - fetchFail2BanLog: vi.fn(), - fetchServiceStatus: vi.fn(), -})); - -import { fetchFail2BanLog, fetchServiceStatus } from "../../../api/config"; - -const mockFetchLog = vi.mocked(fetchFail2BanLog); -const mockFetchStatus = vi.mocked(fetchServiceStatus); - -// --------------------------------------------------------------------------- -// Fixtures -// --------------------------------------------------------------------------- - -const onlineStatus: ServiceStatusResponse = { - online: true, - version: "1.0.2", - jail_count: 3, - total_bans: 12, - total_failures: 5, - log_level: "INFO", - log_target: "/var/log/fail2ban.log", -}; - -const offlineStatus: ServiceStatusResponse = { - online: false, - version: null, - jail_count: 0, - total_bans: 0, - total_failures: 0, - log_level: "UNKNOWN", - log_target: "UNKNOWN", -}; - -const logResponse: Fail2BanLogResponse = { - log_path: "/var/log/fail2ban.log", - lines: [ - "2025-01-01 12:00:00 INFO sshd Found 1.2.3.4", - "2025-01-01 12:00:01 WARNING sshd Too many failures", - "2025-01-01 12:00:02 ERROR fail2ban something went wrong", - ], - total_lines: 1000, - log_level: "INFO", - log_target: "/var/log/fail2ban.log", -}; - -const nonFileLogResponse: Fail2BanLogResponse = { - ...logResponse, - log_target: "STDOUT", - lines: [], -}; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function renderTab() { - return render( - - - , - ); -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -describe("LogTab", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("shows a spinner while loading", () => { - // Never resolves during this test. - mockFetchStatus.mockReturnValue(new Promise(() => undefined)); - mockFetchLog.mockReturnValue(new Promise(() => undefined)); - - renderTab(); - - expect(screen.getByText(/loading log viewer/i)).toBeInTheDocument(); - }); - - it("renders the health panel with Running badge when online", async () => { - mockFetchStatus.mockResolvedValue(onlineStatus); - mockFetchLog.mockResolvedValue(logResponse); - - renderTab(); - - await waitFor(() => { expect(screen.queryByText(/loading log viewer/i)).toBeNull(); }); - - expect(screen.getByText("Running")).toBeInTheDocument(); - expect(screen.getByText("1.0.2")).toBeInTheDocument(); - expect(screen.getByText("3")).toBeInTheDocument(); // active jails - expect(screen.getByText("12")).toBeInTheDocument(); // total bans - }); - - it("renders the Offline badge and warning when fail2ban is down", async () => { - mockFetchStatus.mockResolvedValue(offlineStatus); - mockFetchLog.mockRejectedValue(new Error("not running")); - - renderTab(); - - await waitFor(() => { expect(screen.queryByText(/loading log viewer/i)).toBeNull(); }); - - expect(screen.getByText("Offline")).toBeInTheDocument(); - expect(screen.getByText(/not running or unreachable/i)).toBeInTheDocument(); - }); - - it("renders log lines in the log viewer", async () => { - mockFetchStatus.mockResolvedValue(onlineStatus); - mockFetchLog.mockResolvedValue(logResponse); - - renderTab(); - - await waitFor(() => { - expect(screen.getByText(/2025-01-01 12:00:00 INFO/)).toBeInTheDocument(); - }); - - expect(screen.getByText(/2025-01-01 12:00:01 WARNING/)).toBeInTheDocument(); - expect(screen.getByText(/2025-01-01 12:00:02 ERROR/)).toBeInTheDocument(); - }); - - it("shows a non-file target info banner when log_target is STDOUT", async () => { - mockFetchStatus.mockResolvedValue(onlineStatus); - mockFetchLog.mockResolvedValue(nonFileLogResponse); - - renderTab(); - - await waitFor(() => { - expect(screen.getByText(/fail2ban is logging to/i)).toBeInTheDocument(); - }); - - expect(screen.getByText(/STDOUT/)).toBeInTheDocument(); - expect(screen.queryByText(/Refresh/)).toBeNull(); - }); - - it("shows empty state when no lines match the filter", async () => { - mockFetchStatus.mockResolvedValue(onlineStatus); - mockFetchLog.mockResolvedValue({ ...logResponse, lines: [] }); - - renderTab(); - - await waitFor(() => { - expect(screen.getByText(/no log entries found/i)).toBeInTheDocument(); - }); - }); - - it("shows truncation notice when total_lines > lines.length", async () => { - mockFetchStatus.mockResolvedValue(onlineStatus); - mockFetchLog.mockResolvedValue({ ...logResponse, lines: logResponse.lines, total_lines: 1000 }); - - renderTab(); - - await waitFor(() => { - expect(screen.getByText(/showing last/i)).toBeInTheDocument(); - }); - }); - - it("calls fetchFail2BanLog again on Refresh button click", async () => { - mockFetchStatus.mockResolvedValue(onlineStatus); - mockFetchLog.mockResolvedValue(logResponse); - - const user = userEvent.setup(); - renderTab(); - - await waitFor(() => { expect(screen.getByText(/Refresh/)).toBeInTheDocument(); }); - - const refreshBtn = screen.getByRole("button", { name: /refresh/i }); - await user.click(refreshBtn); - - await waitFor(() => { expect(mockFetchLog).toHaveBeenCalledTimes(2); }); - }); -}); diff --git a/frontend/src/components/config/index.ts b/frontend/src/components/config/index.ts index 1a0b7a1..0daaaf5 100644 --- a/frontend/src/components/config/index.ts +++ b/frontend/src/components/config/index.ts @@ -33,11 +33,11 @@ export { FiltersTab } from "./FiltersTab"; export { JailFilesTab } from "./JailFilesTab"; export { JailFileForm } from "./JailFileForm"; export { JailsTab } from "./JailsTab"; -export { LogTab } from "./LogTab"; export { RawConfigSection } from "./RawConfigSection"; export type { RawConfigSectionProps } from "./RawConfigSection"; export { RegexList } from "./RegexList"; export type { RegexListProps } from "./RegexList"; export { RegexTesterTab } from "./RegexTesterTab"; export { ServerTab } from "./ServerTab"; +export { ServerHealthSection } from "./ServerHealthSection"; export { useConfigStyles } from "./configStyles"; diff --git a/frontend/src/pages/ConfigPage.tsx b/frontend/src/pages/ConfigPage.tsx index a11590b..b3bb7d0 100644 --- a/frontend/src/pages/ConfigPage.tsx +++ b/frontend/src/pages/ConfigPage.tsx @@ -8,7 +8,7 @@ * Jails — per-jail config accordion with inline editing * Filters — structured filter.d form editor * Actions — structured action.d form editor - * Server — server-level settings, logging, database config, map thresholds + flush logs + * Server — server-level settings, map thresholds, service health + log viewer * Regex Tester — live pattern tester * Export — raw file editors for jail, filter, and action files */ @@ -19,7 +19,6 @@ import { ActionsTab, FiltersTab, JailsTab, - LogTab, RegexTesterTab, ServerTab, } from "../components/config"; @@ -55,8 +54,7 @@ type TabValue = | "filters" | "actions" | "server" - | "regex" - | "log"; + | "regex"; export function ConfigPage(): React.JSX.Element { const styles = useStyles(); @@ -85,7 +83,6 @@ export function ConfigPage(): React.JSX.Element { Actions Server Regex Tester - Log
@@ -94,7 +91,6 @@ export function ConfigPage(): React.JSX.Element { {tab === "actions" && } {tab === "server" && } {tab === "regex" && } - {tab === "log" && }
);