diff --git a/Docs/Tasks.md b/Docs/Tasks.md index c0cc2bd..973e913 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -1,23 +1,3 @@ -## 11) Logging semantics are inconsistent across backend modules -- Where found: - - [backend/app/services](backend/app/services) - - [backend/app/tasks](backend/app/tasks) -- Why this is needed: - - Uneven level usage reduces observability quality. -- Goal: - - Standardize logging levels and event naming. -- What to do: - - Define logging conventions. - - Align service/task logging with that convention. -- Possible traps and issues: - - Excessive log volume if levels are set too low globally. -- Docs changes needed: - - Add logging policy and examples. -- Doc references: - - [Docs/Backend-Development.md](Docs/Backend-Development.md) - ---- - ## 12) Prop drilling in jail overview page - Where found: - [frontend/src/pages/jails/JailOverviewSection.tsx](frontend/src/pages/jails/JailOverviewSection.tsx) diff --git a/Docs/Web-Development.md b/Docs/Web-Development.md index 26386de..001716c 100644 --- a/Docs/Web-Development.md +++ b/Docs/Web-Development.md @@ -266,6 +266,49 @@ If a React Context provider is used by **only one page** (e.g., `DashboardFilter **Example:** `DashboardFilterProvider` manages dashboard time-range and origin filters. It is instantiated only inside `DashboardPage.tsx` and its sub-components. Therefore, it lives in `pages/DashboardFilterProvider.tsx` (or `pages/dashboard/DashboardFilterProvider.tsx` if the page is split into a subdirectory), not in `providers/`. +### State Ownership & Prop Drilling + +When a page uses a hook (e.g., `useJails()`) that provides state and actions, and this state needs to be accessed by multiple child components, **eliminate prop drilling by wrapping the page in a context provider**. This reduces coupling, simplifies refactoring, and keeps prop lists focused on component-specific data. + +**When to use context instead of props:** +- A page calls a hook that returns both data and actions (e.g., `useJails()` returns `jails`, `loading`, `refresh`, `startJail`, etc.) +- Two or more child components need access to the same hook's state +- The prop chain would be longer than 2 levels deep + +**Pattern:** +1. Create a context and provider in the same directory as the page's components (e.g., `pages/jails/JailContext.tsx`) +2. Wrap the page content with the provider, passing the hook's result as the context value +3. Child components use the context hook instead of receiving props +4. Update tests to wrap components with the provider + +**Example:** +```tsx +// pages/jails/JailContext.tsx +const JailContext = createContext(undefined); + +export function JailProvider({ children }: { children: ReactNode }): JSX.Element { + const jailState = useJails(); + return {children}; +} + +export function useJailContext(): JailContextValue { + const context = useContext(JailContext); + if (!context) throw new Error("useJailContext must be used within JailProvider"); + return context; +} + +// pages/JailsPage.tsx +export function JailsPage(): JSX.Element { + return ( + + {/* Uses useJailContext() */} + + ); +} +``` + +**Important:** Context should only wrap the minimal subtree that needs access. Do not wrap the entire app with page-specific contexts — keep providers scoped to where they're used. + --- ## 5. UI Framework — Fluent UI React (v9) diff --git a/frontend/src/pages/JailsPage.tsx b/frontend/src/pages/JailsPage.tsx index 337470f..e4aaf7d 100644 --- a/frontend/src/pages/JailsPage.tsx +++ b/frontend/src/pages/JailsPage.tsx @@ -3,12 +3,12 @@ import { useJailsPageStyles } from "./jails/jailsPageStyles"; import { JailOverviewSection } from "./jails/JailOverviewSection"; import { BanUnbanForm } from "./jails/BanUnbanForm"; import { IpLookupSection } from "./jails/IpLookupSection"; +import { JailProvider, useJailContext } from "./jails/JailContext"; import { useActiveBans } from "../hooks/useActiveBans"; -import { useJails } from "../hooks/useJailList"; -export function JailsPage(): React.JSX.Element { +function JailsPageContent(): React.JSX.Element { const styles = useJailsPageStyles(); - const { jails, total, loading, error, refresh, startJail, stopJail, setIdle, reloadJail, reloadAll } = useJails(); + const { jails } = useJailContext(); const { banIp, unbanIp } = useActiveBans(); const jailNames = jails.map((j) => j.name); @@ -19,18 +19,7 @@ export function JailsPage(): React.JSX.Element { Jails - + @@ -38,3 +27,11 @@ export function JailsPage(): React.JSX.Element { ); } + +export function JailsPage(): React.JSX.Element { + return ( + + + + ); +} diff --git a/frontend/src/pages/jails/JailContext.tsx b/frontend/src/pages/jails/JailContext.tsx new file mode 100644 index 0000000..e9bf083 --- /dev/null +++ b/frontend/src/pages/jails/JailContext.tsx @@ -0,0 +1,52 @@ +/** + * Context for managing jail state and actions across the jails page. + * Eliminates prop drilling by providing jail data and operations to all descendants. + */ + +import { createContext, useContext, useCallback, ReactNode } from "react"; +import { useJails } from "../../hooks/useJailList"; +import type { JailSummary } from "../../types/jail"; + +interface JailContextValue { + jails: JailSummary[]; + total: number; + loading: boolean; + error: string | null; + refresh: () => void; + startJail: (name: string) => Promise; + stopJail: (name: string) => Promise; + setIdle: (name: string, on: boolean) => Promise; + reloadJail: (name: string) => Promise; + reloadAll: () => Promise; +} + +const JailContext = createContext(undefined); + +interface JailProviderProps { + children: ReactNode; +} + +/** + * Provider component for jail state. Wrap JailsPage and its children with this. + */ +export function JailProvider({ children }: JailProviderProps): React.JSX.Element { + const jailState = useJails(); + + const value: JailContextValue = useCallback(() => ({ + ...jailState, + }), [jailState])() as JailContextValue; + + return {children}; +} + +/** + * Hook to access jail state and actions. + * Must be used within a JailProvider. + */ +export function useJailContext(): JailContextValue { + const context = useContext(JailContext); + if (!context) { + throw new Error("useJailContext must be used within a JailProvider"); + } + return context; +} diff --git a/frontend/src/pages/jails/JailOverviewSection.tsx b/frontend/src/pages/jails/JailOverviewSection.tsx index e30a9c6..a6759a1 100644 --- a/frontend/src/pages/jails/JailOverviewSection.tsx +++ b/frontend/src/pages/jails/JailOverviewSection.tsx @@ -27,6 +27,7 @@ import { } from "@fluentui/react-icons"; import { useCommonSectionStyles } from "../../components/commonStyles"; import { useJailsPageStyles } from "./jailsPageStyles"; +import { useJailContext } from "./JailContext"; import type { JailSummary } from "../../types/jail"; const useOverviewStyles = makeStyles({ @@ -47,21 +48,8 @@ const useOverviewStyles = makeStyles({ }, }); -interface JailOverviewSectionProps { - jails: JailSummary[]; - total: number; - loading: boolean; - error: string | null; - refresh: () => void; - startJail: (name: string) => Promise; - stopJail: (name: string) => Promise; - setIdle: (name: string, on: boolean) => Promise; - reloadJail: (name: string) => Promise; - reloadAll: () => Promise; -} - -export function JailOverviewSection(props: JailOverviewSectionProps): React.JSX.Element { - const { jails, total, loading, error, refresh, startJail, stopJail, setIdle, reloadJail, reloadAll } = props; +export function JailOverviewSection(): React.JSX.Element { + const { jails, total, loading, error, refresh, startJail, stopJail, setIdle, reloadJail, reloadAll } = useJailContext(); const pageStyles = useJailsPageStyles(); const overviewStyles = useOverviewStyles(); const sectionStyles = useCommonSectionStyles();