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 11a9c91..5a7de95 100644 --- a/frontend/src/pages/JailsPage.tsx +++ b/frontend/src/pages/JailsPage.tsx @@ -10,6 +10,7 @@ */ import { useState } from "react"; +import { formatSeconds } from "../utils/formatDate"; import { Badge, Button, @@ -140,17 +141,6 @@ const useStyles = makeStyles({ lookupLabel: { fontWeight: tokens.fontWeightSemibold }, }); -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -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`; -} - // --------------------------------------------------------------------------- // Jail overview columns // --------------------------------------------------------------------------- @@ -198,12 +188,12 @@ const jailColumns: TableColumnDefinition[] = [ createTableColumn({ columnId: "findTime", renderHeaderCell: () => "Find Time", - renderCell: (j) => {fmtSeconds(j.find_time)}, + renderCell: (j) => {formatSeconds(j.find_time)}, }), createTableColumn({ columnId: "banTime", renderHeaderCell: () => "Ban Time", - renderCell: (j) => {fmtSeconds(j.ban_time)}, + renderCell: (j) => {formatSeconds(j.ban_time)}, }), createTableColumn({ columnId: "maxRetry", 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`; +}