refactor: Decompose ConfigPage into focused routing and component layers

Split the over-centralized ConfigPage into focused, composable layers:

1. useTabRouter hook: Encapsulates tab state management and URL synchronization
   - Maintains selected tab and active item (e.g., jail name)
   - Syncs state to location.state for deep linking and browser history
   - Supports bookmarkable URLs and back/forward navigation

2. ConfigPageContainer: Orchestrates tab navigation
   - Manages TabList and routes tab selection events
   - Conditionally renders tab content panels
   - Delegates domain-specific logic to tab components

3. ConfigPage: Focused page layout component
   - Renders page structure (header, title, description)
   - Delegates tab orchestration to ConfigPageContainer
   - No routing or tab state logic

Benefits:
- Page is now 30 lines vs 125 lines (76% reduction)
- Tab state management is reusable for other multi-tab pages
- Each tab component remains focused on domain-specific UI
- Deep linking and browser history work out of the box
- Easier to test and maintain

Added comprehensive tests:
- useTabRouter: 6 tests covering state initialization, tab selection, and deep linking
- ConfigPageContainer: 8 tests covering tab rendering and navigation
- ConfigPage: 3 tests for page structure

Updated Web-Development.md with tab orchestration pattern documentation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-28 08:27:36 +02:00
parent 69a5f0ceb1
commit 42beb9cf3b
9 changed files with 457 additions and 152 deletions

View File

@@ -242,6 +242,78 @@ The distinction between **`pages/`** and **`components/`** is fundamental to the
**Example:** `BannedIpsSection` lives in `components/jail/` (not `pages/jail/`) because it is a reusable UI section that presents banned IPs. If a future report or dashboard also needed to show banned IPs, the same component could be imported and reused. By contrast, `JailDetailPage.tsx` lives in `pages/` because it is the top-level route component.
### Tab Orchestration — ConfigPage Example
When a page contains tab-based navigation (like the configuration page), isolate routing and tab state management into a dedicated **container component** to prevent the page from becoming over-centralized. This pattern applies to any multi-tab page.
**Architecture:**
1. **Page component** (`ConfigPage.tsx`) — renders page layout (header, title, description) and delegates tab routing to a container.
2. **Container component** (`ConfigPageContainer.tsx`) — orchestrates tab navigation, manages which tab content is visible, and routes tab selection events.
3. **Tab router hook** (`useTabRouter.ts`) — encapsulates tab state synchronization with browser history and supports deep linking (e.g., navigating directly to a specific tab with optional active item like a jail name).
4. **Tab components** (`JailsTab.tsx`, `FiltersTab.tsx`, etc.) — domain-specific tab content; each is fully self-contained and receives tab-specific props only.
**Component tree:**
```
ConfigPage (page layout)
└── ConfigPageContainer (tab orchestration)
├── useTabRouter (routing logic)
├── JailsTab (jail editing UI)
├── FiltersTab (filter editing UI)
├── ActionsTab (action editing UI)
├── ServerTab (server settings UI)
└── RegexTesterTab (regex testing UI)
```
**Benefits:**
- **Focused pages** — `ConfigPage` renders only layout; routing logic is in the container.
- **Reusable routing** — `useTabRouter` can be used by other pages with tab navigation.
- **Isolated tabs** — each tab is a focused component; no shared state entanglement.
- **Deep linking** — tab state is synchronized to browser history, allowing bookmarkable URLs and the back/forward buttons to work correctly.
**Key pattern:**
```tsx
// hooks/useTabRouter.ts — routes and state
export type ConfigTabId = "jails" | "filters" | "actions" | "server" | "regex";
export function useTabRouter(): { activeTab: ConfigTabId; selectTab: (tab: ConfigTabId) => void; ... } {
const location = useLocation();
const navigate = useNavigate();
// Sync tab state to location.state for deep linking
// ... (see implementation)
}
// components/config/ConfigPageContainer.tsx — renders tabs
export function ConfigPageContainer(): JSX.Element {
const { activeTab, selectTab } = useTabRouter();
return (
<>
<TabList selectedValue={activeTab} onTabSelect={...}>
{/* Tabs */}
</TabList>
{/* Tab content panels, conditionally rendered via CSS */}
</>
);
}
// pages/ConfigPage.tsx — layout only
export function ConfigPage(): JSX.Element {
return (
<div>
{/* Page header, title, description */}
<ConfigPageContainer />
</div>
);
}
```
**Avoid:**
- Mixing page layout, tab orchestration, and tab content in one file.
- Duplicating tab state across multiple hooks or components.
- Hardcoding tab IDs as strings — use the `ConfigTabId` type.
### Separation of Concerns
- **Pages** handle routing and compose layout + components — they contain no business logic.