Refactor frontend date formatting helpers and mark Task 10 done

This commit is contained in:
2026-03-21 17:25:45 +01:00
parent 5a49106f4d
commit ffaa5c3adb
6 changed files with 54 additions and 82 deletions

View File

@@ -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 **Priority**: Low
**Refactoring ref**: Refactoring.md §7 **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/JailDetailPage.tsx` (has `fmtSeconds()` ~L152)
- `frontend/src/pages/JailsPage.tsx` (has `fmtSeconds()` ~L147) - `frontend/src/pages/JailsPage.tsx` (has `fmtSeconds()` ~L147)
**What to do**: **What was done**:
1. Create `frontend/src/utils/formatDate.ts`. 1. Added shared helper `frontend/src/utils/formatDate.ts` with `formatTimestamp()` + `formatSeconds()`.
2. Define three exported functions: 2. Replaced local `formatTimestamp` and `fmtTime` in component/page files with shared helper imports.
- `formatTimestamp(ts: string): string` — consolidation of `formatTimestamp` and `fmtTime` 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 - `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. 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. 4. Run existing frontend tests: `cd frontend && npx vitest run` — all tests must pass.

View File

@@ -27,6 +27,7 @@ import {
import { PageEmpty, PageError, PageLoading } from "./PageFeedback"; import { PageEmpty, PageError, PageLoading } from "./PageFeedback";
import { ChevronLeftRegular, ChevronRightRegular } from "@fluentui/react-icons"; import { ChevronLeftRegular, ChevronRightRegular } from "@fluentui/react-icons";
import { useBans } from "../hooks/useBans"; import { useBans } from "../hooks/useBans";
import { formatTimestamp } from "../utils/formatDate";
import type { DashboardBanItem, TimeRange, BanOriginFilter } from "../types/ban"; 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 // Column definitions
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -32,6 +32,7 @@ import {
type TableColumnDefinition, type TableColumnDefinition,
createTableColumn, createTableColumn,
} from "@fluentui/react-components"; } from "@fluentui/react-components";
import { formatTimestamp } from "../../utils/formatDate";
import { import {
ArrowClockwiseRegular, ArrowClockwiseRegular,
ChevronLeftRegular, 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 // Column definitions
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -191,12 +167,16 @@ const columns: TableColumnDefinition<BanRow>[] = [
createTableColumn<BanRow>({ createTableColumn<BanRow>({
columnId: "banned_at", columnId: "banned_at",
renderHeaderCell: () => "Banned At", renderHeaderCell: () => "Banned At",
renderCell: ({ ban }) => <Text size={200}>{fmtTime(ban.banned_at)}</Text>, renderCell: ({ ban }) => (
<Text size={200}>{ban.banned_at ? formatTimestamp(ban.banned_at) : "—"}</Text>
),
}), }),
createTableColumn<BanRow>({ createTableColumn<BanRow>({
columnId: "expires_at", columnId: "expires_at",
renderHeaderCell: () => "Expires At", renderHeaderCell: () => "Expires At",
renderCell: ({ ban }) => <Text size={200}>{fmtTime(ban.expires_at)}</Text>, renderCell: ({ ban }) => (
<Text size={200}>{ban.expires_at ? formatTimestamp(ban.expires_at) : "—"}</Text>
),
}), }),
createTableColumn<BanRow>({ createTableColumn<BanRow>({
columnId: "actions", columnId: "actions",

View File

@@ -34,6 +34,7 @@ import {
} from "@fluentui/react-icons"; } from "@fluentui/react-icons";
import { Link, useNavigate, useParams } from "react-router-dom"; import { Link, useNavigate, useParams } from "react-router-dom";
import { useJailDetail, useJailBannedIps } from "../hooks/useJails"; import { useJailDetail, useJailBannedIps } from "../hooks/useJails";
import { formatSeconds } from "../utils/formatDate";
import type { Jail } from "../types/jail"; import type { Jail } from "../types/jail";
import { BannedIpsSection } from "../components/jail/BannedIpsSection"; 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 { function CodeList({ items, empty }: { items: string[]; empty: string }): React.JSX.Element {
const styles = useStyles(); const styles = useStyles();
if (items.length === 0) { if (items.length === 0) {
@@ -313,9 +307,9 @@ function JailInfoSection({ jail, onRefresh, onStart, onStop, onSetIdle, onReload
<Text className={styles.label}>Backend:</Text> <Text className={styles.label}>Backend:</Text>
<Text className={styles.mono}>{jail.backend}</Text> <Text className={styles.mono}>{jail.backend}</Text>
<Text className={styles.label}>Find time:</Text> <Text className={styles.label}>Find time:</Text>
<Text>{fmtSeconds(jail.find_time)}</Text> <Text>{formatSeconds(jail.find_time)}</Text>
<Text className={styles.label}>Ban time:</Text> <Text className={styles.label}>Ban time:</Text>
<Text>{fmtSeconds(jail.ban_time)}</Text> <Text>{formatSeconds(jail.ban_time)}</Text>
<Text className={styles.label}>Max retry:</Text> <Text className={styles.label}>Max retry:</Text>
<Text>{String(jail.max_retry)}</Text> <Text>{String(jail.max_retry)}</Text>
{jail.date_pattern && ( {jail.date_pattern && (
@@ -413,13 +407,13 @@ function BantimeEscalationSection({ jail }: { jail: Jail }): React.JSX.Element |
{esc.max_time !== null && ( {esc.max_time !== null && (
<> <>
<Text className={styles.label}>Max time:</Text> <Text className={styles.label}>Max time:</Text>
<Text>{fmtSeconds(esc.max_time)}</Text> <Text>{formatSeconds(esc.max_time)}</Text>
</> </>
)} )}
{esc.rnd_time !== null && ( {esc.rnd_time !== null && (
<> <>
<Text className={styles.label}>Random jitter:</Text> <Text className={styles.label}>Random jitter:</Text>
<Text>{fmtSeconds(esc.rnd_time)}</Text> <Text>{formatSeconds(esc.rnd_time)}</Text>
</> </>
)} )}
<Text className={styles.label}>Count across all jails:</Text> <Text className={styles.label}>Count across all jails:</Text>

View File

@@ -10,6 +10,7 @@
*/ */
import { useState } from "react"; import { useState } from "react";
import { formatSeconds } from "../utils/formatDate";
import { import {
Badge, Badge,
Button, Button,
@@ -140,17 +141,6 @@ const useStyles = makeStyles({
lookupLabel: { fontWeight: tokens.fontWeightSemibold }, 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 // Jail overview columns
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -198,12 +188,12 @@ const jailColumns: TableColumnDefinition<JailSummary>[] = [
createTableColumn<JailSummary>({ createTableColumn<JailSummary>({
columnId: "findTime", columnId: "findTime",
renderHeaderCell: () => "Find Time", renderHeaderCell: () => "Find Time",
renderCell: (j) => <Text size={200}>{fmtSeconds(j.find_time)}</Text>, renderCell: (j) => <Text size={200}>{formatSeconds(j.find_time)}</Text>,
}), }),
createTableColumn<JailSummary>({ createTableColumn<JailSummary>({
columnId: "banTime", columnId: "banTime",
renderHeaderCell: () => "Ban Time", renderHeaderCell: () => "Ban Time",
renderCell: (j) => <Text size={200}>{fmtSeconds(j.ban_time)}</Text>, renderCell: (j) => <Text size={200}>{formatSeconds(j.ban_time)}</Text>,
}), }),
createTableColumn<JailSummary>({ createTableColumn<JailSummary>({
columnId: "maxRetry", columnId: "maxRetry",

View File

@@ -130,3 +130,34 @@ export function formatRelative(
return formatDate(isoUtc, timezone); 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`;
}