Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/components/RefetchIntervalPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
* Copyright Oxide Computer Company
*/
import cn from 'classnames'
import { format } from 'date-fns'
import { useEffect, useState } from 'react'

import { Refresh16Icon, Time16Icon } from '@oxide/design-system/icons/react'

import { Listbox, type ListboxItem } from '~/ui/lib/Listbox'
import { SpinnerLoader } from '~/ui/lib/Spinner'
import { useInterval } from '~/ui/lib/use-interval'
import { toLocaleTimeString } from '~/util/date'

const intervalPresets = {
Off: undefined,
Expand Down Expand Up @@ -55,7 +55,8 @@ export function useIntervalPicker({ enabled, isLoading, fn }: Props) {
intervalPicker: (
<div className="mb-12 flex items-center justify-between">
<div className="hidden items-center gap-2 text-right text-mono-sm text-quaternary lg+:flex">
<Time16Icon className="text-quinary" /> Refreshed {format(lastFetched, 'HH:mm')}
<Time16Icon className="text-quinary" /> Refreshed{' '}
{toLocaleTimeString(lastFetched)}
</div>
<div className="flex">
<button
Expand Down
5 changes: 2 additions & 3 deletions app/components/TimeAgo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
* Copyright Oxide Computer Company
*/
import type { Placement } from '@floating-ui/react'
import { format } from 'date-fns'

import { Tooltip } from '~/ui/lib/Tooltip'
import { timeAgoAbbr } from '~/util/date'
import { timeAgoAbbr, toLocaleDateTimeString } from '~/util/date'

export const TimeAgo = ({
datetime,
Expand All @@ -23,7 +22,7 @@ export const TimeAgo = ({
const content = (
<div className="flex flex-col">
<span className="text-tertiary">{tooltipText}</span>
<span>{format(datetime, 'MMM d, yyyy p')}</span>
<span>{toLocaleDateTimeString(datetime)}</span>
</div>
)
return (
Expand Down
4 changes: 2 additions & 2 deletions app/forms/disk-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*
* Copyright Oxide Computer Company
*/
import { format } from 'date-fns'
import { filesize } from 'filesize'
import { useMemo } from 'react'
import { useController, type Control } from 'react-hook-form'
Expand Down Expand Up @@ -35,6 +34,7 @@ import { FormDivider } from '~/ui/lib/Divider'
import { FieldLabel } from '~/ui/lib/FieldLabel'
import { Radio } from '~/ui/lib/Radio'
import { RadioGroup } from '~/ui/lib/RadioGroup'
import { toLocaleDateString } from '~/util/date'
import { bytesToGiB, GiB } from '~/util/units'

const blankDiskSource: DiskSource = {
Expand Down Expand Up @@ -258,7 +258,7 @@ const SnapshotSelectField = ({ control }: { control: Control<DiskCreate> }) => {
<>
<div>{i.name}</div>
<div className="text-tertiary selected:text-accent-secondary">
Created on {format(i.timeCreated, 'MMM d, yyyy')}
Created on {toLocaleDateString(i.timeCreated)}
<DiskNameFromId disk={i.diskId} />{' '}
<span className="mx-1 text-quinary selected:text-accent-disabled">/</span>{' '}
{formattedSize.value} {formattedSize.unit}
Expand Down
6 changes: 3 additions & 3 deletions app/forms/idp/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -62,10 +62,10 @@ export function EditIdpSideModalForm() {
<Truncate text={idp.id} maxLength={32} hasCopyButton />
</PropertiesTable.Row>
<PropertiesTable.Row label="Created">
{formatDateTime(idp.timeCreated)}
<DateTime date={idp.timeCreated} />
</PropertiesTable.Row>
<PropertiesTable.Row label="Updated">
{formatDateTime(idp.timeModified)}
<DateTime date={idp.timeModified} />
</PropertiesTable.Row>
</PropertiesTable>

Expand Down
6 changes: 3 additions & 3 deletions app/forms/image-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -94,10 +94,10 @@ export function EditImageSideModalForm({
<span className="ml-1 inline-block text-quaternary">GiB</span>
</PropertiesTable.Row>
<PropertiesTable.Row label="Created">
{formatDateTime(image.timeCreated)}
<DateTime date={image.timeCreated} />
</PropertiesTable.Row>
<PropertiesTable.Row label="Updated">
{formatDateTime(image.timeModified)}
<DateTime date={image.timeModified} />
</PropertiesTable.Row>
</PropertiesTable>

Expand Down
9 changes: 2 additions & 7 deletions app/pages/project/instances/instance/InstancePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -172,12 +172,7 @@ export function InstancePage() {
</span>
</PropertiesTable.Row>
<PropertiesTable.Row label="created">
<span className="text-secondary">
{format(instance.timeCreated, 'MMM d, yyyy')}{' '}
</span>
<span className="ml-1 text-quaternary">
{format(instance.timeCreated, 'p')}
</span>
<DateTime date={instance.timeCreated} />
</PropertiesTable.Row>
<PropertiesTable.Row label="id">
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-secondary">
Expand Down
6 changes: 3 additions & 3 deletions app/pages/project/vpcs/VpcPage/VpcPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -56,10 +56,10 @@ export function VpcPage() {
</PropertiesTable>
<PropertiesTable>
<PropertiesTable.Row label="Created">
{formatDateTime(vpc.timeCreated)}
<DateTime date={vpc.timeCreated} />
</PropertiesTable.Row>
<PropertiesTable.Row label="Last Modified">
{formatDateTime(vpc.timeModified)}
<DateTime date={vpc.timeModified} />
</PropertiesTable.Row>
</PropertiesTable>
</PropertiesTable.Group>
Expand Down
6 changes: 3 additions & 3 deletions app/pages/system/silos/SiloPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -64,10 +64,10 @@ export function SiloPage() {
</PropertiesTable>
<PropertiesTable>
<PropertiesTable.Row label="Created">
{formatDateTime(silo.timeCreated)}
<DateTime date={silo.timeCreated} />
</PropertiesTable.Row>
<PropertiesTable.Row label="Last Modified">
{formatDateTime(silo.timeModified)}
<DateTime date={silo.timeModified} />
</PropertiesTable.Row>
</PropertiesTable>
</PropertiesTable.Group>
Expand Down
6 changes: 2 additions & 4 deletions app/table/columns/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Row, Item> from RT, but in these
// cells we only care about the return value of getValue
type Info<T> = { getValue: () => T }

function dateCell(info: Info<Date>) {
const date = info.getValue()
return <TwoLineCell value={[format(date, 'MMM d, yyyy'), format(date, 'p')]} />
return <DateTime date={info.getValue()} />
}

function sizeCell(info: Info<number>) {
Expand Down
16 changes: 16 additions & 0 deletions app/ui/lib/DateTime.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<time dateTime={date.toISOString()} className="flex flex-wrap gap-x-2">
<span>{toLocaleDateString(date, locale)}</span>
<span className="text-quaternary">{toLocaleTimeString(date, locale)}</span>
</time>
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stylish

31 changes: 30 additions & 1 deletion app/util/date.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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')
})
})
17 changes: 11 additions & 6 deletions app/util/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
29 changes: 29 additions & 0 deletions test/e2e/dates.e2e.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
1 change: 1 addition & 0 deletions test/e2e/silos.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]')

Expand Down
1 change: 1 addition & 0 deletions test/e2e/vpcs.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down