From be1c23f6ce4bff40bee560ab392fc2e0967f28ec Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Tue, 2 Dec 2025 16:44:11 +0100 Subject: [PATCH 01/11] Copy utils from v1 to v2 Signed-off-by: Adhitya Mamallan --- .../config/workflow-history-event-group-details.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/views/workflow-history-v2/config/workflow-history-event-group-details.config.ts b/src/views/workflow-history-v2/config/workflow-history-event-group-details.config.ts index 71d7761b9..6a002c670 100644 --- a/src/views/workflow-history-v2/config/workflow-history-event-group-details.config.ts +++ b/src/views/workflow-history-v2/config/workflow-history-event-group-details.config.ts @@ -55,7 +55,10 @@ const workflowHistoryEventGroupDetailsConfig = [ pathRegex: '(input|result|details|failureDetails|Error|lastCompletionResult|heartbeatDetails|lastFailureDetails)$', showInPanels: true, +<<<<<<< HEAD valueComponent: WorkflowHistoryGroupDetailsJson, +======= +>>>>>>> e99f9e96 (Copy utils from v1 to v2) }, { name: 'Duration & interval seconds', From 441ca780f23023c01b8a15478e98a50983a158b4 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Wed, 3 Dec 2025 10:47:29 +0100 Subject: [PATCH 02/11] Add JSON component and allow custom components for panels Signed-off-by: Adhitya Mamallan --- .../config/workflow-history-event-group-details.config.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/views/workflow-history-v2/config/workflow-history-event-group-details.config.ts b/src/views/workflow-history-v2/config/workflow-history-event-group-details.config.ts index 6a002c670..71d7761b9 100644 --- a/src/views/workflow-history-v2/config/workflow-history-event-group-details.config.ts +++ b/src/views/workflow-history-v2/config/workflow-history-event-group-details.config.ts @@ -55,10 +55,7 @@ const workflowHistoryEventGroupDetailsConfig = [ pathRegex: '(input|result|details|failureDetails|Error|lastCompletionResult|heartbeatDetails|lastFailureDetails)$', showInPanels: true, -<<<<<<< HEAD valueComponent: WorkflowHistoryGroupDetailsJson, -======= ->>>>>>> e99f9e96 (Copy utils from v1 to v2) }, { name: 'Duration & interval seconds', From d21c7b30e0226f023bd173656a62305a10189b80 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Tue, 2 Dec 2025 17:29:17 +0100 Subject: [PATCH 03/11] add changes Signed-off-by: Adhitya Mamallan --- .../workflow-history-event-details.test.tsx | 204 ++++++++++++++ .../workflow-history-event-details.styles.ts | 31 +++ .../workflow-history-event-details.tsx | 71 +++++ .../workflow-history-event-group.test.tsx | 255 ++++++++++++++++-- .../workflow-history-event-group.styles.ts | 34 ++- .../workflow-history-event-group.tsx | 110 +++++++- .../workflow-history-event-group.types.ts | 1 + .../workflow-history-group-details.test.tsx | 220 +++++++++++++++ .../workflow-history-group-details.styles.ts | 36 +++ .../workflow-history-group-details.tsx | 73 +++++ .../workflow-history-group-details.types.ts | 8 + .../workflow-history-grouped-table.tsx | 3 +- 12 files changed, 1013 insertions(+), 33 deletions(-) create mode 100644 src/views/workflow-history-v2/workflow-history-event-details/__tests__/workflow-history-event-details.test.tsx create mode 100644 src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.styles.ts create mode 100644 src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx create mode 100644 src/views/workflow-history-v2/workflow-history-group-details/__tests__/workflow-history-group-details.test.tsx create mode 100644 src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.styles.ts create mode 100644 src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.tsx create mode 100644 src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.types.ts diff --git a/src/views/workflow-history-v2/workflow-history-event-details/__tests__/workflow-history-event-details.test.tsx b/src/views/workflow-history-v2/workflow-history-event-details/__tests__/workflow-history-event-details.test.tsx new file mode 100644 index 000000000..db87285fa --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-event-details/__tests__/workflow-history-event-details.test.tsx @@ -0,0 +1,204 @@ +import { render, screen } from '@/test-utils/rtl'; + +import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; + +import WorkflowHistoryEventDetails from '../workflow-history-event-details'; +import { type EventDetailsEntries } from '../workflow-history-event-details.types'; + +jest.mock( + '../../workflow-history-group-details-json/workflow-history-group-details-json', + () => + jest.fn( + ({ + entryPath, + entryValue, + isNegative, + }: { + entryPath: string; + entryValue: any; + isNegative?: boolean; + }) => ( +
+ JSON: {entryPath} = {JSON.stringify(entryValue)} + {isNegative && ' (negative)'} +
+ ) + ) +); + +jest.mock( + '@/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group', + () => + jest.fn(({ entries }: { entries: EventDetailsEntries }) => ( +
+ Event Details Group ({entries.length} entries) +
+ )) +); + +describe(WorkflowHistoryEventDetails.name, () => { + it('renders "No Details" when eventDetails is empty', () => { + setup({ eventDetails: [] }); + + expect(screen.getByText('No Details')).toBeInTheDocument(); + expect(screen.queryByTestId('group-details-json')).not.toBeInTheDocument(); + expect(screen.queryByTestId('event-details-group')).not.toBeInTheDocument(); + }); + + it('renders only rest details when no entries have showInPanels', () => { + const eventDetails: EventDetailsEntries = [ + { + key: 'key1', + path: 'path1', + value: 'value1', + isGroup: false, + renderConfig: { + name: 'Test Config', + key: 'key1', + }, + }, + { + key: 'key2', + path: 'path2', + value: 'value2', + isGroup: false, + renderConfig: null, + }, + ]; + + setup({ eventDetails }); + + expect(screen.queryByTestId('group-details-json')).not.toBeInTheDocument(); + expect(screen.getByTestId('event-details-group')).toBeInTheDocument(); + expect( + screen.getByText('Event Details Group (2 entries)') + ).toBeInTheDocument(); + }); + + it('renders panel details when entries have showInPanels flag', () => { + const eventDetails: EventDetailsEntries = [ + { + key: 'key1', + path: 'path1', + value: 'value1', + isGroup: false, + renderConfig: { + name: 'Panel Config', + key: 'key1', + showInPanels: true, + }, + }, + { + key: 'key2', + path: 'path2', + value: 'value2', + isGroup: false, + renderConfig: { + name: 'Rest Config', + key: 'key2', + }, + }, + ]; + + setup({ eventDetails }); + + expect(screen.getByTestId('group-details-json')).toBeInTheDocument(); + expect(screen.getByText(/JSON: path1 = "value1"/)).toBeInTheDocument(); + expect(screen.getByTestId('event-details-group')).toBeInTheDocument(); + expect( + screen.getByText('Event Details Group (1 entries)') + ).toBeInTheDocument(); + }); + + it('renders multiple panel details', () => { + const eventDetails: EventDetailsEntries = [ + { + key: 'key1', + path: 'path1', + value: 'value1', + isGroup: false, + renderConfig: { + name: 'Panel Config 1', + key: 'key1', + showInPanels: true, + }, + }, + { + key: 'key2', + path: 'path2', + value: { nested: 'value2' }, + isGroup: false, + renderConfig: { + name: 'Panel Config 2', + key: 'key2', + showInPanels: true, + }, + }, + { + key: 'key3', + path: 'path3', + value: 'value3', + isGroup: false, + renderConfig: { + name: 'Rest Config', + key: 'key3', + }, + }, + ]; + + setup({ eventDetails }); + + const jsonComponents = screen.getAllByTestId('group-details-json'); + expect(jsonComponents).toHaveLength(2); + expect(screen.getByText(/JSON: path1 = "value1"/)).toBeInTheDocument(); + expect( + screen.getByText(/JSON: path2 = \{"nested":"value2"\}/) + ).toBeInTheDocument(); + expect(screen.getByTestId('event-details-group')).toBeInTheDocument(); + expect( + screen.getByText('Event Details Group (1 entries)') + ).toBeInTheDocument(); + }); + + it('passes correct props to WorkflowHistoryGroupDetailsJson', () => { + const eventDetails: EventDetailsEntries = [ + { + key: 'key1', + path: 'path1', + value: 'value1', + isGroup: false, + isNegative: true, + renderConfig: { + name: 'Panel Config', + key: 'key1', + showInPanels: true, + }, + }, + ]; + + setup({ eventDetails }); + + expect(screen.getByText(/JSON: path1 = "value1"/)).toBeInTheDocument(); + expect(screen.getByText(/\(negative\)/)).toBeInTheDocument(); + }); +}); + +function setup({ + eventDetails, + workflowPageParams = { + domain: 'test-domain', + cluster: 'test-cluster', + workflowId: 'test-workflow-id', + runId: 'test-run-id', + }, +}: { + eventDetails: EventDetailsEntries; + workflowPageParams?: WorkflowPageParams; +}) { + render( + + ); +} diff --git a/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.styles.ts b/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.styles.ts new file mode 100644 index 000000000..5d86f6247 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.styles.ts @@ -0,0 +1,31 @@ +import { styled as createStyled, type Theme } from 'baseui'; + +export const styled = { + EmptyDetails: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + ...$theme.typography.LabelXSmall, + color: $theme.colors.contentTertiary, + textAlign: 'center', + padding: `${$theme.sizing.scale700} 0`, + })), + EventDetailsContainer: createStyled('div', { + display: 'flex', + flexDirection: 'column', + }), + PanelDetails: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + display: 'flex', + flexDirection: 'column', + [$theme.mediaQuery.medium]: { + flexDirection: 'row', + }, + gap: $theme.sizing.scale500, + paddingBottom: $theme.sizing.scale500, + alignItems: 'stretch', + })), + PanelContainer: createStyled('div', { + flex: 1, + }), + RestDetails: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + paddingLeft: $theme.sizing.scale100, + paddingRight: $theme.sizing.scale100, + })), +}; diff --git a/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx b/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx new file mode 100644 index 000000000..ab77fcedc --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx @@ -0,0 +1,71 @@ +import { useMemo } from 'react'; + +import WorkflowHistoryEventDetailsGroup from '@/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group'; +import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; + +import WorkflowHistoryGroupDetailsJson from '../workflow-history-group-details-json/workflow-history-group-details-json'; + +import { styled } from './workflow-history-event-details.styles'; +import { + type EventDetailsEntries, + type EventDetailsSingleEntry, +} from './workflow-history-event-details.types'; + +export default function WorkflowHistoryEventDetails({ + eventDetails, + workflowPageParams, +}: { + eventDetails: EventDetailsEntries; + workflowPageParams: WorkflowPageParams; +}) { + const [panelDetails, restDetails] = useMemo( + () => + eventDetails.reduce< + [Array, EventDetailsEntries] + >( + ([panels, rest], entry) => { + if (entry.renderConfig?.showInPanels && !entry.isGroup) { + panels.push(entry); + } else { + rest.push(entry); + } + + return [panels, rest]; + }, + [[], []] + ), + [eventDetails] + ); + + if (eventDetails.length === 0) { + return No Details; + } + + return ( + + {panelDetails.length > 0 && ( + + {panelDetails.map((detail) => ( + + + + ))} + + )} + + + + + ); +} diff --git a/src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx b/src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx index 9bac73118..6afa73f52 100644 --- a/src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx +++ b/src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx @@ -13,6 +13,8 @@ import type WorkflowHistoryGroupLabel from '@/views/workflow-history/workflow-hi import type WorkflowHistoryTimelineResetButton from '@/views/workflow-history/workflow-history-timeline-reset-button/workflow-history-timeline-reset-button'; import { type HistoryEventsGroup } from '@/views/workflow-history/workflow-history.types'; +import type { EventDetailsEntries } from '../../workflow-history-event-details/workflow-history-event-details.types'; +import type WorkflowHistoryGroupDetails from '../../workflow-history-group-details/workflow-history-group-details'; import WorkflowHistoryEventGroup from '../workflow-history-event-group'; import type { Props } from '../workflow-history-event-group.types'; @@ -20,6 +22,45 @@ jest.mock('@/utils/data-formatters/format-date', () => jest.fn((timeMs: number) => `Formatted: ${timeMs}`) ); +jest.mock('@/utils/data-formatters/format-pending-workflow-history-event', () => + jest.fn(() => ({ mockFormatted: true })) +); + +jest.mock('@/utils/data-formatters/format-workflow-history-event', () => + jest.fn(() => ({ mockFormatted: true })) +); + +const mockGenerateHistoryEventDetails = jest.fn(); + +jest.mock('../../helpers/generate-history-event-details', () => + jest.fn(() => mockGenerateHistoryEventDetails()) +); + +jest.mock( + '../../workflow-history-group-details/workflow-history-group-details', + () => + jest.fn(({ groupDetailsEntries, initialEventId, onClose }) => ( +
+
+ {groupDetailsEntries.length} events +
+ {groupDetailsEntries.map(([eventId, { eventLabel }]) => ( +
+ {eventLabel} +
+ ))} + {initialEventId && ( +
{initialEventId}
+ )} + {onClose && ( + + )} +
+ )) +); + jest.mock( '@/views/workflow-history/workflow-history-event-status-badge/workflow-history-event-status-badge', () => @@ -54,20 +95,6 @@ jest.mock('../helpers/get-event-group-filtering-type', () => jest.fn(() => 'ACTIVITY') ); -jest.mock( - '../../config/workflow-history-event-filtering-type-colors.config', - () => ({ - __esModule: true, - default: { - ACTIVITY: { - content: '#FF5733', - background: '#FFE5E0', - backgroundHighlighted: '#FFD4CC', - }, - }, - }) -); - const mockActivityEventGroupWithMetadata: HistoryEventsGroup = { ...mockActivityEventGroup, eventsMetadata: [ @@ -77,6 +104,12 @@ const mockActivityEventGroupWithMetadata: HistoryEventsGroup = { timeMs: 1725747370599, timeLabel: 'Scheduled at 07 Sep, 22:16:10 UTC', }, + { + label: 'Started', + status: 'COMPLETED', + timeMs: 1725747370612, + timeLabel: 'Started at 07 Sep, 22:16:10 UTC', + }, { label: 'Completed', status: 'COMPLETED', @@ -162,8 +195,13 @@ describe(WorkflowHistoryEventGroup.name, () => { setup({ eventGroup, getIsEventExpanded }); - // Panel should be expanded if any event is expanded, showing content - expect(screen.getByText('TODO: Full event details')).toBeInTheDocument(); + // Panel should be expanded if any event is expanded, showing WorkflowHistoryGroupDetails + expect( + screen.getByTestId('workflow-history-group-details') + ).toBeInTheDocument(); + expect(screen.getByTestId('group-details-count')).toHaveTextContent( + `${completedActivityTaskEvents.length} events` + ); }); it('calls toggleIsEventExpanded when panel is toggled', async () => { @@ -217,6 +255,173 @@ describe(WorkflowHistoryEventGroup.name, () => { expect(screen.getByText('Loading')).toBeInTheDocument(); expect(screen.queryByText('COMPLETED')).not.toBeInTheDocument(); }); + + it('calls toggleIsEventExpanded when WorkflowHistoryGroupDetails onClose is called', async () => { + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroupWithMetadata, + events: completedActivityTaskEvents, + }; + + const toggleIsEventExpanded = jest.fn(); + const getIsEventExpanded = jest.fn( + (eventId: string) => eventId === completedActivityTaskEvents[0].eventId + ); + + const { user } = setup({ + eventGroup, + getIsEventExpanded, + toggleIsEventExpanded, + }); + + const closeButton = screen.getByTestId('group-details-close'); + await user.click(closeButton); + + // Should call toggleIsEventExpanded for each expanded event + completedActivityTaskEvents.forEach((event) => { + if (event.eventId && getIsEventExpanded(event.eventId)) { + expect(toggleIsEventExpanded).toHaveBeenCalledWith(event.eventId); + } + }); + }); + + it('shows summary tab when summaryFields are available to show', () => { + const mockEventDetails: EventDetailsEntries = [ + { + key: 'input', + path: 'input', + value: 'test input value', + isGroup: false, + renderConfig: null, + }, + { + key: 'activityType', + path: 'activityType', + value: 'TestActivity', + isGroup: false, + renderConfig: null, + }, + { + key: 'result', + path: 'result', + value: 'test result', + isGroup: false, + renderConfig: null, + }, + ]; + + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroupWithMetadata, + events: completedActivityTaskEvents, + eventsMetadata: [ + { + label: 'Scheduled', + status: 'COMPLETED', + timeMs: 1725747370599, + timeLabel: 'Scheduled at 07 Sep, 22:16:10 UTC', + summaryFields: ['input', 'activityType'], + }, + { + label: 'Started', + status: 'COMPLETED', + timeMs: 1725747370612, + timeLabel: 'Started at 07 Sep, 22:16:10 UTC', + summaryFields: ['activityType'], + }, + { + label: 'Completed', + status: 'COMPLETED', + timeMs: 1725747370632, + timeLabel: 'Completed at 07 Sep, 22:16:10 UTC', + summaryFields: ['result'], + }, + ], + }; + + const getIsEventExpanded = jest.fn( + (eventId: string) => eventId === completedActivityTaskEvents[0].eventId + ); + + setup({ eventGroup, getIsEventExpanded, mockEventDetails }); + + // Summary tab should appear in groupDetailsEntries when there are multiple events and summary details + expect(screen.getByText('Summary')).toBeInTheDocument(); + }); + + it('does not show summary tab when there is only one event', () => { + const mockEventDetails: EventDetailsEntries = [ + { + key: 'input', + path: 'input', + value: 'test input value', + isGroup: false, + renderConfig: null, + }, + ]; + + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroupWithMetadata, + events: [scheduleActivityTaskEvent], + eventsMetadata: [ + { + label: 'Scheduled', + status: 'COMPLETED', + timeMs: 1725747370599, + timeLabel: 'Scheduled at 07 Sep, 22:16:10 UTC', + summaryFields: ['input'], + }, + ], + }; + + setup({ eventGroup, mockEventDetails }); + + // Summary tab should not appear when there's only one event + expect(screen.queryByTestId('event-summary_7')).not.toBeInTheDocument(); + expect(screen.queryByText('Summary')).not.toBeInTheDocument(); + }); + + it('does not show summary tab when summaryFields do not match any event details', () => { + const mockEventDetails: EventDetailsEntries = [ + { + key: 'input', + path: 'input', + value: 'test input value', + isGroup: false, + renderConfig: null, + }, + ]; + + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroupWithMetadata, + events: completedActivityTaskEvents, + eventsMetadata: [ + { + label: 'Scheduled', + status: 'COMPLETED', + timeMs: 1725747370599, + timeLabel: 'Scheduled at 07 Sep, 22:16:10 UTC', + summaryFields: ['nonExistentField'], + }, + { + label: 'Started', + status: 'COMPLETED', + timeMs: 1725747370612, + timeLabel: 'Started at 07 Sep, 22:16:10 UTC', + summaryFields: ['anotherNonExistentField'], + }, + { + label: 'Completed', + status: 'COMPLETED', + timeMs: 1725747370632, + timeLabel: 'Completed at 07 Sep, 22:16:10 UTC', + }, + ], + }; + + setup({ eventGroup, mockEventDetails }); + + // Summary tab should not appear when no summary details match + expect(screen.queryByText('Summary')).not.toBeInTheDocument(); + }); }); function setup({ @@ -236,13 +441,29 @@ function setup({ onReset = jest.fn(), getIsEventExpanded = jest.fn(() => false), toggleIsEventExpanded = jest.fn(), -}: Partial = {}) { + mockEventDetails, +}: Partial & { + mockEventDetails?: EventDetailsEntries; +} = {}) { + mockGenerateHistoryEventDetails.mockReturnValue( + mockEventDetails ?? [ + { + key: 'testKey', + path: 'testPath', + value: 'testValue', + isGroup: false, + renderConfig: null, + }, + ] + ); + const mockOnReset = onReset || jest.fn(); const user = userEvent.setup(); render( ({ + display: 'none', + [$theme.mediaQuery.medium]: { + gridColumn: '1 / span 2', + }, + }) + ), + GroupDetailsContainer: createStyled( + 'div', + ({ $theme }: { $theme: Theme }) => ({ + gridColumn: '1 / -1', + [$theme.mediaQuery.medium]: { + gridColumn: '3 / -1', + }, + border: `2px solid ${$theme.colors.borderOpaque}`, + borderRadius: $theme.borders.radius400, + padding: $theme.sizing.scale500, + backgroundColor: $theme.colors.backgroundPrimary, + }) + ), }; export const overrides = ( @@ -103,8 +129,12 @@ export const overrides = ( // Since the original Panel uses longhand properties, we need to use longhand in overrides paddingTop: 0, paddingBottom: $theme.sizing.scale600, - paddingLeft: $theme.sizing.scale700, - paddingRight: $theme.sizing.scale700, + paddingLeft: 0, + paddingRight: 0, + [$theme.mediaQuery.medium]: { + paddingLeft: $theme.sizing.scale700, + paddingRight: $theme.sizing.scale700, + }, backgroundColor: 'inherit', }), }, diff --git a/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.tsx b/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.tsx index 904137bf4..83cf41e8f 100644 --- a/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.tsx +++ b/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.tsx @@ -1,15 +1,21 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { Panel } from 'baseui/accordion'; import { MdCircle } from 'react-icons/md'; import formatDate from '@/utils/data-formatters/format-date'; +import formatPendingWorkflowHistoryEvent from '@/utils/data-formatters/format-pending-workflow-history-event'; +import formatWorkflowHistoryEvent from '@/utils/data-formatters/format-workflow-history-event'; +import isPendingHistoryEvent from '@/views/workflow-history/workflow-history-event-details/helpers/is-pending-history-event'; import WorkflowHistoryEventStatusBadge from '@/views/workflow-history/workflow-history-event-status-badge/workflow-history-event-status-badge'; import WorkflowHistoryGroupLabel from '@/views/workflow-history/workflow-history-group-label/workflow-history-group-label'; import WorkflowHistoryTimelineResetButton from '@/views/workflow-history/workflow-history-timeline-reset-button/workflow-history-timeline-reset-button'; import workflowHistoryEventFilteringTypeColorsConfig from '../config/workflow-history-event-filtering-type-colors.config'; +import generateHistoryEventDetails from '../helpers/generate-history-event-details'; import WorkflowHistoryEventGroupDuration from '../workflow-history-event-group-duration/workflow-history-event-group-duration'; +import WorkflowHistoryGroupDetails from '../workflow-history-group-details/workflow-history-group-details'; +import { type EventDetailsTabContent } from '../workflow-history-group-details/workflow-history-group-details.types'; import getEventGroupFilteringType from './helpers/get-event-group-filtering-type'; import { @@ -20,6 +26,7 @@ import { type Props } from './workflow-history-event-group.types'; export default function WorkflowHistoryEventGroup({ eventGroup, + groupId, selected, workflowCloseTimeMs, workflowCloseStatus, @@ -34,13 +41,13 @@ export default function WorkflowHistoryEventGroup({ status, label, shortLabel, + timeMs, startTimeMs, closeTimeMs, // expectedEndTimeInfo, events, eventsMetadata, hasMissingEvents, - // badges, resetToDecisionEventId, } = eventGroup; @@ -54,6 +61,80 @@ export default function WorkflowHistoryEventGroup({ } }, [onReset]); + const { groupDetailsEntries } = useMemo(() => { + const eventsEntries: Array<[string, EventDetailsTabContent]> = [], + summaryEntry: [string, EventDetailsTabContent] = [ + `summary_${groupId}`, + { + eventLabel: 'Summary', + eventDetails: [], + }, + ]; + + events.forEach((event, index) => { + if (!event.eventId) return; + + const eventMetadata = eventsMetadata[index]; + + const result = isPendingHistoryEvent(event) + ? formatPendingWorkflowHistoryEvent(event) + : formatWorkflowHistoryEvent(event); + + const eventDetails = result + ? generateHistoryEventDetails({ + details: { + ...result, + ...eventMetadata.additionalDetails, + }, + negativeFields: eventMetadata.negativeFields, + }) + : []; + + eventsEntries.push([ + event.eventId, + { + eventLabel: eventMetadata.label, + eventDetails, + } satisfies EventDetailsTabContent, + ]); + + summaryEntry[1].eventDetails.push( + ...eventDetails.filter((detail) => + eventMetadata.summaryFields?.includes(detail.path) + ) + ); + }); + + const shouldShowSummaryTab = + events.length > 1 && summaryEntry[1].eventDetails.length > 0; + + return { + groupDetailsEntries: [ + ...(shouldShowSummaryTab ? [summaryEntry] : []), + ...eventsEntries, + ], + // To be used for rendering one-line summaries + summaryDetails: summaryEntry[1].eventDetails, + }; + }, [events, eventsMetadata, groupId]); + + const onChangeGroupExpansionState = useCallback( + (newExpanded: boolean) => { + if (newExpanded) { + events.forEach(({ eventId }) => { + if (eventId && !getIsEventExpanded(eventId)) + toggleIsEventExpanded(eventId); + }); + } else { + events.forEach(({ eventId }) => { + if (eventId && getIsEventExpanded(eventId)) + toggleIsEventExpanded(eventId); + }); + } + }, + [events, getIsEventExpanded, toggleIsEventExpanded] + ); + return ( {eventsMetadata.at(-1)?.label} -
{eventGroup.timeMs ? formatDate(eventGroup.timeMs) : null}
+
{timeMs ? formatDate(timeMs) : null}
- {/* TODO: add as event details: - - Existing event details - - Badges - - Expected end time info - */} Placeholder for event details @@ -110,13 +186,21 @@ export default function WorkflowHistoryEventGroup({ expanded={events.some( ({ eventId }) => eventId && getIsEventExpanded(eventId) )} - onChange={() => { - if (events.length > 0 && events[0].eventId) - toggleIsEventExpanded(events[0].eventId); - }} + onChange={({ expanded }) => onChangeGroupExpansionState(expanded)} overrides={overrides.panel} > -
TODO: Full event details
+ + + + onChangeGroupExpansionState(false)} + /> + +
); } diff --git a/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.types.ts b/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.types.ts index eb6049dea..c6ab77e8d 100644 --- a/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.types.ts +++ b/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.types.ts @@ -5,6 +5,7 @@ import { type Props as WorkflowHistoryProps } from '../workflow-history-v2.types export type Props = { eventGroup: HistoryEventsGroup; + groupId: string; getIsEventExpanded: (eventId: string) => boolean; toggleIsEventExpanded: (eventId: string) => void; showLoadingMoreEvents: boolean; diff --git a/src/views/workflow-history-v2/workflow-history-group-details/__tests__/workflow-history-group-details.test.tsx b/src/views/workflow-history-v2/workflow-history-group-details/__tests__/workflow-history-group-details.test.tsx new file mode 100644 index 000000000..6551067aa --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-group-details/__tests__/workflow-history-group-details.test.tsx @@ -0,0 +1,220 @@ +import { render, screen, userEvent } from '@/test-utils/rtl'; + +import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; + +import WorkflowHistoryGroupDetails from '../workflow-history-group-details'; +import { type GroupDetailsEntries } from '../workflow-history-group-details.types'; + +jest.mock( + '../../workflow-history-event-details/workflow-history-event-details', + () => + jest.fn( + ({ + eventDetails, + }: { + eventDetails: Array; + workflowPageParams: WorkflowPageParams; + }) => ( +
+ Event Details ({eventDetails.length} entries) +
+ ) + ) +); + +describe(WorkflowHistoryGroupDetails.name, () => { + const mockGroupDetails: GroupDetailsEntries = [ + [ + 'event-1', + { + eventLabel: 'Event 1 Label', + eventDetails: [ + { + key: 'key1', + path: 'path1', + value: 'value1', + isGroup: false, + renderConfig: null, + }, + ], + }, + ], + [ + 'event-2', + { + eventLabel: 'Event 2 Label', + eventDetails: [ + { + key: 'key2', + path: 'path2', + value: 'value2', + isGroup: false, + renderConfig: null, + }, + ], + }, + ], + [ + 'event-3', + { + eventLabel: 'Event 3 Label', + eventDetails: [ + { + key: 'key3', + path: 'path3', + value: 'value3', + isGroup: false, + renderConfig: null, + }, + ], + }, + ], + ]; + + it('renders all event labels as buttons', () => { + setup({ groupDetailsEntries: mockGroupDetails }); + + expect(screen.getByText('Event 1 Label')).toBeInTheDocument(); + expect(screen.getByText('Event 2 Label')).toBeInTheDocument(); + expect(screen.getByText('Event 3 Label')).toBeInTheDocument(); + }); + + it('renders WorkflowHistoryEventDetails with first event details by default', () => { + setup({ groupDetailsEntries: mockGroupDetails }); + + expect( + screen.getByLabelText('Workflow history event details') + ).toBeInTheDocument(); + expect(screen.getByText('Event Details (1 entries)')).toBeInTheDocument(); + }); + + it('selects the event matching initialEventId', () => { + setup({ + groupDetailsEntries: mockGroupDetails, + initialEventId: 'event-2', + }); + + const eventDetails = screen.getByLabelText( + 'Workflow history event details' + ); + expect(eventDetails).toBeInTheDocument(); + expect(screen.getByText('Event Details (1 entries)')).toBeInTheDocument(); + }); + + it('defaults to first event when initialEventId is not provided', () => { + setup({ groupDetailsEntries: mockGroupDetails }); + + const eventDetails = screen.getByLabelText( + 'Workflow history event details' + ); + expect(eventDetails).toBeInTheDocument(); + }); + + it('defaults to first event when initialEventId is not found', () => { + setup({ + groupDetailsEntries: mockGroupDetails, + initialEventId: 'non-existent-event', + }); + + const eventDetails = screen.getByLabelText( + 'Workflow history event details' + ); + expect(eventDetails).toBeInTheDocument(); + }); + + it('changes selected event when a button is clicked', async () => { + const { user } = setup({ groupDetailsEntries: mockGroupDetails }); + + // Initially should show first event + expect(screen.getByText('Event Details (1 entries)')).toBeInTheDocument(); + + // Click on second event button + const event2Button = screen.getByText('Event 2 Label'); + await user.click(event2Button); + + // Should still show event details (the mock returns same format) + expect( + screen.getByLabelText('Workflow history event details') + ).toBeInTheDocument(); + }); + + it('calls onClose when close button is clicked', async () => { + const mockOnClose = jest.fn(); + const { user } = setup({ + groupDetailsEntries: mockGroupDetails, + onClose: mockOnClose, + }); + + const closeButton = screen.getByLabelText('Close event details'); + await user.click(closeButton); + + expect(mockOnClose).toHaveBeenCalledTimes(1); + }); + + it('does not render the close button when onClose is not provided', () => { + setup({ + groupDetailsEntries: mockGroupDetails, + onClose: undefined, + }); + + const closeButton = screen.queryByLabelText('Close event details'); + expect(closeButton).not.toBeInTheDocument(); + }); + + it('handles single event in groupDetails', () => { + const singleEventGroupDetails: GroupDetailsEntries = [ + [ + 'event-1', + { + eventLabel: 'Single Event', + eventDetails: [ + { + key: 'key1', + path: 'path1', + value: 'value1', + isGroup: false, + renderConfig: null, + }, + ], + }, + ], + ]; + + setup({ groupDetailsEntries: singleEventGroupDetails }); + + expect(screen.getByText('Single Event')).toBeInTheDocument(); + expect( + screen.getByLabelText('Workflow history event details') + ).toBeInTheDocument(); + }); +}); + +function setup({ + groupDetailsEntries, + initialEventId, + workflowPageParams = { + domain: 'test-domain', + cluster: 'test-cluster', + workflowId: 'test-workflow-id', + runId: 'test-run-id', + }, + onClose, +}: { + groupDetailsEntries: GroupDetailsEntries; + initialEventId?: string; + workflowPageParams?: WorkflowPageParams; + onClose?: () => void; +}) { + const user = userEvent.setup(); + + render( + + ); + + return { user }; +} diff --git a/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.styles.ts b/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.styles.ts new file mode 100644 index 000000000..54e8f0354 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.styles.ts @@ -0,0 +1,36 @@ +import { styled as createStyled, type Theme } from 'baseui'; +import { type ButtonGroupProps } from 'baseui/button-group'; +import { type StyleObject } from 'styletron-react'; + +export const styled = { + GroupDetailsContainer: createStyled( + 'div', + ({ $theme }: { $theme: Theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: $theme.sizing.scale500, + alignItems: 'stretch', + }) + ), + ActionsRow: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + gap: $theme.sizing.scale400, + })), + ExtraActions: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + display: 'flex', + gap: $theme.sizing.scale100, + alignItems: 'center', + })), +}; + +export const overrides = { + buttonGroup: { + Root: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + gap: $theme.sizing.scale0, + }), + }, + } satisfies ButtonGroupProps['overrides'], +}; diff --git a/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.tsx b/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.tsx new file mode 100644 index 000000000..1418011f6 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.tsx @@ -0,0 +1,73 @@ +import { useState } from 'react'; + +import { Button } from 'baseui/button'; +import { ButtonGroup } from 'baseui/button-group'; +import { MdClose } from 'react-icons/md'; + +import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; + +import WorkflowHistoryEventDetails from '../workflow-history-event-details/workflow-history-event-details'; + +import { overrides, styled } from './workflow-history-group-details.styles'; +import { type GroupDetailsEntries } from './workflow-history-group-details.types'; + +export default function WorkflowHistoryGroupDetails({ + groupDetailsEntries, + initialEventId, + workflowPageParams, + onClose, +}: { + groupDetailsEntries: GroupDetailsEntries; + initialEventId: string | undefined; + workflowPageParams: WorkflowPageParams; + onClose?: () => void; +}) { + const [selectedIndex, setSelectedIndex] = useState( + (() => { + const selectedIdx = groupDetailsEntries.findIndex( + ([eventId]) => eventId === initialEventId + ); + return selectedIdx >= 0 ? selectedIdx : 0; + })() + ); + + return ( + + + { + setSelectedIndex(index); + }} + overrides={overrides.buttonGroup} + > + {groupDetailsEntries.map(([eventId, eventDetailsTabContent]) => ( + + ))} + + + {/* Copy Link Button */} + {onClose && ( + + )} + + + + + ); +} diff --git a/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.types.ts b/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.types.ts new file mode 100644 index 000000000..7b51587f2 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.types.ts @@ -0,0 +1,8 @@ +import { type EventDetailsEntries } from '../workflow-history-event-details/workflow-history-event-details.types'; + +export type EventDetailsTabContent = { + eventDetails: EventDetailsEntries; + eventLabel: string; +}; + +export type GroupDetailsEntries = Array<[string, EventDetailsTabContent]>; diff --git a/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.tsx b/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.tsx index 41aeb8797..f2533950e 100644 --- a/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.tsx +++ b/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.tsx @@ -51,9 +51,10 @@ export default function WorkflowHistoryGroupedTable({ behavior: 'auto', }, })} - itemContent={(_, [__, group]) => ( + itemContent={(_, [groupId, group]) => ( Date: Wed, 3 Dec 2025 11:16:45 +0100 Subject: [PATCH 04/11] Add incomplete implementation of panel details entry, without styles Signed-off-by: Adhitya Mamallan --- .../workflow-history-event-details.tsx | 46 +++++++++++-------- ...flow-history-panel-details-entry.styles.ts | 0 .../workflow-history-panel-details-entry.tsx | 26 +++++++++++ ...kflow-history-panel-details-entry.types.ts | 8 ++++ 4 files changed, 61 insertions(+), 19 deletions(-) create mode 100644 src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.styles.ts create mode 100644 src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.tsx create mode 100644 src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.types.ts diff --git a/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx b/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx index ab77fcedc..1d3fe47bc 100644 --- a/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx +++ b/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx @@ -1,15 +1,11 @@ import { useMemo } from 'react'; +import WorkflowHistoryEventDetailsEntry from '@/views/workflow-history/workflow-history-event-details-entry/workflow-history-event-details-entry'; import WorkflowHistoryEventDetailsGroup from '@/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group'; import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; -import WorkflowHistoryGroupDetailsJson from '../workflow-history-group-details-json/workflow-history-group-details-json'; - import { styled } from './workflow-history-event-details.styles'; -import { - type EventDetailsEntries, - type EventDetailsSingleEntry, -} from './workflow-history-event-details.types'; +import { type EventDetailsEntries } from './workflow-history-event-details.types'; export default function WorkflowHistoryEventDetails({ eventDetails, @@ -20,9 +16,7 @@ export default function WorkflowHistoryEventDetails({ }) { const [panelDetails, restDetails] = useMemo( () => - eventDetails.reduce< - [Array, EventDetailsEntries] - >( + eventDetails.reduce<[EventDetailsEntries, EventDetailsEntries]>( ([panels, rest], entry) => { if (entry.renderConfig?.showInPanels && !entry.isGroup) { panels.push(entry); @@ -45,16 +39,30 @@ export default function WorkflowHistoryEventDetails({ {panelDetails.length > 0 && ( - {panelDetails.map((detail) => ( - - - - ))} + {panelDetails.map((detail) => { + return ( + + {!detail.isGroup ? ( + + ) : ( + + )} + + ); + })} )} diff --git a/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.styles.ts b/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.styles.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.tsx b/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.tsx new file mode 100644 index 000000000..c2a0ad09d --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.tsx @@ -0,0 +1,26 @@ +import { type Props } from './workflow-history-panel-details-entry.types'; + +export default function WorkflowHistoryPanelDetailsEntry({ + entryKey, + entryPath, + entryValue, + renderConfig, + isNegative, + ...decodedPageUrlParams +}: Props) { + const ValueComponent = renderConfig?.valueComponent; + + if (ValueComponent !== undefined) { + return ( + + ); + } + + return String(entryValue); +} diff --git a/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.types.ts b/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.types.ts new file mode 100644 index 000000000..3a9681d91 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.types.ts @@ -0,0 +1,8 @@ +import { + type EventDetailsConfig, + type EventDetailsValueComponentProps, +} from '../workflow-history-event-details/workflow-history-event-details.types'; + +export type Props = EventDetailsValueComponentProps & { + renderConfig: EventDetailsConfig | null; +}; From 0d7153299dd8632422995541b6d1ea6a67cabef8 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Wed, 3 Dec 2025 12:37:29 +0100 Subject: [PATCH 05/11] More changes Signed-off-by: Adhitya Mamallan --- .../workflow-history-event-details.test.tsx | 54 +++++++++---------- .../workflow-history-event-details.tsx | 27 +++------- ...flow-history-panel-details-entry.styles.ts | 33 ++++++++++++ .../workflow-history-panel-details-entry.tsx | 47 +++++++++++----- ...kflow-history-panel-details-entry.types.ts | 12 +++-- .../workflow-history-event-details.config.ts | 40 ++++++++++++++ 6 files changed, 146 insertions(+), 67 deletions(-) diff --git a/src/views/workflow-history-v2/workflow-history-event-details/__tests__/workflow-history-event-details.test.tsx b/src/views/workflow-history-v2/workflow-history-event-details/__tests__/workflow-history-event-details.test.tsx index db87285fa..53f464c9f 100644 --- a/src/views/workflow-history-v2/workflow-history-event-details/__tests__/workflow-history-event-details.test.tsx +++ b/src/views/workflow-history-v2/workflow-history-event-details/__tests__/workflow-history-event-details.test.tsx @@ -2,28 +2,20 @@ import { render, screen } from '@/test-utils/rtl'; import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; +import type WorkflowHistoryPanelDetailsEntry from '../../workflow-history-panel-details-entry/workflow-history-panel-details-entry'; import WorkflowHistoryEventDetails from '../workflow-history-event-details'; import { type EventDetailsEntries } from '../workflow-history-event-details.types'; -jest.mock( - '../../workflow-history-group-details-json/workflow-history-group-details-json', +jest.mock( + '../../workflow-history-panel-details-entry/workflow-history-panel-details-entry', () => - jest.fn( - ({ - entryPath, - entryValue, - isNegative, - }: { - entryPath: string; - entryValue: any; - isNegative?: boolean; - }) => ( -
- JSON: {entryPath} = {JSON.stringify(entryValue)} - {isNegative && ' (negative)'} -
- ) - ) + jest.fn(({ detail }) => ( +
+ Panel Entry: {detail.path} ={' '} + {JSON.stringify(detail.isGroup ? detail.groupEntries : detail.value)} + {detail.isNegative && ' (negative)'} +
+ )) ); jest.mock( @@ -41,7 +33,7 @@ describe(WorkflowHistoryEventDetails.name, () => { setup({ eventDetails: [] }); expect(screen.getByText('No Details')).toBeInTheDocument(); - expect(screen.queryByTestId('group-details-json')).not.toBeInTheDocument(); + expect(screen.queryByTestId('panel-details-entry')).not.toBeInTheDocument(); expect(screen.queryByTestId('event-details-group')).not.toBeInTheDocument(); }); @@ -68,7 +60,7 @@ describe(WorkflowHistoryEventDetails.name, () => { setup({ eventDetails }); - expect(screen.queryByTestId('group-details-json')).not.toBeInTheDocument(); + expect(screen.queryByTestId('panel-details-entry')).not.toBeInTheDocument(); expect(screen.getByTestId('event-details-group')).toBeInTheDocument(); expect( screen.getByText('Event Details Group (2 entries)') @@ -102,8 +94,10 @@ describe(WorkflowHistoryEventDetails.name, () => { setup({ eventDetails }); - expect(screen.getByTestId('group-details-json')).toBeInTheDocument(); - expect(screen.getByText(/JSON: path1 = "value1"/)).toBeInTheDocument(); + expect(screen.getByTestId('panel-details-entry')).toBeInTheDocument(); + expect( + screen.getByText(/Panel Entry: path1 = "value1"/) + ).toBeInTheDocument(); expect(screen.getByTestId('event-details-group')).toBeInTheDocument(); expect( screen.getByText('Event Details Group (1 entries)') @@ -148,11 +142,13 @@ describe(WorkflowHistoryEventDetails.name, () => { setup({ eventDetails }); - const jsonComponents = screen.getAllByTestId('group-details-json'); - expect(jsonComponents).toHaveLength(2); - expect(screen.getByText(/JSON: path1 = "value1"/)).toBeInTheDocument(); + const panelEntries = screen.getAllByTestId('panel-details-entry'); + expect(panelEntries).toHaveLength(2); expect( - screen.getByText(/JSON: path2 = \{"nested":"value2"\}/) + screen.getByText(/Panel Entry: path1 = "value1"/) + ).toBeInTheDocument(); + expect( + screen.getByText(/Panel Entry: path2 = \{"nested":"value2"\}/) ).toBeInTheDocument(); expect(screen.getByTestId('event-details-group')).toBeInTheDocument(); expect( @@ -160,7 +156,7 @@ describe(WorkflowHistoryEventDetails.name, () => { ).toBeInTheDocument(); }); - it('passes correct props to WorkflowHistoryGroupDetailsJson', () => { + it('passes correct props to WorkflowHistoryPanelDetailsEntry', () => { const eventDetails: EventDetailsEntries = [ { key: 'key1', @@ -178,7 +174,9 @@ describe(WorkflowHistoryEventDetails.name, () => { setup({ eventDetails }); - expect(screen.getByText(/JSON: path1 = "value1"/)).toBeInTheDocument(); + expect( + screen.getByText(/Panel Entry: path1 = "value1"/) + ).toBeInTheDocument(); expect(screen.getByText(/\(negative\)/)).toBeInTheDocument(); }); }); diff --git a/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx b/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx index 1d3fe47bc..62cf53e5f 100644 --- a/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx +++ b/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx @@ -1,9 +1,10 @@ import { useMemo } from 'react'; -import WorkflowHistoryEventDetailsEntry from '@/views/workflow-history/workflow-history-event-details-entry/workflow-history-event-details-entry'; import WorkflowHistoryEventDetailsGroup from '@/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group'; import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; +import WorkflowHistoryPanelDetailsEntry from '../workflow-history-panel-details-entry/workflow-history-panel-details-entry'; + import { styled } from './workflow-history-event-details.styles'; import { type EventDetailsEntries } from './workflow-history-event-details.types'; @@ -18,7 +19,7 @@ export default function WorkflowHistoryEventDetails({ () => eventDetails.reduce<[EventDetailsEntries, EventDetailsEntries]>( ([panels, rest], entry) => { - if (entry.renderConfig?.showInPanels && !entry.isGroup) { + if (entry.renderConfig?.showInPanels) { panels.push(entry); } else { rest.push(entry); @@ -42,24 +43,10 @@ export default function WorkflowHistoryEventDetails({ {panelDetails.map((detail) => { return ( - {!detail.isGroup ? ( - - ) : ( - - )} + ); })} diff --git a/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.styles.ts b/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.styles.ts index e69de29bb..41d815466 100644 --- a/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.styles.ts +++ b/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.styles.ts @@ -0,0 +1,33 @@ +import { styled as createStyled, type Theme } from 'baseui'; + +export const styled = { + PanelContainer: createStyled<'div', { $isNegative?: boolean }>( + 'div', + ({ $theme, $isNegative }: { $theme: Theme; $isNegative?: boolean }) => ({ + padding: $theme.sizing.scale600, + backgroundColor: $isNegative + ? $theme.colors.backgroundNegativeLight + : $theme.colors.backgroundSecondary, + borderRadius: $theme.borders.radius300, + height: '100%', + }) + ), + PanelLabel: createStyled<'div', { $isNegative?: boolean }>( + 'div', + ({ $theme, $isNegative }: { $theme: Theme; $isNegative?: boolean }) => ({ + color: $isNegative + ? $theme.colors.contentNegative + : $theme.colors.contentPrimary, + ...$theme.typography.LabelSmall, + }) + ), + PanelValue: createStyled<'div', { $isNegative?: boolean }>( + 'div', + ({ $theme, $isNegative }: { $theme: Theme; $isNegative?: boolean }) => ({ + padding: `${$theme.sizing.scale500} 0`, + color: $isNegative + ? $theme.colors.contentNegative + : $theme.colors.contentPrimary, + }) + ), +}; diff --git a/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.tsx b/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.tsx index c2a0ad09d..20db18b0e 100644 --- a/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.tsx +++ b/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.tsx @@ -1,26 +1,45 @@ +import WorkflowHistoryEventDetailsGroup from '@/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group'; + +import { styled } from './workflow-history-panel-details-entry.styles'; import { type Props } from './workflow-history-panel-details-entry.types'; export default function WorkflowHistoryPanelDetailsEntry({ - entryKey, - entryPath, - entryValue, - renderConfig, - isNegative, - ...decodedPageUrlParams + detail, + ...workflowPageParams }: Props) { - const ValueComponent = renderConfig?.valueComponent; + const ValueComponent = detail.renderConfig?.valueComponent; - if (ValueComponent !== undefined) { + if (ValueComponent !== undefined && !detail.isGroup) { return ( ); } - return String(entryValue); + return ( + + + {detail.path} + + + {detail.isGroup ? ( + + ) : ( + detail.value + )} + + + ); } diff --git a/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.types.ts b/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.types.ts index 3a9681d91..75190199f 100644 --- a/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.types.ts +++ b/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.types.ts @@ -1,8 +1,10 @@ +import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; + import { - type EventDetailsConfig, - type EventDetailsValueComponentProps, + type EventDetailsGroupEntry, + type EventDetailsSingleEntry, } from '../workflow-history-event-details/workflow-history-event-details.types'; -export type Props = EventDetailsValueComponentProps & { - renderConfig: EventDetailsConfig | null; -}; +export type Props = { + detail: EventDetailsSingleEntry | EventDetailsGroupEntry; +} & WorkflowPageParams; diff --git a/src/views/workflow-history/config/workflow-history-event-details.config.ts b/src/views/workflow-history/config/workflow-history-event-details.config.ts index 1eb57d8fa..c73c14dbc 100644 --- a/src/views/workflow-history/config/workflow-history-event-details.config.ts +++ b/src/views/workflow-history/config/workflow-history-event-details.config.ts @@ -9,17 +9,32 @@ import { type WorkflowHistoryEventDetailsConfig } from '../workflow-history-even import WorkflowHistoryEventDetailsJson from '../workflow-history-event-details-json/workflow-history-event-details-json'; import WorkflowHistoryEventDetailsPlaceholderText from '../workflow-history-event-details-placeholder-text/workflow-history-event-details-placeholder-text'; +/** + * Configuration array for customizing how workflow history event details are rendered. + * Each config entry defines matching criteria and rendering behavior for specific event fields. + * Configs are evaluated in order, and the first matching config is applied to each field. + */ const workflowHistoryEventDetailsConfig = [ + /** + * Hides fields with null or undefined values from the event details display. + */ { name: 'Filter empty value', customMatcher: ({ value }) => value === null || value === undefined, hide: () => true, }, + /** + * Hides internal fields (taskId, eventType) that are not useful for display. + */ { name: 'Filter unneeded values', pathRegex: '(taskId|eventType)$', hide: () => true, }, + /** + * Displays a placeholder text for timeout/retry fields that are set to 0 (not configured). + * Also removes the "Seconds" suffix from labels since formatted durations may be in minutes/hours. + */ { name: 'Not set placeholder', customMatcher: ({ value, path }) => { @@ -34,11 +49,17 @@ const workflowHistoryEventDetailsConfig = [ valueComponent: () => createElement(WorkflowHistoryEventDetailsPlaceholderText), }, + /** + * Formats Date objects as human-readable time strings. + */ { name: 'Date object as time string', customMatcher: ({ value }) => value instanceof Date, valueComponent: ({ entryValue }) => formatDate(entryValue), }, + /** + * Renders task list names as clickable links that navigate to the task list view. + */ { name: 'Tasklists as links', key: 'taskList', @@ -50,6 +71,10 @@ const workflowHistoryEventDetailsConfig = [ }); }, }, + /** + * Renders JSON fields (input, result, details, etc.) as formatted PrettyJson components. + * Uses forceWrap to ensure proper wrapping of long JSON content. + */ { name: 'Json as PrettyJson', pathRegex: @@ -57,6 +82,10 @@ const workflowHistoryEventDetailsConfig = [ valueComponent: WorkflowHistoryEventDetailsJson, forceWrap: true, }, + /** + * Formats duration fields (ending in TimeoutSeconds, BackoffSeconds, or InSeconds) as human-readable durations. + * Removes the "Seconds" suffix from labels since formatted durations may be in minutes/hours. + */ { name: 'Duration & interval seconds', pathRegex: '(TimeoutSeconds|BackoffSeconds|InSeconds)$', @@ -64,6 +93,10 @@ const workflowHistoryEventDetailsConfig = [ valueComponent: ({ entryValue }) => formatDuration({ seconds: entryValue > 0 ? entryValue : 0, nanos: 0 }), }, + /** + * Renders workflow execution objects as clickable links that navigate to the workflow view. + * Applies to parentWorkflowExecution, externalWorkflowExecution, and workflowExecution fields. + */ { name: 'WorkflowExecution as link', pathRegex: @@ -77,6 +110,10 @@ const workflowHistoryEventDetailsConfig = [ }); }, }, + /** + * Renders run ID fields as clickable links that navigate to the corresponding workflow run. + * Applies to firstExecutionRunId, originalExecutionRunId, newExecutionRunId, and continuedExecutionRunId. + */ { name: 'RunIds as link', pathRegex: @@ -90,6 +127,9 @@ const workflowHistoryEventDetailsConfig = [ }); }, }, + /** + * Renames the "attempt" field label to "retryAttempt" for better clarity. + */ { name: 'Retry config attempt as retryAttempt', key: 'attempt', From e3b78b60d4d01f2963cf5d5ccc33cfcf8a27467b Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Wed, 3 Dec 2025 14:58:18 +0100 Subject: [PATCH 06/11] Add unit tests and isolate prop types Signed-off-by: Adhitya Mamallan --- .../workflow-history-event-details.tsx | 11 +- .../workflow-history-event-details.types.ts | 5 + .../workflow-history-group-details.tsx | 11 +- .../workflow-history-group-details.types.ts | 9 + ...kflow-history-panel-details-entry.test.tsx | 176 ++++++++++++++++++ .../workflow-history-event-details.config.ts | 40 ---- 6 files changed, 197 insertions(+), 55 deletions(-) create mode 100644 src/views/workflow-history-v2/workflow-history-panel-details-entry/__tests__/workflow-history-panel-details-entry.test.tsx diff --git a/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx b/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx index 62cf53e5f..d3da63093 100644 --- a/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx +++ b/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx @@ -1,20 +1,19 @@ import { useMemo } from 'react'; import WorkflowHistoryEventDetailsGroup from '@/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group'; -import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; import WorkflowHistoryPanelDetailsEntry from '../workflow-history-panel-details-entry/workflow-history-panel-details-entry'; import { styled } from './workflow-history-event-details.styles'; -import { type EventDetailsEntries } from './workflow-history-event-details.types'; +import { + type Props, + type EventDetailsEntries, +} from './workflow-history-event-details.types'; export default function WorkflowHistoryEventDetails({ eventDetails, workflowPageParams, -}: { - eventDetails: EventDetailsEntries; - workflowPageParams: WorkflowPageParams; -}) { +}: Props) { const [panelDetails, restDetails] = useMemo( () => eventDetails.reduce<[EventDetailsEntries, EventDetailsEntries]>( diff --git a/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.types.ts b/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.types.ts index 883c5d233..1929ad4ad 100644 --- a/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.types.ts +++ b/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.types.ts @@ -1,5 +1,10 @@ import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; +export type Props = { + eventDetails: EventDetailsEntries; + workflowPageParams: WorkflowPageParams; +}; + export type EventDetailsFuncArgs = { path: string; key: string; diff --git a/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.tsx b/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.tsx index 1418011f6..f79fd544c 100644 --- a/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.tsx +++ b/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.tsx @@ -4,24 +4,17 @@ import { Button } from 'baseui/button'; import { ButtonGroup } from 'baseui/button-group'; import { MdClose } from 'react-icons/md'; -import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; - import WorkflowHistoryEventDetails from '../workflow-history-event-details/workflow-history-event-details'; import { overrides, styled } from './workflow-history-group-details.styles'; -import { type GroupDetailsEntries } from './workflow-history-group-details.types'; +import { type Props } from './workflow-history-group-details.types'; export default function WorkflowHistoryGroupDetails({ groupDetailsEntries, initialEventId, workflowPageParams, onClose, -}: { - groupDetailsEntries: GroupDetailsEntries; - initialEventId: string | undefined; - workflowPageParams: WorkflowPageParams; - onClose?: () => void; -}) { +}: Props) { const [selectedIndex, setSelectedIndex] = useState( (() => { const selectedIdx = groupDetailsEntries.findIndex( diff --git a/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.types.ts b/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.types.ts index 7b51587f2..507fa3b6f 100644 --- a/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.types.ts +++ b/src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.types.ts @@ -1,3 +1,5 @@ +import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; + import { type EventDetailsEntries } from '../workflow-history-event-details/workflow-history-event-details.types'; export type EventDetailsTabContent = { @@ -6,3 +8,10 @@ export type EventDetailsTabContent = { }; export type GroupDetailsEntries = Array<[string, EventDetailsTabContent]>; + +export type Props = { + groupDetailsEntries: GroupDetailsEntries; + initialEventId: string | undefined; + workflowPageParams: WorkflowPageParams; + onClose?: () => void; +}; diff --git a/src/views/workflow-history-v2/workflow-history-panel-details-entry/__tests__/workflow-history-panel-details-entry.test.tsx b/src/views/workflow-history-v2/workflow-history-panel-details-entry/__tests__/workflow-history-panel-details-entry.test.tsx new file mode 100644 index 000000000..abc9f95b6 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-panel-details-entry/__tests__/workflow-history-panel-details-entry.test.tsx @@ -0,0 +1,176 @@ +import { render, screen } from '@/test-utils/rtl'; + +import type WorkflowHistoryEventDetailsGroup from '@/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group'; +import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; + +import { + type EventDetailsGroupEntry, + type EventDetailsSingleEntry, +} from '../../workflow-history-event-details/workflow-history-event-details.types'; +import WorkflowHistoryPanelDetailsEntry from '../workflow-history-panel-details-entry'; + +jest.mock( + '@/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group', + () => + jest.fn(({ entries, parentGroupPath }) => ( +
+
{`Event Details Group (${entries.length} entries)`}
+
{parentGroupPath && ` - Parent: ${parentGroupPath}`}
+
+ )) +); + +describe(WorkflowHistoryPanelDetailsEntry.name, () => { + it('renders a single entry with value', () => { + const detail: EventDetailsSingleEntry = { + key: 'test-key', + path: 'test.path', + value: 'test-value', + isGroup: false, + renderConfig: null, + }; + + setup({ detail }); + + expect(screen.getByText('test.path')).toBeInTheDocument(); + expect(screen.getByText('test-value')).toBeInTheDocument(); + }); + + it('renders a group entry with WorkflowHistoryEventDetailsGroup', () => { + const detail: EventDetailsGroupEntry = { + key: 'test-key', + path: 'test.group.path', + isGroup: true, + groupEntries: [ + { + key: 'entry1', + path: 'test.group.path.entry1', + value: 'value1', + isGroup: false, + renderConfig: null, + }, + { + key: 'entry2', + path: 'test.group.path.entry2', + value: 'value2', + isGroup: false, + renderConfig: null, + }, + ], + renderConfig: null, + }; + + setup({ detail }); + + expect(screen.getByText('test.group.path')).toBeInTheDocument(); + expect(screen.getByTestId('event-details-group')).toBeInTheDocument(); + expect( + screen.getByText('Event Details Group (2 entries)') + ).toBeInTheDocument(); + expect(screen.getByText(/Parent: test\.group\.path/)).toBeInTheDocument(); + }); + + it('renders a custom ValueComponent when provided for a single entry', () => { + const MockValueComponent = jest.fn( + ({ + entryKey, + entryPath, + entryValue, + isNegative, + domain, + cluster, + }: { + entryKey: string; + entryPath: string; + entryValue: any; + isNegative?: boolean; + domain: string; + cluster: string; + }) => ( +
+ Custom: {entryKey} - {entryPath} - {JSON.stringify(entryValue)} + {isNegative && ' (negative)'} + {domain} - {cluster} +
+ ) + ); + + const detail: EventDetailsSingleEntry = { + key: 'test-key', + path: 'test.path', + value: 'test-value', + isGroup: false, + renderConfig: { + name: 'Test Config', + key: 'test-key', + valueComponent: MockValueComponent, + }, + }; + + setup({ + detail, + workflowPageParams: { + domain: 'test-domain', + cluster: 'test-cluster', + workflowId: 'test-workflow-id', + runId: 'test-run-id', + }, + }); + + expect(screen.getByTestId('custom-value-component')).toBeInTheDocument(); + expect( + screen.getByText(/Custom: test-key - test\.path - "test-value"/) + ).toBeInTheDocument(); + expect(screen.getByText(/test-domain - test-cluster/)).toBeInTheDocument(); + }); + + it('does not render ValueComponent for group entries', () => { + const MockValueComponent = jest.fn(() => ( +
Custom Component
+ )); + + const detail: EventDetailsGroupEntry = { + key: 'test-key', + path: 'test.group.path', + isGroup: true, + groupEntries: [ + { + key: 'entry1', + path: 'test.group.path.entry1', + value: 'value1', + isGroup: false, + renderConfig: null, + }, + ], + renderConfig: { + name: 'Test Config', + key: 'test-key', + valueComponent: MockValueComponent, + }, + }; + + setup({ detail }); + + expect( + screen.queryByTestId('custom-value-component') + ).not.toBeInTheDocument(); + expect(screen.getByTestId('event-details-group')).toBeInTheDocument(); + }); +}); + +function setup({ + detail, + workflowPageParams = { + domain: 'test-domain', + cluster: 'test-cluster', + workflowId: 'test-workflow-id', + runId: 'test-run-id', + }, +}: { + detail: EventDetailsSingleEntry | EventDetailsGroupEntry; + workflowPageParams?: WorkflowPageParams; +}) { + render( + + ); +} diff --git a/src/views/workflow-history/config/workflow-history-event-details.config.ts b/src/views/workflow-history/config/workflow-history-event-details.config.ts index c73c14dbc..1eb57d8fa 100644 --- a/src/views/workflow-history/config/workflow-history-event-details.config.ts +++ b/src/views/workflow-history/config/workflow-history-event-details.config.ts @@ -9,32 +9,17 @@ import { type WorkflowHistoryEventDetailsConfig } from '../workflow-history-even import WorkflowHistoryEventDetailsJson from '../workflow-history-event-details-json/workflow-history-event-details-json'; import WorkflowHistoryEventDetailsPlaceholderText from '../workflow-history-event-details-placeholder-text/workflow-history-event-details-placeholder-text'; -/** - * Configuration array for customizing how workflow history event details are rendered. - * Each config entry defines matching criteria and rendering behavior for specific event fields. - * Configs are evaluated in order, and the first matching config is applied to each field. - */ const workflowHistoryEventDetailsConfig = [ - /** - * Hides fields with null or undefined values from the event details display. - */ { name: 'Filter empty value', customMatcher: ({ value }) => value === null || value === undefined, hide: () => true, }, - /** - * Hides internal fields (taskId, eventType) that are not useful for display. - */ { name: 'Filter unneeded values', pathRegex: '(taskId|eventType)$', hide: () => true, }, - /** - * Displays a placeholder text for timeout/retry fields that are set to 0 (not configured). - * Also removes the "Seconds" suffix from labels since formatted durations may be in minutes/hours. - */ { name: 'Not set placeholder', customMatcher: ({ value, path }) => { @@ -49,17 +34,11 @@ const workflowHistoryEventDetailsConfig = [ valueComponent: () => createElement(WorkflowHistoryEventDetailsPlaceholderText), }, - /** - * Formats Date objects as human-readable time strings. - */ { name: 'Date object as time string', customMatcher: ({ value }) => value instanceof Date, valueComponent: ({ entryValue }) => formatDate(entryValue), }, - /** - * Renders task list names as clickable links that navigate to the task list view. - */ { name: 'Tasklists as links', key: 'taskList', @@ -71,10 +50,6 @@ const workflowHistoryEventDetailsConfig = [ }); }, }, - /** - * Renders JSON fields (input, result, details, etc.) as formatted PrettyJson components. - * Uses forceWrap to ensure proper wrapping of long JSON content. - */ { name: 'Json as PrettyJson', pathRegex: @@ -82,10 +57,6 @@ const workflowHistoryEventDetailsConfig = [ valueComponent: WorkflowHistoryEventDetailsJson, forceWrap: true, }, - /** - * Formats duration fields (ending in TimeoutSeconds, BackoffSeconds, or InSeconds) as human-readable durations. - * Removes the "Seconds" suffix from labels since formatted durations may be in minutes/hours. - */ { name: 'Duration & interval seconds', pathRegex: '(TimeoutSeconds|BackoffSeconds|InSeconds)$', @@ -93,10 +64,6 @@ const workflowHistoryEventDetailsConfig = [ valueComponent: ({ entryValue }) => formatDuration({ seconds: entryValue > 0 ? entryValue : 0, nanos: 0 }), }, - /** - * Renders workflow execution objects as clickable links that navigate to the workflow view. - * Applies to parentWorkflowExecution, externalWorkflowExecution, and workflowExecution fields. - */ { name: 'WorkflowExecution as link', pathRegex: @@ -110,10 +77,6 @@ const workflowHistoryEventDetailsConfig = [ }); }, }, - /** - * Renders run ID fields as clickable links that navigate to the corresponding workflow run. - * Applies to firstExecutionRunId, originalExecutionRunId, newExecutionRunId, and continuedExecutionRunId. - */ { name: 'RunIds as link', pathRegex: @@ -127,9 +90,6 @@ const workflowHistoryEventDetailsConfig = [ }); }, }, - /** - * Renames the "attempt" field label to "retryAttempt" for better clarity. - */ { name: 'Retry config attempt as retryAttempt', key: 'attempt', From 10114a623d4c10d134221059577f8a52bd9b4a26 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Wed, 3 Dec 2025 15:12:38 +0100 Subject: [PATCH 07/11] address copilot comment Signed-off-by: Adhitya Mamallan --- .../workflow-history-event-group.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.tsx b/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.tsx index 83cf41e8f..825a908f4 100644 --- a/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.tsx +++ b/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.tsx @@ -75,6 +75,7 @@ export default function WorkflowHistoryEventGroup({ if (!event.eventId) return; const eventMetadata = eventsMetadata[index]; + if (!eventMetadata) return; const result = isPendingHistoryEvent(event) ? formatPendingWorkflowHistoryEvent(event) @@ -157,16 +158,18 @@ export default function WorkflowHistoryEventGroup({ {eventsMetadata.at(-1)?.label}
{timeMs ? formatDate(timeMs) : null}
- +
+ +
Placeholder for event details From d6d8c9a7c835270e2a4d0fa3040f9288f06226de Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Thu, 4 Dec 2025 13:03:52 +0100 Subject: [PATCH 08/11] update test Signed-off-by: Adhitya Mamallan --- .../__tests__/workflow-history-event-group.test.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx b/src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx index 6afa73f52..e6df7e877 100644 --- a/src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx +++ b/src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx @@ -3,6 +3,7 @@ import { render, screen, userEvent } from '@/test-utils/rtl'; import { completedActivityTaskEvents, scheduleActivityTaskEvent, + startActivityTaskEvent, } from '@/views/workflow-history/__fixtures__/workflow-history-activity-events'; import { mockActivityEventGroup, @@ -204,10 +205,10 @@ describe(WorkflowHistoryEventGroup.name, () => { ); }); - it('calls toggleIsEventExpanded when panel is toggled', async () => { + it('calls toggleIsEventExpanded for each event when panel is toggled', async () => { const eventGroup: HistoryEventsGroup = { ...mockActivityEventGroupWithMetadata, - events: [scheduleActivityTaskEvent], + events: [scheduleActivityTaskEvent, startActivityTaskEvent], }; const toggleIsEventExpanded = jest.fn(); @@ -217,9 +218,7 @@ describe(WorkflowHistoryEventGroup.name, () => { const headerLabel = screen.getByText('Mock event'); await user.click(headerLabel); - expect(toggleIsEventExpanded).toHaveBeenCalledWith( - scheduleActivityTaskEvent.eventId - ); + expect(toggleIsEventExpanded).toHaveBeenCalledTimes(2); }); it('handles missing event group time gracefully', () => { From 0e28730cbad2d68cb60c6c631d9911cd50a0faa1 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Fri, 5 Dec 2025 12:06:30 +0100 Subject: [PATCH 09/11] address comments Signed-off-by: Adhitya Mamallan --- .../generate-history-group-details.test.ts | 468 ++++++++++++++++++ .../helpers/generate-history-group-details.ts | 62 +++ .../workflow-history-event-details.test.tsx | 2 +- .../workflow-history-event-details.tsx | 20 +- .../workflow-history-event-group.test.tsx | 177 +++++-- .../helpers/get-summary-tab-content-entry.ts | 18 + .../workflow-history-event-group.tsx | 109 ++-- .../workflow-history-event-group.types.ts | 1 - .../workflow-history-grouped-table.tsx | 3 +- 9 files changed, 729 insertions(+), 131 deletions(-) create mode 100644 src/views/workflow-history-v2/helpers/__tests__/generate-history-group-details.test.ts create mode 100644 src/views/workflow-history-v2/helpers/generate-history-group-details.ts create mode 100644 src/views/workflow-history-v2/workflow-history-event-group/helpers/get-summary-tab-content-entry.ts diff --git a/src/views/workflow-history-v2/helpers/__tests__/generate-history-group-details.test.ts b/src/views/workflow-history-v2/helpers/__tests__/generate-history-group-details.test.ts new file mode 100644 index 000000000..a10e5c651 --- /dev/null +++ b/src/views/workflow-history-v2/helpers/__tests__/generate-history-group-details.test.ts @@ -0,0 +1,468 @@ +import formatPendingWorkflowHistoryEvent from '@/utils/data-formatters/format-pending-workflow-history-event'; +import formatWorkflowHistoryEvent from '@/utils/data-formatters/format-workflow-history-event'; +import { + scheduleActivityTaskEvent, + startActivityTaskEvent, + completeActivityTaskEvent, +} from '@/views/workflow-history/__fixtures__/workflow-history-activity-events'; +import { mockActivityEventGroup } from '@/views/workflow-history/__fixtures__/workflow-history-event-groups'; +import { pendingActivityTaskStartEvent } from '@/views/workflow-history/__fixtures__/workflow-history-pending-events'; +import isPendingHistoryEvent from '@/views/workflow-history/workflow-history-event-details/helpers/is-pending-history-event'; +import { type HistoryEventsGroup } from '@/views/workflow-history/workflow-history.types'; + +import generateHistoryEventDetails from '../generate-history-event-details'; +import generateHistoryGroupDetails from '../generate-history-group-details'; + +jest.mock('@/utils/data-formatters/format-pending-workflow-history-event'); +jest.mock('@/utils/data-formatters/format-workflow-history-event'); +jest.mock( + '@/views/workflow-history/workflow-history-event-details/helpers/is-pending-history-event' +); +jest.mock('../generate-history-event-details'); + +const mockedFormatPendingWorkflowHistoryEvent = + formatPendingWorkflowHistoryEvent as jest.Mock; +const mockedFormatWorkflowHistoryEvent = + formatWorkflowHistoryEvent as jest.Mock; +const mockedIsPendingHistoryEvent = + isPendingHistoryEvent as unknown as jest.Mock; +const mockedGenerateHistoryEventDetails = + generateHistoryEventDetails as jest.Mock; + +describe(generateHistoryGroupDetails.name, () => { + beforeEach(() => { + jest.resetAllMocks(); + mockedIsPendingHistoryEvent.mockReturnValue(false); + mockedFormatWorkflowHistoryEvent.mockReturnValue({ + activityId: '0', + activityType: 'TestActivity', + input: 'test input', + }); + mockedFormatPendingWorkflowHistoryEvent.mockReturnValue({ + activityId: '0', + activityType: 'TestActivity', + }); + mockedGenerateHistoryEventDetails.mockReturnValue([ + { + key: 'activityId', + path: 'activityId', + value: '0', + isGroup: false, + renderConfig: null, + }, + { + key: 'activityType', + path: 'activityType', + value: 'TestActivity', + isGroup: false, + renderConfig: null, + }, + ]); + }); + + it('should return empty arrays when eventGroup has no events', () => { + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroup, + events: [], + eventsMetadata: [], + }; + + const result = generateHistoryGroupDetails(eventGroup); + + expect(result.groupDetailsEntries).toEqual([]); + expect(result.summaryDetailsEntries).toEqual([]); + }); + + it('should generate group details entries for events with eventId and metadata', () => { + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroup, + events: [ + scheduleActivityTaskEvent, + { ...startActivityTaskEvent, eventId: '8' }, + ], + eventsMetadata: [ + { + label: 'Scheduled', + status: 'COMPLETED', + timeMs: 1725747370599, + timeLabel: 'Scheduled at 07 Sep, 22:16:10 UTC', + }, + { + label: 'Started', + status: 'COMPLETED', + timeMs: 1725747370612, + timeLabel: 'Started at 07 Sep, 22:16:10 UTC', + }, + ], + }; + + const result = generateHistoryGroupDetails(eventGroup); + + expect(result.groupDetailsEntries).toHaveLength(2); + expect(result.groupDetailsEntries[0][0]).toBe('7'); + expect(result.groupDetailsEntries[0][1].eventLabel).toBe('Scheduled'); + expect(result.groupDetailsEntries[1][0]).toBe('8'); + expect(result.groupDetailsEntries[1][1].eventLabel).toBe('Started'); + expect(mockedFormatWorkflowHistoryEvent).toHaveBeenCalledTimes(2); + expect(mockedGenerateHistoryEventDetails).toHaveBeenCalledTimes(2); + }); + + it('should skip events without corresponding metadata', () => { + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroup, + events: [ + scheduleActivityTaskEvent, + { ...startActivityTaskEvent, eventId: '8' }, + ], + eventsMetadata: [ + { + label: 'Scheduled', + status: 'COMPLETED', + timeMs: 1725747370599, + timeLabel: 'Scheduled at 07 Sep, 22:16:10 UTC', + }, + ], + }; + + const result = generateHistoryGroupDetails(eventGroup); + + expect(result.groupDetailsEntries).toHaveLength(1); + expect(result.groupDetailsEntries[0][0]).toBe('7'); + expect(mockedFormatWorkflowHistoryEvent).toHaveBeenCalledTimes(1); + }); + + it('should use formatPendingWorkflowHistoryEvent for pending events', () => { + mockedIsPendingHistoryEvent + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); + + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroup, + events: [scheduleActivityTaskEvent, pendingActivityTaskStartEvent], + eventsMetadata: [ + { + label: 'Scheduled', + status: 'COMPLETED', + timeMs: 1725747370599, + timeLabel: 'Scheduled at 07 Sep, 22:16:10 UTC', + }, + { + label: 'Pending', + status: 'WAITING', + timeMs: null, + timeLabel: 'Pending', + }, + ], + }; + + const result = generateHistoryGroupDetails(eventGroup); + + expect(result.groupDetailsEntries).toHaveLength(2); + expect(mockedIsPendingHistoryEvent).toHaveBeenCalled(); + expect(mockedFormatPendingWorkflowHistoryEvent).toHaveBeenCalledTimes(1); + expect(mockedFormatWorkflowHistoryEvent).toHaveBeenCalledTimes(1); + }); + + it('should use formatWorkflowHistoryEvent for non-pending events', () => { + mockedIsPendingHistoryEvent.mockReturnValue(false); + + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroup, + events: [scheduleActivityTaskEvent], + eventsMetadata: [ + { + label: 'Scheduled', + status: 'COMPLETED', + timeMs: 1725747370599, + timeLabel: 'Scheduled at 07 Sep, 22:16:10 UTC', + }, + ], + }; + + const result = generateHistoryGroupDetails(eventGroup); + + expect(result.groupDetailsEntries).toHaveLength(1); + expect(mockedIsPendingHistoryEvent).toHaveBeenCalled(); + expect(mockedFormatWorkflowHistoryEvent).toHaveBeenCalledTimes(1); + expect(mockedFormatPendingWorkflowHistoryEvent).not.toHaveBeenCalled(); + }); + + it('should merge additionalDetails from metadata into formatted event details', () => { + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroup, + events: [scheduleActivityTaskEvent], + eventsMetadata: [ + { + label: 'Scheduled', + status: 'COMPLETED', + timeMs: 1725747370599, + timeLabel: 'Scheduled at 07 Sep, 22:16:10 UTC', + additionalDetails: { + customField: 'custom value', + }, + }, + ], + }; + + generateHistoryGroupDetails(eventGroup); + + expect(mockedGenerateHistoryEventDetails).toHaveBeenCalledWith({ + details: { + activityId: '0', + activityType: 'TestActivity', + input: 'test input', + customField: 'custom value', + }, + negativeFields: undefined, + }); + }); + + it('should pass negativeFields from metadata to generateHistoryEventDetails', () => { + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroup, + events: [scheduleActivityTaskEvent], + eventsMetadata: [ + { + label: 'Scheduled', + status: 'COMPLETED', + timeMs: 1725747370599, + timeLabel: 'Scheduled at 07 Sep, 22:16:10 UTC', + negativeFields: ['error', 'failureReason'], + }, + ], + }; + + generateHistoryGroupDetails(eventGroup); + + expect(mockedGenerateHistoryEventDetails).toHaveBeenCalledWith({ + details: { + activityId: '0', + activityType: 'TestActivity', + input: 'test input', + }, + negativeFields: ['error', 'failureReason'], + }); + }); + + it('should generate summary details entries when summaryFields match event details', () => { + mockedGenerateHistoryEventDetails.mockReturnValue([ + { + key: 'activityId', + path: 'activityId', + value: '0', + isGroup: false, + renderConfig: null, + }, + { + key: 'activityType', + path: 'activityType', + value: 'TestActivity', + isGroup: false, + renderConfig: null, + }, + { + key: 'input', + path: 'input', + value: 'test input', + isGroup: false, + renderConfig: null, + }, + ]); + + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroup, + events: [scheduleActivityTaskEvent], + eventsMetadata: [ + { + label: 'Scheduled', + status: 'COMPLETED', + timeMs: 1725747370599, + timeLabel: 'Scheduled at 07 Sep, 22:16:10 UTC', + summaryFields: ['activityId', 'activityType'], + }, + ], + }; + + const result = generateHistoryGroupDetails(eventGroup); + + expect(result.summaryDetailsEntries).toHaveLength(1); + expect(result.summaryDetailsEntries[0][0]).toBe('7'); + expect(result.summaryDetailsEntries[0][1].eventLabel).toBe('Scheduled'); + expect(result.summaryDetailsEntries[0][1].eventDetails).toHaveLength(2); + expect(result.summaryDetailsEntries[0][1].eventDetails[0].path).toBe( + 'activityId' + ); + expect(result.summaryDetailsEntries[0][1].eventDetails[1].path).toBe( + 'activityType' + ); + }); + + it('should not generate summary details entries when summaryFields do not match any event details', () => { + mockedGenerateHistoryEventDetails.mockReturnValue([ + { + key: 'activityId', + path: 'activityId', + value: '0', + isGroup: false, + renderConfig: null, + }, + ]); + + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroup, + events: [scheduleActivityTaskEvent], + eventsMetadata: [ + { + label: 'Scheduled', + status: 'COMPLETED', + timeMs: 1725747370599, + timeLabel: 'Scheduled at 07 Sep, 22:16:10 UTC', + summaryFields: ['nonExistentField'], + }, + ], + }; + + const result = generateHistoryGroupDetails(eventGroup); + + expect(result.summaryDetailsEntries).toHaveLength(0); + }); + + it('should not generate summary details entries when summaryFields is not provided', () => { + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroup, + events: [scheduleActivityTaskEvent], + eventsMetadata: [ + { + label: 'Scheduled', + status: 'COMPLETED', + timeMs: 1725747370599, + timeLabel: 'Scheduled at 07 Sep, 22:16:10 UTC', + }, + ], + }; + + const result = generateHistoryGroupDetails(eventGroup); + + expect(result.summaryDetailsEntries).toHaveLength(0); + }); + + it('should handle events where formatWorkflowHistoryEvent returns null', () => { + mockedFormatWorkflowHistoryEvent.mockReturnValue(null); + + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroup, + events: [scheduleActivityTaskEvent], + eventsMetadata: [ + { + label: 'Scheduled', + status: 'COMPLETED', + timeMs: 1725747370599, + timeLabel: 'Scheduled at 07 Sep, 22:16:10 UTC', + }, + ], + }; + + const result = generateHistoryGroupDetails(eventGroup); + + expect(result.groupDetailsEntries).toHaveLength(1); + expect(result.groupDetailsEntries[0][1].eventDetails).toEqual([]); + expect(mockedGenerateHistoryEventDetails).not.toHaveBeenCalled(); + }); + + it('should handle multiple events with mixed summaryFields', () => { + mockedGenerateHistoryEventDetails.mockReturnValue([ + { + key: 'activityId', + path: 'activityId', + value: '0', + isGroup: false, + renderConfig: null, + }, + { + key: 'activityType', + path: 'activityType', + value: 'TestActivity', + isGroup: false, + renderConfig: null, + }, + { + key: 'result', + path: 'result', + value: 'success', + isGroup: false, + renderConfig: null, + }, + ]); + + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroup, + events: [ + scheduleActivityTaskEvent, + { ...startActivityTaskEvent, eventId: '8' }, + { ...completeActivityTaskEvent, eventId: '9' }, + ], + eventsMetadata: [ + { + label: 'Scheduled', + status: 'COMPLETED', + timeMs: 1725747370599, + timeLabel: 'Scheduled at 07 Sep, 22:16:10 UTC', + summaryFields: ['activityId'], + }, + { + label: 'Started', + status: 'COMPLETED', + timeMs: 1725747370612, + timeLabel: 'Started at 07 Sep, 22:16:10 UTC', + summaryFields: ['activityType'], + }, + { + label: 'Completed', + status: 'COMPLETED', + timeMs: 1725747370632, + timeLabel: 'Completed at 07 Sep, 22:16:10 UTC', + summaryFields: ['result'], + }, + ], + }; + + const result = generateHistoryGroupDetails(eventGroup); + + expect(result.groupDetailsEntries).toHaveLength(3); + expect(result.summaryDetailsEntries).toHaveLength(3); + expect(result.summaryDetailsEntries[0][0]).toBe('7'); + expect(result.summaryDetailsEntries[1][0]).toBe('8'); + expect(result.summaryDetailsEntries[2][0]).toBe('9'); + }); + + it('should handle events with both additionalDetails and negativeFields', () => { + const eventGroup: HistoryEventsGroup = { + ...mockActivityEventGroup, + events: [scheduleActivityTaskEvent], + eventsMetadata: [ + { + label: 'Scheduled', + status: 'COMPLETED', + timeMs: 1725747370599, + timeLabel: 'Scheduled at 07 Sep, 22:16:10 UTC', + additionalDetails: { + customField: 'custom value', + }, + negativeFields: ['error'], + }, + ], + }; + + generateHistoryGroupDetails(eventGroup); + + expect(mockedGenerateHistoryEventDetails).toHaveBeenCalledWith({ + details: { + activityId: '0', + activityType: 'TestActivity', + input: 'test input', + customField: 'custom value', + }, + negativeFields: ['error'], + }); + }); +}); diff --git a/src/views/workflow-history-v2/helpers/generate-history-group-details.ts b/src/views/workflow-history-v2/helpers/generate-history-group-details.ts new file mode 100644 index 000000000..1e58324f5 --- /dev/null +++ b/src/views/workflow-history-v2/helpers/generate-history-group-details.ts @@ -0,0 +1,62 @@ +import formatPendingWorkflowHistoryEvent from '@/utils/data-formatters/format-pending-workflow-history-event'; +import formatWorkflowHistoryEvent from '@/utils/data-formatters/format-workflow-history-event'; +import isPendingHistoryEvent from '@/views/workflow-history/workflow-history-event-details/helpers/is-pending-history-event'; +import { type HistoryEventsGroup } from '@/views/workflow-history/workflow-history.types'; + +import generateHistoryEventDetails from '../helpers/generate-history-event-details'; +import { type EventDetailsTabContent } from '../workflow-history-group-details/workflow-history-group-details.types'; + +export default function generateHistoryGroupDetails( + eventGroup: HistoryEventsGroup +) { + const groupDetailsEntries: Array<[string, EventDetailsTabContent]> = [], + summaryDetailsEntries: Array<[string, EventDetailsTabContent]> = []; + + eventGroup.events.forEach((event, index) => { + const eventId = event.eventId ?? event.computedEventId; + + const eventMetadata = eventGroup.eventsMetadata[index]; + if (!eventMetadata) return; + + const result = isPendingHistoryEvent(event) + ? formatPendingWorkflowHistoryEvent(event) + : formatWorkflowHistoryEvent(event); + + const eventDetails = result + ? generateHistoryEventDetails({ + details: { + ...result, + ...eventMetadata.additionalDetails, + }, + negativeFields: eventMetadata.negativeFields, + }) + : []; + + groupDetailsEntries.push([ + eventId, + { + eventLabel: eventMetadata.label, + eventDetails, + } satisfies EventDetailsTabContent, + ]); + + const eventSummaryDetails = eventDetails.filter((detail) => + eventMetadata.summaryFields?.includes(detail.path) + ); + + if (eventSummaryDetails.length > 0) { + summaryDetailsEntries.push([ + eventId, + { + eventLabel: eventMetadata.label, + eventDetails: eventSummaryDetails, + } satisfies EventDetailsTabContent, + ]); + } + }); + + return { + groupDetailsEntries, + summaryDetailsEntries, + }; +} diff --git a/src/views/workflow-history-v2/workflow-history-event-details/__tests__/workflow-history-event-details.test.tsx b/src/views/workflow-history-v2/workflow-history-event-details/__tests__/workflow-history-event-details.test.tsx index 53f464c9f..db0c18231 100644 --- a/src/views/workflow-history-v2/workflow-history-event-details/__tests__/workflow-history-event-details.test.tsx +++ b/src/views/workflow-history-v2/workflow-history-event-details/__tests__/workflow-history-event-details.test.tsx @@ -156,7 +156,7 @@ describe(WorkflowHistoryEventDetails.name, () => { ).toBeInTheDocument(); }); - it('passes correct props to WorkflowHistoryPanelDetailsEntry', () => { + it('correctly renders panel details entry', () => { const eventDetails: EventDetailsEntries = [ { key: 'key1', diff --git a/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx b/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx index d3da63093..b93416aa8 100644 --- a/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx +++ b/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx @@ -1,14 +1,13 @@ import { useMemo } from 'react'; +import partition from 'lodash/partition'; + import WorkflowHistoryEventDetailsGroup from '@/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group'; import WorkflowHistoryPanelDetailsEntry from '../workflow-history-panel-details-entry/workflow-history-panel-details-entry'; import { styled } from './workflow-history-event-details.styles'; -import { - type Props, - type EventDetailsEntries, -} from './workflow-history-event-details.types'; +import { type Props } from './workflow-history-event-details.types'; export default function WorkflowHistoryEventDetails({ eventDetails, @@ -16,18 +15,7 @@ export default function WorkflowHistoryEventDetails({ }: Props) { const [panelDetails, restDetails] = useMemo( () => - eventDetails.reduce<[EventDetailsEntries, EventDetailsEntries]>( - ([panels, rest], entry) => { - if (entry.renderConfig?.showInPanels) { - panels.push(entry); - } else { - rest.push(entry); - } - - return [panels, rest]; - }, - [[], []] - ), + partition(eventDetails, (detail) => detail.renderConfig?.showInPanels), [eventDetails] ); diff --git a/src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx b/src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx index e6df7e877..01dc89a20 100644 --- a/src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx +++ b/src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx @@ -14,8 +14,10 @@ import type WorkflowHistoryGroupLabel from '@/views/workflow-history/workflow-hi import type WorkflowHistoryTimelineResetButton from '@/views/workflow-history/workflow-history-timeline-reset-button/workflow-history-timeline-reset-button'; import { type HistoryEventsGroup } from '@/views/workflow-history/workflow-history.types'; +import * as generateHistoryGroupDetailsModule from '../../helpers/generate-history-group-details'; import type { EventDetailsEntries } from '../../workflow-history-event-details/workflow-history-event-details.types'; import type WorkflowHistoryGroupDetails from '../../workflow-history-group-details/workflow-history-group-details'; +import type { GroupDetailsEntries } from '../../workflow-history-group-details/workflow-history-group-details.types'; import WorkflowHistoryEventGroup from '../workflow-history-event-group'; import type { Props } from '../workflow-history-event-group.types'; @@ -23,19 +25,7 @@ jest.mock('@/utils/data-formatters/format-date', () => jest.fn((timeMs: number) => `Formatted: ${timeMs}`) ); -jest.mock('@/utils/data-formatters/format-pending-workflow-history-event', () => - jest.fn(() => ({ mockFormatted: true })) -); - -jest.mock('@/utils/data-formatters/format-workflow-history-event', () => - jest.fn(() => ({ mockFormatted: true })) -); - -const mockGenerateHistoryEventDetails = jest.fn(); - -jest.mock('../../helpers/generate-history-event-details', () => - jest.fn(() => mockGenerateHistoryEventDetails()) -); +jest.mock('../../helpers/generate-history-group-details', () => jest.fn()); jest.mock( '../../workflow-history-group-details/workflow-history-group-details', @@ -145,6 +135,10 @@ const mockDecisionEventGroupWithMetadata: HistoryEventsGroup = { }; describe(WorkflowHistoryEventGroup.name, () => { + beforeEach(() => { + jest.restoreAllMocks(); + }); + it('renders group correctly', () => { setup({ eventGroup: mockActivityEventGroupWithMetadata }); @@ -283,7 +277,7 @@ describe(WorkflowHistoryEventGroup.name, () => { }); }); - it('shows summary tab when summaryFields are available to show', () => { + it('shows summary tab when summaryDetailsEntries are available', () => { const mockEventDetails: EventDetailsEntries = [ { key: 'input', @@ -299,10 +293,20 @@ describe(WorkflowHistoryEventGroup.name, () => { isGroup: false, renderConfig: null, }, + ]; + + const mockSummaryDetails: EventDetailsEntries = [ + { + key: 'input', + path: 'input', + value: 'test input value', + isGroup: false, + renderConfig: null, + }, { - key: 'result', - path: 'result', - value: 'test result', + key: 'activityType', + path: 'activityType', + value: 'TestActivity', isGroup: false, renderConfig: null, }, @@ -340,13 +344,57 @@ describe(WorkflowHistoryEventGroup.name, () => { (eventId: string) => eventId === completedActivityTaskEvents[0].eventId ); - setup({ eventGroup, getIsEventExpanded, mockEventDetails }); + setup({ + eventGroup, + getIsEventExpanded, + mockGroupDetails: { + groupDetailsEntries: [ + [ + completedActivityTaskEvents[0].eventId!, + { + eventLabel: 'Scheduled', + eventDetails: mockEventDetails, + }, + ], + [ + completedActivityTaskEvents[1].eventId!, + { + eventLabel: 'Started', + eventDetails: mockEventDetails, + }, + ], + [ + completedActivityTaskEvents[2].eventId!, + { + eventLabel: 'Completed', + eventDetails: mockEventDetails, + }, + ], + ], + summaryDetailsEntries: [ + [ + completedActivityTaskEvents[0].eventId!, + { + eventLabel: 'Scheduled', + eventDetails: mockSummaryDetails, + }, + ], + [ + completedActivityTaskEvents[1].eventId!, + { + eventLabel: 'Started', + eventDetails: mockSummaryDetails, + }, + ], + ], + }, + }); - // Summary tab should appear in groupDetailsEntries when there are multiple events and summary details + // Summary tab should appear in groupDetailsEntries when there are summary details expect(screen.getByText('Summary')).toBeInTheDocument(); }); - it('does not show summary tab when there is only one event', () => { + it('does not show summary tab when summaryDetailsEntries is empty', () => { const mockEventDetails: EventDetailsEntries = [ { key: 'input', @@ -371,14 +419,27 @@ describe(WorkflowHistoryEventGroup.name, () => { ], }; - setup({ eventGroup, mockEventDetails }); + setup({ + eventGroup, + mockGroupDetails: { + groupDetailsEntries: [ + [ + scheduleActivityTaskEvent.eventId!, + { + eventLabel: 'Scheduled', + eventDetails: mockEventDetails, + }, + ], + ], + summaryDetailsEntries: [], + }, + }); - // Summary tab should not appear when there's only one event - expect(screen.queryByTestId('event-summary_7')).not.toBeInTheDocument(); + // Summary tab should not appear when summaryDetailsEntries is empty expect(screen.queryByText('Summary')).not.toBeInTheDocument(); }); - it('does not show summary tab when summaryFields do not match any event details', () => { + it('does not show summary tab when summaryDetailsEntries is empty', () => { const mockEventDetails: EventDetailsEntries = [ { key: 'input', @@ -416,9 +477,37 @@ describe(WorkflowHistoryEventGroup.name, () => { ], }; - setup({ eventGroup, mockEventDetails }); + setup({ + eventGroup, + mockGroupDetails: { + groupDetailsEntries: [ + [ + completedActivityTaskEvents[0].eventId!, + { + eventLabel: 'Scheduled', + eventDetails: mockEventDetails, + }, + ], + [ + completedActivityTaskEvents[1].eventId!, + { + eventLabel: 'Started', + eventDetails: mockEventDetails, + }, + ], + [ + completedActivityTaskEvents[2].eventId!, + { + eventLabel: 'Completed', + eventDetails: mockEventDetails, + }, + ], + ], + summaryDetailsEntries: [], + }, + }); - // Summary tab should not appear when no summary details match + // Summary tab should not appear when summaryDetailsEntries is empty expect(screen.queryByText('Summary')).not.toBeInTheDocument(); }); }); @@ -440,12 +529,22 @@ function setup({ onReset = jest.fn(), getIsEventExpanded = jest.fn(() => false), toggleIsEventExpanded = jest.fn(), - mockEventDetails, + mockGroupDetails, }: Partial & { - mockEventDetails?: EventDetailsEntries; + mockGroupDetails?: { + groupDetailsEntries: GroupDetailsEntries; + summaryDetailsEntries: GroupDetailsEntries; + }; } = {}) { - mockGenerateHistoryEventDetails.mockReturnValue( - mockEventDetails ?? [ + const mockGenerateHistoryGroupDetails = jest.spyOn( + generateHistoryGroupDetailsModule, + 'default' + ); + + if (mockGroupDetails) { + mockGenerateHistoryGroupDetails.mockReturnValue(mockGroupDetails); + } else { + const defaultMockEventDetails: EventDetailsEntries = [ { key: 'testKey', path: 'testPath', @@ -453,8 +552,21 @@ function setup({ isGroup: false, renderConfig: null, }, - ] - ); + ]; + + mockGenerateHistoryGroupDetails.mockReturnValue({ + groupDetailsEntries: eventGroup.events + .filter((event) => event.eventId) + .map((event, index) => [ + event.eventId!, + { + eventLabel: eventGroup.eventsMetadata[index]?.label ?? 'Unknown', + eventDetails: defaultMockEventDetails, + }, + ]), + summaryDetailsEntries: [], + }); + } const mockOnReset = onReset || jest.fn(); const user = userEvent.setup(); @@ -462,7 +574,6 @@ function setup({ render( { - const eventsEntries: Array<[string, EventDetailsTabContent]> = [], - summaryEntry: [string, EventDetailsTabContent] = [ - `summary_${groupId}`, - { - eventLabel: 'Summary', - eventDetails: [], - }, - ]; - - events.forEach((event, index) => { - if (!event.eventId) return; - - const eventMetadata = eventsMetadata[index]; - if (!eventMetadata) return; - - const result = isPendingHistoryEvent(event) - ? formatPendingWorkflowHistoryEvent(event) - : formatWorkflowHistoryEvent(event); - - const eventDetails = result - ? generateHistoryEventDetails({ - details: { - ...result, - ...eventMetadata.additionalDetails, - }, - negativeFields: eventMetadata.negativeFields, - }) - : []; - - eventsEntries.push([ - event.eventId, - { - eventLabel: eventMetadata.label, - eventDetails, - } satisfies EventDetailsTabContent, - ]); - - summaryEntry[1].eventDetails.push( - ...eventDetails.filter((detail) => - eventMetadata.summaryFields?.includes(detail.path) - ) - ); - }); - - const shouldShowSummaryTab = - events.length > 1 && summaryEntry[1].eventDetails.length > 0; - - return { - groupDetailsEntries: [ - ...(shouldShowSummaryTab ? [summaryEntry] : []), - ...eventsEntries, - ], - // To be used for rendering one-line summaries - summaryDetails: summaryEntry[1].eventDetails, - }; - }, [events, eventsMetadata, groupId]); + const { groupDetailsEntries, summaryDetailsEntries } = useMemo( + () => generateHistoryGroupDetails(eventGroup), + [eventGroup] + ); - const onChangeGroupExpansionState = useCallback( + const handleGroupExpansionStateChange = useCallback( (newExpanded: boolean) => { - if (newExpanded) { - events.forEach(({ eventId }) => { - if (eventId && !getIsEventExpanded(eventId)) - toggleIsEventExpanded(eventId); - }); - } else { - events.forEach(({ eventId }) => { - if (eventId && getIsEventExpanded(eventId)) - toggleIsEventExpanded(eventId); - }); - } + events.forEach(({ eventId }) => { + if (eventId && getIsEventExpanded(eventId) !== newExpanded) + toggleIsEventExpanded(eventId); + }); }, [events, getIsEventExpanded, toggleIsEventExpanded] ); + const groupDetailsEntriesWithSummary = useMemo( + () => [ + ...(summaryDetailsEntries.length > 0 + ? [ + getSummaryTabContentEntry({ + groupId: eventGroup.firstEventId ?? '', + summaryDetails: summaryDetailsEntries.flatMap( + ([_, { eventDetails }]) => eventDetails + ), + }), + ] + : []), + ...groupDetailsEntries, + ], + [eventGroup.firstEventId, groupDetailsEntries, summaryDetailsEntries] + ); + return ( eventId && getIsEventExpanded(eventId) )} - onChange={({ expanded }) => onChangeGroupExpansionState(expanded)} + onChange={({ expanded }) => handleGroupExpansionStateChange(expanded)} overrides={overrides.panel} > onChangeGroupExpansionState(false)} + onClose={() => handleGroupExpansionStateChange(false)} /> diff --git a/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.types.ts b/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.types.ts index c6ab77e8d..eb6049dea 100644 --- a/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.types.ts +++ b/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.types.ts @@ -5,7 +5,6 @@ import { type Props as WorkflowHistoryProps } from '../workflow-history-v2.types export type Props = { eventGroup: HistoryEventsGroup; - groupId: string; getIsEventExpanded: (eventId: string) => boolean; toggleIsEventExpanded: (eventId: string) => void; showLoadingMoreEvents: boolean; diff --git a/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.tsx b/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.tsx index f2533950e..41aeb8797 100644 --- a/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.tsx +++ b/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.tsx @@ -51,10 +51,9 @@ export default function WorkflowHistoryGroupedTable({ behavior: 'auto', }, })} - itemContent={(_, [groupId, group]) => ( + itemContent={(_, [__, group]) => ( Date: Fri, 5 Dec 2025 14:07:00 +0100 Subject: [PATCH 10/11] remove workflowTab from params in v1 component Signed-off-by: Adhitya Mamallan --- .../workflow-history-event-details.tsx | 5 +---- .../workflow-history-panel-details-entry.tsx | 5 +---- .../workflow-history-event-details-group.types.ts | 4 ++-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx b/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx index b93416aa8..68bd7c155 100644 --- a/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx +++ b/src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.tsx @@ -42,10 +42,7 @@ export default function WorkflowHistoryEventDetails({
diff --git a/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.tsx b/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.tsx index 20db18b0e..4f4edd376 100644 --- a/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.tsx +++ b/src/views/workflow-history-v2/workflow-history-panel-details-entry/workflow-history-panel-details-entry.tsx @@ -31,10 +31,7 @@ export default function WorkflowHistoryPanelDetailsEntry({ ) : ( detail.value diff --git a/src/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group.types.ts b/src/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group.types.ts index 02ba5f193..084024717 100644 --- a/src/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group.types.ts +++ b/src/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group.types.ts @@ -1,11 +1,11 @@ -import { type WorkflowPageTabsParams } from '@/views/workflow-page/workflow-page-tabs/workflow-page-tabs.types'; +import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; import { type WorkflowHistoryEventDetailsEntries } from '../workflow-history-event-details/workflow-history-event-details.types'; export type Props = { entries: WorkflowHistoryEventDetailsEntries; parentGroupPath?: string; - decodedPageUrlParams: WorkflowPageTabsParams; + decodedPageUrlParams: WorkflowPageParams; }; export type EventDetailsLabelKind = 'regular' | 'group' | 'negative'; From 833daa1f86a257e048374abbc976b9d87070a247 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Fri, 5 Dec 2025 14:39:57 +0100 Subject: [PATCH 11/11] address copilot comments Signed-off-by: Adhitya Mamallan --- .../workflow-history-event-group.test.tsx | 14 +++++++++++--- .../workflow-history-event-group.tsx | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx b/src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx index 01dc89a20..fa732b165 100644 --- a/src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx +++ b/src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx @@ -394,7 +394,7 @@ describe(WorkflowHistoryEventGroup.name, () => { expect(screen.getByText('Summary')).toBeInTheDocument(); }); - it('does not show summary tab when summaryDetailsEntries is empty', () => { + it('does not show summary tab when only one event is present in the group', () => { const mockEventDetails: EventDetailsEntries = [ { key: 'input', @@ -431,11 +431,19 @@ describe(WorkflowHistoryEventGroup.name, () => { }, ], ], - summaryDetailsEntries: [], + summaryDetailsEntries: [ + [ + scheduleActivityTaskEvent.eventId!, + { + eventLabel: 'Scheduled', + eventDetails: mockEventDetails, + }, + ], + ], }, }); - // Summary tab should not appear when summaryDetailsEntries is empty + // Summary tab should not appear when there is only one event expect(screen.queryByText('Summary')).not.toBeInTheDocument(); }); diff --git a/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.tsx b/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.tsx index b64836898..4ccd94c13 100644 --- a/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.tsx +++ b/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.tsx @@ -77,9 +77,9 @@ export default function WorkflowHistoryEventGroup({ ...(summaryDetailsEntries.length > 0 ? [ getSummaryTabContentEntry({ - groupId: eventGroup.firstEventId ?? '', + groupId: eventGroup.firstEventId ?? 'unknown', summaryDetails: summaryDetailsEntries.flatMap( - ([_, { eventDetails }]) => eventDetails + ([_eventId, { eventDetails }]) => eventDetails ), }), ]