- Created on {format(i.timeCreated, 'MMM d, yyyy')}
+ Created on {toLocaleDateString(i.timeCreated)}
{' '}
/{' '}
{formattedSize.value} {formattedSize.unit}
diff --git a/app/forms/idp/edit.tsx b/app/forms/idp/edit.tsx
index 0ab889d0f0..dea56cd58f 100644
--- a/app/forms/idp/edit.tsx
+++ b/app/forms/idp/edit.tsx
@@ -15,10 +15,10 @@ import { NameField } from '~/components/form/fields/NameField'
import { TextField } from '~/components/form/fields/TextField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { getIdpSelector, useForm, useIdpSelector } from '~/hooks'
+import { DateTime } from '~/ui/lib/DateTime'
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { ResourceLabel } from '~/ui/lib/SideModal'
import { Truncate } from '~/ui/lib/Truncate'
-import { formatDateTime } from '~/util/date'
import { pb } from '~/util/path-builder'
EditIdpSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
@@ -62,10 +62,10 @@ export function EditIdpSideModalForm() {
- {formatDateTime(idp.timeCreated)}
+
- {formatDateTime(idp.timeModified)}
+
diff --git a/app/forms/image-edit.tsx b/app/forms/image-edit.tsx
index 1871960436..85bf2ba275 100644
--- a/app/forms/image-edit.tsx
+++ b/app/forms/image-edit.tsx
@@ -21,10 +21,10 @@ import {
useProjectImageSelector,
useSiloImageSelector,
} from '~/hooks'
+import { DateTime } from '~/ui/lib/DateTime'
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { ResourceLabel } from '~/ui/lib/SideModal'
import { Truncate } from '~/ui/lib/Truncate'
-import { formatDateTime } from '~/util/date'
import { pb } from '~/util/path-builder'
import { bytesToGiB } from '~/util/units'
@@ -94,10 +94,10 @@ export function EditImageSideModalForm({
GiB
- {formatDateTime(image.timeCreated)}
+
- {formatDateTime(image.timeModified)}
+
diff --git a/app/pages/project/instances/instance/InstancePage.tsx b/app/pages/project/instances/instance/InstancePage.tsx
index 1d5a36fe8d..51e4082621 100644
--- a/app/pages/project/instances/instance/InstancePage.tsx
+++ b/app/pages/project/instances/instance/InstancePage.tsx
@@ -5,7 +5,6 @@
*
* Copyright Oxide Computer Company
*/
-import { format } from 'date-fns'
import { filesize } from 'filesize'
import { useMemo } from 'react'
import { Link, useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
@@ -25,6 +24,7 @@ import { RouteTabs, Tab } from '~/components/RouteTabs'
import { InstanceStatusBadge } from '~/components/StatusBadge'
import { getInstanceSelector, useInstanceSelector, useQuickActions } from '~/hooks'
import { EmptyCell } from '~/table/cells/EmptyCell'
+import { DateTime } from '~/ui/lib/DateTime'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { Truncate } from '~/ui/lib/Truncate'
@@ -172,12 +172,7 @@ export function InstancePage() {
-
- {format(instance.timeCreated, 'MMM d, yyyy')}{' '}
-
-
- {format(instance.timeCreated, 'p')}
-
+
diff --git a/app/pages/project/vpcs/VpcPage/VpcPage.tsx b/app/pages/project/vpcs/VpcPage/VpcPage.tsx
index 864a0098c7..f8ffdcb21d 100644
--- a/app/pages/project/vpcs/VpcPage/VpcPage.tsx
+++ b/app/pages/project/vpcs/VpcPage/VpcPage.tsx
@@ -13,10 +13,10 @@ import { Networking24Icon } from '@oxide/design-system/icons/react'
import { QueryParamTabs } from '~/components/QueryParamTabs'
import { getVpcSelector, useVpcSelector } from '~/hooks'
import { EmptyCell } from '~/table/cells/EmptyCell'
+import { DateTime } from '~/ui/lib/DateTime'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { Tabs } from '~/ui/lib/Tabs'
-import { formatDateTime } from '~/util/date'
import { VpcFirewallRulesTab } from './tabs/VpcFirewallRulesTab'
import { VpcSubnetsTab } from './tabs/VpcSubnetsTab'
@@ -56,10 +56,10 @@ export function VpcPage() {
- {formatDateTime(vpc.timeCreated)}
+
- {formatDateTime(vpc.timeModified)}
+
diff --git a/app/pages/system/silos/SiloPage.tsx b/app/pages/system/silos/SiloPage.tsx
index 91c8f2f9f8..5142e81b41 100644
--- a/app/pages/system/silos/SiloPage.tsx
+++ b/app/pages/system/silos/SiloPage.tsx
@@ -14,12 +14,12 @@ import { QueryParamTabs } from '~/components/QueryParamTabs'
import { getSiloSelector, useSiloSelector } from '~/hooks'
import { EmptyCell } from '~/table/cells/EmptyCell'
import { Badge } from '~/ui/lib/Badge'
+import { DateTime } from '~/ui/lib/DateTime'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { TableEmptyBox } from '~/ui/lib/Table'
import { Tabs } from '~/ui/lib/Tabs'
-import { formatDateTime } from '~/util/date'
import { SiloIdpsTab } from './SiloIdpsTab'
import { SiloIpPoolsTab } from './SiloIpPoolsTab'
@@ -64,10 +64,10 @@ export function SiloPage() {
- {formatDateTime(silo.timeCreated)}
+
- {formatDateTime(silo.timeModified)}
+
diff --git a/app/table/columns/common.tsx b/app/table/columns/common.tsx
index 727268463b..c8de914199 100644
--- a/app/table/columns/common.tsx
+++ b/app/table/columns/common.tsx
@@ -6,21 +6,19 @@
* Copyright Oxide Computer Company
*/
-import { format } from 'date-fns/format'
import { filesize } from 'filesize'
+import { DateTime } from '~/ui/lib/DateTime'
import { Truncate } from '~/ui/lib/Truncate'
import { EmptyCell } from '../cells/EmptyCell'
-import { TwoLineCell } from '../cells/TwoLineCell'
// the full type of the info arg is CellContext from RT, but in these
// cells we only care about the return value of getValue
type Info = { getValue: () => T }
function dateCell(info: Info) {
- const date = info.getValue()
- return
+ return
}
function sizeCell(info: Info) {
diff --git a/app/ui/lib/DateTime.tsx b/app/ui/lib/DateTime.tsx
new file mode 100644
index 0000000000..828d1bf1ed
--- /dev/null
+++ b/app/ui/lib/DateTime.tsx
@@ -0,0 +1,16 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * Copyright Oxide Computer Company
+ */
+
+import { toLocaleDateString, toLocaleTimeString } from '~/util/date'
+
+export const DateTime = ({ date, locale }: { date: Date; locale?: string }) => (
+
+)
diff --git a/app/util/date.spec.ts b/app/util/date.spec.ts
index d59ea38e11..63382e082a 100644
--- a/app/util/date.spec.ts
+++ b/app/util/date.spec.ts
@@ -8,7 +8,12 @@
import { subDays, subHours, subMinutes, subSeconds } from 'date-fns'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { timeAgoAbbr } from './date'
+import {
+ timeAgoAbbr,
+ toLocaleDateString,
+ toLocaleDateTimeString,
+ toLocaleTimeString,
+} from './date'
const baseDate = new Date(2021, 5, 7)
@@ -54,4 +59,28 @@ describe('timeAgoAbbr', () => {
expect(timeAgoAbbr(subDays(baseDate, 200), { addSuffix: true })).toEqual('7mo ago')
expect(timeAgoAbbr(subDays(baseDate, 3), { addSuffix: true })).toEqual('3d ago')
})
+
+ it('formats toLocaleDateString', () => {
+ expect(toLocaleDateString(baseDate)).toEqual('Jun 7, 2021')
+ expect(toLocaleDateString(baseDate, 'en-US')).toEqual('Jun 7, 2021')
+ expect(toLocaleDateString(baseDate, 'fr-FR')).toEqual('7 juin 2021')
+ expect(toLocaleDateString(baseDate, 'de-DE')).toEqual('07.06.2021')
+ expect(toLocaleDateString(baseDate, 'ja-JP')).toEqual('2021/06/07')
+ })
+
+ it('formats toLocaleTimeString', () => {
+ expect(toLocaleTimeString(baseDate)).toEqual('12:00 AM')
+ expect(toLocaleTimeString(baseDate, 'en-US')).toEqual('12:00 AM')
+ expect(toLocaleTimeString(baseDate, 'fr-FR')).toEqual('00:00')
+ expect(toLocaleTimeString(baseDate, 'de-DE')).toEqual('00:00')
+ expect(toLocaleTimeString(baseDate, 'ja-JP')).toEqual('0:00')
+ })
+
+ it('formats toLocaleDateTimeString', () => {
+ expect(toLocaleDateTimeString(baseDate)).toEqual('Jun 7, 2021, 12:00 AM')
+ expect(toLocaleDateTimeString(baseDate, 'en-US')).toEqual('Jun 7, 2021, 12:00 AM')
+ expect(toLocaleDateTimeString(baseDate, 'fr-FR')).toEqual('7 juin 2021, 00:00')
+ expect(toLocaleDateTimeString(baseDate, 'de-DE')).toEqual('07.06.2021, 00:00')
+ expect(toLocaleDateTimeString(baseDate, 'ja-JP')).toEqual('2021/06/07 0:00')
+ })
})
diff --git a/app/util/date.ts b/app/util/date.ts
index 8715641522..9f504267df 100644
--- a/app/util/date.ts
+++ b/app/util/date.ts
@@ -5,11 +5,7 @@
*
* Copyright Oxide Computer Company
*/
-import {
- format,
- formatDistanceToNowStrict,
- type FormatDistanceToNowStrictOptions,
-} from 'date-fns'
+import { formatDistanceToNowStrict, type FormatDistanceToNowStrictOptions } from 'date-fns'
// locale setup and formatDistance function copied from here and modified
// https://github.com/date-fns/date-fns/blob/56a3856/src/locale/en-US/_lib/formatDistance/index.js
@@ -47,4 +43,13 @@ export const timeAgoAbbr = (d: Date, options?: FormatDistanceToNowStrictOptions)
},
})
-export const formatDateTime = (d: Date) => format(d, 'MMM d, yyyy H:mm aa')
+// dateStyle: 'medium' looks like `Apr 16, 2024` for en-US
+export const toLocaleDateString = (d: Date, locale?: string) =>
+ new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(d)
+
+// timeStyle: 'short' looks like `8:33 PM` for en-US
+export const toLocaleTimeString = (d: Date, locale?: string) =>
+ new Intl.DateTimeFormat(locale, { timeStyle: 'short' }).format(d)
+
+export const toLocaleDateTimeString = (d: Date, locale?: string) =>
+ new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' }).format(d)
diff --git a/test/e2e/dates.e2e.ts b/test/e2e/dates.e2e.ts
new file mode 100644
index 0000000000..df5cd7b405
--- /dev/null
+++ b/test/e2e/dates.e2e.ts
@@ -0,0 +1,29 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * Copyright Oxide Computer Company
+ */
+import { expect, test } from '@playwright/test'
+
+test('date formatting - English locale', async ({ page }) => {
+ await page.goto('/system/silos')
+ await expect(page.getByText('Feb 28, 202312:00 AM')).toBeVisible()
+})
+
+test.describe('date formatting - German locale', () => {
+ test.use({ locale: 'de-DE' })
+ test('date formatting - German locale', async ({ page }) => {
+ await page.goto('/system/silos')
+ await expect(page.getByText('28.02.202300:00')).toBeVisible()
+ })
+})
+
+test.describe('date formatting - French locale', () => {
+ test.use({ locale: 'fr-FR' })
+ test('date formatting - French locale', async ({ page }) => {
+ await page.goto('/system/silos')
+ await expect(page.getByText('28 févr. 202300:00')).toBeVisible()
+ })
+})
diff --git a/test/e2e/silos.e2e.ts b/test/e2e/silos.e2e.ts
index af733c3afa..9d08ef2e8a 100644
--- a/test/e2e/silos.e2e.ts
+++ b/test/e2e/silos.e2e.ts
@@ -28,6 +28,7 @@ test('Create silo', async ({ page }) => {
// not easy to assert this until we can calculate accessible name instead of text content
// discoverable: 'true',
})
+ await expect(page.getByText('Feb 28, 202312:00 AM')).toBeVisible()
await page.click('role=link[name="New silo"]')
diff --git a/test/e2e/vpcs.e2e.ts b/test/e2e/vpcs.e2e.ts
index 2a0d0a1484..17527f38f7 100644
--- a/test/e2e/vpcs.e2e.ts
+++ b/test/e2e/vpcs.e2e.ts
@@ -12,6 +12,7 @@ test('can nav to VpcPage from /', async ({ page }) => {
await page.getByRole('table').getByRole('link', { name: 'mock-project' }).click()
await page.getByRole('link', { name: 'VPCs' }).click()
await page.getByRole('link', { name: 'mock-vpc' }).click()
+ await expect(page.getByText('Jan 1, 202112:00 AM')).toBeVisible()
await expect(page.getByRole('tab', { name: 'Firewall rules' })).toBeVisible()
await expect(page.getByRole('cell', { name: 'allow-icmp' })).toBeVisible()
expect(await page.title()).toEqual('mock-vpc / VPCs / mock-project / Oxide Console')