From 38b9d352555cd4c0e2664bbaa0c7f108ce4849bb Mon Sep 17 00:00:00 2001 From: Lukas Date: Sun, 19 Apr 2026 09:30:35 +0200 Subject: [PATCH] Refactor frontend pages and config components into single-component files for Task 13 --- .../blocklist/BlocklistSourcesSection.tsx | 168 +---- .../components/blocklist/PreviewDialog.tsx | 83 +++ .../components/blocklist/SourceFormDialog.tsx | 102 +++ .../src/components/config/ActionDetail.tsx | 94 +++ frontend/src/components/config/ActionForm.tsx | 290 +------- .../components/config/ActionFormEditor.tsx | 131 ++++ frontend/src/components/config/ActionsTab.tsx | 153 +---- .../src/components/config/CommandField.tsx | 21 + .../src/components/config/FilterDetail.tsx | 48 ++ frontend/src/components/config/FilterForm.tsx | 298 +-------- .../components/config/FilterFormEditor.tsx | 162 +++++ frontend/src/components/config/FiltersTab.tsx | 95 +-- .../src/components/config/JailFileForm.tsx | 434 +----------- .../components/config/JailFileFormInner.tsx | 84 +++ .../components/config/JailSectionPanel.tsx | 157 +++++ frontend/src/components/config/KVEditor.tsx | 79 +++ .../components/config/StringListEditor.tsx | 63 ++ frontend/src/pages/BlocklistsPage.tsx | 32 +- frontend/src/pages/JailDetailPage.tsx | 551 +-------------- frontend/src/pages/JailsPage.tsx | 629 +----------------- .../pages/blocklists/ImportResultDialog.tsx | 32 + .../pages/jail/BantimeEscalationSection.tsx | 61 ++ frontend/src/pages/jail/CodeList.tsx | 24 + frontend/src/pages/jail/IgnoreListSection.tsx | 137 ++++ frontend/src/pages/jail/JailInfoSection.tsx | 146 ++++ frontend/src/pages/jail/PatternsSection.tsx | 46 ++ .../src/pages/jail/jailDetailPageStyles.ts | 75 +++ frontend/src/pages/jails/BanUnbanForm.tsx | 179 +++++ frontend/src/pages/jails/IpLookupSection.tsx | 123 ++++ .../src/pages/jails/JailOverviewSection.tsx | 218 ++++++ frontend/src/pages/jails/jailsPageStyles.ts | 46 ++ 31 files changed, 2158 insertions(+), 2603 deletions(-) create mode 100644 frontend/src/components/blocklist/PreviewDialog.tsx create mode 100644 frontend/src/components/blocklist/SourceFormDialog.tsx create mode 100644 frontend/src/components/config/ActionDetail.tsx create mode 100644 frontend/src/components/config/ActionFormEditor.tsx create mode 100644 frontend/src/components/config/CommandField.tsx create mode 100644 frontend/src/components/config/FilterDetail.tsx create mode 100644 frontend/src/components/config/FilterFormEditor.tsx create mode 100644 frontend/src/components/config/JailFileFormInner.tsx create mode 100644 frontend/src/components/config/JailSectionPanel.tsx create mode 100644 frontend/src/components/config/KVEditor.tsx create mode 100644 frontend/src/components/config/StringListEditor.tsx create mode 100644 frontend/src/pages/blocklists/ImportResultDialog.tsx create mode 100644 frontend/src/pages/jail/BantimeEscalationSection.tsx create mode 100644 frontend/src/pages/jail/CodeList.tsx create mode 100644 frontend/src/pages/jail/IgnoreListSection.tsx create mode 100644 frontend/src/pages/jail/JailInfoSection.tsx create mode 100644 frontend/src/pages/jail/PatternsSection.tsx create mode 100644 frontend/src/pages/jail/jailDetailPageStyles.ts create mode 100644 frontend/src/pages/jails/BanUnbanForm.tsx create mode 100644 frontend/src/pages/jails/IpLookupSection.tsx create mode 100644 frontend/src/pages/jails/JailOverviewSection.tsx create mode 100644 frontend/src/pages/jails/jailsPageStyles.ts diff --git a/frontend/src/components/blocklist/BlocklistSourcesSection.tsx b/frontend/src/components/blocklist/BlocklistSourcesSection.tsx index 3cfdfa9..16ee6d5 100644 --- a/frontend/src/components/blocklist/BlocklistSourcesSection.tsx +++ b/frontend/src/components/blocklist/BlocklistSourcesSection.tsx @@ -1,14 +1,6 @@ import { useCallback, useState } from "react"; import { Button, - Dialog, - DialogActions, - DialogBody, - DialogContent, - DialogSurface, - DialogTitle, - Field, - Input, MessageBar, MessageBarBody, Spinner, @@ -32,8 +24,10 @@ import { PlayRegular, } from "@fluentui/react-icons"; import { useBlocklists } from "../../hooks/useBlocklist"; -import type { BlocklistSource, PreviewResponse } from "../../types/blocklist"; +import type { BlocklistSource } from "../../types/blocklist"; import { useBlocklistStyles } from "./blocklistStyles"; +import { SourceFormDialog } from "./SourceFormDialog"; +import { PreviewDialog } from "./PreviewDialog"; interface SourceFormValues { name: string; @@ -41,162 +35,6 @@ interface SourceFormValues { enabled: boolean; } -interface SourceFormDialogProps { - open: boolean; - mode: "add" | "edit"; - initial: SourceFormValues; - saving: boolean; - error: string | null; - onClose: () => void; - onSubmit: (values: SourceFormValues) => void; -} - -function SourceFormDialog({ - open, - mode, - initial, - saving, - error, - onClose, - onSubmit, -}: SourceFormDialogProps): React.JSX.Element { - const styles = useBlocklistStyles(); - const [values, setValues] = useState(initial); - - const handleOpen = useCallback((): void => { - setValues(initial); - }, [initial]); - - return ( - { - if (!data.open) onClose(); - }} - > - - - {mode === "add" ? "Add Blocklist Source" : "Edit Blocklist Source"} - -
- {error && ( - - {error} - - )} - - { setValues((p) => ({ ...p, name: d.value })); }} - placeholder="e.g. Blocklist.de — All" - /> - - - { setValues((p) => ({ ...p, url: d.value })); }} - placeholder="https://lists.blocklist.de/lists/all.txt" - /> - - { setValues((p) => ({ ...p, enabled: d.checked })); }} - /> -
-
- - - - -
-
-
- ); -} - -interface PreviewDialogProps { - open: boolean; - source: BlocklistSource | null; - onClose: () => void; - fetchPreview: (id: number) => Promise; -} - -function PreviewDialog({ open, source, onClose, fetchPreview }: PreviewDialogProps): React.JSX.Element { - const styles = useBlocklistStyles(); - const [data, setData] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const handleOpen = useCallback((): void => { - if (!source) return; - setData(null); - setError(null); - setLoading(true); - fetchPreview(source.id) - .then((result) => { - setData(result); - setLoading(false); - }) - .catch((err: unknown) => { - setError(err instanceof Error ? err.message : "Failed to fetch preview"); - setLoading(false); - }); - }, [source, fetchPreview]); - - return ( - { - if (!d.open) onClose(); - }} - > - - - Preview — {source?.name ?? ""} - - {loading && ( -
- -
- )} - {error && ( - - {error} - - )} - {data && ( -
- - {data.valid_count} valid IPs / {data.skipped_count} skipped of {data.total_lines} total lines. Showing first {data.entries.length}: - -
- {data.entries.map((entry) => ( -
{entry}
- ))} -
-
- )} -
- - - -
-
-
- ); -} - interface SourcesSectionProps { onRunImport: () => void; runImportRunning: boolean; diff --git a/frontend/src/components/blocklist/PreviewDialog.tsx b/frontend/src/components/blocklist/PreviewDialog.tsx new file mode 100644 index 0000000..ff9f653 --- /dev/null +++ b/frontend/src/components/blocklist/PreviewDialog.tsx @@ -0,0 +1,83 @@ +import { useCallback, useState } from "react"; +import { + Button, + Dialog, + DialogActions, + DialogBody, + DialogContent, + DialogSurface, + DialogTitle, + MessageBar, + MessageBarBody, + Spinner, + Text, +} from "@fluentui/react-components"; +import type { BlocklistSource, PreviewResponse } from "../../types/blocklist"; + +interface PreviewDialogProps { + open: boolean; + source: BlocklistSource | null; + onClose: () => void; + fetchPreview: (id: number) => Promise; +} + +export function PreviewDialog({ open, source, onClose, fetchPreview }: PreviewDialogProps): React.JSX.Element { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleOpen = useCallback((): void => { + if (!source) return; + setData(null); + setError(null); + setLoading(true); + fetchPreview(source.id) + .then((result) => { + setData(result); + setLoading(false); + }) + .catch((err: unknown) => { + setError(err instanceof Error ? err.message : "Failed to fetch preview"); + setLoading(false); + }); + }, [source, fetchPreview]); + + return ( + { if (!data.open) onClose(); }}> + + + Preview — {source?.name ?? ""} + + {loading && ( +
+ +
+ )} + {error && ( + + {error} + + )} + {data && ( +
+ + {data.valid_count} valid IPs / {data.skipped_count} skipped of {data.total_lines} total lines. Showing first {data.entries.length}: + +
+ {data.entries.map((entry) => ( +
{entry}
+ ))} +
+
+ )} +
+ + + +
+
+
+ ); +} diff --git a/frontend/src/components/blocklist/SourceFormDialog.tsx b/frontend/src/components/blocklist/SourceFormDialog.tsx new file mode 100644 index 0000000..7c5e1f3 --- /dev/null +++ b/frontend/src/components/blocklist/SourceFormDialog.tsx @@ -0,0 +1,102 @@ +import { useCallback, useState } from "react"; +import { + Button, + Dialog, + DialogActions, + DialogBody, + DialogContent, + DialogSurface, + DialogTitle, + Field, + Input, + MessageBar, + MessageBarBody, + Switch, +} from "@fluentui/react-components"; + +interface SourceFormValues { + name: string; + url: string; + enabled: boolean; +} + +interface SourceFormDialogProps { + open: boolean; + mode: "add" | "edit"; + initial: SourceFormValues; + saving: boolean; + error: string | null; + onClose: () => void; + onSubmit: (values: SourceFormValues) => void; +} + +export function SourceFormDialog({ + open, + mode, + initial, + saving, + error, + onClose, + onSubmit, +}: SourceFormDialogProps): React.JSX.Element { + const [values, setValues] = useState(initial); + + const handleOpen = useCallback((): void => { + setValues(initial); + }, [initial]); + + return ( + { + if (!data.open) onClose(); + }} + > + + + {mode === "add" ? "Add Blocklist Source" : "Edit Blocklist Source"} + +
+ {error && ( + + {error} + + )} + + { setValues((p) => ({ ...p, name: d.value })); }} + placeholder="e.g. Blocklist.de — All" + /> + + + { setValues((p) => ({ ...p, url: d.value })); }} + placeholder="https://lists.blocklist.de/lists/all.txt" + /> + + { setValues((p) => ({ ...p, enabled: d.checked })); }} + /> +
+
+ + + + +
+
+
+ ); +} diff --git a/frontend/src/components/config/ActionDetail.tsx b/frontend/src/components/config/ActionDetail.tsx new file mode 100644 index 0000000..45f2eb1 --- /dev/null +++ b/frontend/src/components/config/ActionDetail.tsx @@ -0,0 +1,94 @@ +import { useCallback, useState } from "react"; +import { Button, Field, Input, MessageBar, MessageBarBody } from "@fluentui/react-components"; +import { Delete24Regular, LinkEdit24Regular } from "@fluentui/react-icons"; +import type { ActionConfig } from "../../types/config"; +import { useActionRawFile } from "../../hooks/useActionRawFile"; +import { ActionForm } from "./ActionForm"; +import { RawConfigSection } from "./RawConfigSection"; +import { useConfigStyles } from "./configStyles"; + +interface ActionDetailProps { + action: ActionConfig; + onAssignClick: () => void; + onRemovedFromJail: (jailName: string) => Promise; +} + +export function ActionDetail({ action, onAssignClick, onRemovedFromJail }: ActionDetailProps): React.JSX.Element { + const styles = useConfigStyles(); + const [removingJail, setRemovingJail] = useState(null); + const [removeError, setRemoveError] = useState(null); + const { fetchRawContent, saveRawContent } = useActionRawFile(action.name); + + const handleRemoveFromJail = useCallback( + (jailName: string): void => { + setRemovingJail(jailName); + setRemoveError(null); + onRemovedFromJail(jailName) + .catch((err: unknown) => { + setRemoveError(err instanceof Error ? err.message : "Failed to remove action from jail."); + }) + .finally(() => { + setRemovingJail(null); + }); + }, + [onRemovedFromJail], + ); + + return ( +
+
+ + + +
+ + + +
+ +
+ + {action.used_by_jails.length > 0 && ( +
+ {removeError !== null && ( + + {removeError} + + )} +
+ {action.used_by_jails.map((jailName) => ( +
+ {jailName} + +
+ ))} +
+
+ )} + +
+ +
+
+ ); +} diff --git a/frontend/src/components/config/ActionForm.tsx b/frontend/src/components/config/ActionForm.tsx index 8903744..303163a 100644 --- a/frontend/src/components/config/ActionForm.tsx +++ b/frontend/src/components/config/ActionForm.tsx @@ -1,5 +1,5 @@ /** - * ActionForm — structured form editor for a single ``action.d/*.conf`` file. + * ActionForm — structured form editor for a single `action.d/*.conf` file. * * Displays parsed fields grouped into collapsible sections: * - Includes (before / after) @@ -11,289 +11,15 @@ * Provides a Save button and shows saving/error state. */ -import { useEffect, useMemo, useState } from "react"; -import { - Accordion, - AccordionHeader, - AccordionItem, - AccordionPanel, - Button, - Field, - Input, - MessageBar, - MessageBarBody, - Skeleton, - SkeletonItem, - Text, - Textarea, -} from "@fluentui/react-components"; -import { Add24Regular, Delete24Regular } from "@fluentui/react-icons"; -import type { ActionConfig, ActionConfigUpdate } from "../../types/config"; +import { MessageBar, MessageBarBody, Skeleton, SkeletonItem } from "@fluentui/react-components"; import { useActionConfig } from "../../hooks/useActionConfig"; -import { useAutoSave } from "../../hooks/useAutoSave"; -import { AutoSaveIndicator } from "./AutoSaveIndicator"; -import { useConfigStyles } from "./configStyles"; - -// --------------------------------------------------------------------------- -// Internal helpers -// --------------------------------------------------------------------------- - -/** Editable key-value table for definition_vars / init_vars. */ -interface KVEditorProps { - entries: Record; - onChange: (next: Record) => void; -} - -function KVEditor({ entries, onChange }: KVEditorProps): React.JSX.Element { - const styles = useConfigStyles(); - const rows = Object.entries(entries); - - const handleKeyChange = (oldKey: string, newKey: string): void => { - const next: Record = {}; - for (const [k, v] of Object.entries(entries)) { - next[k === oldKey ? newKey : k] = v; - } - onChange(next); - }; - - const handleValueChange = (key: string, value: string): void => { - onChange({ ...entries, [key]: value }); - }; - - const handleDelete = (key: string): void => { - const { [key]: _removed, ...rest } = entries; - onChange(rest); - }; - - const handleAdd = (): void => { - let newKey = "new_var"; - let n = 1; - while (newKey in entries) { - newKey = `new_var_${String(n)}`; - n++; - } - onChange({ ...entries, [newKey]: "" }); - }; - - return ( -
- {rows.map(([key, value]) => ( -
- { handleKeyChange(key, d.value); }} - /> -