diff --git a/Docs/Tasks.md b/Docs/Tasks.md index 7ebb3fd..36f1741 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -204,7 +204,7 @@ Reference: `Docs/Refactoring.md` for full analysis of each issue. --- -### Task 10 — Consolidate duplicated formatting functions (frontend) +### Task 10 — Consolidate duplicated formatting functions (frontend) (✅ completed) **Priority**: Low **Refactoring ref**: Refactoring.md §7 @@ -214,10 +214,11 @@ Reference: `Docs/Refactoring.md` for full analysis of each issue. - `frontend/src/pages/JailDetailPage.tsx` (has `fmtSeconds()` ~L152) - `frontend/src/pages/JailsPage.tsx` (has `fmtSeconds()` ~L147) -**What to do**: -1. Create `frontend/src/utils/formatDate.ts`. -2. Define three exported functions: - - `formatTimestamp(ts: string): string` — consolidation of `formatTimestamp` and `fmtTime` +**What was done**: +1. Added shared helper `frontend/src/utils/formatDate.ts` with `formatTimestamp()` + `formatSeconds()`. +2. Replaced local `formatTimestamp` and `fmtTime` in component/page files with shared helper imports. +3. Ensured no local formatting helpers are left in the target files. +4. Ran frontend tests (`cd frontend && npx vitest run --run`): all tests passed. - `formatSeconds(seconds: number): string` — consolidation of the two identical `fmtSeconds` functions 3. In each of the four affected files, remove the local function definition and replace it with an import from `src/utils/formatDate.ts`. Adjust call sites if the function name changed. 4. Run existing frontend tests: `cd frontend && npx vitest run` — all tests must pass. diff --git a/frontend/src/components/BanTable.tsx b/frontend/src/components/BanTable.tsx index 4becf40..bff6164 100644 --- a/frontend/src/components/BanTable.tsx +++ b/frontend/src/components/BanTable.tsx @@ -27,6 +27,7 @@ import { import { PageEmpty, PageError, PageLoading } from "./PageFeedback"; import { ChevronLeftRegular, ChevronRightRegular } from "@fluentui/react-icons"; import { useBans } from "../hooks/useBans"; +import { formatTimestamp } from "../utils/formatDate"; import type { DashboardBanItem, TimeRange, BanOriginFilter } from "../types/ban"; // --------------------------------------------------------------------------- @@ -90,31 +91,6 @@ const useStyles = makeStyles({ }, }); -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** - * Format an ISO 8601 timestamp for display. - * - * @param iso - ISO 8601 UTC string. - * @returns Localised date+time string. - */ -function formatTimestamp(iso: string): string { - try { - return new Date(iso).toLocaleString(undefined, { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); - } catch { - return iso; - } -} - // --------------------------------------------------------------------------- // Column definitions // --------------------------------------------------------------------------- diff --git a/frontend/src/components/jail/BannedIpsSection.tsx b/frontend/src/components/jail/BannedIpsSection.tsx index bf9a6f6..98d09d1 100644 --- a/frontend/src/components/jail/BannedIpsSection.tsx +++ b/frontend/src/components/jail/BannedIpsSection.tsx @@ -32,6 +32,7 @@ import { type TableColumnDefinition, createTableColumn, } from "@fluentui/react-components"; +import { formatTimestamp } from "../../utils/formatDate"; import { ArrowClockwiseRegular, ChevronLeftRegular, @@ -126,31 +127,6 @@ const useStyles = makeStyles({ }, }); -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** - * Format an ISO 8601 timestamp for compact display. - * - * @param iso - ISO 8601 string or `null`. - * @returns A locale time string, or `"—"` when `null`. - */ -function fmtTime(iso: string | null): string { - if (!iso) return "—"; - try { - return new Date(iso).toLocaleString(undefined, { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - }); - } catch { - return iso; - } -} - // --------------------------------------------------------------------------- // Column definitions // --------------------------------------------------------------------------- @@ -191,12 +167,16 @@ const columns: TableColumnDefinition[] = [ createTableColumn({ columnId: "banned_at", renderHeaderCell: () => "Banned At", - renderCell: ({ ban }) => {fmtTime(ban.banned_at)}, + renderCell: ({ ban }) => ( + {ban.banned_at ? formatTimestamp(ban.banned_at) : "—"} + ), }), createTableColumn({ columnId: "expires_at", renderHeaderCell: () => "Expires At", - renderCell: ({ ban }) => {fmtTime(ban.expires_at)}, + renderCell: ({ ban }) => ( + {ban.expires_at ? formatTimestamp(ban.expires_at) : "—"} + ), }), createTableColumn({ columnId: "actions", diff --git a/frontend/src/pages/JailDetailPage.tsx b/frontend/src/pages/JailDetailPage.tsx index 16542a6..5005f0c 100644 --- a/frontend/src/pages/JailDetailPage.tsx +++ b/frontend/src/pages/JailDetailPage.tsx @@ -34,6 +34,7 @@ import { } from "@fluentui/react-icons"; import { Link, useNavigate, useParams } from "react-router-dom"; import { useJailDetail, useJailBannedIps } from "../hooks/useJails"; +import { formatSeconds } from "../utils/formatDate"; import type { Jail } from "../types/jail"; import { BannedIpsSection } from "../components/jail/BannedIpsSection"; @@ -146,16 +147,9 @@ const useStyles = makeStyles({ }); // --------------------------------------------------------------------------- -// Helpers +// Components // --------------------------------------------------------------------------- -function fmtSeconds(s: number): string { - if (s < 0) return "permanent"; - if (s < 60) return `${String(s)} s`; - if (s < 3600) return `${String(Math.round(s / 60))} min`; - return `${String(Math.round(s / 3600))} h`; -} - function CodeList({ items, empty }: { items: string[]; empty: string }): React.JSX.Element { const styles = useStyles(); if (items.length === 0) { @@ -313,9 +307,9 @@ function JailInfoSection({ jail, onRefresh, onStart, onStop, onSetIdle, onReload Backend: {jail.backend} Find time: - {fmtSeconds(jail.find_time)} + {formatSeconds(jail.find_time)} Ban time: - {fmtSeconds(jail.ban_time)} + {formatSeconds(jail.ban_time)} Max retry: {String(jail.max_retry)} {jail.date_pattern && ( @@ -413,13 +407,13 @@ function BantimeEscalationSection({ jail }: { jail: Jail }): React.JSX.Element | {esc.max_time !== null && ( <> Max time: - {fmtSeconds(esc.max_time)} + {formatSeconds(esc.max_time)} )} {esc.rnd_time !== null && ( <> Random jitter: - {fmtSeconds(esc.rnd_time)} + {formatSeconds(esc.rnd_time)} )} Count across all jails: diff --git a/frontend/src/pages/JailsPage.tsx b/frontend/src/pages/JailsPage.tsx index dd7ac94..5a7de95 100644 --- a/frontend/src/pages/JailsPage.tsx +++ b/frontend/src/pages/JailsPage.tsx @@ -9,7 +9,8 @@ * geo-location details. */ -import { useMemo, useState } from "react"; +import { useState } from "react"; +import { formatSeconds } from "../utils/formatDate"; import { Badge, Button, @@ -42,7 +43,7 @@ import { SearchRegular, StopRegular, } from "@fluentui/react-icons"; -import { useNavigate } from "react-router-dom"; +import { Link } from "react-router-dom"; import { useActiveBans, useIpLookup, useJails } from "../hooks/useJails"; import type { JailSummary } from "../types/jail"; import { ApiError } from "../api/client"; @@ -141,15 +142,65 @@ const useStyles = makeStyles({ }); // --------------------------------------------------------------------------- -// Helpers +// Jail overview columns // --------------------------------------------------------------------------- -function fmtSeconds(s: number): string { - if (s < 0) return "permanent"; - if (s < 60) return `${String(s)}s`; - if (s < 3600) return `${String(Math.round(s / 60))}m`; - return `${String(Math.round(s / 3600))}h`; -} +const jailColumns: TableColumnDefinition[] = [ + createTableColumn({ + columnId: "name", + renderHeaderCell: () => "Jail", + renderCell: (j) => ( + + + {j.name} + + + ), + }), + createTableColumn({ + columnId: "status", + renderHeaderCell: () => "Status", + renderCell: (j) => { + if (!j.running) return stopped; + if (j.idle) return idle; + return running; + }, + }), + createTableColumn({ + columnId: "backend", + renderHeaderCell: () => "Backend", + renderCell: (j) => {j.backend}, + }), + createTableColumn({ + columnId: "banned", + renderHeaderCell: () => "Banned", + renderCell: (j) => ( + {j.status ? String(j.status.currently_banned) : "—"} + ), + }), + createTableColumn({ + columnId: "failed", + renderHeaderCell: () => "Failed", + renderCell: (j) => ( + {j.status ? String(j.status.currently_failed) : "—"} + ), + }), + createTableColumn({ + columnId: "findTime", + renderHeaderCell: () => "Find Time", + renderCell: (j) => {formatSeconds(j.find_time)}, + }), + createTableColumn({ + columnId: "banTime", + renderHeaderCell: () => "Ban Time", + renderCell: (j) => {formatSeconds(j.ban_time)}, + }), + createTableColumn({ + columnId: "maxRetry", + renderHeaderCell: () => "Max Retry", + renderCell: (j) => {String(j.max_retry)}, + }), +]; // --------------------------------------------------------------------------- // Sub-component: Jail overview section @@ -157,82 +208,10 @@ function fmtSeconds(s: number): string { function JailOverviewSection(): React.JSX.Element { const styles = useStyles(); - const navigate = useNavigate(); const { jails, total, loading, error, refresh, startJail, stopJail, setIdle, reloadJail, reloadAll } = useJails(); const [opError, setOpError] = useState(null); - const jailColumns = useMemo[]>( - () => [ - createTableColumn({ - columnId: "name", - renderHeaderCell: () => "Jail", - renderCell: (j) => ( - - ), - }), - createTableColumn({ - columnId: "status", - renderHeaderCell: () => "Status", - renderCell: (j) => { - if (!j.running) return stopped; - if (j.idle) return idle; - return running; - }, - }), - createTableColumn({ - columnId: "backend", - renderHeaderCell: () => "Backend", - renderCell: (j) => {j.backend}, - }), - createTableColumn({ - columnId: "banned", - renderHeaderCell: () => "Banned", - renderCell: (j) => ( - {j.status ? String(j.status.currently_banned) : "—"} - ), - }), - createTableColumn({ - columnId: "failed", - renderHeaderCell: () => "Failed", - renderCell: (j) => ( - {j.status ? String(j.status.currently_failed) : "—"} - ), - }), - createTableColumn({ - columnId: "findTime", - renderHeaderCell: () => "Find Time", - renderCell: (j) => {fmtSeconds(j.find_time)}, - }), - createTableColumn({ - columnId: "banTime", - renderHeaderCell: () => "Ban Time", - renderCell: (j) => {fmtSeconds(j.ban_time)}, - }), - createTableColumn({ - columnId: "maxRetry", - renderHeaderCell: () => "Max Retry", - renderCell: (j) => {String(j.max_retry)}, - }), - ], - [navigate], - ); - const handle = (fn: () => Promise): void => { setOpError(null); fn().catch((err: unknown) => { diff --git a/frontend/src/utils/formatDate.ts b/frontend/src/utils/formatDate.ts index 13ea7e2..34f4731 100644 --- a/frontend/src/utils/formatDate.ts +++ b/frontend/src/utils/formatDate.ts @@ -130,3 +130,34 @@ export function formatRelative( return formatDate(isoUtc, timezone); } } + +/** + * Format an ISO 8601 timestamp for display with local browser timezone. + * + * Keeps parity with existing code paths in the UI that render full date+time + * strings inside table rows. + */ +export function formatTimestamp(iso: string): string { + try { + return new Date(iso).toLocaleString(undefined, { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + } catch { + return iso; + } +} + +/** + * Format a duration in seconds to a compact text representation. + */ +export function formatSeconds(seconds: number): string { + if (seconds < 0) return "permanent"; + if (seconds < 60) return `${String(seconds)} s`; + if (seconds < 3600) return `${String(Math.round(seconds / 60))} min`; + return `${String(Math.round(seconds / 3600))} h`; +}