From 5413198e704465893208116c85c89de4fff77543 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Fri, 14 Nov 2025 12:02:47 -0700 Subject: [PATCH 01/17] add ability for users to chose between 3 timestamp formats --- .../batch-operations/details.svelte | 15 +---- .../components/batch-operations/table.svelte | 15 +---- .../deployments/deployment-table-row.svelte | 45 +++++--------- .../deployments/version-details.svelte | 8 ++- .../deployments/version-table-row.svelte | 21 +++---- src/lib/components/event/event-card.svelte | 23 ++++--- .../components/event/event-summary-row.svelte | 10 ++- .../event/pending-activity-summary-row.svelte | 10 ++- .../event/pending-nexus-summary-row.svelte | 10 ++- .../lines-and-dots/svg/timeline-graph.svelte | 19 +++--- .../lines-and-dots/workflow-details.svelte | 36 +++++++---- .../lines-and-dots/workflow-error.svelte | 7 +-- .../workflow-pending-task.svelte | 25 ++++++-- .../schedule/schedule-recent-runs.svelte | 15 +---- .../schedule/schedule-upcoming-runs.svelte | 18 +++--- .../schedule/schedules-table-row.svelte | 19 +++--- .../filter-list.svelte | 20 +++--- src/lib/components/timestamp.svelte | 62 +++++++++++++++++++ src/lib/components/timezone-select.svelte | 58 ++++++++++++++++- src/lib/components/worker-table.svelte | 14 ++--- .../filter-bar/dropdown-filter-chip.svelte | 3 +- .../metadata/workflow-current-details.svelte | 13 ++-- .../workflow/pending-activities.svelte | 11 +--- .../pending-activity-card.svelte | 43 +++++++++---- .../pending-nexus-operation-card.svelte | 32 +++++++--- ...low-family-node-description-details.svelte | 15 +---- .../workflow/workflow-callback.svelte | 20 +++++- .../workflow/workflow-summary.svelte | 46 +++++++------- .../table-body-cell.svelte | 25 +++----- src/lib/pages/nexus-endpoints.svelte | 30 +++++---- src/lib/pages/schedule-view.svelte | 30 +++++++-- src/lib/pages/workflow-call-stack.svelte | 19 ++++-- .../pages/workflows-with-new-search.svelte | 8 ++- src/lib/stores/time-format.ts | 12 ++++ src/lib/utilities/format-date.ts | 19 +++--- src/lib/utilities/format-event-attributes.ts | 7 +-- .../query/search-attribute-filter.ts | 17 +++-- 37 files changed, 500 insertions(+), 300 deletions(-) create mode 100644 src/lib/components/timestamp.svelte diff --git a/src/lib/components/batch-operations/details.svelte b/src/lib/components/batch-operations/details.svelte index 82bfe1a958..a615c9b96c 100644 --- a/src/lib/components/batch-operations/details.svelte +++ b/src/lib/components/batch-operations/details.svelte @@ -1,8 +1,7 @@ @@ -27,19 +26,11 @@

{translate('common.start-time')}

-

- {formatDate(operation.startTime, $timeFormat, { - relative: $relativeTime, - })} -

+

{translate('common.close-time')}

-

- {formatDate(operation.closeTime, $timeFormat, { - relative: $relativeTime, - })} -

+

diff --git a/src/lib/components/batch-operations/table.svelte b/src/lib/components/batch-operations/table.svelte index 7562cb48bb..d891f13097 100644 --- a/src/lib/components/batch-operations/table.svelte +++ b/src/lib/components/batch-operations/table.svelte @@ -1,4 +1,5 @@

{translate('schedules.upcoming-runs')}

{#each futureRuns.slice(0, 5) as run}
-

- {formatDate(run, $timeFormat, { - relative: $relativeTime, - relativeLabel: translate('common.from-now'), - })} -

+
{:else} import { page } from '$app/stores'; + import Timestamp from '$lib/components/timestamp.svelte'; import WorkflowStatus from '$lib/components/workflow-status.svelte'; import Link from '$lib/holocene/link.svelte'; import { translate } from '$lib/i18n/translate'; import type { ConfigurableTableHeader } from '$lib/stores/configurable-table-columns'; - import { relativeTime, timeFormat } from '$lib/stores/time-format'; import { decodePayloadAttributes } from '$lib/utilities/decode-payload'; - import { formatDate } from '$lib/utilities/format-date'; import { routeForEventHistory, routeForSchedule, @@ -71,22 +70,20 @@ workflow: run?.startWorkflowResult?.workflowId, run: run?.startWorkflowResult?.runId, })} - >{formatDate(run?.actualTime, $timeFormat, { - relative: $relativeTime, - })} + +

{/each} {:else if label === translate('schedules.upcoming-runs')} {#each schedule?.info?.futureActionTimes?.slice(0, 5) ?? [] as run} -
- {formatDate(run, $timeFormat, { - relative: $relativeTime, - relativeLabel: translate('common.from-now'), - })} -
+ {/each} {:else if label === translate('schedules.schedule-spec')} diff --git a/src/lib/components/search-attribute-filter/filter-list.svelte b/src/lib/components/search-attribute-filter/filter-list.svelte index c11d8825b1..895f7b9caa 100644 --- a/src/lib/components/search-attribute-filter/filter-list.svelte +++ b/src/lib/components/search-attribute-filter/filter-list.svelte @@ -5,14 +5,18 @@ import { page } from '$app/stores'; + import Timestamp from '$lib/components/timestamp.svelte'; import WorkflowStatus from '$lib/components/workflow-status.svelte'; import Button from '$lib/holocene/button.svelte'; import Chip from '$lib/holocene/chip.svelte'; import { translate } from '$lib/i18n/translate'; import type { SearchAttributeFilter } from '$lib/models/search-attribute-filters'; import { isWorkflowStatusType } from '$lib/models/workflow-status'; - import { relativeTime, timeFormat } from '$lib/stores/time-format'; - import { formatDate } from '$lib/utilities/format-date'; + import { + relativeTime, + timeFormat, + timestampFormat, + } from '$lib/stores/time-format'; import { isNullConditional, isStartsWith } from '$lib/utilities/is'; import { formatDateTimeRange, @@ -102,13 +106,15 @@ {String(value)} {:else if isDateTimeFilter(workflowFilter)} {#if customDate} - {formatDateTimeRange(value, $timeFormat, $relativeTime)} + {formatDateTimeRange( + value, + $timeFormat, + $timestampFormat, + $relativeTime, + )} {:else} {getDateTimeConditonal(conditional)} - {formatDate(value, $timeFormat, { - relative: $relativeTime, - abbrFormat: true, - })} + {/if} {:else} {isStartsWith(conditional) diff --git a/src/lib/components/timestamp.svelte b/src/lib/components/timestamp.svelte new file mode 100644 index 0000000000..49f3204b79 --- /dev/null +++ b/src/lib/components/timestamp.svelte @@ -0,0 +1,62 @@ + + +{#snippet timestamp(dateTime: DateTime, options: Options)} + {formatDate(dateTime, $timeFormat, $timestampFormat, { + relative: $relativeTime, + ...options, + })} +{/snippet} + +{#if as} + + {#if leading} + {@render leading()}  + {/if} + {#if dateTime} + {@render timestamp(dateTime, options)} + {:else if fallback} + {fallback} + {/if} + +{:else} + {@render timestamp(dateTime, options)} +{/if} diff --git a/src/lib/components/timezone-select.svelte b/src/lib/components/timezone-select.svelte index 51c2b42944..806de15014 100644 --- a/src/lib/components/timezone-select.svelte +++ b/src/lib/components/timezone-select.svelte @@ -1,6 +1,7 @@ @@ -90,7 +109,7 @@ {/each} -
+ + +
+ {#if !$relativeTime} +
+
+

Timestamp Format

+ +
+ + setTimestampFormat('short')}>Short + setTimestampFormat('medium')} + >Default + setTimestampFormat('long')}>Long + +
+ {/if} + {/if} diff --git a/src/lib/components/worker-table.svelte b/src/lib/components/worker-table.svelte index ee8db45587..c66592c3e2 100644 --- a/src/lib/components/worker-table.svelte +++ b/src/lib/components/worker-table.svelte @@ -12,15 +12,14 @@ import { translate } from '$lib/i18n/translate'; import { getWorkflowPollersWithVersions } from '$lib/runes/workflow-versions.svelte'; import { type PollerWithTaskQueueTypes } from '$lib/services/pollers-service'; - import { relativeTime, timeFormat } from '$lib/stores/time-format'; import { workflowRun } from '$lib/stores/workflow-run'; import type { TaskQueueResponse } from '$lib/types'; import type { DeploymentStatus as Status } from '$lib/types/deployments'; - import { formatDate } from '$lib/utilities/format-date'; import { routeForWorkerDeployment } from '$lib/utilities/route-for'; import DeploymentStatus from './deployments/deployment-status.svelte'; import PollerIcon from './poller-icon.svelte'; + import Timestamp from './timestamp.svelte'; type Props = { workers: TaskQueueResponse; @@ -149,11 +148,12 @@ {/if} - - {formatDate(poller.lastAccessTime, $timeFormat, { - relative: $relativeTime, - })} - +
diff --git a/src/lib/components/workflow/pending-activities.svelte b/src/lib/components/workflow/pending-activities.svelte index 07b8fdcfaf..e13ef6170e 100644 --- a/src/lib/components/workflow/pending-activities.svelte +++ b/src/lib/components/workflow/pending-activities.svelte @@ -1,6 +1,7 @@
@@ -80,30 +100,14 @@
{#if elapsedTime} diff --git a/src/lib/components/workflow/workflows-summary-configurable-table/table-body-cell.svelte b/src/lib/components/workflow/workflows-summary-configurable-table/table-body-cell.svelte index 8f97ca9002..18b09d2e92 100644 --- a/src/lib/components/workflow/workflows-summary-configurable-table/table-body-cell.svelte +++ b/src/lib/components/workflow/workflows-summary-configurable-table/table-body-cell.svelte @@ -1,6 +1,7 @@ - -
- {#if $searchInputViewOpen && workflowsPage} - -
diff --git a/src/lib/pages/workflow-call-stack.svelte b/src/lib/pages/workflow-call-stack.svelte index 3b0d14e5fe..466bea1970 100644 --- a/src/lib/pages/workflow-call-stack.svelte +++ b/src/lib/pages/workflow-call-stack.svelte @@ -10,11 +10,7 @@ import type { ParsedQuery } from '$lib/services/query-service'; import { getWorkflowStackTrace } from '$lib/services/query-service'; import { authUser } from '$lib/stores/auth-user'; - import { - relativeTime, - timeFormat, - timestampFormat, - } from '$lib/stores/time-format'; + import { relativeTime, timeFormat } from '$lib/stores/time-format'; import { refresh, workflowRun } from '$lib/stores/workflow-run'; import type { Eventual } from '$lib/types/global'; import { formatDate } from '$lib/utilities/format-date'; @@ -27,10 +23,9 @@ formatDate( $refresh ? new Date($refresh) : new Date(), $timeFormat, - $timestampFormat, + 'abbreviated', { relative: $relativeTime, - abbrFormat: true, }, ), ); diff --git a/src/lib/stores/time-format.ts b/src/lib/stores/time-format.ts index 79dc577173..42a1211239 100644 --- a/src/lib/stores/time-format.ts +++ b/src/lib/stores/time-format.ts @@ -8,8 +8,9 @@ type TimeFormatTypes = 'relative' | 'absolute'; export const timestampFormats = { short: 'do MMM yyyy H:mm:ss.SS', - medium: 'yyyy-MM-dd z HH:mm:ss.SS', - long: 'MMMM do yyyy hh:mm:ss.SS a z', + medium: 'yyyy-MM-dd HH:mm:ss.SS z', + long: 'MMMM do yyyy, hh:mm:ss.SS a z', + abbreviated: 'yyyy-MM-dd HH:mm:ss', } as const; export type TimestampFormat = keyof typeof timestampFormats; diff --git a/src/lib/utilities/format-date.ts b/src/lib/utilities/format-date.ts index 6a2042e3f1..5d52db333c 100644 --- a/src/lib/utilities/format-date.ts +++ b/src/lib/utilities/format-date.ts @@ -20,7 +20,6 @@ import { isTimestamp, timestampToDate, type ValidTime } from './format-time'; export type FormatDateOptions = { relative?: boolean; relativeLabel?: string; - abbrFormat?: boolean; flexibleUnits?: boolean; }; @@ -42,17 +41,12 @@ export function formatDate( const { relative = false, relativeLabel = isFutureDate ? 'from now' : 'ago', - abbrFormat = false, flexibleUnits = false, } = options; const parsed = parseJSON(new Date(date)); - const format = abbrFormat - ? parsed.getSeconds() - ? 'yyyy-MM-dd HH:mm:ss a' - : 'yyyy-MM-dd HH:mm a' - : timestampFormats[timestampFormat]; + const format = timestampFormats[timestampFormat]; if (timeFormat === 'local') { if (relative) diff --git a/src/lib/utilities/query/search-attribute-filter.test.ts b/src/lib/utilities/query/search-attribute-filter.test.ts index ace0af34d1..f54c1a283b 100644 --- a/src/lib/utilities/query/search-attribute-filter.test.ts +++ b/src/lib/utilities/query/search-attribute-filter.test.ts @@ -182,14 +182,12 @@ describe('formatDateTimeRange', () => { formatDateTimeRange( 'BETWEEN "2025-07-17T00:00:00.000Z" AND "2025-07-17T00:00:00.000Z"', 'UTC', - false, ), ).toStrictEqual('between 2025-07-17 00:00 AM and 2025-07-17 00:00 AM'); expect( formatDateTimeRange( 'BETWEEN 2025-07-17T00:00:00.000Z AND 2025-07-17T00:00:00.000Z', 'UTC', - false, ), ).toStrictEqual('between 2025-07-17 00:00 AM and 2025-07-17 00:00 AM'); }); @@ -199,7 +197,6 @@ describe('formatDateTimeRange', () => { formatDateTimeRange( 'BETWEEN "2025-07-17T00:00:00.000Z" AND "2025-07-17T00:00:00.000Z"', 'Pacific Daylight Time', - false, ), ).toStrictEqual('between 2025-07-16 17:00 PM and 2025-07-16 17:00 PM'); }); diff --git a/src/lib/utilities/query/search-attribute-filter.ts b/src/lib/utilities/query/search-attribute-filter.ts index d6d4ce3896..1ed2e00bce 100644 --- a/src/lib/utilities/query/search-attribute-filter.ts +++ b/src/lib/utilities/query/search-attribute-filter.ts @@ -2,7 +2,7 @@ import { get } from 'svelte/store'; import type { SearchAttributeFilter } from '$lib/models/search-attribute-filters'; import { searchAttributes } from '$lib/stores/search-attributes'; -import type { TimeFormat, TimestampFormat } from '$lib/stores/time-format'; +import type { TimeFormat } from '$lib/stores/time-format'; import { SEARCH_ATTRIBUTE_TYPE } from '$lib/types/workflows'; import { formatDate } from '$lib/utilities/format-date'; @@ -103,25 +103,18 @@ export function formatListFilterValue(value: string | null): string[] { export const formatDateTimeRange = ( value: string, format: TimeFormat, - timestampFormat: TimestampFormat, relative: boolean, ) => { const [conditon, start, operator, end] = value.split(' '); return `${conditon.toLowerCase()} ${formatDate( start.replace(/"/g, ''), format, - timestampFormat, - { - relative, - abbrFormat: true, - }, + 'abbreviated', + { relative }, )} ${operator.toLowerCase()} ${formatDate( end.replace(/"/g, ''), format, - timestampFormat, - { - relative, - abbrFormat: true, - }, + 'abbreviated', + { relative }, )}`; }; From 802821c038d16050a5ff99b9d2f79f5e162827dd Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Fri, 14 Nov 2025 14:08:46 -0700 Subject: [PATCH 03/17] fix formatDate api --- .../deployments/version-details.svelte | 9 ++-- .../components/event/event-summary-row.svelte | 18 +++++--- .../event/pending-activity-summary-row.svelte | 10 ++--- .../event/pending-nexus-summary-row.svelte | 9 ++-- .../lines-and-dots/workflow-details.svelte | 44 +++++++++++-------- .../workflow-pending-task.svelte | 9 +++- src/lib/components/timestamp.svelte | 10 ++--- .../filter-bar/dropdown-filter-chip.svelte | 4 +- .../pending-activity-card.svelte | 34 +++++++------- .../pending-nexus-operation-card.svelte | 15 ++++--- .../workflow/workflow-callback.svelte | 4 +- .../workflow/workflow-summary.svelte | 6 +-- src/lib/pages/workflow-call-stack.svelte | 4 +- .../pages/workflows-with-new-search.svelte | 4 +- src/lib/stores/time-format.ts | 5 ++- src/lib/utilities/format-date.test.ts | 40 +++++++---------- src/lib/utilities/format-date.ts | 10 +++-- .../query/search-attribute-filter.ts | 4 +- 18 files changed, 123 insertions(+), 116 deletions(-) diff --git a/src/lib/components/deployments/version-details.svelte b/src/lib/components/deployments/version-details.svelte index c33c9ab9cf..abd5812d22 100644 --- a/src/lib/components/deployments/version-details.svelte +++ b/src/lib/components/deployments/version-details.svelte @@ -21,9 +21,12 @@ />
diff --git a/src/lib/components/event/event-summary-row.svelte b/src/lib/components/event/event-summary-row.svelte index 77b6e0b08a..0783f92bd7 100644 --- a/src/lib/components/event/event-summary-row.svelte +++ b/src/lib/components/event/event-summary-row.svelte @@ -163,15 +163,21 @@ ); const eventTime = $derived( - formatDate(currentEvent?.eventTime, $timeFormat, $timestampFormat, { - relative: $relativeTime, - }), + formatDate( + currentEvent?.eventTime, + $timeFormat, + $relativeTime, + $timestampFormat, + ), ); const abbrEventTime = $derived( - formatDate(currentEvent?.eventTime, $timeFormat, 'abbreviated', { - relative: $relativeTime, - }), + formatDate( + currentEvent?.eventTime, + $timeFormat, + $relativeTime, + 'abbreviated', + ), ); const onLinkClick = (event) => { diff --git a/src/lib/components/event/pending-activity-summary-row.svelte b/src/lib/components/event/pending-activity-summary-row.svelte index 5cd5690049..fc9df57277 100644 --- a/src/lib/components/event/pending-activity-summary-row.svelte +++ b/src/lib/components/event/pending-activity-summary-row.svelte @@ -49,15 +49,13 @@ run, }), ); + let eventTime = $derived( - formatDate(group?.eventTime, $timeFormat, $timestampFormat, { - relative: $relativeTime, - }), + formatDate(group?.eventTime, $timeFormat, $relativeTime, $timestampFormat), ); + let abbrEventTime = $derived( - formatDate(group?.eventTime, $timeFormat, 'abbreviated', { - relative: $relativeTime, - }), + formatDate(group?.eventTime, $timeFormat, $relativeTime, 'abbreviated'), ); const onLinkClick = (e: Event) => { diff --git a/src/lib/components/event/pending-nexus-summary-row.svelte b/src/lib/components/event/pending-nexus-summary-row.svelte index 9d6bffa202..192147d188 100644 --- a/src/lib/components/event/pending-nexus-summary-row.svelte +++ b/src/lib/components/event/pending-nexus-summary-row.svelte @@ -48,14 +48,11 @@ ); let eventTime = $derived( - formatDate(group?.eventTime, $timeFormat, $timestampFormat, { - relative: $relativeTime, - }), + formatDate(group?.eventTime, $timeFormat, $relativeTime, $timestampFormat), ); + let abbrEventTime = $derived( - formatDate(group?.eventTime, $timeFormat, 'abbreviated', { - relative: $relativeTime, - }), + formatDate(group?.eventTime, $timeFormat, $relativeTime, 'abbreviated'), ); const onLinkClick = (e: Event) => { diff --git a/src/lib/components/lines-and-dots/workflow-details.svelte b/src/lib/components/lines-and-dots/workflow-details.svelte index 7e424e07fb..7b3f7b3ebc 100644 --- a/src/lib/components/lines-and-dots/workflow-details.svelte +++ b/src/lib/components/lines-and-dots/workflow-details.svelte @@ -104,32 +104,34 @@ {translate('common.start')} {#if workflow?.startDelay} {translate('workflows.execution-start')} {/if} @@ -137,13 +139,19 @@ {translate('common.end')} diff --git a/src/lib/components/lines-and-dots/workflow-pending-task.svelte b/src/lib/components/lines-and-dots/workflow-pending-task.svelte index d3720a1a60..d0e28c9cec 100644 --- a/src/lib/components/lines-and-dots/workflow-pending-task.svelte +++ b/src/lib/components/lines-and-dots/workflow-pending-task.svelte @@ -2,7 +2,11 @@ import Accordion from '$lib/holocene/accordion/accordion.svelte'; import Badge from '$lib/holocene/badge.svelte'; import { translate } from '$lib/i18n/translate'; - import { timeFormat, timestampFormat } from '$lib/stores/time-format'; + import { + relativeTime, + timeFormat, + timestampFormat, + } from '$lib/stores/time-format'; import type { PendingWorkflowTaskInfo } from '$lib/types'; import { formatDate } from '$lib/utilities/format-date'; @@ -25,6 +29,7 @@ >{formatDate( pendingTask.originalScheduledTime, $timeFormat, + $relativeTime, $timestampFormat, )} @@ -35,6 +40,7 @@ >{formatDate( pendingTask.scheduledTime, $timeFormat, + $relativeTime, $timestampFormat, )} @@ -45,6 +51,7 @@ >{formatDate( pendingTask.startedTime, $timeFormat, + $relativeTime, $timestampFormat, )} diff --git a/src/lib/components/timestamp.svelte b/src/lib/components/timestamp.svelte index 49f3204b79..3df9123466 100644 --- a/src/lib/components/timestamp.svelte +++ b/src/lib/components/timestamp.svelte @@ -16,13 +16,12 @@ type T = $$Generic; type DateTime = ValidTime | null | undefined; - type Options = Omit; export { timestamp }; type Props = SvelteHTMLElements[T] & { dateTime: DateTime; - options?: Options; + options?: FormatDateOptions; as?: T; fallback?: string; leading?: Snippet<[]>; @@ -39,11 +38,8 @@ }: Props = $props(); -{#snippet timestamp(dateTime: DateTime, options: Options)} - {formatDate(dateTime, $timeFormat, $timestampFormat, { - relative: $relativeTime, - ...options, - })} +{#snippet timestamp(dateTime: DateTime, options: FormatDateOptions)} + {formatDate(dateTime, $timeFormat, $relativeTime, $timestampFormat, options)} {/snippet} {#if as} diff --git a/src/lib/components/workflow/filter-bar/dropdown-filter-chip.svelte b/src/lib/components/workflow/filter-bar/dropdown-filter-chip.svelte index fbbdc5af7c..be2027e9b5 100644 --- a/src/lib/components/workflow/filter-bar/dropdown-filter-chip.svelte +++ b/src/lib/components/workflow/filter-bar/dropdown-filter-chip.svelte @@ -174,9 +174,7 @@ if (isDateTimeFilter(filter)) { if (filter.customDate) return value.split('BETWEEN')[1]; - return formatDate(value, $timeFormat, 'abbreviated', { - relative: true, - }); + return formatDate(value, $timeFormat, true, 'abbreviated'); } if (isTextFilter(filter)) { diff --git a/src/lib/components/workflow/pending-activity/pending-activity-card.svelte b/src/lib/components/workflow/pending-activity/pending-activity-card.svelte index 1940fd5e71..683ab2fb76 100644 --- a/src/lib/components/workflow/pending-activity/pending-activity-card.svelte +++ b/src/lib/components/workflow/pending-activity/pending-activity-card.svelte @@ -72,10 +72,8 @@ formatDate( activity.pauseInfo?.pauseTime, $timeFormat, + $relativeTime, $timestampFormat, - { - relative: $relativeTime, - }, ), )} {@render detail( @@ -99,10 +97,8 @@ formatDate( activity.lastAttemptCompleteTime, $timeFormat, + $relativeTime, $timestampFormat, - { - relative: $relativeTime, - }, ), )} {/if} @@ -126,19 +122,20 @@ formatDate( activity.lastHeartbeatTime, $timeFormat, + $relativeTime, $timestampFormat, - { - relative: $relativeTime, - }, ), )} {/if} {#if activity.lastStartedTime} {@render detail( translate('workflows.last-started-time'), - formatDate(activity.lastStartedTime, $timeFormat, $timestampFormat, { - relative: $relativeTime, - }), + formatDate( + activity.lastStartedTime, + $timeFormat, + $relativeTime, + $timestampFormat, + ), )} {/if} {#if activity.lastWorkerIdentity} @@ -271,10 +268,15 @@ {translate('workflows.next-retry')}

- {formatDate(activity.scheduledTime, $timeFormat, $timestampFormat, { - relative: $relativeTime, - relativeLabel: '', - })} + {formatDate( + activity.scheduledTime, + $timeFormat, + $relativeTime, + $timestampFormat, + { + relativeLabel: '', + }, + )} ({timeDifference})

diff --git a/src/lib/components/workflow/pending-nexus-operation/pending-nexus-operation-card.svelte b/src/lib/components/workflow/pending-nexus-operation/pending-nexus-operation-card.svelte index 9014e3559d..1e72f71c43 100644 --- a/src/lib/components/workflow/pending-nexus-operation/pending-nexus-operation-card.svelte +++ b/src/lib/components/workflow/pending-nexus-operation/pending-nexus-operation-card.svelte @@ -64,10 +64,8 @@ formatDate( operation.lastAttemptCompleteTime, $timeFormat, + $relativeTime, $timestampFormat, - { - relative: $relativeTime, - }, ), )} {/if} @@ -80,9 +78,12 @@ {#if operation.scheduledTime} {@render detail( translate('workflows.scheduled-time'), - formatDate(operation.scheduledTime, $timeFormat, $timestampFormat, { - relative: $relativeTime, - }), + formatDate( + operation.scheduledTime, + $timeFormat, + $relativeTime, + $timestampFormat, + ), )} {/if} {#if operation.scheduleToCloseTimeout} @@ -135,9 +136,9 @@ {formatDate( operation.nextAttemptScheduleTime, $timeFormat, + $relativeTime, $timestampFormat, { - relative: $relativeTime, relativeLabel: '', }, )} diff --git a/src/lib/components/workflow/workflow-callback.svelte b/src/lib/components/workflow/workflow-callback.svelte index 3dff71fa91..f79e247c5c 100644 --- a/src/lib/components/workflow/workflow-callback.svelte +++ b/src/lib/components/workflow/workflow-callback.svelte @@ -28,16 +28,16 @@ formatDate( callback.lastAttemptCompleteTime, $timeFormat, + $relativeTime, $timestampFormat, - { relative: $relativeTime }, ), ); const nextTime = $derived( formatDate( callback.nextAttemptScheduleTime, $timeFormat, + $relativeTime, $timestampFormat, - { relative: $relativeTime }, ), ); const failure = $derived(callback?.lastAttemptFailure?.message); diff --git a/src/lib/components/workflow/workflow-summary.svelte b/src/lib/components/workflow/workflow-summary.svelte index 25558aab7d..86c5979ca5 100644 --- a/src/lib/components/workflow/workflow-summary.svelte +++ b/src/lib/components/workflow/workflow-summary.svelte @@ -27,17 +27,15 @@ $: startTimestamp = formatDate( workflow?.startTime, $timeFormat, + $relativeTime, $timestampFormat, - { relative: $relativeTime }, ); $: endTimestamp = formatDate( workflow?.endTime, $timeFormat, + $relativeTime, $timestampFormat, - { - relative: $relativeTime, - }, ); diff --git a/src/lib/pages/workflow-call-stack.svelte b/src/lib/pages/workflow-call-stack.svelte index 466bea1970..d871abf0c4 100644 --- a/src/lib/pages/workflow-call-stack.svelte +++ b/src/lib/pages/workflow-call-stack.svelte @@ -23,10 +23,8 @@ formatDate( $refresh ? new Date($refresh) : new Date(), $timeFormat, + $relativeTime, 'abbreviated', - { - relative: $relativeTime, - }, ), ); diff --git a/src/lib/pages/workflows-with-new-search.svelte b/src/lib/pages/workflows-with-new-search.svelte index c9874564ac..a3ce55cf72 100644 --- a/src/lib/pages/workflows-with-new-search.svelte +++ b/src/lib/pages/workflows-with-new-search.svelte @@ -76,9 +76,7 @@ let refreshTime = $state(new Date()); const refreshTimeFormatted = $derived( - formatDate(refreshTime, $timeFormat, $timestampFormat, { - relative: $relativeTime, - }), + formatDate(refreshTime, $timeFormat, $relativeTime, $timestampFormat), ); const availableColumns = $derived( diff --git a/src/lib/stores/time-format.ts b/src/lib/stores/time-format.ts index 42a1211239..778fc657b9 100644 --- a/src/lib/stores/time-format.ts +++ b/src/lib/stores/time-format.ts @@ -8,9 +8,10 @@ type TimeFormatTypes = 'relative' | 'absolute'; export const timestampFormats = { short: 'do MMM yyyy H:mm:ss.SS', - medium: 'yyyy-MM-dd HH:mm:ss.SS z', + medium: 'yyyy-MM-dd z HH:mm:ss.SS', long: 'MMMM do yyyy, hh:mm:ss.SS a z', - abbreviated: 'yyyy-MM-dd HH:mm:ss', + abbreviated: 'yyyy-MM-dd HH:mm:ss a', + abbreviatedWithoutSeconds: 'yyyy-MM-dd HH:mm a', } as const; export type TimestampFormat = keyof typeof timestampFormats; diff --git a/src/lib/utilities/format-date.test.ts b/src/lib/utilities/format-date.test.ts index 74866c3dd4..b43ba2bf7e 100644 --- a/src/lib/utilities/format-date.test.ts +++ b/src/lib/utilities/format-date.test.ts @@ -59,83 +59,75 @@ describe('formatDate', () => { }); it('should format relative local time', () => { - expect(formatDate(date, 'local', { relative: true })).toContain('ago'); + expect(formatDate(date, 'local', true)).toContain('ago'); const currentDate = new Date(); const futureDate = currentDate.setDate(currentDate.getDate() + 1); - expect(formatDate(futureDate, 'local', { relative: true })).toContain( - 'from now', - ); + expect(formatDate(futureDate, 'local', true)).toContain('from now'); }); it('should not format other timezones as relative', () => { - expect(formatDate(date, 'UTC', { relative: true })).toEqual( - '2022-04-13 UTC 16:29:35.63', - ); + expect(formatDate(date, 'UTC', true)).toEqual('2022-04-13 UTC 16:29:35.63'); }); it('should format relative local time with a custom label', () => { expect( - formatDate(date, 'local', { relative: true, relativeLabel: 'custom' }), + formatDate(date, 'local', true, 'medium', { relativeLabel: 'custom' }), ).toContain('custom'); }); it('should format relative time with days instead of months for past dates if flexibleUnits is not enabled', () => { const currentDate = new Date(); const pastDate = currentDate.setDate(currentDate.getDate() - 90); - let formattedDate = formatDate(pastDate, 'local', { - relative: true, + let formattedDate = formatDate(pastDate, 'local', true, 'medium', { flexibleUnits: true, }); expect(formattedDate).toEqual('3 months ago'); - formattedDate = formatDate(pastDate, 'local', { relative: true }); + formattedDate = formatDate(pastDate, 'local', true); expect(formattedDate).toEqual('90 days ago'); }); it('should format relative time with days instead of months for future dates if flexibleUnits is not enabled', () => { const currentDate = new Date(); const futureDate = currentDate.setDate(currentDate.getDate() + 90); - let formattedDate = formatDate(futureDate, 'local', { - relative: true, + let formattedDate = formatDate(futureDate, 'local', true, 'medium', { flexibleUnits: true, }); expect(formattedDate).toEqual('3 months from now'); - formattedDate = formatDate(futureDate, 'local', { relative: true }); + formattedDate = formatDate(futureDate, 'local', true); expect(formattedDate).toEqual('90 days from now'); }); it('should not format relative time with days if less than a day for past dates even if flexibleUnits is enabled', () => { const currentDate = new Date(); const pastDate = currentDate.setHours(currentDate.getHours() - 23); - let formattedDate = formatDate(pastDate, 'local', { - relative: true, + let formattedDate = formatDate(pastDate, 'local', true, 'medium', { flexibleUnits: true, }); expect(formattedDate).toEqual('23 hours ago'); - formattedDate = formatDate(pastDate, 'local', { relative: true }); + formattedDate = formatDate(pastDate, 'local', true); expect(formattedDate).toEqual('23 hours ago'); }); it('should not format relative time with days if less than a day for future dates even if flexibleUnits is enabled', () => { const currentDate = new Date(); const futureDate = currentDate.setHours(currentDate.getHours() + 23); - let formattedDate = formatDate(futureDate, 'local', { - relative: true, + let formattedDate = formatDate(futureDate, 'local', true, 'medium', { flexibleUnits: true, }); expect(formattedDate).toEqual('23 hours from now'); - formattedDate = formatDate(futureDate, 'local', { relative: true }); + formattedDate = formatDate(futureDate, 'local', true); expect(formattedDate).toEqual('23 hours from now'); }); it('should shorten format for local and other timezones', () => { - expect(formatDate(date, 'local', { abbrFormat: true })).toEqual( + expect(formatDate(date, 'local', false, 'abbreviated')).toEqual( '2022-04-13 16:29:35 PM', ); - expect(formatDate(date, 'utc', { abbrFormat: true })).toEqual( + expect(formatDate(date, 'utc', false, 'abbreviated')).toEqual( '2022-04-13 16:29:35 PM', ); }); @@ -143,9 +135,9 @@ describe('formatDate', () => { it('should shorten format without seconds if there are none for local and other timezones', () => { const dateWithoutSeconds = '2022-04-13T16:29:00.630571Z'; expect( - formatDate(dateWithoutSeconds, 'local', { abbrFormat: true }), + formatDate(dateWithoutSeconds, 'local', false, 'abbreviated'), ).toEqual('2022-04-13 16:29 PM'); - expect(formatDate(dateWithoutSeconds, 'utc', { abbrFormat: true })).toEqual( + expect(formatDate(dateWithoutSeconds, 'utc', false, 'abbreviated')).toEqual( '2022-04-13 16:29 PM', ); }); diff --git a/src/lib/utilities/format-date.ts b/src/lib/utilities/format-date.ts index 5d52db333c..292fd8eefa 100644 --- a/src/lib/utilities/format-date.ts +++ b/src/lib/utilities/format-date.ts @@ -18,7 +18,6 @@ import { import { isTimestamp, timestampToDate, type ValidTime } from './format-time'; export type FormatDateOptions = { - relative?: boolean; relativeLabel?: string; flexibleUnits?: boolean; }; @@ -26,6 +25,7 @@ export type FormatDateOptions = { export function formatDate( date: ValidTime | undefined | null, timeFormat: TimeFormat = 'UTC', + relative: boolean = false, timestampFormat: TimestampFormat = 'medium', options: FormatDateOptions = {}, ): string { @@ -39,14 +39,18 @@ export function formatDate( const currentDate = Date.now(); const isFutureDate = new Date(date).getTime() - currentDate > 0; const { - relative = false, relativeLabel = isFutureDate ? 'from now' : 'ago', flexibleUnits = false, } = options; const parsed = parseJSON(new Date(date)); - const format = timestampFormats[timestampFormat]; + const format = + timestampFormat === 'abbreviated' + ? parsed.getSeconds() + ? timestampFormats.abbreviated + : timestampFormats.abbreviatedWithoutSeconds + : timestampFormats[timestampFormat]; if (timeFormat === 'local') { if (relative) diff --git a/src/lib/utilities/query/search-attribute-filter.ts b/src/lib/utilities/query/search-attribute-filter.ts index 1ed2e00bce..8e8f02df2f 100644 --- a/src/lib/utilities/query/search-attribute-filter.ts +++ b/src/lib/utilities/query/search-attribute-filter.ts @@ -109,12 +109,12 @@ export const formatDateTimeRange = ( return `${conditon.toLowerCase()} ${formatDate( start.replace(/"/g, ''), format, + relative, 'abbreviated', - { relative }, )} ${operator.toLowerCase()} ${formatDate( end.replace(/"/g, ''), format, + relative, 'abbreviated', - { relative }, )}`; }; From 5a0d65c6c5063f59405e464587ac05f9a0484b08 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Mon, 17 Nov 2025 10:48:18 -0700 Subject: [PATCH 04/17] add tests --- src/lib/utilities/format-date.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/lib/utilities/format-date.test.ts b/src/lib/utilities/format-date.test.ts index b43ba2bf7e..63ec721fc8 100644 --- a/src/lib/utilities/format-date.test.ts +++ b/src/lib/utilities/format-date.test.ts @@ -141,6 +141,18 @@ describe('formatDate', () => { '2022-04-13 16:29 PM', ); }); + + it('supports different timestamps formats', () => { + expect(formatDate(date, 'utc', false, 'short')).toEqual( + '13th Apr 2022 16:29:35.63', + ); + expect(formatDate(date, 'utc', false, 'medium')).toEqual( + '2022-04-13 UTC 16:29:35.63', + ); + expect(formatDate(date, 'utc', false, 'long')).toEqual( + 'April 13th 2022, 04:29:35.63 PM UTC', + ); + }); }); describe('isValidDate', () => { From a6130338266d10e4ea3f0f3f0951884ed0f15824 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Mon, 17 Nov 2025 12:07:47 -0700 Subject: [PATCH 05/17] re-refactor formatDate API --- .../deployments/version-details.svelte | 10 ++-- .../components/event/event-summary-row.svelte | 20 +++---- .../event/pending-activity-summary-row.svelte | 10 +++- .../event/pending-nexus-summary-row.svelte | 10 +++- .../lines-and-dots/workflow-details.svelte | 60 ++++++++----------- .../workflow-pending-task.svelte | 30 ++++------ src/lib/components/timestamp.svelte | 10 +++- .../filter-bar/dropdown-filter-chip.svelte | 2 +- .../pending-activity-card.svelte | 54 +++++++---------- .../pending-nexus-operation-card.svelte | 34 ++++------- .../workflow/workflow-callback.svelte | 20 +++---- .../workflow/workflow-summary.svelte | 20 +++---- src/lib/pages/workflow-call-stack.svelte | 10 ++-- .../pages/workflows-with-new-search.svelte | 5 +- src/lib/utilities/format-date.test.ts | 50 +++++++++------- src/lib/utilities/format-date.ts | 16 ++--- .../query/search-attribute-filter.ts | 14 ++--- 17 files changed, 177 insertions(+), 198 deletions(-) diff --git a/src/lib/components/deployments/version-details.svelte b/src/lib/components/deployments/version-details.svelte index abd5812d22..6a78c8b255 100644 --- a/src/lib/components/deployments/version-details.svelte +++ b/src/lib/components/deployments/version-details.svelte @@ -21,12 +21,10 @@ />
diff --git a/src/lib/components/event/event-summary-row.svelte b/src/lib/components/event/event-summary-row.svelte index 0783f92bd7..1c6c8b6968 100644 --- a/src/lib/components/event/event-summary-row.svelte +++ b/src/lib/components/event/event-summary-row.svelte @@ -163,21 +163,17 @@ ); const eventTime = $derived( - formatDate( - currentEvent?.eventTime, - $timeFormat, - $relativeTime, - $timestampFormat, - ), + formatDate(currentEvent?.eventTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + }), ); const abbrEventTime = $derived( - formatDate( - currentEvent?.eventTime, - $timeFormat, - $relativeTime, - 'abbreviated', - ), + formatDate(currentEvent?.eventTime, $timeFormat, { + relative: $relativeTime, + format: 'abbreviated', + }), ); const onLinkClick = (event) => { diff --git a/src/lib/components/event/pending-activity-summary-row.svelte b/src/lib/components/event/pending-activity-summary-row.svelte index fc9df57277..7640dd9376 100644 --- a/src/lib/components/event/pending-activity-summary-row.svelte +++ b/src/lib/components/event/pending-activity-summary-row.svelte @@ -51,11 +51,17 @@ ); let eventTime = $derived( - formatDate(group?.eventTime, $timeFormat, $relativeTime, $timestampFormat), + formatDate(group?.eventTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + }), ); let abbrEventTime = $derived( - formatDate(group?.eventTime, $timeFormat, $relativeTime, 'abbreviated'), + formatDate(group?.eventTime, $timeFormat, { + relative: $relativeTime, + format: 'abbreviated', + }), ); const onLinkClick = (e: Event) => { diff --git a/src/lib/components/event/pending-nexus-summary-row.svelte b/src/lib/components/event/pending-nexus-summary-row.svelte index 192147d188..9a534e8379 100644 --- a/src/lib/components/event/pending-nexus-summary-row.svelte +++ b/src/lib/components/event/pending-nexus-summary-row.svelte @@ -48,11 +48,17 @@ ); let eventTime = $derived( - formatDate(group?.eventTime, $timeFormat, $relativeTime, $timestampFormat), + formatDate(group?.eventTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + }), ); let abbrEventTime = $derived( - formatDate(group?.eventTime, $timeFormat, $relativeTime, 'abbreviated'), + formatDate(group?.eventTime, $timeFormat, { + relative: $relativeTime, + format: 'abbreviated', + }), ); const onLinkClick = (e: Event) => { diff --git a/src/lib/components/lines-and-dots/workflow-details.svelte b/src/lib/components/lines-and-dots/workflow-details.svelte index 7b3f7b3ebc..ce3b0e61bb 100644 --- a/src/lib/components/lines-and-dots/workflow-details.svelte +++ b/src/lib/components/lines-and-dots/workflow-details.svelte @@ -104,54 +104,42 @@ {translate('common.start')} {#if workflow?.startDelay} {translate('workflows.execution-start')} {/if} {translate('common.end')} diff --git a/src/lib/components/lines-and-dots/workflow-pending-task.svelte b/src/lib/components/lines-and-dots/workflow-pending-task.svelte index d0e28c9cec..1f66b76a50 100644 --- a/src/lib/components/lines-and-dots/workflow-pending-task.svelte +++ b/src/lib/components/lines-and-dots/workflow-pending-task.svelte @@ -26,34 +26,28 @@

{translate('workflows.original-scheduled-time')} {formatDate( - pendingTask.originalScheduledTime, - $timeFormat, - $relativeTime, - $timestampFormat, - )}{formatDate(pendingTask.originalScheduledTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + })}

{translate('workflows.scheduled-time')} {formatDate( - pendingTask.scheduledTime, - $timeFormat, - $relativeTime, - $timestampFormat, - )}{formatDate(pendingTask.scheduledTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + })}

{translate('workflows.started-time')} {formatDate( - pendingTask.startedTime, - $timeFormat, - $relativeTime, - $timestampFormat, - )}{formatDate(pendingTask.startedTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + })}

diff --git a/src/lib/components/timestamp.svelte b/src/lib/components/timestamp.svelte index 3df9123466..575877dd76 100644 --- a/src/lib/components/timestamp.svelte +++ b/src/lib/components/timestamp.svelte @@ -7,6 +7,7 @@ relativeTime, timeFormat, timestampFormat, + type TimestampFormat, } from '$lib/stores/time-format'; import { formatDate, @@ -25,6 +26,8 @@ as?: T; fallback?: string; leading?: Snippet<[]>; + // only used to override the user's stored preference in certain cases + overrideTimestampFormat?: TimestampFormat; }; let { @@ -33,13 +36,18 @@ as = undefined, class: className = undefined, fallback = undefined, + overrideTimestampFormat = undefined, leading, ...rest }: Props = $props(); {#snippet timestamp(dateTime: DateTime, options: FormatDateOptions)} - {formatDate(dateTime, $timeFormat, $relativeTime, $timestampFormat, options)} + {formatDate(dateTime, $timeFormat, { + relative: $relativeTime, + format: overrideTimestampFormat ?? $timestampFormat, + ...options, + })} {/snippet} {#if as} diff --git a/src/lib/components/workflow/filter-bar/dropdown-filter-chip.svelte b/src/lib/components/workflow/filter-bar/dropdown-filter-chip.svelte index be2027e9b5..7ed3db6b04 100644 --- a/src/lib/components/workflow/filter-bar/dropdown-filter-chip.svelte +++ b/src/lib/components/workflow/filter-bar/dropdown-filter-chip.svelte @@ -174,7 +174,7 @@ if (isDateTimeFilter(filter)) { if (filter.customDate) return value.split('BETWEEN')[1]; - return formatDate(value, $timeFormat, true, 'abbreviated'); + return formatDate(value, $timeFormat, { relative: true }); } if (isTextFilter(filter)) { diff --git a/src/lib/components/workflow/pending-activity/pending-activity-card.svelte b/src/lib/components/workflow/pending-activity/pending-activity-card.svelte index 683ab2fb76..37ffc1ec95 100644 --- a/src/lib/components/workflow/pending-activity/pending-activity-card.svelte +++ b/src/lib/components/workflow/pending-activity/pending-activity-card.svelte @@ -69,12 +69,10 @@ )} {@render detail( translate('activities.paused-since'), - formatDate( - activity.pauseInfo?.pauseTime, - $timeFormat, - $relativeTime, - $timestampFormat, - ), + formatDate(activity.pauseInfo?.pauseTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + }), )} {@render detail( translate('activities.pause-reason'), @@ -94,12 +92,10 @@ {#if activity.lastAttemptCompleteTime} {@render detail( translate('workflows.last-attempt-completed-time'), - formatDate( - activity.lastAttemptCompleteTime, - $timeFormat, - $relativeTime, - $timestampFormat, - ), + formatDate(activity.lastAttemptCompleteTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + }), )} {/if} {#if activity.expirationTime} @@ -119,23 +115,19 @@ {#if activity.lastHeartbeatTime} {@render detail( translate('workflows.last-heartbeat'), - formatDate( - activity.lastHeartbeatTime, - $timeFormat, - $relativeTime, - $timestampFormat, - ), + formatDate(activity.lastHeartbeatTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + }), )} {/if} {#if activity.lastStartedTime} {@render detail( translate('workflows.last-started-time'), - formatDate( - activity.lastStartedTime, - $timeFormat, - $relativeTime, - $timestampFormat, - ), + formatDate(activity.lastStartedTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + }), )} {/if} {#if activity.lastWorkerIdentity} @@ -268,15 +260,11 @@ {translate('workflows.next-retry')}

- {formatDate( - activity.scheduledTime, - $timeFormat, - $relativeTime, - $timestampFormat, - { - relativeLabel: '', - }, - )} + {formatDate(activity.scheduledTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + relativeLabel: '', + })} ({timeDifference})

diff --git a/src/lib/components/workflow/pending-nexus-operation/pending-nexus-operation-card.svelte b/src/lib/components/workflow/pending-nexus-operation/pending-nexus-operation-card.svelte index 1e72f71c43..2636606ac8 100644 --- a/src/lib/components/workflow/pending-nexus-operation/pending-nexus-operation-card.svelte +++ b/src/lib/components/workflow/pending-nexus-operation/pending-nexus-operation-card.svelte @@ -61,12 +61,10 @@ {#if operation.lastAttemptCompleteTime} {@render detail( translate('workflows.last-attempt-completed-time'), - formatDate( - operation.lastAttemptCompleteTime, - $timeFormat, - $relativeTime, - $timestampFormat, - ), + formatDate(operation.lastAttemptCompleteTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + }), )} {/if} {#if operation.scheduledEventId} @@ -78,12 +76,10 @@ {#if operation.scheduledTime} {@render detail( translate('workflows.scheduled-time'), - formatDate( - operation.scheduledTime, - $timeFormat, - $relativeTime, - $timestampFormat, - ), + formatDate(operation.scheduledTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + }), )} {/if} {#if operation.scheduleToCloseTimeout} @@ -133,15 +129,11 @@ {translate('workflows.next-retry')}

- {formatDate( - operation.nextAttemptScheduleTime, - $timeFormat, - $relativeTime, - $timestampFormat, - { - relativeLabel: '', - }, - )} + {formatDate(operation.nextAttemptScheduleTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + relativeLabel: '', + })} ({timeDifference})

diff --git a/src/lib/components/workflow/workflow-callback.svelte b/src/lib/components/workflow/workflow-callback.svelte index f79e247c5c..2b580a68b1 100644 --- a/src/lib/components/workflow/workflow-callback.svelte +++ b/src/lib/components/workflow/workflow-callback.svelte @@ -25,20 +25,16 @@ }: { callback: Callback; link?: Link; children?: Snippet } = $props(); const completedTime = $derived( - formatDate( - callback.lastAttemptCompleteTime, - $timeFormat, - $relativeTime, - $timestampFormat, - ), + formatDate(callback.lastAttemptCompleteTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + }), ); const nextTime = $derived( - formatDate( - callback.nextAttemptScheduleTime, - $timeFormat, - $relativeTime, - $timestampFormat, - ), + formatDate(callback.nextAttemptScheduleTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + }), ); const failure = $derived(callback?.lastAttemptFailure?.message); const blockedReason = $derived(callback?.blockedReason); diff --git a/src/lib/components/workflow/workflow-summary.svelte b/src/lib/components/workflow/workflow-summary.svelte index 86c5979ca5..c09ba4db64 100644 --- a/src/lib/components/workflow/workflow-summary.svelte +++ b/src/lib/components/workflow/workflow-summary.svelte @@ -24,19 +24,15 @@ includeMilliseconds: true, }); - $: startTimestamp = formatDate( - workflow?.startTime, - $timeFormat, - $relativeTime, - $timestampFormat, - ); + $: startTimestamp = formatDate(workflow?.startTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + }); - $: endTimestamp = formatDate( - workflow?.endTime, - $timeFormat, - $relativeTime, - $timestampFormat, - ); + $: endTimestamp = formatDate(workflow?.endTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + });
diff --git a/src/lib/pages/workflow-call-stack.svelte b/src/lib/pages/workflow-call-stack.svelte index d871abf0c4..fe7558e78e 100644 --- a/src/lib/pages/workflow-call-stack.svelte +++ b/src/lib/pages/workflow-call-stack.svelte @@ -20,12 +20,10 @@ let stackTrace: Eventual = $state(); let refreshDate = $derived( - formatDate( - $refresh ? new Date($refresh) : new Date(), - $timeFormat, - $relativeTime, - 'abbreviated', - ), + formatDate($refresh ? new Date($refresh) : new Date(), $timeFormat, { + relative: $relativeTime, + format: 'abbreviated', + }), ); const getStackTrace = () => diff --git a/src/lib/pages/workflows-with-new-search.svelte b/src/lib/pages/workflows-with-new-search.svelte index a3ce55cf72..0da218970f 100644 --- a/src/lib/pages/workflows-with-new-search.svelte +++ b/src/lib/pages/workflows-with-new-search.svelte @@ -76,7 +76,10 @@ let refreshTime = $state(new Date()); const refreshTimeFormatted = $derived( - formatDate(refreshTime, $timeFormat, $relativeTime, $timestampFormat), + formatDate(refreshTime, $timeFormat, { + relative: $relativeTime, + format: $timestampFormat, + }), ); const availableColumns = $derived( diff --git a/src/lib/utilities/format-date.test.ts b/src/lib/utilities/format-date.test.ts index 63ec721fc8..2e54b5b542 100644 --- a/src/lib/utilities/format-date.test.ts +++ b/src/lib/utilities/format-date.test.ts @@ -59,75 +59,83 @@ describe('formatDate', () => { }); it('should format relative local time', () => { - expect(formatDate(date, 'local', true)).toContain('ago'); + expect(formatDate(date, 'local', { relative: true })).toContain('ago'); const currentDate = new Date(); const futureDate = currentDate.setDate(currentDate.getDate() + 1); - expect(formatDate(futureDate, 'local', true)).toContain('from now'); + expect(formatDate(futureDate, 'local', { relative: true })).toContain( + 'from now', + ); }); it('should not format other timezones as relative', () => { - expect(formatDate(date, 'UTC', true)).toEqual('2022-04-13 UTC 16:29:35.63'); + expect(formatDate(date, 'UTC', { relative: true })).toEqual( + '2022-04-13 UTC 16:29:35.63', + ); }); it('should format relative local time with a custom label', () => { expect( - formatDate(date, 'local', true, 'medium', { relativeLabel: 'custom' }), + formatDate(date, 'local', { relative: true, relativeLabel: 'custom' }), ).toContain('custom'); }); it('should format relative time with days instead of months for past dates if flexibleUnits is not enabled', () => { const currentDate = new Date(); const pastDate = currentDate.setDate(currentDate.getDate() - 90); - let formattedDate = formatDate(pastDate, 'local', true, 'medium', { + let formattedDate = formatDate(pastDate, 'local', { + relative: true, flexibleUnits: true, }); expect(formattedDate).toEqual('3 months ago'); - formattedDate = formatDate(pastDate, 'local', true); + formattedDate = formatDate(pastDate, 'local', { relative: true }); expect(formattedDate).toEqual('90 days ago'); }); it('should format relative time with days instead of months for future dates if flexibleUnits is not enabled', () => { const currentDate = new Date(); const futureDate = currentDate.setDate(currentDate.getDate() + 90); - let formattedDate = formatDate(futureDate, 'local', true, 'medium', { + let formattedDate = formatDate(futureDate, 'local', { + relative: true, flexibleUnits: true, }); expect(formattedDate).toEqual('3 months from now'); - formattedDate = formatDate(futureDate, 'local', true); + formattedDate = formatDate(futureDate, 'local', { relative: true }); expect(formattedDate).toEqual('90 days from now'); }); it('should not format relative time with days if less than a day for past dates even if flexibleUnits is enabled', () => { const currentDate = new Date(); const pastDate = currentDate.setHours(currentDate.getHours() - 23); - let formattedDate = formatDate(pastDate, 'local', true, 'medium', { + let formattedDate = formatDate(pastDate, 'local', { + relative: true, flexibleUnits: true, }); expect(formattedDate).toEqual('23 hours ago'); - formattedDate = formatDate(pastDate, 'local', true); + formattedDate = formatDate(pastDate, 'local', { relative: true }); expect(formattedDate).toEqual('23 hours ago'); }); it('should not format relative time with days if less than a day for future dates even if flexibleUnits is enabled', () => { const currentDate = new Date(); const futureDate = currentDate.setHours(currentDate.getHours() + 23); - let formattedDate = formatDate(futureDate, 'local', true, 'medium', { + let formattedDate = formatDate(futureDate, 'local', { + relative: true, flexibleUnits: true, }); expect(formattedDate).toEqual('23 hours from now'); - formattedDate = formatDate(futureDate, 'local', true); + formattedDate = formatDate(futureDate, 'local', { relative: true }); expect(formattedDate).toEqual('23 hours from now'); }); it('should shorten format for local and other timezones', () => { - expect(formatDate(date, 'local', false, 'abbreviated')).toEqual( + expect(formatDate(date, 'local', { format: 'abbreviated' })).toEqual( '2022-04-13 16:29:35 PM', ); - expect(formatDate(date, 'utc', false, 'abbreviated')).toEqual( + expect(formatDate(date, 'utc', { format: 'abbreviated' })).toEqual( '2022-04-13 16:29:35 PM', ); }); @@ -135,21 +143,21 @@ describe('formatDate', () => { it('should shorten format without seconds if there are none for local and other timezones', () => { const dateWithoutSeconds = '2022-04-13T16:29:00.630571Z'; expect( - formatDate(dateWithoutSeconds, 'local', false, 'abbreviated'), + formatDate(dateWithoutSeconds, 'local', { format: 'abbreviated' }), + ).toEqual('2022-04-13 16:29 PM'); + expect( + formatDate(dateWithoutSeconds, 'utc', { format: 'abbreviated' }), ).toEqual('2022-04-13 16:29 PM'); - expect(formatDate(dateWithoutSeconds, 'utc', false, 'abbreviated')).toEqual( - '2022-04-13 16:29 PM', - ); }); it('supports different timestamps formats', () => { - expect(formatDate(date, 'utc', false, 'short')).toEqual( + expect(formatDate(date, 'utc', { format: 'short' })).toEqual( '13th Apr 2022 16:29:35.63', ); - expect(formatDate(date, 'utc', false, 'medium')).toEqual( + expect(formatDate(date, 'utc', { format: 'medium' })).toEqual( '2022-04-13 UTC 16:29:35.63', ); - expect(formatDate(date, 'utc', false, 'long')).toEqual( + expect(formatDate(date, 'utc', { format: 'long' })).toEqual( 'April 13th 2022, 04:29:35.63 PM UTC', ); }); diff --git a/src/lib/utilities/format-date.ts b/src/lib/utilities/format-date.ts index 292fd8eefa..7d4b5bd9f4 100644 --- a/src/lib/utilities/format-date.ts +++ b/src/lib/utilities/format-date.ts @@ -18,6 +18,8 @@ import { import { isTimestamp, timestampToDate, type ValidTime } from './format-time'; export type FormatDateOptions = { + format?: TimestampFormat; + relative?: boolean; relativeLabel?: string; flexibleUnits?: boolean; }; @@ -25,8 +27,6 @@ export type FormatDateOptions = { export function formatDate( date: ValidTime | undefined | null, timeFormat: TimeFormat = 'UTC', - relative: boolean = false, - timestampFormat: TimestampFormat = 'medium', options: FormatDateOptions = {}, ): string { if (!date) return ''; @@ -39,18 +39,20 @@ export function formatDate( const currentDate = Date.now(); const isFutureDate = new Date(date).getTime() - currentDate > 0; const { + relative = false, relativeLabel = isFutureDate ? 'from now' : 'ago', flexibleUnits = false, + format = 'medium', } = options; const parsed = parseJSON(new Date(date)); - const format = - timestampFormat === 'abbreviated' + const timestampFormat = + format === 'abbreviated' ? parsed.getSeconds() ? timestampFormats.abbreviated : timestampFormats.abbreviatedWithoutSeconds - : timestampFormats[timestampFormat]; + : timestampFormats[format]; if (timeFormat === 'local') { if (relative) @@ -62,10 +64,10 @@ export function formatDate( }), }) + ` ${relativeLabel}` ); - return dateTz.format(parsed, format); + return dateTz.format(parsed, timestampFormat); } const timezone = getTimezone(timeFormat); - return dateTz.formatInTimeZone(parsed, timezone, format); + return dateTz.formatInTimeZone(parsed, timezone, timestampFormat); } catch (e) { console.error('Error formatting date:', e); return ''; diff --git a/src/lib/utilities/query/search-attribute-filter.ts b/src/lib/utilities/query/search-attribute-filter.ts index 8e8f02df2f..2e6cb6d768 100644 --- a/src/lib/utilities/query/search-attribute-filter.ts +++ b/src/lib/utilities/query/search-attribute-filter.ts @@ -109,12 +109,12 @@ export const formatDateTimeRange = ( return `${conditon.toLowerCase()} ${formatDate( start.replace(/"/g, ''), format, + { + relative, + format: 'abbreviated', + }, + )} ${operator.toLowerCase()} ${formatDate(end.replace(/"/g, ''), format, { relative, - 'abbreviated', - )} ${operator.toLowerCase()} ${formatDate( - end.replace(/"/g, ''), - format, - relative, - 'abbreviated', - )}`; + format: 'abbreviated', + })}`; }; From f92bc265f227b0841e310bcddfa6aa556dcbc1b7 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Mon, 17 Nov 2025 12:21:44 -0700 Subject: [PATCH 06/17] clean up --- src/lib/components/event/event-card.svelte | 16 ++++++---------- .../filter-bar/dropdown-filter-chip.svelte | 5 ++++- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/lib/components/event/event-card.svelte b/src/lib/components/event/event-card.svelte index d5f39ecf72..a09931586f 100644 --- a/src/lib/components/event/event-card.svelte +++ b/src/lib/components/event/event-card.svelte @@ -248,16 +248,12 @@

{format(key)}

- {#if shouldDisplayAsTime(key)} - - {:else} -

+

+ {#if shouldDisplayAsTime(key)} + + {:else} {value} -

- {/if} + {/if} +

{/snippet} diff --git a/src/lib/components/workflow/filter-bar/dropdown-filter-chip.svelte b/src/lib/components/workflow/filter-bar/dropdown-filter-chip.svelte index 7ed3db6b04..beab38ce38 100644 --- a/src/lib/components/workflow/filter-bar/dropdown-filter-chip.svelte +++ b/src/lib/components/workflow/filter-bar/dropdown-filter-chip.svelte @@ -174,7 +174,10 @@ if (isDateTimeFilter(filter)) { if (filter.customDate) return value.split('BETWEEN')[1]; - return formatDate(value, $timeFormat, { relative: true }); + return formatDate(value, $timeFormat, { + relative: true, + format: 'abbreviated', + }); } if (isTextFilter(filter)) { From d777af31fc22030492ac1a851eb600df5d765fce Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Mon, 17 Nov 2025 12:34:47 -0700 Subject: [PATCH 07/17] fix timestamp in timezone select --- src/lib/components/timezone-select.svelte | 25 ++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/lib/components/timezone-select.svelte b/src/lib/components/timezone-select.svelte index 806de15014..92a7660e39 100644 --- a/src/lib/components/timezone-select.svelte +++ b/src/lib/components/timezone-select.svelte @@ -1,4 +1,6 @@ - + {#if !$relativeTime} -
+

Timestamp Format

Date: Tue, 18 Nov 2025 09:52:55 -0700 Subject: [PATCH 08/17] use Intl.DateTimeFormat for i18n of date strings --- .../components/event/event-summary-row.svelte | 2 +- .../event/pending-activity-summary-row.svelte | 2 +- .../event/pending-nexus-summary-row.svelte | 2 +- src/lib/components/timestamp.svelte | 2 +- src/lib/components/timezone-select.svelte | 94 ++++++++++--------- .../filter-bar/dropdown-filter-chip.svelte | 2 +- src/lib/pages/workflow-call-stack.svelte | 2 +- src/lib/stores/time-format.ts | 14 +-- src/lib/utilities/format-date.test.ts | 39 ++------ src/lib/utilities/format-date.ts | 69 +++++++++++--- .../query/search-attribute-filter.ts | 4 +- 11 files changed, 125 insertions(+), 107 deletions(-) diff --git a/src/lib/components/event/event-summary-row.svelte b/src/lib/components/event/event-summary-row.svelte index 1c6c8b6968..63382f9224 100644 --- a/src/lib/components/event/event-summary-row.svelte +++ b/src/lib/components/event/event-summary-row.svelte @@ -172,7 +172,7 @@ const abbrEventTime = $derived( formatDate(currentEvent?.eventTime, $timeFormat, { relative: $relativeTime, - format: 'abbreviated', + format: 'short', }), ); diff --git a/src/lib/components/event/pending-activity-summary-row.svelte b/src/lib/components/event/pending-activity-summary-row.svelte index 7640dd9376..db519f171c 100644 --- a/src/lib/components/event/pending-activity-summary-row.svelte +++ b/src/lib/components/event/pending-activity-summary-row.svelte @@ -60,7 +60,7 @@ let abbrEventTime = $derived( formatDate(group?.eventTime, $timeFormat, { relative: $relativeTime, - format: 'abbreviated', + format: 'short', }), ); diff --git a/src/lib/components/event/pending-nexus-summary-row.svelte b/src/lib/components/event/pending-nexus-summary-row.svelte index 9a534e8379..1efab1f4ca 100644 --- a/src/lib/components/event/pending-nexus-summary-row.svelte +++ b/src/lib/components/event/pending-nexus-summary-row.svelte @@ -57,7 +57,7 @@ let abbrEventTime = $derived( formatDate(group?.eventTime, $timeFormat, { relative: $relativeTime, - format: 'abbreviated', + format: 'short', }), ); diff --git a/src/lib/components/timestamp.svelte b/src/lib/components/timestamp.svelte index 575877dd76..1592f5b217 100644 --- a/src/lib/components/timestamp.svelte +++ b/src/lib/components/timestamp.svelte @@ -7,11 +7,11 @@ relativeTime, timeFormat, timestampFormat, - type TimestampFormat, } from '$lib/stores/time-format'; import { formatDate, type FormatDateOptions, + type TimestampFormat, } from '$lib/utilities/format-date'; import type { ValidTime } from '$lib/utilities/format-time'; diff --git a/src/lib/components/timezone-select.svelte b/src/lib/components/timezone-select.svelte index 92a7660e39..38b875f22e 100644 --- a/src/lib/components/timezone-select.svelte +++ b/src/lib/components/timezone-select.svelte @@ -24,12 +24,15 @@ timeFormat, type TimeFormatOptions, timestampFormat, - type TimestampFormat, TimezoneOptions, Timezones, } from '$lib/stores/time-format'; import { capitalize } from '$lib/utilities/format-camel-case'; - import { formatUTCOffset, getLocalTime } from '$lib/utilities/format-date'; + import { + formatUTCOffset, + getLocalTime, + type TimestampFormat, + } from '$lib/utilities/format-date'; export let position: 'left' | 'right' = 'right'; export let size: ButtonStyles['size'] = 'md'; @@ -134,6 +137,49 @@ +
+ +
+ + {#if !$relativeTime} +
+
+

Timestamp Format

+ +
+ + setTimestampFormat('short')}>Short + setTimestampFormat('medium')}>Default + setTimestampFormat('long')}>Long + +
+ {/if} + + + {#if !search} {#each QuickTimezoneOptions as { value, label }} - -
- -
- - {#if !$relativeTime} -
-
-

Timestamp Format

- -
- - setTimestampFormat('short')}>Short - setTimestampFormat('medium')} - >Default - setTimestampFormat('long')}>Long - -
- {/if} - - {/if} {#each filteredOptions as { value, label, offset, abbr }} diff --git a/src/lib/components/workflow/filter-bar/dropdown-filter-chip.svelte b/src/lib/components/workflow/filter-bar/dropdown-filter-chip.svelte index beab38ce38..3f90ecc357 100644 --- a/src/lib/components/workflow/filter-bar/dropdown-filter-chip.svelte +++ b/src/lib/components/workflow/filter-bar/dropdown-filter-chip.svelte @@ -176,7 +176,7 @@ if (filter.customDate) return value.split('BETWEEN')[1]; return formatDate(value, $timeFormat, { relative: true, - format: 'abbreviated', + format: 'short', }); } diff --git a/src/lib/pages/workflow-call-stack.svelte b/src/lib/pages/workflow-call-stack.svelte index fe7558e78e..529a782cd9 100644 --- a/src/lib/pages/workflow-call-stack.svelte +++ b/src/lib/pages/workflow-call-stack.svelte @@ -22,7 +22,7 @@ let refreshDate = $derived( formatDate($refresh ? new Date($refresh) : new Date(), $timeFormat, { relative: $relativeTime, - format: 'abbreviated', + format: 'short', }), ); diff --git a/src/lib/stores/time-format.ts b/src/lib/stores/time-format.ts index 778fc657b9..26f055a36d 100644 --- a/src/lib/stores/time-format.ts +++ b/src/lib/stores/time-format.ts @@ -2,19 +2,13 @@ import { startOfDay } from 'date-fns'; import * as dateTz from 'date-fns-tz'; import { persistStore } from '$lib/stores/persist-store'; -import { getLocalTimezone } from '$lib/utilities/format-date'; +import { + getLocalTimezone, + type TimestampFormat, +} from '$lib/utilities/format-date'; type TimeFormatTypes = 'relative' | 'absolute'; -export const timestampFormats = { - short: 'do MMM yyyy H:mm:ss.SS', - medium: 'yyyy-MM-dd z HH:mm:ss.SS', - long: 'MMMM do yyyy, hh:mm:ss.SS a z', - abbreviated: 'yyyy-MM-dd HH:mm:ss a', - abbreviatedWithoutSeconds: 'yyyy-MM-dd HH:mm a', -} as const; - -export type TimestampFormat = keyof typeof timestampFormats; export const timestampFormat = persistStore( 'timestampFormat', 'medium', diff --git a/src/lib/utilities/format-date.test.ts b/src/lib/utilities/format-date.test.ts index 2e54b5b542..8d612ab2a8 100644 --- a/src/lib/utilities/format-date.test.ts +++ b/src/lib/utilities/format-date.test.ts @@ -33,29 +33,29 @@ describe('formatDate', () => { }); it('should default to UTC', () => { - expect(formatDate(date)).toEqual('2022-04-13 UTC 16:29:35.63'); + expect(formatDate(date)).toEqual('Apr 13, 2022, 16:29:35.63 UTC'); }); it('should format other timezones', () => { expect(formatDate(date, 'Greenwich Mean Time')).toEqual( - '2022-04-13 GMT 16:29:35.63', + 'Apr 13, 2022, 16:29:35.63 GMT', ); expect(formatDate(date, 'Central Standard Time')).toEqual( - '2022-04-13 CDT 11:29:35.63', + 'Apr 13, 2022, 11:29:35.63 CDT', ); expect(formatDate(date, 'Pacific Daylight Time')).toEqual( - '2022-04-13 PDT 09:29:35.63', + 'Apr 13, 2022, 09:29:35.63 PDT', ); }); it('should format already formatted strings', () => { expect(formatDate('2022-04-13 UTC 16:29:35.63')).toEqual( - '2022-04-13 UTC 16:29:35.63', + 'Apr 13, 2022, 16:29:35.63 UTC', ); }); it('should format local time', () => { - expect(formatDate(date, 'local')).toEqual('2022-04-13 UTC 16:29:35.63'); + expect(formatDate(date, 'local')).toEqual('Apr 13, 2022, 16:29:35.63 UTC'); }); it('should format relative local time', () => { @@ -69,7 +69,7 @@ describe('formatDate', () => { it('should not format other timezones as relative', () => { expect(formatDate(date, 'UTC', { relative: true })).toEqual( - '2022-04-13 UTC 16:29:35.63', + 'Apr 13, 2022, 16:29:35.63 UTC', ); }); @@ -131,34 +131,15 @@ describe('formatDate', () => { expect(formattedDate).toEqual('23 hours from now'); }); - it('should shorten format for local and other timezones', () => { - expect(formatDate(date, 'local', { format: 'abbreviated' })).toEqual( - '2022-04-13 16:29:35 PM', - ); - expect(formatDate(date, 'utc', { format: 'abbreviated' })).toEqual( - '2022-04-13 16:29:35 PM', - ); - }); - - it('should shorten format without seconds if there are none for local and other timezones', () => { - const dateWithoutSeconds = '2022-04-13T16:29:00.630571Z'; - expect( - formatDate(dateWithoutSeconds, 'local', { format: 'abbreviated' }), - ).toEqual('2022-04-13 16:29 PM'); - expect( - formatDate(dateWithoutSeconds, 'utc', { format: 'abbreviated' }), - ).toEqual('2022-04-13 16:29 PM'); - }); - it('supports different timestamps formats', () => { expect(formatDate(date, 'utc', { format: 'short' })).toEqual( - '13th Apr 2022 16:29:35.63', + '4/13/22, 16:29:35.63 UTC', ); expect(formatDate(date, 'utc', { format: 'medium' })).toEqual( - '2022-04-13 UTC 16:29:35.63', + 'Apr 13, 2022, 16:29:35.63 UTC', ); expect(formatDate(date, 'utc', { format: 'long' })).toEqual( - 'April 13th 2022, 04:29:35.63 PM UTC', + 'April 13, 2022 at 4:29:35.63 PM UTC', ); }); }); diff --git a/src/lib/utilities/format-date.ts b/src/lib/utilities/format-date.ts index 7d4b5bd9f4..ab457e3fd7 100644 --- a/src/lib/utilities/format-date.ts +++ b/src/lib/utilities/format-date.ts @@ -4,13 +4,10 @@ import { parseISO, parseJSON, } from 'date-fns'; -import * as dateTz from 'date-fns-tz'; // `build` script fails on importing some of named CommonJS modules import { getTimezone, type TimeFormat, - type TimestampFormat, - timestampFormats, TimezoneOptions, Timezones, } from '$lib/stores/time-format'; @@ -24,6 +21,48 @@ export type FormatDateOptions = { flexibleUnits?: boolean; }; +export const timestampFormats: Record< + string, + Partial +> = { + short: { + year: '2-digit', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: false, + fractionalSecondDigits: 2, + timeZoneName: 'short', + }, + medium: { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: false, + hourCycle: 'h12', + timeZoneName: 'short', + fractionalSecondDigits: 2, + }, + long: { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: true, + timeZoneName: 'short', + fractionalSecondDigits: 2, + }, +} as const; + +export type TimestampFormat = keyof typeof timestampFormats; + export function formatDate( date: ValidTime | undefined | null, timeFormat: TimeFormat = 'UTC', @@ -47,15 +86,8 @@ export function formatDate( const parsed = parseJSON(new Date(date)); - const timestampFormat = - format === 'abbreviated' - ? parsed.getSeconds() - ? timestampFormats.abbreviated - : timestampFormats.abbreviatedWithoutSeconds - : timestampFormats[format]; - if (timeFormat === 'local') { - if (relative) + if (relative) { return ( formatDistanceToNowStrict(parsed, { ...(!flexibleUnits && @@ -64,10 +96,19 @@ export function formatDate( }), }) + ` ${relativeLabel}` ); - return dateTz.format(parsed, timestampFormat); + } + + return new Intl.DateTimeFormat( + undefined, + timestampFormats[format], + ).format(parsed); } - const timezone = getTimezone(timeFormat); - return dateTz.formatInTimeZone(parsed, timezone, timestampFormat); + + const timeZone = getTimezone(timeFormat); + return new Intl.DateTimeFormat(undefined, { + ...timestampFormats[format], + timeZone, + }).format(parsed); } catch (e) { console.error('Error formatting date:', e); return ''; diff --git a/src/lib/utilities/query/search-attribute-filter.ts b/src/lib/utilities/query/search-attribute-filter.ts index 2e6cb6d768..b772e44e08 100644 --- a/src/lib/utilities/query/search-attribute-filter.ts +++ b/src/lib/utilities/query/search-attribute-filter.ts @@ -111,10 +111,10 @@ export const formatDateTimeRange = ( format, { relative, - format: 'abbreviated', + format: 'short', }, )} ${operator.toLowerCase()} ${formatDate(end.replace(/"/g, ''), format, { relative, - format: 'abbreviated', + format: 'short', })}`; }; From 986782493936a2773bb617e87a92d18b59123135 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Tue, 18 Nov 2025 10:09:13 -0700 Subject: [PATCH 09/17] fix search attribute filter tests --- .../query/search-attribute-filter.test.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/lib/utilities/query/search-attribute-filter.test.ts b/src/lib/utilities/query/search-attribute-filter.test.ts index f54c1a283b..61f3e5a4fd 100644 --- a/src/lib/utilities/query/search-attribute-filter.test.ts +++ b/src/lib/utilities/query/search-attribute-filter.test.ts @@ -182,14 +182,20 @@ describe('formatDateTimeRange', () => { formatDateTimeRange( 'BETWEEN "2025-07-17T00:00:00.000Z" AND "2025-07-17T00:00:00.000Z"', 'UTC', + false, ), - ).toStrictEqual('between 2025-07-17 00:00 AM and 2025-07-17 00:00 AM'); + ).toStrictEqual( + 'between 7/17/25, 00:00:00.00 UTC and 7/17/25, 00:00:00.00 UTC', + ); expect( formatDateTimeRange( 'BETWEEN 2025-07-17T00:00:00.000Z AND 2025-07-17T00:00:00.000Z', 'UTC', + false, ), - ).toStrictEqual('between 2025-07-17 00:00 AM and 2025-07-17 00:00 AM'); + ).toStrictEqual( + 'between 7/17/25, 00:00:00.00 UTC and 7/17/25, 00:00:00.00 UTC', + ); }); it('should format a date range between two dates with a different time format', () => { @@ -197,7 +203,10 @@ describe('formatDateTimeRange', () => { formatDateTimeRange( 'BETWEEN "2025-07-17T00:00:00.000Z" AND "2025-07-17T00:00:00.000Z"', 'Pacific Daylight Time', + false, ), - ).toStrictEqual('between 2025-07-16 17:00 PM and 2025-07-16 17:00 PM'); + ).toStrictEqual( + 'between 7/16/25, 17:00:00.00 PDT and 7/16/25, 17:00:00.00 PDT', + ); }); }); From f854ca44803f2d81099bf314828a493c8a5d5154 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Tue, 18 Nov 2025 10:32:50 -0700 Subject: [PATCH 10/17] fix tests and mobile styles --- src/lib/components/timezone-select.svelte | 4 +++- .../workflows-search-attribute-filter.desktop.spec.ts | 8 ++++++-- .../workflows-search-attribute-filter.mobile.spec.ts | 4 +++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/lib/components/timezone-select.svelte b/src/lib/components/timezone-select.svelte index 38b875f22e..bc7399bffd 100644 --- a/src/lib/components/timezone-select.svelte +++ b/src/lib/components/timezone-select.svelte @@ -149,7 +149,9 @@
{#if !$relativeTime} -
+

Timestamp Format

="2025-12-25T12:00:00.000Z"'); await expect( - page.getByRole('button', { name: 'CloseTime >= 2025-12-25 04:00' }), + page.getByRole('button', { + name: 'CloseTime >= 12/25/25, 04:00:00.00 PST', + }), ).toBeVisible(); await page.getByTestId('toggle-manual-query').click(); @@ -60,7 +62,9 @@ test('it should update the datetime filter based on the selected timezone', asyn await page.getByText('Mountain Daylight Time (MDT) UTC-06:00').click(); await expect( - page.getByRole('button', { name: 'CloseTime >= 2025-12-25 05:00' }), + page.getByRole('button', { + name: 'CloseTime >= 12/25/25, 05:00:00.00 MST', + }), ).toBeVisible(); query = await page.getByTestId('manual-search-input').inputValue(); diff --git a/tests/integration/workflows-search-attribute-filter.mobile.spec.ts b/tests/integration/workflows-search-attribute-filter.mobile.spec.ts index 78314c435a..85a054132b 100644 --- a/tests/integration/workflows-search-attribute-filter.mobile.spec.ts +++ b/tests/integration/workflows-search-attribute-filter.mobile.spec.ts @@ -61,7 +61,9 @@ test('it should update the datetime filter based on the selected timezone', asyn .toBe('`CloseTime`>="2025-12-25T12:00:00.000Z"'); await expect( - page.getByRole('button', { name: 'CloseTime >= 2025-12-25 04:00' }), + page.getByRole('button', { + name: 'CloseTime >= 12/25/25, 04:00:00.00 PST', + }), ).toBeVisible(); let query = await page.getByTestId('manual-search-input').inputValue(); expect(getDatetime(query)).toMatch(validDatetime); From 6667e953599c7d643c850529a09bbc35f924c1b7 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Tue, 18 Nov 2025 10:37:28 -0700 Subject: [PATCH 11/17] remove h12 --- src/lib/utilities/format-date.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/utilities/format-date.ts b/src/lib/utilities/format-date.ts index ab457e3fd7..8608e91a9c 100644 --- a/src/lib/utilities/format-date.ts +++ b/src/lib/utilities/format-date.ts @@ -44,7 +44,6 @@ export const timestampFormats: Record< minute: 'numeric', second: 'numeric', hour12: false, - hourCycle: 'h12', timeZoneName: 'short', fractionalSecondDigits: 2, }, From d234293fc54a3b303e5f8bb4840e522bb4d96bc2 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Tue, 18 Nov 2025 10:47:37 -0700 Subject: [PATCH 12/17] add hourCycle --- src/lib/utilities/format-date.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/utilities/format-date.ts b/src/lib/utilities/format-date.ts index 8608e91a9c..048cdc553e 100644 --- a/src/lib/utilities/format-date.ts +++ b/src/lib/utilities/format-date.ts @@ -44,6 +44,7 @@ export const timestampFormats: Record< minute: 'numeric', second: 'numeric', hour12: false, + hourCycle: 'h24', timeZoneName: 'short', fractionalSecondDigits: 2, }, From acbb21598036664bee43f9d1d94eabb4fc89af25 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Tue, 18 Nov 2025 10:48:38 -0700 Subject: [PATCH 13/17] fix hourCycle --- src/lib/utilities/format-date.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/utilities/format-date.ts b/src/lib/utilities/format-date.ts index 048cdc553e..748964b2f7 100644 --- a/src/lib/utilities/format-date.ts +++ b/src/lib/utilities/format-date.ts @@ -44,7 +44,7 @@ export const timestampFormats: Record< minute: 'numeric', second: 'numeric', hour12: false, - hourCycle: 'h24', + hourCycle: 'h23', timeZoneName: 'short', fractionalSecondDigits: 2, }, From 3137b725444b2c6c82fe40664ae85dc4c3172a59 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Tue, 18 Nov 2025 10:53:21 -0700 Subject: [PATCH 14/17] fix hourCycle --- src/lib/utilities/format-date.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/utilities/format-date.ts b/src/lib/utilities/format-date.ts index 748964b2f7..407ac7414e 100644 --- a/src/lib/utilities/format-date.ts +++ b/src/lib/utilities/format-date.ts @@ -33,6 +33,7 @@ export const timestampFormats: Record< minute: 'numeric', second: 'numeric', hour12: false, + hourCycle: 'h23', fractionalSecondDigits: 2, timeZoneName: 'short', }, @@ -44,7 +45,6 @@ export const timestampFormats: Record< minute: 'numeric', second: 'numeric', hour12: false, - hourCycle: 'h23', timeZoneName: 'short', fractionalSecondDigits: 2, }, From abc60a9c40b9df92305727517cef98fdb504f8b1 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Tue, 18 Nov 2025 11:01:21 -0700 Subject: [PATCH 15/17] fix test --- src/lib/utilities/format-date.ts | 1 - .../utilities/query/search-attribute-filter.test.ts | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/lib/utilities/format-date.ts b/src/lib/utilities/format-date.ts index 407ac7414e..8608e91a9c 100644 --- a/src/lib/utilities/format-date.ts +++ b/src/lib/utilities/format-date.ts @@ -33,7 +33,6 @@ export const timestampFormats: Record< minute: 'numeric', second: 'numeric', hour12: false, - hourCycle: 'h23', fractionalSecondDigits: 2, timeZoneName: 'short', }, diff --git a/src/lib/utilities/query/search-attribute-filter.test.ts b/src/lib/utilities/query/search-attribute-filter.test.ts index 61f3e5a4fd..d40740366e 100644 --- a/src/lib/utilities/query/search-attribute-filter.test.ts +++ b/src/lib/utilities/query/search-attribute-filter.test.ts @@ -180,21 +180,21 @@ describe('formatDateTimeRange', () => { it('should format a date range between two dates', () => { expect( formatDateTimeRange( - 'BETWEEN "2025-07-17T00:00:00.000Z" AND "2025-07-17T00:00:00.000Z"', + 'BETWEEN "2025-07-17T12:00:00.000Z" AND "2025-07-17T13:00:00.000Z"', 'UTC', false, ), ).toStrictEqual( - 'between 7/17/25, 00:00:00.00 UTC and 7/17/25, 00:00:00.00 UTC', + 'between 7/17/25, 12:00:00.00 UTC and 7/17/25, 13:00:00.00 UTC', ); expect( formatDateTimeRange( - 'BETWEEN 2025-07-17T00:00:00.000Z AND 2025-07-17T00:00:00.000Z', + 'BETWEEN 2025-07-17T12:00:00.000Z AND 2025-07-17T13:00:00.000Z', 'UTC', false, ), - ).toStrictEqual( - 'between 7/17/25, 00:00:00.00 UTC and 7/17/25, 00:00:00.00 UTC', + ).toContain( + 'between 7/17/25, 12:00:00.00 UTC and 7/17/25, 13:00:00.00 UTC', ); }); From 52d185b55335eaa89d5089fbde0b67ce72a52e45 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Tue, 18 Nov 2025 12:43:26 -0700 Subject: [PATCH 16/17] fix format and tests --- src/lib/utilities/format-date.test.ts | 28 ++++++++++++------- src/lib/utilities/format-date.ts | 3 -- .../query/search-attribute-filter.test.ts | 14 +++++++--- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/lib/utilities/format-date.test.ts b/src/lib/utilities/format-date.test.ts index 8d612ab2a8..c2dbfba617 100644 --- a/src/lib/utilities/format-date.test.ts +++ b/src/lib/utilities/format-date.test.ts @@ -9,6 +9,12 @@ import { isValidDate, } from './format-date'; +const DateTimeFormat = Intl.DateTimeFormat; +vi.spyOn(global.Intl, 'DateTimeFormat').mockImplementation( + (_, options) => + new DateTimeFormat('en-US', { ...options, hour12: true, hourCycle: 'h11' }), +); + describe('formatDate', () => { const date = '2022-04-13T16:29:35.630571Z'; @@ -33,29 +39,31 @@ describe('formatDate', () => { }); it('should default to UTC', () => { - expect(formatDate(date)).toEqual('Apr 13, 2022, 16:29:35.63 UTC'); + expect(formatDate(date)).toEqual('Apr 13, 2022, 4:29:35.63 PM UTC'); }); it('should format other timezones', () => { expect(formatDate(date, 'Greenwich Mean Time')).toEqual( - 'Apr 13, 2022, 16:29:35.63 GMT', + 'Apr 13, 2022, 4:29:35.63 PM GMT', ); expect(formatDate(date, 'Central Standard Time')).toEqual( - 'Apr 13, 2022, 11:29:35.63 CDT', + 'Apr 13, 2022, 11:29:35.63 AM CDT', ); expect(formatDate(date, 'Pacific Daylight Time')).toEqual( - 'Apr 13, 2022, 09:29:35.63 PDT', + 'Apr 13, 2022, 9:29:35.63 AM PDT', ); }); it('should format already formatted strings', () => { - expect(formatDate('2022-04-13 UTC 16:29:35.63')).toEqual( - 'Apr 13, 2022, 16:29:35.63 UTC', + expect(formatDate('2022-04-13 UTC 4:29:35.63 PM')).toEqual( + 'Apr 13, 2022, 4:29:35.63 PM UTC', ); }); it('should format local time', () => { - expect(formatDate(date, 'local')).toEqual('Apr 13, 2022, 16:29:35.63 UTC'); + expect(formatDate(date, 'local')).toEqual( + 'Apr 13, 2022, 4:29:35.63 PM UTC', + ); }); it('should format relative local time', () => { @@ -69,7 +77,7 @@ describe('formatDate', () => { it('should not format other timezones as relative', () => { expect(formatDate(date, 'UTC', { relative: true })).toEqual( - 'Apr 13, 2022, 16:29:35.63 UTC', + 'Apr 13, 2022, 4:29:35.63 PM UTC', ); }); @@ -133,10 +141,10 @@ describe('formatDate', () => { it('supports different timestamps formats', () => { expect(formatDate(date, 'utc', { format: 'short' })).toEqual( - '4/13/22, 16:29:35.63 UTC', + '4/13/22, 4:29:35.63 PM UTC', ); expect(formatDate(date, 'utc', { format: 'medium' })).toEqual( - 'Apr 13, 2022, 16:29:35.63 UTC', + 'Apr 13, 2022, 4:29:35.63 PM UTC', ); expect(formatDate(date, 'utc', { format: 'long' })).toEqual( 'April 13, 2022 at 4:29:35.63 PM UTC', diff --git a/src/lib/utilities/format-date.ts b/src/lib/utilities/format-date.ts index 8608e91a9c..ec0ffe0275 100644 --- a/src/lib/utilities/format-date.ts +++ b/src/lib/utilities/format-date.ts @@ -32,7 +32,6 @@ export const timestampFormats: Record< hour: 'numeric', minute: 'numeric', second: 'numeric', - hour12: false, fractionalSecondDigits: 2, timeZoneName: 'short', }, @@ -43,7 +42,6 @@ export const timestampFormats: Record< hour: 'numeric', minute: 'numeric', second: 'numeric', - hour12: false, timeZoneName: 'short', fractionalSecondDigits: 2, }, @@ -54,7 +52,6 @@ export const timestampFormats: Record< hour: 'numeric', minute: 'numeric', second: 'numeric', - hour12: true, timeZoneName: 'short', fractionalSecondDigits: 2, }, diff --git a/src/lib/utilities/query/search-attribute-filter.test.ts b/src/lib/utilities/query/search-attribute-filter.test.ts index d40740366e..32ca857ba5 100644 --- a/src/lib/utilities/query/search-attribute-filter.test.ts +++ b/src/lib/utilities/query/search-attribute-filter.test.ts @@ -1,6 +1,6 @@ import { writable } from 'svelte/store'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import type { SearchAttributes } from '$lib/types/workflows'; @@ -30,6 +30,12 @@ const store = writable({ CustomC: 'String', }); +const DateTimeFormat = Intl.DateTimeFormat; +vi.spyOn(global.Intl, 'DateTimeFormat').mockImplementation( + (_, options) => + new DateTimeFormat('en-US', { ...options, hour12: true, hourCycle: 'h11' }), +); + describe('isStatusFilter', () => { it('should return true if the attribute is ExecutionStatus', () => { expect(isStatusFilter({ attribute: 'ExecutionStatus' })).toBe(true); @@ -185,7 +191,7 @@ describe('formatDateTimeRange', () => { false, ), ).toStrictEqual( - 'between 7/17/25, 12:00:00.00 UTC and 7/17/25, 13:00:00.00 UTC', + 'between 7/17/25, 12:00:00.00 PM UTC and 7/17/25, 1:00:00.00 PM UTC', ); expect( formatDateTimeRange( @@ -194,7 +200,7 @@ describe('formatDateTimeRange', () => { false, ), ).toContain( - 'between 7/17/25, 12:00:00.00 UTC and 7/17/25, 13:00:00.00 UTC', + 'between 7/17/25, 12:00:00.00 PM UTC and 7/17/25, 1:00:00.00 PM UTC', ); }); @@ -206,7 +212,7 @@ describe('formatDateTimeRange', () => { false, ), ).toStrictEqual( - 'between 7/16/25, 17:00:00.00 PDT and 7/16/25, 17:00:00.00 PDT', + 'between 7/16/25, 5:00:00.00 PM PDT and 7/16/25, 5:00:00.00 PM PDT', ); }); }); From 422781185641beb2500165c72d0efdb231044af2 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Tue, 18 Nov 2025 13:10:16 -0700 Subject: [PATCH 17/17] fix pw tests, add comments --- src/lib/utilities/format-date.test.ts | 1 + src/lib/utilities/query/search-attribute-filter.test.ts | 1 + .../workflows-search-attribute-filter.desktop.spec.ts | 4 ++-- .../workflows-search-attribute-filter.mobile.spec.ts | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib/utilities/format-date.test.ts b/src/lib/utilities/format-date.test.ts index c2dbfba617..7ec36affe4 100644 --- a/src/lib/utilities/format-date.test.ts +++ b/src/lib/utilities/format-date.test.ts @@ -9,6 +9,7 @@ import { isValidDate, } from './format-date'; +// // force GH action runners to use en-US and 12-hour clocks starting at 0:00 const DateTimeFormat = Intl.DateTimeFormat; vi.spyOn(global.Intl, 'DateTimeFormat').mockImplementation( (_, options) => diff --git a/src/lib/utilities/query/search-attribute-filter.test.ts b/src/lib/utilities/query/search-attribute-filter.test.ts index 32ca857ba5..fe77f85506 100644 --- a/src/lib/utilities/query/search-attribute-filter.test.ts +++ b/src/lib/utilities/query/search-attribute-filter.test.ts @@ -30,6 +30,7 @@ const store = writable({ CustomC: 'String', }); +// force GH action runners to use en-US and 12-hour clocks starting at 0:00 const DateTimeFormat = Intl.DateTimeFormat; vi.spyOn(global.Intl, 'DateTimeFormat').mockImplementation( (_, options) => diff --git a/tests/integration/workflows-search-attribute-filter.desktop.spec.ts b/tests/integration/workflows-search-attribute-filter.desktop.spec.ts index 5e4a529a54..eacff65d10 100644 --- a/tests/integration/workflows-search-attribute-filter.desktop.spec.ts +++ b/tests/integration/workflows-search-attribute-filter.desktop.spec.ts @@ -48,7 +48,7 @@ test('it should update the datetime filter based on the selected timezone', asyn await expect( page.getByRole('button', { - name: 'CloseTime >= 12/25/25, 04:00:00.00 PST', + name: 'CloseTime >= 12/25/25, 4:00:00.00 AM PST', }), ).toBeVisible(); @@ -63,7 +63,7 @@ test('it should update the datetime filter based on the selected timezone', asyn await expect( page.getByRole('button', { - name: 'CloseTime >= 12/25/25, 05:00:00.00 MST', + name: 'CloseTime >= 12/25/25, 5:00:00.00 AM MST', }), ).toBeVisible(); diff --git a/tests/integration/workflows-search-attribute-filter.mobile.spec.ts b/tests/integration/workflows-search-attribute-filter.mobile.spec.ts index 85a054132b..6e443bdae7 100644 --- a/tests/integration/workflows-search-attribute-filter.mobile.spec.ts +++ b/tests/integration/workflows-search-attribute-filter.mobile.spec.ts @@ -62,7 +62,7 @@ test('it should update the datetime filter based on the selected timezone', asyn await expect( page.getByRole('button', { - name: 'CloseTime >= 12/25/25, 04:00:00.00 PST', + name: 'CloseTime >= 12/25/25, 4:00:00.00 AM PST', }), ).toBeVisible(); let query = await page.getByTestId('manual-search-input').inputValue();