From 780d32913dc08112105144cafa59e9b5c2d43cbe Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 22 Sep 2025 11:10:52 +0200 Subject: [PATCH 01/11] Add field for expected duration Signed-off-by: Adhitya Mamallan --- .../get-single-event-group-from-events.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/views/workflow-history/helpers/get-history-group-from-events/get-single-event-group-from-events.ts b/src/views/workflow-history/helpers/get-history-group-from-events/get-single-event-group-from-events.ts index 971b20b56..461e3f249 100644 --- a/src/views/workflow-history/helpers/get-history-group-from-events/get-single-event-group-from-events.ts +++ b/src/views/workflow-history/helpers/get-history-group-from-events/get-single-event-group-from-events.ts @@ -140,6 +140,7 @@ export default function getSingleEventGroupFromEvents( undefined, eventToSummaryFields ), +<<<<<<< HEAD ...(expectedFirstDecisionScheduleTimeMs ? { expectedEndTimeInfo: { @@ -148,5 +149,8 @@ export default function getSingleEventGroupFromEvents( }, } : {}), +======= + expectedDurationMs, +>>>>>>> 1a09cd90 (Add field for expected duration) }; } From 20ff751e15eea33c4d2cdc0a884f797106c67f90 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 22 Sep 2025 11:38:28 +0200 Subject: [PATCH 02/11] Add tests for field for expected duration Signed-off-by: Adhitya Mamallan --- .../get-single-event-group-from-events.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/views/workflow-history/helpers/get-history-group-from-events/get-single-event-group-from-events.ts b/src/views/workflow-history/helpers/get-history-group-from-events/get-single-event-group-from-events.ts index 461e3f249..971b20b56 100644 --- a/src/views/workflow-history/helpers/get-history-group-from-events/get-single-event-group-from-events.ts +++ b/src/views/workflow-history/helpers/get-history-group-from-events/get-single-event-group-from-events.ts @@ -140,7 +140,6 @@ export default function getSingleEventGroupFromEvents( undefined, eventToSummaryFields ), -<<<<<<< HEAD ...(expectedFirstDecisionScheduleTimeMs ? { expectedEndTimeInfo: { @@ -149,8 +148,5 @@ export default function getSingleEventGroupFromEvents( }, } : {}), -======= - expectedDurationMs, ->>>>>>> 1a09cd90 (Add field for expected duration) }; } From 96b73dfab7d982a6f70ecb66328accacfff34e76 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 22 Sep 2025 15:41:55 +0200 Subject: [PATCH 03/11] Use wait timer info with label Signed-off-by: Adhitya Mamallan --- src/views/workflow-history/workflow-history.types.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/views/workflow-history/workflow-history.types.ts b/src/views/workflow-history/workflow-history.types.ts index b3737bb6a..0c1616b26 100644 --- a/src/views/workflow-history/workflow-history.types.ts +++ b/src/views/workflow-history/workflow-history.types.ts @@ -72,7 +72,11 @@ type BaseHistoryGroup = { timeMs: number | null; startTimeMs: number | null; closeTimeMs?: number | null; +<<<<<<< HEAD expectedEndTimeInfo?: { +======= + waitTimerInfo?: { +>>>>>>> e02feb3e (Use wait timer info with label) timeMs: number; prefix: string; }; From d8698b2368a94a970d8412f07374ac011efcf304 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Tue, 23 Sep 2025 16:51:40 +0200 Subject: [PATCH 04/11] Use absolute end time instead of duration Signed-off-by: Adhitya Mamallan --- .../get-single-event-group-from-events.ts | 8 +++++--- src/views/workflow-history/workflow-history.types.ts | 4 ---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/views/workflow-history/helpers/get-history-group-from-events/get-single-event-group-from-events.ts b/src/views/workflow-history/helpers/get-history-group-from-events/get-single-event-group-from-events.ts index 971b20b56..08d914411 100644 --- a/src/views/workflow-history/helpers/get-history-group-from-events/get-single-event-group-from-events.ts +++ b/src/views/workflow-history/helpers/get-history-group-from-events/get-single-event-group-from-events.ts @@ -116,9 +116,11 @@ export default function getSingleEventGroupFromEvents( event.attributes === 'workflowExecutionStartedEventAttributes' && event.workflowExecutionStartedEventAttributes?.firstDecisionTaskBackoff ) { - const firstDecisionTaskBackoffMs = parseGrpcTimestamp( - event.workflowExecutionStartedEventAttributes.firstDecisionTaskBackoff - ); + const firstDecisionTaskBackoffMs = + parseGrpcTimestamp(event.eventTime) + + parseGrpcTimestamp( + event.workflowExecutionStartedEventAttributes.firstDecisionTaskBackoff + ); if (firstDecisionTaskBackoffMs > 0) expectedFirstDecisionScheduleTimeMs = diff --git a/src/views/workflow-history/workflow-history.types.ts b/src/views/workflow-history/workflow-history.types.ts index 0c1616b26..b3737bb6a 100644 --- a/src/views/workflow-history/workflow-history.types.ts +++ b/src/views/workflow-history/workflow-history.types.ts @@ -72,11 +72,7 @@ type BaseHistoryGroup = { timeMs: number | null; startTimeMs: number | null; closeTimeMs?: number | null; -<<<<<<< HEAD expectedEndTimeInfo?: { -======= - waitTimerInfo?: { ->>>>>>> e02feb3e (Use wait timer info with label) timeMs: number; prefix: string; }; From 50a747511fefc4a46680c9db5277d9d48f7e8dc5 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Wed, 24 Sep 2025 12:42:27 +0200 Subject: [PATCH 05/11] Fix unit tests Signed-off-by: Adhitya Mamallan --- .../get-single-event-group-from-events.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/views/workflow-history/helpers/get-history-group-from-events/get-single-event-group-from-events.ts b/src/views/workflow-history/helpers/get-history-group-from-events/get-single-event-group-from-events.ts index 08d914411..971b20b56 100644 --- a/src/views/workflow-history/helpers/get-history-group-from-events/get-single-event-group-from-events.ts +++ b/src/views/workflow-history/helpers/get-history-group-from-events/get-single-event-group-from-events.ts @@ -116,11 +116,9 @@ export default function getSingleEventGroupFromEvents( event.attributes === 'workflowExecutionStartedEventAttributes' && event.workflowExecutionStartedEventAttributes?.firstDecisionTaskBackoff ) { - const firstDecisionTaskBackoffMs = - parseGrpcTimestamp(event.eventTime) + - parseGrpcTimestamp( - event.workflowExecutionStartedEventAttributes.firstDecisionTaskBackoff - ); + const firstDecisionTaskBackoffMs = parseGrpcTimestamp( + event.workflowExecutionStartedEventAttributes.firstDecisionTaskBackoff + ); if (firstDecisionTaskBackoffMs > 0) expectedFirstDecisionScheduleTimeMs = From 1792901508a35b7d4f9e9b7ecf614d762c82a297 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 22 Sep 2025 13:35:33 +0200 Subject: [PATCH 06/11] Add badge to show remaining time Signed-off-by: Adhitya Mamallan --- src/utils/data-formatters/format-duration.ts | 14 ++++- .../helpers/get-formatted-events-duration.ts | 7 +-- ...ow-history-events-duration-badge.styles.ts | 2 - .../get-formatted-remaining-duration.ts | 37 ++++++++++++ ...history-remaining-duration-badge.styles.ts | 23 ++++++++ ...kflow-history-remaining-duration-badge.tsx | 58 +++++++++++++++++++ ...-history-remaining-duration-badge.types.ts | 9 +++ .../workflow-history-timeline-group.tsx | 11 ++++ .../workflow-history-timeline-group.types.ts | 1 + 9 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/get-formatted-remaining-duration.ts create mode 100644 src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.styles.ts create mode 100644 src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.tsx create mode 100644 src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.types.ts diff --git a/src/utils/data-formatters/format-duration.ts b/src/utils/data-formatters/format-duration.ts index e75ed1ac6..775ff0418 100644 --- a/src/utils/data-formatters/format-duration.ts +++ b/src/utils/data-formatters/format-duration.ts @@ -3,7 +3,8 @@ import dayjs from '@/utils/datetime/dayjs'; const formatDuration = ( duration: Duration | null, - { separator = ', ' }: { separator?: string } = {} + { separator = ', ' }: { separator?: string } = {}, + minUnit: 'y' | 'M' | 'd' | 'h' | 'm' | 's' | 'ms' = 'ms' ) => { const defaultReturn = '0s'; if (!duration) { @@ -16,7 +17,16 @@ const formatDuration = ( const intMillis = Math.floor(nanosAsMillis); const remainingNanosAsMillis = nanosAsMillis % 1; const milliseconds = secondsAsMillis + intMillis; - const units = ['y', 'M', 'd', 'h', 'm', 's', 'ms'] as const; + const allUnits: Array = [ + 'y', + 'M', + 'd', + 'h', + 'm', + 's', + 'ms', + ]; + const units = allUnits.slice(0, allUnits.indexOf(minUnit) + 1); const values: Partial> = {}; let d = dayjs.duration(milliseconds); units.forEach((unit) => { diff --git a/src/views/workflow-history/workflow-history-events-duration-badge/helpers/get-formatted-events-duration.ts b/src/views/workflow-history/workflow-history-events-duration-badge/helpers/get-formatted-events-duration.ts index 2a54342f2..44aa44413 100644 --- a/src/views/workflow-history/workflow-history-events-duration-badge/helpers/get-formatted-events-duration.ts +++ b/src/views/workflow-history/workflow-history-events-duration-badge/helpers/get-formatted-events-duration.ts @@ -17,12 +17,9 @@ export default function getFormattedEventsDuration( seconds: seconds.toString(), nanos: (durationObj.asMilliseconds() - seconds * 1000) * 1000000, }, - { separator: ' ' } + { separator: ' ' }, + hideMs ? 's' : 'ms' ); - // TODO: add this functionality to formatDuration in more reusable way - if (hideMs && seconds > 0) { - return duration.replace(/ \d+ms/i, ''); - } return duration; } diff --git a/src/views/workflow-history/workflow-history-events-duration-badge/workflow-history-events-duration-badge.styles.ts b/src/views/workflow-history/workflow-history-events-duration-badge/workflow-history-events-duration-badge.styles.ts index ae8e238ce..4cd935fc7 100644 --- a/src/views/workflow-history/workflow-history-events-duration-badge/workflow-history-events-duration-badge.styles.ts +++ b/src/views/workflow-history/workflow-history-events-duration-badge/workflow-history-events-duration-badge.styles.ts @@ -1,8 +1,6 @@ import { type BadgeOverrides } from 'baseui/badge/types'; import { type Theme } from 'baseui/theme'; -import themeLight from '@/config/theme/theme-light.config'; - export const overrides = { Badge: { Badge: { diff --git a/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/get-formatted-remaining-duration.ts b/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/get-formatted-remaining-duration.ts new file mode 100644 index 000000000..33797a27f --- /dev/null +++ b/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/get-formatted-remaining-duration.ts @@ -0,0 +1,37 @@ +import formatDuration from '@/utils/data-formatters/format-duration'; +import dayjs from '@/utils/datetime/dayjs'; + +export default function getFormattedRemainingDuration( + startTime: Date | string | number, + expectedDurationMs: number +): string | null { + const start = dayjs(startTime); + const now = dayjs(); + const expectedEnd = start.add(expectedDurationMs, 'milliseconds'); + + // If we've already passed the expected end time, return null + if (now.isAfter(expectedEnd)) { + return null; + } + + // Calculate remaining time + const remaining = expectedEnd.diff(now); + const remainingDuration = dayjs.duration(remaining); + const seconds = Math.ceil(remainingDuration.asSeconds()); + + // Don't show if less than 1 second remaining + if (seconds < 1) { + return null; + } + + const duration = formatDuration( + { + seconds: seconds.toString(), + nanos: 0, + }, + { separator: ' ' }, + 's' + ); + + return duration; +} diff --git a/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.styles.ts b/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.styles.ts new file mode 100644 index 000000000..0d8210dd9 --- /dev/null +++ b/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.styles.ts @@ -0,0 +1,23 @@ +import { type BadgeOverrides } from 'baseui/badge/types'; +import { type Theme } from 'baseui/theme'; + +export const overrides = { + badge: { + Badge: { + style: ({ + $theme, + $hierarchy, + }: { + $theme: Theme; + $hierarchy: string; + }) => ({ + ...$theme.typography.LabelXSmall, + ...($hierarchy === 'secondary' + ? { + color: $theme.colors.contentSecondary, + } + : null), + }), + }, + } satisfies BadgeOverrides, +}; diff --git a/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.tsx b/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.tsx new file mode 100644 index 000000000..e1d05a971 --- /dev/null +++ b/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.tsx @@ -0,0 +1,58 @@ +import React, { useEffect, useState } from 'react'; + +import { Badge } from 'baseui/badge'; + +import getFormattedRemainingDuration from './helpers/get-formatted-remaining-duration'; +import { overrides } from './workflow-history-remaining-duration-badge.styles'; +import { type Props } from './workflow-history-remaining-duration-badge.types'; + +export default function WorkflowHistoryRemainingDurationBadge({ + startTime, + expectedGroupDuration, + workflowIsArchived, + workflowCloseStatus, + loadingMoreEvents, +}: Props) { + const workflowEnded = + workflowIsArchived || + workflowCloseStatus !== 'WORKFLOW_EXECUTION_CLOSE_STATUS_INVALID'; + + const shouldHide = loadingMoreEvents || workflowEnded; + + const [remainingDuration, setRemainingDuration] = useState( + null + ); + + useEffect(() => { + if (shouldHide) { + setRemainingDuration(null); + return; + } + + const updateRemainingDuration = () => { + setRemainingDuration( + getFormattedRemainingDuration(startTime, expectedGroupDuration) + ); + }; + + updateRemainingDuration(); + + const interval = setInterval(updateRemainingDuration, 1000); + + return () => clearInterval(interval); + }, [startTime, expectedGroupDuration, workflowIsArchived, shouldHide]); + + if (shouldHide || !remainingDuration) { + return null; + } + + return ( + + ); +} diff --git a/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.types.ts b/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.types.ts new file mode 100644 index 000000000..d818abea1 --- /dev/null +++ b/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.types.ts @@ -0,0 +1,9 @@ +import { type WorkflowExecutionCloseStatus } from '@/__generated__/proto-ts/uber/cadence/api/v1/WorkflowExecutionCloseStatus'; + +export type Props = { + startTime: Date | string | number; + expectedGroupDuration: number; + workflowIsArchived: boolean; + workflowCloseStatus: WorkflowExecutionCloseStatus | null | undefined; + loadingMoreEvents: boolean; +}; diff --git a/src/views/workflow-history/workflow-history-timeline-group/workflow-history-timeline-group.tsx b/src/views/workflow-history/workflow-history-timeline-group/workflow-history-timeline-group.tsx index 53cf22753..d5d963813 100644 --- a/src/views/workflow-history/workflow-history-timeline-group/workflow-history-timeline-group.tsx +++ b/src/views/workflow-history/workflow-history-timeline-group/workflow-history-timeline-group.tsx @@ -9,6 +9,7 @@ import WorkflowHistoryEventStatusBadge from '../workflow-history-event-status-ba import WorkflowHistoryEventsCard from '../workflow-history-events-card/workflow-history-events-card'; import WorkflowHistoryEventsDurationBadge from '../workflow-history-events-duration-badge/workflow-history-events-duration-badge'; import WorkflowHistoryGroupLabel from '../workflow-history-group-label/workflow-history-group-label'; +import WorkflowHistoryRemainingDurationBadge from '../workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge'; import WorkflowHistoryTimelineResetButton from '../workflow-history-timeline-reset-button/workflow-history-timeline-reset-button'; import { @@ -25,6 +26,7 @@ export default function WorkflowHistoryTimelineGroup({ timeLabel, startTimeMs, closeTimeMs, + expectedDurationMs, workflowCloseTimeMs, workflowCloseStatus, workflowIsArchived, @@ -88,6 +90,15 @@ export default function WorkflowHistoryTimelineGroup({ workflowCloseStatus={workflowCloseStatus} /> )} + {expectedDurationMs && startTimeMs ? ( + + ) : null}
{timeLabel}
diff --git a/src/views/workflow-history/workflow-history-timeline-group/workflow-history-timeline-group.types.ts b/src/views/workflow-history/workflow-history-timeline-group/workflow-history-timeline-group.types.ts index 7b0149d10..b21b79905 100644 --- a/src/views/workflow-history/workflow-history-timeline-group/workflow-history-timeline-group.types.ts +++ b/src/views/workflow-history/workflow-history-timeline-group/workflow-history-timeline-group.types.ts @@ -21,6 +21,7 @@ export type Props = Pick< | 'resetToDecisionEventId' | 'startTimeMs' | 'closeTimeMs' + | 'expectedDurationMs' | 'shortLabel' > & { isLastEvent: boolean; From 7ab39f8015dcdb48c26c731f06789644294ca12e Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 22 Sep 2025 14:39:56 +0200 Subject: [PATCH 07/11] Add tests Signed-off-by: Adhitya Mamallan --- .../get-formatted-events-duration.test.ts | 3 +- .../helpers/get-formatted-events-duration.ts | 2 +- ...-history-remaining-duration-badge.test.tsx | 181 ++++++++++++++++++ .../get-formatted-remaining-duration.test.ts | 91 +++++++++ .../get-formatted-remaining-duration.ts | 9 +- 5 files changed, 278 insertions(+), 8 deletions(-) create mode 100644 src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx create mode 100644 src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/__tests__/get-formatted-remaining-duration.test.ts diff --git a/src/views/workflow-history/workflow-history-events-duration-badge/helpers/__tests__/get-formatted-events-duration.test.ts b/src/views/workflow-history/workflow-history-events-duration-badge/helpers/__tests__/get-formatted-events-duration.test.ts index 11ebe2e9e..4450e9115 100644 --- a/src/views/workflow-history/workflow-history-events-duration-badge/helpers/__tests__/get-formatted-events-duration.test.ts +++ b/src/views/workflow-history/workflow-history-events-duration-badge/helpers/__tests__/get-formatted-events-duration.test.ts @@ -3,7 +3,8 @@ import getFormattedEventsDuration from '../get-formatted-events-duration'; jest.mock('@/utils/data-formatters/format-duration', () => ({ __esModule: true, default: jest.fn( - (duration) => `mocked: ${duration.seconds}s ${duration.nanos / 1000000}ms` + (duration, _, unit) => + `mocked: ${duration.seconds}s${unit === 'ms' ? ` ${duration.nanos / 1000000}ms` : ''}` ), })); diff --git a/src/views/workflow-history/workflow-history-events-duration-badge/helpers/get-formatted-events-duration.ts b/src/views/workflow-history/workflow-history-events-duration-badge/helpers/get-formatted-events-duration.ts index 44aa44413..8a7b40b05 100644 --- a/src/views/workflow-history/workflow-history-events-duration-badge/helpers/get-formatted-events-duration.ts +++ b/src/views/workflow-history/workflow-history-events-duration-badge/helpers/get-formatted-events-duration.ts @@ -18,7 +18,7 @@ export default function getFormattedEventsDuration( nanos: (durationObj.asMilliseconds() - seconds * 1000) * 1000000, }, { separator: ' ' }, - hideMs ? 's' : 'ms' + hideMs && seconds > 0 ? 's' : 'ms' ); return duration; diff --git a/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx b/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx new file mode 100644 index 000000000..25c6bb7f4 --- /dev/null +++ b/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx @@ -0,0 +1,181 @@ +import React from 'react'; + +import { render, screen, act } from '@/test-utils/rtl'; + +import { WorkflowExecutionCloseStatus } from '@/__generated__/proto-ts/uber/cadence/api/v1/WorkflowExecutionCloseStatus'; + +import getFormattedRemainingDuration from '../helpers/get-formatted-remaining-duration'; +import WorkflowHistoryRemainingDurationBadge from '../workflow-history-remaining-duration-badge'; +import type { Props } from '../workflow-history-remaining-duration-badge.types'; + +jest.mock('../helpers/get-formatted-remaining-duration', () => jest.fn()); + +const mockStartTime = new Date('2024-01-01T10:00:00Z'); +const mockNow = new Date('2024-01-01T10:02:00Z'); + +const mockGetFormattedRemainingDuration = + getFormattedRemainingDuration as jest.MockedFunction< + typeof getFormattedRemainingDuration + >; + +describe('WorkflowHistoryRemainingDurationBadge', () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(mockNow); + mockGetFormattedRemainingDuration.mockReturnValue('5m 30s'); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.useRealTimers(); + }); + + it('renders remaining duration badge when duration is available', () => { + setup(); + + expect(screen.getByText('Remaining: 5m 30s')).toBeInTheDocument(); + }); + + it('does not render badge when loading more events', () => { + setup({ + loadingMoreEvents: true, + }); + + expect(screen.queryByText(/Remaining:/)).not.toBeInTheDocument(); + }); + + it('does not render badge when workflow is archived', () => { + setup({ + workflowIsArchived: true, + }); + + expect(screen.queryByText(/Remaining:/)).not.toBeInTheDocument(); + }); + + it('does not render badge when workflow has close status', () => { + const closeStatuses = [ + WorkflowExecutionCloseStatus.WORKFLOW_EXECUTION_CLOSE_STATUS_COMPLETED, + WorkflowExecutionCloseStatus.WORKFLOW_EXECUTION_CLOSE_STATUS_FAILED, + WorkflowExecutionCloseStatus.WORKFLOW_EXECUTION_CLOSE_STATUS_CANCELED, + WorkflowExecutionCloseStatus.WORKFLOW_EXECUTION_CLOSE_STATUS_TERMINATED, + WorkflowExecutionCloseStatus.WORKFLOW_EXECUTION_CLOSE_STATUS_CONTINUED_AS_NEW, + WorkflowExecutionCloseStatus.WORKFLOW_EXECUTION_CLOSE_STATUS_TIMED_OUT, + ]; + + closeStatuses.forEach((status) => { + const { unmount } = setup({ + workflowCloseStatus: status, + }); + + expect(screen.queryByText(/Remaining:/)).not.toBeInTheDocument(); + unmount(); + }); + }); + + it('does not render badge when helper returns null', () => { + mockGetFormattedRemainingDuration.mockReturnValue(null); + + setup(); + + expect(screen.queryByText(/Remaining:/)).not.toBeInTheDocument(); + }); + + it('updates remaining duration every second', () => { + setup(); + + expect(screen.getByText('Remaining: 5m 30s')).toBeInTheDocument(); + + // Mock different return values for subsequent calls + mockGetFormattedRemainingDuration.mockReturnValueOnce('5m 29s'); + + act(() => { + jest.advanceTimersByTime(1000); + }); + + expect(screen.getByText('Remaining: 5m 29s')).toBeInTheDocument(); + + mockGetFormattedRemainingDuration.mockReturnValueOnce('5m 28s'); + + act(() => { + jest.advanceTimersByTime(1000); + }); + + expect(screen.getByText('Remaining: 5m 28s')).toBeInTheDocument(); + + // Verify the helper was called the expected number of times + // Initial call + 2 interval updates = 3 calls + expect(mockGetFormattedRemainingDuration).toHaveBeenCalledTimes(3); + }); + + it('hides badge when duration becomes null during countdown', () => { + setup(); + + expect(screen.getByText('Remaining: 5m 30s')).toBeInTheDocument(); + + // Mock helper to return null (indicating overrun) + mockGetFormattedRemainingDuration.mockReturnValue(null); + + act(() => { + jest.advanceTimersByTime(1000); + }); + + expect(screen.queryByText(/Remaining:/)).not.toBeInTheDocument(); + }); + + it('cleans up interval when component unmounts', () => { + const { unmount } = setup(); + + const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); + unmount(); + + expect(clearIntervalSpy).toHaveBeenCalled(); + }); + + it('does not set up interval when shouldHide is true', () => { + const setIntervalSpy = jest.spyOn(global, 'setInterval'); + + setup({ + workflowIsArchived: true, + }); + + expect(setIntervalSpy).not.toHaveBeenCalled(); + }); + + it('clears existing interval when shouldHide becomes true', () => { + const { rerender } = setup(); + + expect(screen.getByText('Remaining: 5m 30s')).toBeInTheDocument(); + + rerender( + + ); + + expect(screen.queryByText(/Remaining:/)).not.toBeInTheDocument(); + }); +}); + +function setup({ + startTime = mockStartTime, + expectedGroupDuration = 5 * 60 * 1000, + workflowIsArchived = false, + workflowCloseStatus = WorkflowExecutionCloseStatus.WORKFLOW_EXECUTION_CLOSE_STATUS_INVALID, + loadingMoreEvents = false, +}: Partial = {}) { + return render( + + ); +} diff --git a/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/__tests__/get-formatted-remaining-duration.test.ts b/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/__tests__/get-formatted-remaining-duration.test.ts new file mode 100644 index 000000000..7534c4129 --- /dev/null +++ b/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/__tests__/get-formatted-remaining-duration.test.ts @@ -0,0 +1,91 @@ +import getFormattedRemainingDuration from '../get-formatted-remaining-duration'; + +jest.mock('@/utils/data-formatters/format-duration', () => ({ + __esModule: true, + default: jest.fn((duration) => `mocked: ${duration.seconds}s`), +})); + +const mockNow = new Date('2024-01-01T10:02:00Z'); + +describe('getFormattedRemainingDuration', () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(mockNow); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should return null when expected duration has passed', () => { + const startTime = new Date('2024-01-01T10:00:00Z'); // 2 minutes ago + const expectedDurationMs = 60 * 1000; // 1 minute expected duration + + const result = getFormattedRemainingDuration(startTime, expectedDurationMs); + + expect(result).toBeNull(); + }); + + it('should return null when expected duration exactly matches current time', () => { + const startTime = new Date('2024-01-01T10:00:00Z'); // 2 minutes ago + const expectedDurationMs = 2 * 60 * 1000; // 2 minutes expected duration + + const result = getFormattedRemainingDuration(startTime, expectedDurationMs); + + expect(result).toBeNull(); + }); + + it('should return remaining time when duration has not passed', () => { + const startTime = new Date('2024-01-01T10:00:00Z'); // 2 minutes ago + const expectedDurationMs = 5 * 60 * 1000; // 5 minutes expected duration (3 minutes remaining) + + const result = getFormattedRemainingDuration(startTime, expectedDurationMs); + + expect(result).toEqual('mocked: 180s'); // 3 minutes = 180 seconds + }); + + it('should return 1s when less than 1 second remaining', () => { + const startTime = new Date('2024-01-01T10:01:59.500Z'); // 0.5 seconds ago + const expectedDurationMs = 1000; // 1 second expected duration (0.5 seconds remaining) + + const result = getFormattedRemainingDuration(startTime, expectedDurationMs); + + expect(result).toEqual('mocked: 1s'); + }); + + it('should handle string start times', () => { + const startTime = '2024-01-01T10:00:00Z'; // 2 minutes ago + const expectedDurationMs = 5 * 60 * 1000; // 5 minutes expected duration + + const result = getFormattedRemainingDuration(startTime, expectedDurationMs); + + expect(result).toEqual('mocked: 180s'); + }); + + it('should handle numeric start times', () => { + const startTime = new Date('2024-01-01T10:00:00Z').getTime(); // 2 minutes ago + const expectedDurationMs = 5 * 60 * 1000; // 5 minutes expected duration + + const result = getFormattedRemainingDuration(startTime, expectedDurationMs); + + expect(result).toEqual('mocked: 180s'); + }); + + it('should round up partial seconds using Math.ceil', () => { + const startTime = new Date('2024-01-01T10:01:58.700Z'); // 1.3 seconds ago + const expectedDurationMs = 3000; // 3 seconds expected duration (1.3 seconds remaining) + + const result = getFormattedRemainingDuration(startTime, expectedDurationMs); + + expect(result).toEqual('mocked: 2s'); // Math.ceil(1.3) = 2 + }); + + it('should handle exactly 1 second remaining', () => { + const startTime = new Date('2024-01-01T10:01:59Z'); // 1 second ago + const expectedDurationMs = 2000; // 2 seconds expected duration (1 second remaining) + + const result = getFormattedRemainingDuration(startTime, expectedDurationMs); + + expect(result).toEqual('mocked: 1s'); + }); +}); diff --git a/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/get-formatted-remaining-duration.ts b/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/get-formatted-remaining-duration.ts index 33797a27f..7be7f45e9 100644 --- a/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/get-formatted-remaining-duration.ts +++ b/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/get-formatted-remaining-duration.ts @@ -9,17 +9,14 @@ export default function getFormattedRemainingDuration( const now = dayjs(); const expectedEnd = start.add(expectedDurationMs, 'milliseconds'); - // If we've already passed the expected end time, return null if (now.isAfter(expectedEnd)) { return null; } - // Calculate remaining time - const remaining = expectedEnd.diff(now); - const remainingDuration = dayjs.duration(remaining); - const seconds = Math.ceil(remainingDuration.asSeconds()); + const remainingDurationMs = expectedEnd.diff(now); - // Don't show if less than 1 second remaining + // Round up, to compensate for the rounding-down in the events duration badge + const seconds = Math.ceil(remainingDurationMs / 1000); if (seconds < 1) { return null; } From a593638caa740bb4b8b4d6f363707471ba0f7b30 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 22 Sep 2025 15:52:52 +0200 Subject: [PATCH 08/11] Modify badge to accept new format Signed-off-by: Adhitya Mamallan --- .../workflow-history-remaining-duration-badge.test.tsx | 9 ++++++--- .../helpers/get-formatted-remaining-duration.ts | 4 ++-- .../workflow-history-remaining-duration-badge.tsx | 9 +++++---- .../workflow-history-remaining-duration-badge.types.ts | 3 ++- .../workflow-history-timeline-group.tsx | 7 ++++--- .../workflow-history-timeline-group.types.ts | 2 +- 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx b/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx index 25c6bb7f4..f88a23ff9 100644 --- a/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx +++ b/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx @@ -149,7 +149,8 @@ describe('WorkflowHistoryRemainingDurationBadge', () => { rerender( { function setup({ startTime = mockStartTime, - expectedGroupDuration = 5 * 60 * 1000, + expectedWaitTime = 5 * 60 * 1000, + prefix = 'Remaining:', workflowIsArchived = false, workflowCloseStatus = WorkflowExecutionCloseStatus.WORKFLOW_EXECUTION_CLOSE_STATUS_INVALID, loadingMoreEvents = false, @@ -172,7 +174,8 @@ function setup({ return render( { setRemainingDuration( - getFormattedRemainingDuration(startTime, expectedGroupDuration) + getFormattedRemainingDuration(startTime, expectedWaitTime) ); }; @@ -40,7 +41,7 @@ export default function WorkflowHistoryRemainingDurationBadge({ const interval = setInterval(updateRemainingDuration, 1000); return () => clearInterval(interval); - }, [startTime, expectedGroupDuration, workflowIsArchived, shouldHide]); + }, [startTime, expectedWaitTime, workflowIsArchived, shouldHide]); if (shouldHide || !remainingDuration) { return null; @@ -49,7 +50,7 @@ export default function WorkflowHistoryRemainingDurationBadge({ return ( )} - {expectedDurationMs && startTimeMs ? ( + {waitTimerInfo && startTimeMs ? ( & { isLastEvent: boolean; From 796d60a5b72b9f8390bf4fe3ce5da178ac94a844 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 22 Sep 2025 16:08:26 +0200 Subject: [PATCH 09/11] Add unit test for custom unit in formatDuration Signed-off-by: Adhitya Mamallan --- src/utils/data-formatters/__tests__/format-duration.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/data-formatters/__tests__/format-duration.test.ts b/src/utils/data-formatters/__tests__/format-duration.test.ts index 3d3ea518d..bf1e9546a 100644 --- a/src/utils/data-formatters/__tests__/format-duration.test.ts +++ b/src/utils/data-formatters/__tests__/format-duration.test.ts @@ -31,4 +31,9 @@ describe('formatDuration', () => { const duration: Duration = { seconds: '31556952', nanos: 0 }; expect(formatDuration(duration, { separator: ' ' })).toBe('1y 5h 49m 12s'); }); + + it('should format duration with custom min unit', () => { + const duration: Duration = { seconds: '31556952', nanos: 0 }; + expect(formatDuration(duration, { separator: ' ' }, 'h')).toBe('1y 5h'); + }); }); From 55f270f9d0ab906af9026a9184a82759930b33b6 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Wed, 24 Sep 2025 12:59:56 +0200 Subject: [PATCH 10/11] Fix impl to use absolute time Signed-off-by: Adhitya Mamallan --- ...-history-remaining-duration-badge.test.tsx | 6 +-- .../get-formatted-remaining-duration.test.ts | 46 ++++++---------- .../get-formatted-remaining-duration.ts | 6 +-- ...kflow-history-remaining-duration-badge.tsx | 8 ++- ...-history-remaining-duration-badge.types.ts | 2 +- .../workflow-history-timeline-group.test.tsx | 52 +++++++++++++++++-- .../workflow-history-timeline-group.tsx | 8 +-- .../workflow-history-timeline-group.types.ts | 2 +- 8 files changed, 76 insertions(+), 54 deletions(-) diff --git a/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx b/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx index f88a23ff9..2fa09c023 100644 --- a/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx +++ b/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx @@ -149,7 +149,7 @@ describe('WorkflowHistoryRemainingDurationBadge', () => { rerender( { function setup({ startTime = mockStartTime, - expectedWaitTime = 5 * 60 * 1000, + expectedEndTime = new Date('2024-01-01T10:07:00Z').getTime(), // 5 minutes from mockNow prefix = 'Remaining:', workflowIsArchived = false, workflowCloseStatus = WorkflowExecutionCloseStatus.WORKFLOW_EXECUTION_CLOSE_STATUS_INVALID, @@ -174,7 +174,7 @@ function setup({ return render( { }); it('should return null when expected duration has passed', () => { - const startTime = new Date('2024-01-01T10:00:00Z'); // 2 minutes ago - const expectedDurationMs = 60 * 1000; // 1 minute expected duration + const expectedEndTimeMs = new Date('2024-01-01T10:01:00Z').getTime(); // 1 minute ago - const result = getFormattedRemainingDuration(startTime, expectedDurationMs); + const result = getFormattedRemainingDuration(expectedEndTimeMs); expect(result).toBeNull(); }); it('should return null when expected duration exactly matches current time', () => { - const startTime = new Date('2024-01-01T10:00:00Z'); // 2 minutes ago - const expectedDurationMs = 2 * 60 * 1000; // 2 minutes expected duration + const expectedEndTimeMs = new Date('2024-01-01T10:02:00Z').getTime(); // exactly now - const result = getFormattedRemainingDuration(startTime, expectedDurationMs); + const result = getFormattedRemainingDuration(expectedEndTimeMs); expect(result).toBeNull(); }); it('should return remaining time when duration has not passed', () => { - const startTime = new Date('2024-01-01T10:00:00Z'); // 2 minutes ago - const expectedDurationMs = 5 * 60 * 1000; // 5 minutes expected duration (3 minutes remaining) + const expectedEndTimeMs = new Date('2024-01-01T10:05:00Z').getTime(); // 3 minutes from now - const result = getFormattedRemainingDuration(startTime, expectedDurationMs); + const result = getFormattedRemainingDuration(expectedEndTimeMs); expect(result).toEqual('mocked: 180s'); // 3 minutes = 180 seconds }); it('should return 1s when less than 1 second remaining', () => { - const startTime = new Date('2024-01-01T10:01:59.500Z'); // 0.5 seconds ago - const expectedDurationMs = 1000; // 1 second expected duration (0.5 seconds remaining) + const expectedEndTimeMs = new Date('2024-01-01T10:02:00.500Z').getTime(); // 0.5 seconds from now - const result = getFormattedRemainingDuration(startTime, expectedDurationMs); + const result = getFormattedRemainingDuration(expectedEndTimeMs); expect(result).toEqual('mocked: 1s'); }); - it('should handle string start times', () => { - const startTime = '2024-01-01T10:00:00Z'; // 2 minutes ago - const expectedDurationMs = 5 * 60 * 1000; // 5 minutes expected duration + it('should work with numeric timestamp for expected end time', () => { + const expectedEndTimeMs = new Date('2024-01-01T10:05:00Z').getTime(); // 3 minutes from now - const result = getFormattedRemainingDuration(startTime, expectedDurationMs); - - expect(result).toEqual('mocked: 180s'); - }); - - it('should handle numeric start times', () => { - const startTime = new Date('2024-01-01T10:00:00Z').getTime(); // 2 minutes ago - const expectedDurationMs = 5 * 60 * 1000; // 5 minutes expected duration - - const result = getFormattedRemainingDuration(startTime, expectedDurationMs); + const result = getFormattedRemainingDuration(expectedEndTimeMs); expect(result).toEqual('mocked: 180s'); }); it('should round up partial seconds using Math.ceil', () => { - const startTime = new Date('2024-01-01T10:01:58.700Z'); // 1.3 seconds ago - const expectedDurationMs = 3000; // 3 seconds expected duration (1.3 seconds remaining) + const expectedEndTimeMs = new Date('2024-01-01T10:02:01.300Z').getTime(); // 1.3 seconds from now - const result = getFormattedRemainingDuration(startTime, expectedDurationMs); + const result = getFormattedRemainingDuration(expectedEndTimeMs); expect(result).toEqual('mocked: 2s'); // Math.ceil(1.3) = 2 }); it('should handle exactly 1 second remaining', () => { - const startTime = new Date('2024-01-01T10:01:59Z'); // 1 second ago - const expectedDurationMs = 2000; // 2 seconds expected duration (1 second remaining) + const expectedEndTimeMs = new Date('2024-01-01T10:02:01Z').getTime(); // exactly 1 second from now - const result = getFormattedRemainingDuration(startTime, expectedDurationMs); + const result = getFormattedRemainingDuration(expectedEndTimeMs); expect(result).toEqual('mocked: 1s'); }); diff --git a/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/get-formatted-remaining-duration.ts b/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/get-formatted-remaining-duration.ts index 3c58dcff7..fbc02ad4b 100644 --- a/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/get-formatted-remaining-duration.ts +++ b/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/get-formatted-remaining-duration.ts @@ -2,12 +2,10 @@ import formatDuration from '@/utils/data-formatters/format-duration'; import dayjs from '@/utils/datetime/dayjs'; export default function getFormattedRemainingDuration( - startTime: Date | string | number, - expectedWaitTimeMs: number + expectedEndTimeMs: number ): string | null { - const start = dayjs(startTime); const now = dayjs(); - const expectedEnd = start.add(expectedWaitTimeMs, 'milliseconds'); + const expectedEnd = dayjs(expectedEndTimeMs); if (now.isAfter(expectedEnd)) { return null; diff --git a/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.tsx b/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.tsx index 6366698f9..53d266db4 100644 --- a/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.tsx +++ b/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.tsx @@ -8,7 +8,7 @@ import { type Props } from './workflow-history-remaining-duration-badge.types'; export default function WorkflowHistoryRemainingDurationBadge({ startTime, - expectedWaitTime, + expectedEndTime, prefix, workflowIsArchived, workflowCloseStatus, @@ -31,9 +31,7 @@ export default function WorkflowHistoryRemainingDurationBadge({ } const updateRemainingDuration = () => { - setRemainingDuration( - getFormattedRemainingDuration(startTime, expectedWaitTime) - ); + setRemainingDuration(getFormattedRemainingDuration(expectedEndTime)); }; updateRemainingDuration(); @@ -41,7 +39,7 @@ export default function WorkflowHistoryRemainingDurationBadge({ const interval = setInterval(updateRemainingDuration, 1000); return () => clearInterval(interval); - }, [startTime, expectedWaitTime, workflowIsArchived, shouldHide]); + }, [startTime, expectedEndTime, workflowIsArchived, shouldHide]); if (shouldHide || !remainingDuration) { return null; diff --git a/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.types.ts b/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.types.ts index c879ad6ef..8e3c80fd2 100644 --- a/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.types.ts +++ b/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.types.ts @@ -2,7 +2,7 @@ import { type WorkflowExecutionCloseStatus } from '@/__generated__/proto-ts/uber export type Props = { startTime: Date | string | number; - expectedWaitTime: number; + expectedEndTime: number; prefix: string; workflowIsArchived: boolean; workflowCloseStatus: WorkflowExecutionCloseStatus | null | undefined; diff --git a/src/views/workflow-history/workflow-history-timeline-group/__tests__/workflow-history-timeline-group.test.tsx b/src/views/workflow-history/workflow-history-timeline-group/__tests__/workflow-history-timeline-group.test.tsx index 69805752f..4169b0efa 100644 --- a/src/views/workflow-history/workflow-history-timeline-group/__tests__/workflow-history-timeline-group.test.tsx +++ b/src/views/workflow-history/workflow-history-timeline-group/__tests__/workflow-history-timeline-group.test.tsx @@ -4,6 +4,7 @@ import { startWorkflowExecutionEvent } from '../../__fixtures__/workflow-history import type WorkflowHistoryEventStatusBadge from '../../workflow-history-event-status-badge/workflow-history-event-status-badge'; import type WorkflowHistoryEventsCard from '../../workflow-history-events-card/workflow-history-events-card'; import type WorkflowHistoryEventsDurationBadge from '../../workflow-history-events-duration-badge/workflow-history-events-duration-badge'; +import type WorkflowHistoryRemainingDurationBadge from '../../workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge'; import type WorkflowHistoryTimelineResetButton from '../../workflow-history-timeline-reset-button/workflow-history-timeline-reset-button'; import WorkflowHistoryTimelineGroup from '../workflow-history-timeline-group'; import { type styled } from '../workflow-history-timeline-group.styles'; @@ -16,7 +17,12 @@ jest.mock( jest.mock( '../../workflow-history-events-duration-badge/workflow-history-events-duration-badge', - () => jest.fn(() =>
Duration Badge
) + () => jest.fn(() =>
Events Duration Badge
) +); + +jest.mock( + '../../workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge', + () => jest.fn(() =>
Remaining Duration Badge
) ); jest.mock( @@ -120,18 +126,52 @@ describe('WorkflowHistoryTimelineGroup', () => { expect(mockOnReset).toHaveBeenCalledTimes(1); }); - it('should render duration badge when startTimeMs is provided', () => { + it('should render events duration badge when startTimeMs is provided', () => { setup({ startTimeMs: 1726652232190.7927, }); - expect(screen.getByText('Duration Badge')).toBeInTheDocument(); + expect(screen.getByText('Events Duration Badge')).toBeInTheDocument(); }); - it('should not render duration badge when startTimeMs is not provided', () => { + it('should not render events duration badge when startTimeMs is not provided', () => { setup({ startTimeMs: null, }); - expect(screen.queryByText('Duration Badge')).not.toBeInTheDocument(); + expect(screen.queryByText('Events Duration Badge')).not.toBeInTheDocument(); + }); + + it('should render remaining duration badge when expectedEndTimeInfo and startTimeMs are provided', () => { + setup({ + expectedEndTimeInfo: { + timeMs: Date.now() + 300000, // 5 minutes from now + prefix: 'Timer expires in:', + }, + startTimeMs: 1726652232190.7927, + }); + expect(screen.getByText('Remaining Duration Badge')).toBeInTheDocument(); + }); + + it('should not render remaining duration badge when expectedEndTimeInfo is not provided', () => { + setup({ + expectedEndTimeInfo: undefined, + startTimeMs: 1726652232190.7927, + }); + expect( + screen.queryByText('Remaining Duration Badge') + ).not.toBeInTheDocument(); + }); + + it('should not render remaining duration badge when startTimeMs is not provided', () => { + setup({ + expectedEndTimeInfo: { + timeMs: Date.now() + 300000, + prefix: 'Timer expires in:', + }, + startTimeMs: null, + }); + expect( + screen.queryByText('Remaining Duration Badge') + ).not.toBeInTheDocument(); }); }); @@ -164,6 +204,7 @@ function setup({ workflowIsArchived = false, workflowCloseTimeMs = null, startTimeMs = 1726652232190.7927, + expectedEndTimeInfo, }: Partial) { const mockOnReset = jest.fn(); const user = userEvent.setup(); @@ -187,6 +228,7 @@ function setup({ workflowCloseStatus={workflowCloseStatus} workflowIsArchived={workflowIsArchived} workflowCloseTimeMs={workflowCloseTimeMs} + expectedEndTimeInfo={expectedEndTimeInfo} /> ); return { mockOnReset, user }; diff --git a/src/views/workflow-history/workflow-history-timeline-group/workflow-history-timeline-group.tsx b/src/views/workflow-history/workflow-history-timeline-group/workflow-history-timeline-group.tsx index 064b1c729..18fac6325 100644 --- a/src/views/workflow-history/workflow-history-timeline-group/workflow-history-timeline-group.tsx +++ b/src/views/workflow-history/workflow-history-timeline-group/workflow-history-timeline-group.tsx @@ -26,7 +26,7 @@ export default function WorkflowHistoryTimelineGroup({ timeLabel, startTimeMs, closeTimeMs, - waitTimerInfo, + expectedEndTimeInfo, workflowCloseTimeMs, workflowCloseStatus, workflowIsArchived, @@ -90,11 +90,11 @@ export default function WorkflowHistoryTimelineGroup({ workflowCloseStatus={workflowCloseStatus} /> )} - {waitTimerInfo && startTimeMs ? ( + {expectedEndTimeInfo && startTimeMs ? ( & { isLastEvent: boolean; From 85050c3bf7e84a5c41fe64a598df99ddf25dac26 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Fri, 26 Sep 2025 17:35:23 +0200 Subject: [PATCH 11/11] Fix formatDuration utils --- .../data-formatters/__tests__/format-duration.test.ts | 4 +++- src/utils/data-formatters/format-duration.ts | 10 +++++++--- .../__tests__/get-formatted-events-duration.test.ts | 4 ++-- .../helpers/get-formatted-events-duration.ts | 3 +-- .../workflow-history-remaining-duration-badge.test.tsx | 2 +- .../helpers/get-formatted-remaining-duration.ts | 3 +-- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/utils/data-formatters/__tests__/format-duration.test.ts b/src/utils/data-formatters/__tests__/format-duration.test.ts index bf1e9546a..dc188a0bb 100644 --- a/src/utils/data-formatters/__tests__/format-duration.test.ts +++ b/src/utils/data-formatters/__tests__/format-duration.test.ts @@ -34,6 +34,8 @@ describe('formatDuration', () => { it('should format duration with custom min unit', () => { const duration: Duration = { seconds: '31556952', nanos: 0 }; - expect(formatDuration(duration, { separator: ' ' }, 'h')).toBe('1y 5h'); + expect(formatDuration(duration, { separator: ' ', minUnit: 'h' })).toBe( + '1y 5h' + ); }); }); diff --git a/src/utils/data-formatters/format-duration.ts b/src/utils/data-formatters/format-duration.ts index 775ff0418..a2c1ff672 100644 --- a/src/utils/data-formatters/format-duration.ts +++ b/src/utils/data-formatters/format-duration.ts @@ -1,10 +1,14 @@ import { type Duration } from '@/__generated__/proto-ts/google/protobuf/Duration'; import dayjs from '@/utils/datetime/dayjs'; +export type FormatDurationUnitType = 'y' | 'M' | 'd' | 'h' | 'm' | 's' | 'ms'; + const formatDuration = ( duration: Duration | null, - { separator = ', ' }: { separator?: string } = {}, - minUnit: 'y' | 'M' | 'd' | 'h' | 'm' | 's' | 'ms' = 'ms' + { + separator = ', ', + minUnit = 'ms', + }: { separator?: string; minUnit?: FormatDurationUnitType } = {} ) => { const defaultReturn = '0s'; if (!duration) { @@ -17,7 +21,7 @@ const formatDuration = ( const intMillis = Math.floor(nanosAsMillis); const remainingNanosAsMillis = nanosAsMillis % 1; const milliseconds = secondsAsMillis + intMillis; - const allUnits: Array = [ + const allUnits: Array = [ 'y', 'M', 'd', diff --git a/src/views/workflow-history/workflow-history-events-duration-badge/helpers/__tests__/get-formatted-events-duration.test.ts b/src/views/workflow-history/workflow-history-events-duration-badge/helpers/__tests__/get-formatted-events-duration.test.ts index 4450e9115..98e9fb328 100644 --- a/src/views/workflow-history/workflow-history-events-duration-badge/helpers/__tests__/get-formatted-events-duration.test.ts +++ b/src/views/workflow-history/workflow-history-events-duration-badge/helpers/__tests__/get-formatted-events-duration.test.ts @@ -3,8 +3,8 @@ import getFormattedEventsDuration from '../get-formatted-events-duration'; jest.mock('@/utils/data-formatters/format-duration', () => ({ __esModule: true, default: jest.fn( - (duration, _, unit) => - `mocked: ${duration.seconds}s${unit === 'ms' ? ` ${duration.nanos / 1000000}ms` : ''}` + (duration, { minUnit }) => + `mocked: ${duration.seconds}s${minUnit === 'ms' ? ` ${duration.nanos / 1000000}ms` : ''}` ), })); diff --git a/src/views/workflow-history/workflow-history-events-duration-badge/helpers/get-formatted-events-duration.ts b/src/views/workflow-history/workflow-history-events-duration-badge/helpers/get-formatted-events-duration.ts index 8a7b40b05..5a4e4f101 100644 --- a/src/views/workflow-history/workflow-history-events-duration-badge/helpers/get-formatted-events-duration.ts +++ b/src/views/workflow-history/workflow-history-events-duration-badge/helpers/get-formatted-events-duration.ts @@ -17,8 +17,7 @@ export default function getFormattedEventsDuration( seconds: seconds.toString(), nanos: (durationObj.asMilliseconds() - seconds * 1000) * 1000000, }, - { separator: ' ' }, - hideMs && seconds > 0 ? 's' : 'ms' + { separator: ' ', minUnit: hideMs && seconds > 0 ? 's' : 'ms' } ); return duration; diff --git a/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx b/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx index 2fa09c023..2b52f9160 100644 --- a/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx +++ b/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx @@ -8,7 +8,7 @@ import getFormattedRemainingDuration from '../helpers/get-formatted-remaining-du import WorkflowHistoryRemainingDurationBadge from '../workflow-history-remaining-duration-badge'; import type { Props } from '../workflow-history-remaining-duration-badge.types'; -jest.mock('../helpers/get-formatted-remaining-duration', () => jest.fn()); +jest.mock('../helpers/get-formatted-remaining-duration'); const mockStartTime = new Date('2024-01-01T10:00:00Z'); const mockNow = new Date('2024-01-01T10:02:00Z'); diff --git a/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/get-formatted-remaining-duration.ts b/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/get-formatted-remaining-duration.ts index fbc02ad4b..c64c81851 100644 --- a/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/get-formatted-remaining-duration.ts +++ b/src/views/workflow-history/workflow-history-remaining-duration-badge/helpers/get-formatted-remaining-duration.ts @@ -24,8 +24,7 @@ export default function getFormattedRemainingDuration( seconds: seconds.toString(), nanos: 0, }, - { separator: ' ' }, - 's' + { separator: ' ', minUnit: 's' } ); return duration;