From 1a9a1106ca6f03848956f382512009e1ef2d74ae Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Thu, 8 Jan 2026 10:52:23 +1100 Subject: [PATCH] feat: implement customised `formatDate()` function (#21423) Closes #21301 This pull-request adds a very simple `function formatDate(date: Date, options: Intl.DateTimeFormatOptions)` function which will allow us to standardise the rendering of dates across the application. This is inline with the content of #21252. --- .../ActiveUserChart/ActiveUserChart.tsx | 7 ++++- .../components/Timeline/TimelineDateRow.tsx | 3 ++- site/src/components/Timeline/utils.ts | 7 ++++- .../RequestLogsRow/RequestLogsRow.tsx | 27 +++---------------- .../LicenseSeatConsumptionChart.tsx | 7 ++++- .../OverviewPage/UserEngagementChart.tsx | 7 ++++- .../WorkspaceBuildPageView.tsx | 3 ++- .../WorkspaceNotifications.tsx | 17 ++++++------ .../pages/WorkspacePage/WorkspaceTopbar.tsx | 5 ++-- site/src/utils/time.ts | 19 +++++++++++++ 10 files changed, 62 insertions(+), 40 deletions(-) diff --git a/site/src/components/ActiveUserChart/ActiveUserChart.tsx b/site/src/components/ActiveUserChart/ActiveUserChart.tsx index 8a19640233..b75419fdf4 100644 --- a/site/src/components/ActiveUserChart/ActiveUserChart.tsx +++ b/site/src/components/ActiveUserChart/ActiveUserChart.tsx @@ -13,6 +13,7 @@ import { } from "components/HelpTooltip/HelpTooltip"; import type { FC } from "react"; import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; +import { formatDate } from "utils/time"; const chartConfig = { amount: { @@ -43,9 +44,13 @@ export const ActiveUserChart: FC = ({ data }) => { tickMargin={12} minTickGap={24} tickFormatter={(value: string) => - new Date(value).toLocaleDateString(undefined, { + formatDate(new Date(value), { month: "short", day: "numeric", + year: undefined, + hour: undefined, + minute: undefined, + second: undefined, }) } /> diff --git a/site/src/components/Timeline/TimelineDateRow.tsx b/site/src/components/Timeline/TimelineDateRow.tsx index 4f38f7b34a..6ac696c4dc 100644 --- a/site/src/components/Timeline/TimelineDateRow.tsx +++ b/site/src/components/Timeline/TimelineDateRow.tsx @@ -1,6 +1,7 @@ import { css, useTheme } from "@emotion/react"; import { TableCell, TableRow } from "components/Table/Table"; import type { FC } from "react"; +import { formatDate } from "utils/time"; import { createDisplayDate } from "./utils"; export interface TimelineDateRow { @@ -27,7 +28,7 @@ export const TimelineDateRow: FC = ({ date }) => { color: theme.palette.text.secondary, textTransform: "capitalize", }} - title={date.toLocaleDateString()} + title={formatDate(date)} > {createDisplayDate(date)} diff --git a/site/src/components/Timeline/utils.ts b/site/src/components/Timeline/utils.ts index 61745e0a6f..235f945330 100644 --- a/site/src/components/Timeline/utils.ts +++ b/site/src/components/Timeline/utils.ts @@ -1,5 +1,6 @@ import dayjs from "dayjs"; import calendar from "dayjs/plugin/calendar"; +import { formatDate } from "utils/time"; dayjs.extend(calendar); @@ -16,5 +17,9 @@ export const createDisplayDate = ( sameElse: "MM/DD/YYYY", }); } - return date.toLocaleDateString(); + return formatDate(date, { + hour: undefined, + minute: undefined, + second: undefined, + }); }; diff --git a/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsRow/RequestLogsRow.tsx b/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsRow/RequestLogsRow.tsx index b9eb3ca4a6..b525584f54 100644 --- a/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsRow/RequestLogsRow.tsx +++ b/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsRow/RequestLogsRow.tsx @@ -16,23 +16,13 @@ import { } from "lucide-react"; import { type FC, Fragment, useState } from "react"; import { cn } from "utils/cn"; -import { humanDuration } from "utils/time"; +import { formatDate, humanDuration } from "utils/time"; import { AIBridgeProviderIcon } from "../AIBridgeProviderIcon"; type RequestLogsRowProps = { interception: AIBridgeInterception; }; -const customisedDateLocale: Intl.DateTimeFormatOptions = { - second: "2-digit", - minute: "2-digit", - hour: "2-digit", - day: "numeric", - // Show the month as a short name - month: "short", - year: "numeric", -}; - type TokenUsageMetadataMerged = | null | Record @@ -161,10 +151,7 @@ export const RequestLogsRow: FC = ({ interception }) => { )} ({isOpen ? "Hide" : "Show more"}) - {new Date(interception.started_at).toLocaleString( - undefined, - customisedDateLocale, - )} + {formatDate(new Date(interception.started_at))} @@ -268,20 +255,14 @@ export const RequestLogsRow: FC = ({ interception }) => {
Start Time:
- {new Date(interception.started_at).toLocaleString( - undefined, - customisedDateLocale, - )} + {formatDate(new Date(interception.started_at))}
{interception.ended_at && ( <>
End Time:
- {new Date(interception.ended_at).toLocaleString( - undefined, - customisedDateLocale, - )} + {formatDate(new Date(interception.ended_at))}
)} diff --git a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseSeatConsumptionChart.tsx b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseSeatConsumptionChart.tsx index 537de99d70..d844e7a6ae 100644 --- a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseSeatConsumptionChart.tsx +++ b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseSeatConsumptionChart.tsx @@ -24,6 +24,7 @@ import { YAxis, } from "recharts"; import { docs } from "utils/docs"; +import { formatDate } from "utils/time"; const chartConfig = { users: { @@ -153,9 +154,13 @@ export const LicenseSeatConsumptionChart: FC< tickMargin={12} minTickGap={24} tickFormatter={(value: string) => - new Date(value).toLocaleDateString(undefined, { + formatDate(new Date(value), { month: "short", day: "numeric", + year: undefined, + hour: undefined, + minute: undefined, + second: undefined, }) } /> diff --git a/site/src/pages/DeploymentSettingsPage/OverviewPage/UserEngagementChart.tsx b/site/src/pages/DeploymentSettingsPage/OverviewPage/UserEngagementChart.tsx index 6605f5e60a..b18fd69a1b 100644 --- a/site/src/pages/DeploymentSettingsPage/OverviewPage/UserEngagementChart.tsx +++ b/site/src/pages/DeploymentSettingsPage/OverviewPage/UserEngagementChart.tsx @@ -16,6 +16,7 @@ import { ChevronRightIcon } from "lucide-react"; import type { FC } from "react"; import { Link as RouterLink } from "react-router"; import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; +import { formatDate } from "utils/time"; const chartConfig = { users: { @@ -105,9 +106,13 @@ export const UserEngagementChart: FC = ({ data }) => { tickMargin={12} minTickGap={24} tickFormatter={(value: string) => - new Date(value).toLocaleDateString(undefined, { + formatDate(new Date(value), { month: "short", day: "numeric", + year: undefined, + hour: undefined, + minute: undefined, + second: undefined, }) } /> diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx index b6d2f0d825..07dd701006 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx @@ -34,6 +34,7 @@ import { } from "react"; import { Link } from "react-router"; import { cn } from "utils/cn"; +import { formatDate } from "utils/time"; import { displayWorkspaceBuildDuration } from "utils/workspace"; import { Sidebar, SidebarCaption, SidebarItem } from "./Sidebar"; @@ -118,7 +119,7 @@ export const WorkspaceBuildPageView: FC = ({ {displayWorkspaceBuildDuration(build)} - {new Date(build.created_at).toLocaleString()} + {formatDate(new Date(build.created_at))} {build.transition} diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx index f0e91ad7b4..b000bea06c 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx @@ -17,6 +17,7 @@ import { type FC, useEffect, useState } from "react"; dayjs.extend(relativeTime); import { useQuery } from "react-query"; +import { formatDate } from "utils/time"; import type { WorkspacePermissions } from "../../../modules/workspaces/permissions"; import { NotificationActionButton, @@ -128,9 +129,9 @@ export const WorkspaceNotifications: FC = ({ const advancedSchedulingEnabled = entitlements.features.advanced_template_scheduling.enabled; if (advancedSchedulingEnabled && workspace.dormant_at) { - const formatDate = (dateStr: string, timestamp: boolean): string => { + const formatDateTime = (dateStr: string, timestamp: boolean): string => { const date = new Date(dateStr); - return date.toLocaleDateString(undefined, { + return formatDate(date, { month: "long", day: "numeric", year: "numeric", @@ -150,17 +151,17 @@ export const WorkspaceNotifications: FC = ({ <> This workspace has not been used for{" "} {dayjs(workspace.last_used_at).fromNow(true)} and was marked dormant - on {formatDate(workspace.dormant_at, false)}. It is scheduled to be - deleted on {formatDate(workspace.deleting_at, true)}. To keep it you - must activate the workspace. + on {formatDateTime(workspace.dormant_at, false)}. It is scheduled to + be deleted on {formatDateTime(workspace.deleting_at, true)}. To keep + it you must activate the workspace. ) : ( <> This workspace has not been used for{" "} {dayjs(workspace.last_used_at).fromNow(true)} and was marked dormant - on {formatDate(workspace.dormant_at, false)}. It is not scheduled for - auto-deletion but will become a candidate if auto-deletion is enabled - on this template. To keep it you must activate the workspace. + on {formatDateTime(workspace.dormant_at, false)}. It is not scheduled + for auto-deletion but will become a candidate if auto-deletion is + enabled on this template. To keep it you must activate the workspace. ), }); diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 371b999f59..36bfc26aa1 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -31,6 +31,7 @@ import type { FC } from "react"; import { useQuery } from "react-query"; import { Link as RouterLink } from "react-router"; import { displayDormantDeletion } from "utils/dormant"; +import { formatDate } from "utils/time"; import type { WorkspacePermissions } from "../../modules/workspaces/permissions"; import { WorkspaceActions } from "./WorkspaceActions/WorkspaceActions"; import { WorkspaceNotifications } from "./WorkspaceNotifications/WorkspaceNotifications"; @@ -201,9 +202,7 @@ export const WorkspaceTopbar: FC = ({ css={{ color: "inherit" }} > {workspace.deleting_at ? ( - <> - Deletion on {new Date(workspace.deleting_at).toLocaleString()} - + <>Deletion on {formatDate(new Date(workspace.deleting_at))} ) : ( "Deletion soon" )} diff --git a/site/src/utils/time.ts b/site/src/utils/time.ts index fec73268cc..fd6bbedb04 100644 --- a/site/src/utils/time.ts +++ b/site/src/utils/time.ts @@ -48,6 +48,25 @@ export function formatDateTime( return dayjs(date).format(format); } +const defaultDateLocaleOptions: Intl.DateTimeFormatOptions = { + second: "2-digit", + minute: "2-digit", + hour: "2-digit", + day: "numeric", + month: "short", + year: "numeric", +}; + +export function formatDate( + date: Date, + options?: { locale?: Intl.LocalesArgument } & Intl.DateTimeFormatOptions, +) { + return date.toLocaleDateString(options?.locale, { + ...defaultDateLocaleOptions, + ...options, + }); +} + // Duration functions export function humanDuration(durationInMs: number) { return humanizeDuration(durationInMs, {