diff --git a/src/lib/components/standalone-activities/activity-execution-detail-card.svelte b/src/lib/components/standalone-activities/activity-execution-detail-card.svelte new file mode 100644 index 0000000000..099bc53981 --- /dev/null +++ b/src/lib/components/standalone-activities/activity-execution-detail-card.svelte @@ -0,0 +1,40 @@ + + + +
+ {title} +
+
+

+ {#if content} + {content} + {:else} + {fallbackContent} + {/if} +

+ {@render children?.()} +
+ {#if description} + {description} + {/if} +
diff --git a/src/lib/components/standalone-activities/activity-execution-last-failure.svelte b/src/lib/components/standalone-activities/activity-execution-last-failure.svelte new file mode 100644 index 0000000000..2a3ce8e80e --- /dev/null +++ b/src/lib/components/standalone-activities/activity-execution-last-failure.svelte @@ -0,0 +1,17 @@ + + +{#if failure} + + + +{/if} diff --git a/src/lib/components/standalone-activities/activity-execution-last-heartbeat.svelte b/src/lib/components/standalone-activities/activity-execution-last-heartbeat.svelte new file mode 100644 index 0000000000..cee6fd8df8 --- /dev/null +++ b/src/lib/components/standalone-activities/activity-execution-last-heartbeat.svelte @@ -0,0 +1,33 @@ + + + +
Last Heartbeat
+

+ {#if lastHeartbeat} + {$timestamp(lastHeartbeat)} + {:else} + None + {/if} +

+ {#if heartbeatDetails} + + {#snippet children(content)} + + {/snippet} + + {/if} +
diff --git a/src/lib/components/standalone-activities/activity-execution-last-worker.svelte b/src/lib/components/standalone-activities/activity-execution-last-worker.svelte new file mode 100644 index 0000000000..fa9a2e1aed --- /dev/null +++ b/src/lib/components/standalone-activities/activity-execution-last-worker.svelte @@ -0,0 +1,16 @@ + + + +
Last Worker
+

+ {identity} +

+
diff --git a/src/lib/components/standalone-activities/activity-execution-progress-bar.svelte b/src/lib/components/standalone-activities/activity-execution-progress-bar.svelte new file mode 100644 index 0000000000..cbe0e68e08 --- /dev/null +++ b/src/lib/components/standalone-activities/activity-execution-progress-bar.svelte @@ -0,0 +1,29 @@ + + +{#if maximumAttempts} + +
+
+ Progress +
+ {attempt} / {maximumAttempts} +
+
+
+
+
+{/if} diff --git a/src/lib/components/standalone-activities/activity-execution-retry-schedule.svelte b/src/lib/components/standalone-activities/activity-execution-retry-schedule.svelte new file mode 100644 index 0000000000..ebcf005b90 --- /dev/null +++ b/src/lib/components/standalone-activities/activity-execution-retry-schedule.svelte @@ -0,0 +1,186 @@ + + + +
+
+ Retry Schedule +
+ {#if !activity.maximumAttempts} + (only showing the first 50 attempts) + {/if} +
+ + + Initial Interval + + Maximum Interval + + + + Backoff Coefficient + + Maximum Attempts + + + +
+ + {#each yAxisLabels as label (label.val)} + + {label.val} + {/each} + + {#each activity.schedule as entry, i (entry.attempt)} + {@const isCurrentAttempt = entry.attempt === activity.currentAttempt} + + + + {#if i % 2 === 0} + {entry.attempt} + {/if} + {/each} + + attempt + +
+
+
+ Started + + + +
+
+ {activity.running ? 'Stops' : 'Stopped'} + + {#if activity.running && activity.deadlineTime} + + {:else if !activity.running} + + {/if} + +
+
+
diff --git a/src/lib/components/standalone-activities/activity-execution-run-state-badge.svelte b/src/lib/components/standalone-activities/activity-execution-run-state-badge.svelte new file mode 100644 index 0000000000..36d3afd432 --- /dev/null +++ b/src/lib/components/standalone-activities/activity-execution-run-state-badge.svelte @@ -0,0 +1,15 @@ + + + + {fromScreamingEnum(state, 'PendingActivityState')} + diff --git a/src/lib/components/standalone-activities/activity-execution-timeouts.svelte b/src/lib/components/standalone-activities/activity-execution-timeouts.svelte new file mode 100644 index 0000000000..7c8a083adc --- /dev/null +++ b/src/lib/components/standalone-activities/activity-execution-timeouts.svelte @@ -0,0 +1,78 @@ + + + +
+ Activity Timeouts +
+ +
+
+

Schedule to Start Timeout

+

{scheduleToStartValue}

+
+
+

Schedule to Close Timeout

+

{scheduleToCloseValue}

+ {#if activity.running && activity.scheduleToCloseSecondsLeft} +

+ Times out in {formatSecondsAbbreviated( + activity.scheduleToCloseSecondsLeft, + false, + )} +

+ {/if} +
+
+

Start to Close Timeout

+

{startToCloseValue}

+ {#if activity.running && activity.startToCloseSecondsLeft} +

+ Times out in {formatSecondsAbbreviated( + activity.startToCloseSecondsLeft, + false, + )} +

+ {/if} +
+
+

Heartbeat Timeout

+

{heartbeatValue}

+
+
+
diff --git a/src/lib/components/standalone-activities/activity-execution-upcoming-attempts.svelte b/src/lib/components/standalone-activities/activity-execution-upcoming-attempts.svelte new file mode 100644 index 0000000000..538f863e80 --- /dev/null +++ b/src/lib/components/standalone-activities/activity-execution-upcoming-attempts.svelte @@ -0,0 +1,84 @@ + + + +
+
+ Upcoming Attempts +
+ {#if !activity.maximumAttempts} + (only showing the next 50 attempts) + {/if} +
+ + {#if activity.nextAttemptScheduleTime} +
+ Next retry in: + {activity.nextRetrySecondsLeft} +
+ {/if} + +
+ {#each activity.upcomingAttempts as entry (entry.attempt)} + {@const isCurrent = entry.attempt === activity.currentAttempt} +
+ {#if isCurrent && activity.runState === 'PENDING_ACTIVITY_STATE_STARTED'} + + + + {:else if isCurrent} + + {:else} + + {/if} + Attempt {entry.attempt} + + wait {formatSecondsAbbreviated(entry.waitSeconds, false)} + + {#if isCurrent} + + {fromScreamingEnum(activity.runState, 'PendingActivityState')} + + {/if} + + {$timestamp(entry.runAt)} + +
+ {:else} +
+

No upcoming attempts

+
+ {/each} +
+
diff --git a/src/lib/components/standalone-activities/activity-header.svelte b/src/lib/components/standalone-activities/activity-header.svelte index 7bfdb40bb1..540b0ba8ec 100644 --- a/src/lib/components/standalone-activities/activity-header.svelte +++ b/src/lib/components/standalone-activities/activity-header.svelte @@ -1,6 +1,7 @@
-
-
+
+
+ + {#if $isCloud} + Billable Actions + + {:else} + State Transitions + + {/if} +
diff --git a/src/lib/components/standalone-activities/activity-input-and-outcome.svelte b/src/lib/components/standalone-activities/activity-input-and-outcome.svelte index 8c6ffc8ed6..a2085c9dcf 100644 --- a/src/lib/components/standalone-activities/activity-input-and-outcome.svelte +++ b/src/lib/components/standalone-activities/activity-input-and-outcome.svelte @@ -1,40 +1,39 @@
-
-
Input
- - {#snippet children(decodedValue)} - - {/snippet} - -
-
-
Result
- {#if has(outcome, 'failure')} - - {:else if has(outcome, 'result')} - - {#snippet children(decodedValue)} - - {/snippet} - - {:else} - - {/if} -
+ + {#if has(outcome, 'failure')} + + {:else if has(outcome, 'result')} + + {:else} + + {/if}
diff --git a/src/lib/components/standalone-activities/start-standalone-activity-form/types.ts b/src/lib/components/standalone-activities/start-standalone-activity-form/types.ts index 4f6887d3a5..d356d0f4b1 100644 --- a/src/lib/components/standalone-activities/start-standalone-activity-form/types.ts +++ b/src/lib/components/standalone-activities/start-standalone-activity-form/types.ts @@ -10,6 +10,7 @@ export interface StandaloneActivityFormData { startToCloseTimeout: string; scheduleToCloseTimeout: string; scheduleToStartTimeout: string; + heartbeatTimeout: string; input: string; encoding: PayloadInputEncoding; messageType: string; diff --git a/src/lib/pages/standalone-activity-details.svelte b/src/lib/pages/standalone-activity-details.svelte index 089302c39d..beb40e2aa1 100644 --- a/src/lib/pages/standalone-activity-details.svelte +++ b/src/lib/pages/standalone-activity-details.svelte @@ -1,246 +1,106 @@ -{#snippet activityExecutionAttemptsBadge( - attempt: number, - maximumAttempts: number | undefined, - lastFailure: Failure, -)} - {@const failed = attempt > 1 && !!lastFailure} - {@const badgeType = failed ? 'danger' : 'default'} +{#if activity} +
+ + + - Attempt - - - - {attempt} of {formatMaximumAttempts(maximumAttempts)} - - - {#if maximumAttempts && !isClosed} -

- {formatAttemptsLeft(maximumAttempts, attempt)} remaining -

- {/if} -
-{/snippet} - -{#if $activityExecution} - - -

{$activityExecution.info.activityType.name}

-
-
- {#if !isClosed} -
Current State
- - Run State - - {@render activityExecutionAttemptsBadge( - $activityExecution.info.attempt, - $activityExecution.info.retryPolicy?.maximumAttempts, - $activityExecution.info.lastFailure, - )} - - {/if} -
Timing and Progress
- + + +
+ {#if activity.running} +
+ - Schedule Time - - Last Started Time - - {#if isClosed} - Execution Duration - - Close Time - - {/if} - - {#if !isClosed} -
Health
- - Last Heartbeat - - Heartbeat Timeout - - - {/if} - {#if isRetrying} -
Retry State
- - Current Retry Interval - - - Last Attempted Complete Time - - - Next Attempt Scheduled Time - - - {/if} + +
+ + + +
+ -
Timeout Configuration
- - Schedule to Start Timeout - - Schedule to Close Timeout - - Start to Close Timeout - - -
Worker
- - Task Queue - +
+ - Last Worker Identity - - -
-
- {#if $activityExecution.info.lastFailure} -
-

Last Failure

- -
- {/if} - {#if $activityExecution.info.retryPolicy} -
-

Retry Policy

- -
- {/if} - {#if $activityExecution.info.heartbeatDetails} -
-

Heartbeat Details

- - {#snippet children(content)} - - {/snippet} - -
- {/if} - {#if $activityExecution.info.header} -
-

Header

- - {#snippet children(content)} - - {/snippet} - -
- {/if} - {#if $activityExecution.info.priority} -
-

Priority

- -
- {/if} +
-
-
+ {:else} + + {/if} +
{/if} diff --git a/src/lib/pages/standalone-activity.svelte.ts b/src/lib/pages/standalone-activity.svelte.ts new file mode 100644 index 0000000000..2fb744f09a --- /dev/null +++ b/src/lib/pages/standalone-activity.svelte.ts @@ -0,0 +1,231 @@ +import { SvelteDate } from 'svelte/reactivity'; + +import type { + ActivityExecution, + ActivityExecutionRunState, +} from '$lib/types/activity-execution'; +import { + formatSecondsAbbreviated, + fromDurationToNumber, +} from '$lib/utilities/format-time'; + +interface ScheduleEntry { + attempt: number; + waitSeconds: number; + runAt: Date; +} + +export const MAX_ATTEMPTS = 50; + +export class StandaloneActivity { + private activityExecution: ActivityExecution | undefined = $state(); + + public initialInterval: number | undefined = $state(); + public maximumInterval: number | undefined = $state(); + public maximumAttempts: number | undefined = $state(); + public backoffCoefficient: number | undefined = $state(); + public currentInterval: string = $state('-'); + public currentAttempt: number | undefined = $state(); + public scheduleTime: string | undefined = $state(); + public lastAttemptCompletedTime: string | undefined = $state(); + public nextAttemptScheduleTime: string | undefined = $state(); + public startToCloseTimeout: string = $state(); + public scheduleToCloseTimeout: string = $state(); + public scheduleToStartTimeout: string = $state(); + public heartbeatTimeout: string = $state(); + public closeTime: string | undefined = $state(); + public runState: ActivityExecutionRunState | undefined = $state(); + public now = $state(SvelteDate.now()); + + private scheduleToCloseTimeoutSeconds: number = $derived( + parseInt(fromDurationToNumber(this.scheduleToCloseTimeout), 10), + ); + + private startToCloseTimeoutSeconds: number = $derived( + parseInt(fromDurationToNumber(this.startToCloseTimeout), 10), + ); + + public running = $derived( + this.activityExecution?.info?.status === + 'ACTIVITY_EXECUTION_STATUS_RUNNING', + ); + + public schedule = $derived( + this.buildSchedule(this.activityExecution?.info?.scheduleTime), + ); + + public upcomingAttempts = $derived( + this.running + ? this.buildUpcomingAttempts( + this.activityExecution?.info?.lastAttemptCompleteTime, + this.currentAttempt, + ) + : [], + ); + + public attemptsRemaining = $derived( + this.maximumAttempts - this.currentAttempt, + ); + + public secondsRemaining = $derived.by(() => { + if (this.maximumAttempts) { + let total = 0; + for (let a = this.currentAttempt; a < this.maximumAttempts; a++) { + total += this.intervalForAttempt(a); + + total += Math.max( + this.scheduleToCloseTimeoutSeconds, + this.startToCloseTimeoutSeconds, + ); + } + + return total; + } + }); + + public nextRetrySecondsLeft = $derived.by(() => { + if (!this.nextAttemptScheduleTime) return 0; + + return formatSecondsAbbreviated( + Math.max( + 0, + (new SvelteDate(this.nextAttemptScheduleTime).getTime() - this.now) / + 1000, + ), + false, + ); + }); + + public deadlineTime = $derived.by(() => { + if (!this.secondsRemaining) return undefined; + + return new SvelteDate( + new SvelteDate(this.lastAttemptCompletedTime).getTime() + + this.secondsRemaining * 1000, + ); + }); + + public startToCloseSecondsLeft = $derived.by(() => { + if (this.startToCloseTimeoutSeconds === 0) return; + + const startToCloseTimeoutMillis = this.startToCloseTimeoutSeconds * 1000; + + const endTime = new SvelteDate( + new SvelteDate(this.activityExecution.info.lastStartedTime).getTime() + + startToCloseTimeoutMillis, + ); + + return Math.max(0, (new SvelteDate(endTime).getTime() - this.now) / 1000); + }); + + public scheduleToCloseSecondsLeft = $derived.by(() => { + if (this.scheduleToCloseTimeoutSeconds === 0) return; + + const scheduleToCloseTimeoutMillis = + this.scheduleToCloseTimeoutSeconds * 1000; + + const endTime = new SvelteDate( + new SvelteDate(this.activityExecution.info.scheduleTime).getTime() + + scheduleToCloseTimeoutMillis, + ); + + return Math.max(0, (new SvelteDate(endTime).getTime() - this.now) / 1000); + }); + + constructor(activityExecution: ActivityExecution | undefined) { + this.activityExecution = activityExecution; + this.currentAttempt = activityExecution?.info?.attempt; + this.initialInterval = parseInt( + fromDurationToNumber( + activityExecution?.info?.retryPolicy.initialInterval, + ), + 10, + ); + this.maximumInterval = parseInt( + fromDurationToNumber( + activityExecution?.info?.retryPolicy.maximumInterval, + ), + 10, + ); + this.maximumAttempts = activityExecution?.info?.retryPolicy.maximumAttempts; + this.backoffCoefficient = + activityExecution?.info?.retryPolicy.backoffCoefficient; + this.scheduleTime = activityExecution?.info?.scheduleTime; + this.currentInterval = formatSecondsAbbreviated( + fromDurationToNumber(activityExecution?.info?.currentRetryInterval), + false, + ); + this.lastAttemptCompletedTime = + activityExecution?.info?.lastAttemptCompleteTime; + this.nextAttemptScheduleTime = + activityExecution?.info?.nextAttemptScheduleTime; + this.closeTime = activityExecution?.info?.closeTime; + this.scheduleToCloseTimeout = + activityExecution?.info.scheduleToCloseTimeout; + this.startToCloseTimeout = activityExecution?.info?.startToCloseTimeout; + this.scheduleToStartTimeout = + activityExecution?.info?.scheduleToStartTimeout; + this.heartbeatTimeout = activityExecution?.info?.heartbeatTimeout; + this.runState = activityExecution?.info?.runState; + } + + public intervalForAttempt(attempt: number): number { + return Math.min( + this.initialInterval * Math.pow(this.backoffCoefficient, attempt - 1), + this.maximumInterval, + ); + } + + private buildUpcomingAttempts( + lastAttemptCompletedTime: string | undefined, + currentAttempt: number, + ): ScheduleEntry[] { + if (!lastAttemptCompletedTime) return []; + const entries: ScheduleEntry[] = []; + let offset = 0; + + const totalAttempts = this.maximumAttempts ?? MAX_ATTEMPTS + currentAttempt; + + for (let attempt = currentAttempt; attempt <= totalAttempts; attempt += 1) { + const runAt = new SvelteDate(lastAttemptCompletedTime); + const waitSeconds = this.intervalForAttempt(attempt); + runAt.setSeconds(runAt.getSeconds() + offset); + + entries.push({ + attempt, + waitSeconds, + runAt, + }); + + offset += waitSeconds; + } + + return entries; + } + + private buildSchedule(scheduleTime: string | undefined): ScheduleEntry[] { + if (!scheduleTime) return []; + const entries: ScheduleEntry[] = []; + let offset = 0; + const totalAttempts = this.maximumAttempts ?? MAX_ATTEMPTS; + + for (let attempt = 1; attempt <= totalAttempts; attempt += 1) { + const runAt = new SvelteDate(scheduleTime); + const waitSeconds = this.intervalForAttempt(attempt); + + runAt.setSeconds(runAt.getSeconds() + offset); + + if (attempt <= MAX_ATTEMPTS) { + entries.push({ + attempt, + waitSeconds, + runAt, + }); + } + + offset += waitSeconds; + } + + return entries; + } +} diff --git a/src/lib/pages/standalone-activity.test.ts b/src/lib/pages/standalone-activity.test.ts new file mode 100644 index 0000000000..9cce80d7f7 --- /dev/null +++ b/src/lib/pages/standalone-activity.test.ts @@ -0,0 +1,112 @@ +import * as SvelteReactivity from 'svelte/reactivity'; + +import { describe, expect, test, vi } from 'vitest'; + +import type { ActivityExecution } from '$lib/types/activity-execution'; + +import { StandaloneActivity } from './standalone-activity.svelte'; + +const MOCK_RETRYING_ACTIVITY: ActivityExecution = { + runId: '019d2ad0-26a1-72f9-8295-d4d82cb56c22', + info: { + activityId: '91246338-45c8-4f5b-9f8e-dd5aa9c8e450', + runId: '019d2ad0-26a1-72f9-8295-d4d82cb56c22', + activityType: { + name: 'LongRunningActivity', + }, + status: 'ACTIVITY_EXECUTION_STATUS_RUNNING', + runState: 'PENDING_ACTIVITY_STATE_SCHEDULED', + taskQueue: 'session-failure', + scheduleToCloseTimeout: '0s', + scheduleToStartTimeout: '0s', + startToCloseTimeout: '10s', + heartbeatTimeout: '0s', + retryPolicy: { + initialInterval: '1s', + backoffCoefficient: 2, + maximumInterval: '100s', + maximumAttempts: 60, + }, + lastStartedTime: '2026-01-01T00:01:00.000000Z', + attempt: 10, + scheduleTime: '2026-01-01T00:00:00.000000Z', + lastFailure: {}, + lastWorkerIdentity: '34096@MacBookPro.localdomain@', + currentRetryInterval: '100s', + lastAttemptCompleteTime: '2026-01-01T00:01:05.000000Z', + nextAttemptScheduleTime: '2026-01-01T00:01:40.000000Z', + stateTransitionCount: '33', + searchAttributes: {}, + userMetadata: {}, + }, + longPollToken: '', +}; + +vi.mock('svelte/reactivity', async (importOriginal) => { + const original = (await importOriginal()) as typeof SvelteReactivity; + const date = new Date(2026, 0, 1, 0, 0, 0, 0); + + class MockSvelteDate { + static now() { + return date.getTime(); + } + + getSeconds() { + return date.getSeconds(); + } + + setSeconds(seconds: number) { + date.setSeconds(seconds); + } + + getTime() { + return date.getTime(); + } + + constructor(...args: ConstructorParameters) { + return new Date(...args); + } + } + + return { + ...original, + SvelteDate: MockSvelteDate, + }; +}); + +describe('Standalone Activity Page Class', () => { + test('derived properties are set for a retrying activity', () => { + const activity = new StandaloneActivity(MOCK_RETRYING_ACTIVITY); + + expect(activity.attemptsRemaining).toEqual(50); + expect(activity.running).toBe(true); + expect(activity.upcomingAttempts.length).toBe(51); // includes current attempt + expect(activity.secondsRemaining).toBe(5500); + expect(activity.nextRetrySecondsLeft).toBe('1m 40s'); + expect(activity.deadlineTime.toISOString()).toBe( + '2026-01-01T01:32:45.000Z', + ); + expect(activity.startToCloseSecondsLeft).toBe(70); + expect(activity.scheduleToCloseSecondsLeft).toBe(undefined); + }); + + const cases = [ + [1, 1], + [2, 2], + [3, 4], + [4, 8], + [5, 16], + [6, 32], + [7, 64], + [8, 100], + ]; + + test.each(cases)( + 'intervalForAttempt %i is %i seconds', + (attempt, interval) => { + const activity = new StandaloneActivity(MOCK_RETRYING_ACTIVITY); + + expect(activity.intervalForAttempt(attempt)).toBe(interval); + }, + ); +}); diff --git a/src/lib/services/standalone-activities.ts b/src/lib/services/standalone-activities.ts index fe40d39d4d..70598d53db 100644 --- a/src/lib/services/standalone-activities.ts +++ b/src/lib/services/standalone-activities.ts @@ -149,6 +149,9 @@ const toStartActivityExecutionRequest = async ( ...(activityFormData.scheduleToStartTimeout && { scheduleToStartTimeout: activityFormData.scheduleToStartTimeout, }), + ...(activityFormData.heartbeatTimeout && { + heartbeatTimeout: activityFormData.heartbeatTimeout, + }), retryPolicy: { ...(activityFormData.initialInterval && { initialInterval: activityFormData.initialInterval, diff --git a/src/lib/theme/plugin.ts b/src/lib/theme/plugin.ts index 9097c73524..e232498a16 100644 --- a/src/lib/theme/plugin.ts +++ b/src/lib/theme/plugin.ts @@ -243,6 +243,13 @@ const temporal = plugin( danger: css('--color-text-danger'), }), + fill: ({ theme }) => ({ + ...theme('colors'), + + secondary: css('--color-surface-secondary'), + brand: css('--color-surface-brand'), + subtle: css('--color-surface-subtle'), + }), extend: { transitionProperty: { width: 'width', diff --git a/src/lib/types/activity-execution.ts b/src/lib/types/activity-execution.ts index 25433c54cb..100fc049c0 100644 --- a/src/lib/types/activity-execution.ts +++ b/src/lib/types/activity-execution.ts @@ -21,6 +21,14 @@ export type ActivityExecutionStatus = | 'ACTIVITY_EXECUTION_STATUS_TERMINATED' | 'ACTIVITY_EXECUTION_STATUS_TIMED_OUT'; +export type ActivityExecutionRunState = + | 'PENDING_ACTIVITY_STATE_UNSPECIFIED' + | 'PENDING_ACTIVITY_STATE_SCHEDULED' + | 'PENDING_ACTIVITY_STATE_STARTED' + | 'PENDING_ACTIVITY_STATE_CANCEL_REQUESTED' + | 'PENDING_ACTIVITY_STATE_PAUSED' + | 'PENDING_ACTIVITY_STATE_PAUSE_REQUESTED'; + export const ACTIVITY_ID_REUSE_POLICIES = [ 'ACTIVITY_ID_REUSE_POLICY_UNSPECIFIED', 'ACTIVITY_ID_REUSE_POLICY_ALLOW_DUPLICATE', @@ -54,7 +62,7 @@ export type ActivityExecutionOutcome = failure: Failure; }; -interface RetryPolicy { +export interface RetryPolicy { initialInterval: string; backoffCoefficient: number; maximumInterval: string; @@ -66,12 +74,12 @@ export interface ActivityExecutionInfo { runId: string; activityType: ActivityType; status: ActivityExecutionStatus; - runState?: string; // only for running activities + runState?: ActivityExecutionRunState; // only for running activities taskQueue: string; scheduleToCloseTimeout: string; scheduleToStartTimeout: string; startToCloseTimeout: string; - lastHeartbeatTime: string; + lastHeartbeatTime?: string; heartbeatDetails?: Payloads; heartbeatTimeout: string; retryPolicy: RetryPolicy; @@ -79,7 +87,7 @@ export interface ActivityExecutionInfo { attempt: number; executionDuration?: string; scheduleTime: string; - closeTime: string; + closeTime?: string; lastWorkerIdentity: string; lastAttemptCompleteTime: string; nextAttemptScheduleTime: string; @@ -110,6 +118,7 @@ export interface StartActivityExecutionRequest { startToCloseTimeout: string; scheduleToCloseTimeout: string; scheduleToStartTimeout: string; + heartbeatTimeout?: string; input?: Payloads; userMetadata?: UserMetadata; retryPolicy?: RetryPolicy; diff --git a/src/lib/utilities/format-time.ts b/src/lib/utilities/format-time.ts index 6ad3e5ba9e..b81415af35 100644 --- a/src/lib/utilities/format-time.ts +++ b/src/lib/utilities/format-time.ts @@ -250,10 +250,13 @@ export const getTimestampDifference = ( return Math.abs(parse1 - parse2); }; -export const formatSecondsAbbreviated = (seconds: number | string): string => { +export const formatSecondsAbbreviated = ( + seconds: number | string, + includeMilliseconds = true, +): string => { const start = new Date(); const end = new Date(start.getTime() + Number(seconds) * 1000); - return formatDistanceAbbreviated({ start, end, includeMilliseconds: true }); + return formatDistanceAbbreviated({ start, end, includeMilliseconds }); }; export const fromDurationToNumber = (duration: string): string => { @@ -261,10 +264,10 @@ export const fromDurationToNumber = (duration: string): string => { return ''; } - return duration?.replace('s', ''); + return duration.replace('s', ''); }; -export const fromNumberToDuration = (duration: string): string => { +export const fromNumberToDuration = (duration: string | number): string => { if (!duration) return undefined; - return duration + 's'; + return `${duration}s`; }; diff --git a/src/lib/utilities/to-duration.ts b/src/lib/utilities/to-duration.ts index 297dc84cb1..46250416ff 100644 --- a/src/lib/utilities/to-duration.ts +++ b/src/lib/utilities/to-duration.ts @@ -6,6 +6,7 @@ import { sub, } from 'date-fns'; +import { fromNumberToDuration } from './format-time'; import { isObject, isString } from './is'; import { pluralize } from './pluralize'; @@ -128,15 +129,18 @@ export const fromDate = ( }; export const fromSeconds = ( - seconds: string, + seconds: string | number, { delimiter = ', ' } = {}, ): string => { if (!seconds) return ''; - const parsedSeconds = parseInt(seconds); - const parsedDecimal = parseFloat(`.${seconds.split('.')[1] ?? 0}`); + const parsedSeconds = + typeof seconds === 'number' ? seconds : parseInt(seconds); + const stringSeconds = + typeof seconds === 'number' ? fromNumberToDuration(seconds) : seconds; + const parsedDecimal = parseFloat(`.${stringSeconds.split('.')[1] ?? 0}`); - if (!seconds.endsWith('s')) return ''; + if (!stringSeconds.endsWith('s')) return ''; if (isNaN(parsedSeconds) || isNaN(parsedDecimal)) return ''; const start = new Date(Date.UTC(0, 0, 0, 0, 0, 0)); diff --git a/src/routes/(app)/namespaces/[namespace]/activities/[activityId]/[runId]/details/+page.svelte b/src/routes/(app)/namespaces/[namespace]/activities/[activityId]/[runId]/details/+page.svelte index e807d38b01..521072f8f7 100644 --- a/src/routes/(app)/namespaces/[namespace]/activities/[activityId]/[runId]/details/+page.svelte +++ b/src/routes/(app)/namespaces/[namespace]/activities/[activityId]/[runId]/details/+page.svelte @@ -4,7 +4,6 @@ import PageTitle from '$lib/components/page-title.svelte'; import StandaloneActivityDetails from '$lib/pages/standalone-activity-details.svelte'; - const namespace = $derived(page.params.namespace); const activityId = $derived(page.params.activityId); @@ -13,4 +12,4 @@ url={page.url.href} /> - +