From f831a6fdaeea3464873ff90135911beafb6de69a 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 54bb04134b4e58ca35dbe0d99dc7459a6dc7fc9c 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 9a9ed3c815041e8bb1d3d2445ddff4e2e53d2dfa 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 --- .../__tests__/workflow-history-event-group.test.tsx | 1 + .../workflow-history-grouped-table.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) 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 fa732b165..d1f6cd592 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 @@ -582,6 +582,7 @@ function setup({ render( ( + itemContent={(_, [groupId, group]) => ( Date: Wed, 3 Dec 2025 12:37:29 +0100 Subject: [PATCH 04/11] More changes Signed-off-by: Adhitya Mamallan --- .../workflow-history-event-details.config.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) 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 23683ecc534c977fbbf7e415192fc3a41d336d65 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Wed, 3 Dec 2025 14:58:18 +0100 Subject: [PATCH 05/11] Add unit tests and isolate prop types Signed-off-by: Adhitya Mamallan --- .../workflow-history-event-details.config.ts | 40 ------------------- 1 file changed, 40 deletions(-) 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 f513c7a2050ad2c73dc1554c30366c4f3d1a96a1 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Fri, 5 Dec 2025 12:06:30 +0100 Subject: [PATCH 06/11] address comments Signed-off-by: Adhitya Mamallan --- .../__tests__/workflow-history-event-group.test.tsx | 1 - .../workflow-history-grouped-table.tsx | 3 +-- 2 files changed, 1 insertion(+), 3 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 d1f6cd592..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 @@ -582,7 +582,6 @@ function setup({ render( ( + itemContent={(_, [__, group]) => ( Date: Wed, 3 Dec 2025 17:27:59 +0100 Subject: [PATCH 07/11] Add single-line summary for history events Signed-off-by: Adhitya Mamallan --- ...flow-history-details-row-parsers.config.ts | 44 +++ ...orkflow-history-details-row-json.styles.ts | 15 + .../workflow-history-details-row-json.tsx | 16 + ...workflow-history-details-row-json.types.ts | 3 + ...history-details-row-tooltip-json.styles.ts | 13 + ...kflow-history-details-row-tooltip-json.tsx | 21 ++ ...-history-details-row-tooltip-json.types.ts | 3 + .../workflow-history-details-row.test.tsx | 129 +++++++ .../get-parsed-details-row-items.test.ts | 357 ++++++++++++++++++ .../helpers/get-parsed-details-row-items.ts | 58 +++ .../workflow-history-details-row.styles.ts | 69 ++++ .../workflow-history-details-row.tsx | 67 ++++ .../workflow-history-details-row.types.ts | 72 ++++ .../workflow-history-event-group.styles.ts | 16 +- .../workflow-history-event-group.tsx | 21 +- 15 files changed, 897 insertions(+), 7 deletions(-) create mode 100644 src/views/workflow-history-v2/config/workflow-history-details-row-parsers.config.ts create mode 100644 src/views/workflow-history-v2/workflow-history-details-row-json/workflow-history-details-row-json.styles.ts create mode 100644 src/views/workflow-history-v2/workflow-history-details-row-json/workflow-history-details-row-json.tsx create mode 100644 src/views/workflow-history-v2/workflow-history-details-row-json/workflow-history-details-row-json.types.ts create mode 100644 src/views/workflow-history-v2/workflow-history-details-row-tooltip-json/workflow-history-details-row-tooltip-json.styles.ts create mode 100644 src/views/workflow-history-v2/workflow-history-details-row-tooltip-json/workflow-history-details-row-tooltip-json.tsx create mode 100644 src/views/workflow-history-v2/workflow-history-details-row-tooltip-json/workflow-history-details-row-tooltip-json.types.ts create mode 100644 src/views/workflow-history-v2/workflow-history-details-row/__tests__/workflow-history-details-row.test.tsx create mode 100644 src/views/workflow-history-v2/workflow-history-details-row/helpers/__tests__/get-parsed-details-row-items.test.ts create mode 100644 src/views/workflow-history-v2/workflow-history-details-row/helpers/get-parsed-details-row-items.ts create mode 100644 src/views/workflow-history-v2/workflow-history-details-row/workflow-history-details-row.styles.ts create mode 100644 src/views/workflow-history-v2/workflow-history-details-row/workflow-history-details-row.tsx create mode 100644 src/views/workflow-history-v2/workflow-history-details-row/workflow-history-details-row.types.ts diff --git a/src/views/workflow-history-v2/config/workflow-history-details-row-parsers.config.ts b/src/views/workflow-history-v2/config/workflow-history-details-row-parsers.config.ts new file mode 100644 index 000000000..84c04edd4 --- /dev/null +++ b/src/views/workflow-history-v2/config/workflow-history-details-row-parsers.config.ts @@ -0,0 +1,44 @@ +import { + MdHourglassBottom, + MdOutlineMonitorHeart, + MdReplay, +} from 'react-icons/md'; + +import { type DetailsRowItemParser } from '../workflow-history-details-row/workflow-history-details-row.types'; +import WorkflowHistoryDetailsRowJson from '../workflow-history-details-row-json/workflow-history-details-row-json'; +import WorkflowHistoryDetailsRowTooltipJson from '../workflow-history-details-row-tooltip-json/workflow-history-details-row-tooltip-json'; + +const workflowHistoryDetailsRowParsersConfig: Array = [ + { + name: 'Heartbeat time', + matcher: (name) => name === 'lastHeartbeatTime', + icon: MdOutlineMonitorHeart, + }, + { + name: 'Json as PrettyJson', + matcher: (name, value) => + value !== null && + new RegExp( + '(input|result|details|failureDetails|Error|lastCompletionResult|heartbeatDetails|lastFailureDetails)$' + ).test(name), + icon: null, + customRenderValue: WorkflowHistoryDetailsRowJson, + customTooltipContent: WorkflowHistoryDetailsRowTooltipJson, + invertTooltipColors: true, + }, + { + name: 'Timeouts with timer icon', + matcher: (name) => + new RegExp('(TimeoutSeconds|BackoffSeconds|InSeconds)$').test(name), + icon: MdHourglassBottom, + }, + { + name: '"attempt" greater than 1, as "retries"', + matcher: (name) => name === 'attempt', + hide: (_, value) => typeof value === 'number' && value <= 0, + icon: MdReplay, + customTooltipContent: () => 'retries', + }, +]; + +export default workflowHistoryDetailsRowParsersConfig; diff --git a/src/views/workflow-history-v2/workflow-history-details-row-json/workflow-history-details-row-json.styles.ts b/src/views/workflow-history-v2/workflow-history-details-row-json/workflow-history-details-row-json.styles.ts new file mode 100644 index 000000000..66e4201d2 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-details-row-json/workflow-history-details-row-json.styles.ts @@ -0,0 +1,15 @@ +import { styled as createStyled } from 'baseui'; + +export const styled = { + JsonViewContainer: createStyled<'div', { $isNegative: boolean }>( + 'div', + ({ $theme, $isNegative }) => ({ + color: $isNegative ? $theme.colors.contentNegative : '#A964F7', + maxWidth: '360px', + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + ...$theme.typography.MonoParagraphXSmall, + }) + ), +}; diff --git a/src/views/workflow-history-v2/workflow-history-details-row-json/workflow-history-details-row-json.tsx b/src/views/workflow-history-v2/workflow-history-details-row-json/workflow-history-details-row-json.tsx new file mode 100644 index 000000000..620c320c6 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-details-row-json/workflow-history-details-row-json.tsx @@ -0,0 +1,16 @@ +import losslessJsonStringify from '@/utils/lossless-json-stringify'; + +import { type DetailsRowValueComponentProps } from '../workflow-history-details-row/workflow-history-details-row.types'; + +import { styled } from './workflow-history-details-row-json.styles'; + +export default function WorkflowHistoryDetailsRowJson({ + value, + isNegative, +}: DetailsRowValueComponentProps) { + return ( + + {losslessJsonStringify(value)} + + ); +} diff --git a/src/views/workflow-history-v2/workflow-history-details-row-json/workflow-history-details-row-json.types.ts b/src/views/workflow-history-v2/workflow-history-details-row-json/workflow-history-details-row-json.types.ts new file mode 100644 index 000000000..3875f43c1 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-details-row-json/workflow-history-details-row-json.types.ts @@ -0,0 +1,3 @@ +import { type DetailsRowValueComponentProps } from '../workflow-history-details-row/workflow-history-details-row.types'; + +export type Props = DetailsRowValueComponentProps; diff --git a/src/views/workflow-history-v2/workflow-history-details-row-tooltip-json/workflow-history-details-row-tooltip-json.styles.ts b/src/views/workflow-history-v2/workflow-history-details-row-tooltip-json/workflow-history-details-row-tooltip-json.styles.ts new file mode 100644 index 000000000..6506a4c71 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-details-row-tooltip-json/workflow-history-details-row-tooltip-json.styles.ts @@ -0,0 +1,13 @@ +import { styled as createStyled } from 'baseui'; + +export const styled = { + JsonPreviewContainer: createStyled('div', ({ $theme }) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + gap: $theme.sizing.scale200, + })), + JsonPreviewLabel: createStyled('div', ({ $theme }) => ({ + ...$theme.typography.LabelXSmall, + })), +}; diff --git a/src/views/workflow-history-v2/workflow-history-details-row-tooltip-json/workflow-history-details-row-tooltip-json.tsx b/src/views/workflow-history-v2/workflow-history-details-row-tooltip-json/workflow-history-details-row-tooltip-json.tsx new file mode 100644 index 000000000..f9866b59d --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-details-row-tooltip-json/workflow-history-details-row-tooltip-json.tsx @@ -0,0 +1,21 @@ +import WorkflowHistoryEventDetailsJson from '@/views/workflow-history/workflow-history-event-details-json/workflow-history-event-details-json'; + +import { type DetailsRowValueComponentProps } from '../workflow-history-details-row/workflow-history-details-row.types'; + +import { styled } from './workflow-history-details-row-tooltip-json.styles'; + +export default function WorkflowHistoryDetailsRowTooltipJson({ + value, + label, + isNegative, +}: DetailsRowValueComponentProps) { + return ( + + {label} + + + ); +} diff --git a/src/views/workflow-history-v2/workflow-history-details-row-tooltip-json/workflow-history-details-row-tooltip-json.types.ts b/src/views/workflow-history-v2/workflow-history-details-row-tooltip-json/workflow-history-details-row-tooltip-json.types.ts new file mode 100644 index 000000000..3875f43c1 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-details-row-tooltip-json/workflow-history-details-row-tooltip-json.types.ts @@ -0,0 +1,3 @@ +import { type DetailsRowValueComponentProps } from '../workflow-history-details-row/workflow-history-details-row.types'; + +export type Props = DetailsRowValueComponentProps; diff --git a/src/views/workflow-history-v2/workflow-history-details-row/__tests__/workflow-history-details-row.test.tsx b/src/views/workflow-history-v2/workflow-history-details-row/__tests__/workflow-history-details-row.test.tsx new file mode 100644 index 000000000..9d3a791b6 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-details-row/__tests__/workflow-history-details-row.test.tsx @@ -0,0 +1,129 @@ +import { render, screen, userEvent } from '@/test-utils/rtl'; + +import type { WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; + +import { type EventDetailsEntries } from '../../workflow-history-event-details/workflow-history-event-details.types'; +import WorkflowHistoryDetailsRow from '../workflow-history-details-row'; + +jest.mock('../helpers/get-parsed-details-row-items', () => + jest.fn((detailsEntries: EventDetailsEntries) => + detailsEntries + .filter((entry) => !entry.isGroup) + .map((entry, index) => ({ + path: entry.path, + label: entry.path, + value: entry.value, + icon: ({ size }: any) => ( + + ), + renderValue: ({ value, isNegative }: any) => ( + + {value} + + ), + renderTooltip: ({ label }: any) => ( + {label} + ), + invertTooltipColors: index === 1, // Second item has inverted tooltip + omitWrapping: index === 2, // Third item omits wrapping + })) + ) +); + +const mockWorkflowPageParams: WorkflowPageParams = { + cluster: 'test-cluster', + domain: 'test-domain', + workflowId: 'test-workflow', + runId: 'test-run', +}; + +const mockDetailsEntries: EventDetailsEntries = [ + { + key: 'field1', + path: 'field1', + isGroup: false, + value: 'value1', + isNegative: false, + renderConfig: null, + }, + { + key: 'field2', + path: 'field2', + isGroup: false, + value: 'value2', + isNegative: true, + renderConfig: null, + }, + { + key: 'field3', + path: 'field3', + isGroup: false, + value: 'value3', + isNegative: false, + renderConfig: null, + }, +]; + +describe(WorkflowHistoryDetailsRow.name, () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render details row items when detailsEntries has items', () => { + setup(); + + expect(screen.getByTestId('field-field1')).toBeInTheDocument(); + expect(screen.getByTestId('field-field2')).toBeInTheDocument(); + expect(screen.getByTestId('field-field3')).toBeInTheDocument(); + expect(screen.getByText('value1')).toBeInTheDocument(); + expect(screen.getByText('value2')).toBeInTheDocument(); + expect(screen.getByText('value3')).toBeInTheDocument(); + }); + + it('should mark negative fields correctly', () => { + setup(); + + const negativeField = screen.getByTestId('field-field2'); + expect(negativeField).toHaveAttribute('data-negative', 'true'); + + const positiveField = screen.getByTestId('field-field1'); + expect(positiveField).toHaveAttribute('data-negative', 'false'); + }); + + it('should render icons when provided in item config', () => { + setup(); + + expect(screen.getByTestId('icon-field1')).toBeInTheDocument(); + expect(screen.getByTestId('icon-field2')).toBeInTheDocument(); + expect(screen.getByTestId('icon-field3')).toBeInTheDocument(); + }); + + it('should render tooltip content on hover', async () => { + const { user } = setup(); + + const field1 = screen.getByTestId('field-field1'); + await user.hover(field1); + + expect(await screen.findByTestId('tooltip-field1')).toBeInTheDocument(); + expect(screen.getByText('field1')).toBeInTheDocument(); + }); +}); + +function setup({ + detailsEntries = mockDetailsEntries, + workflowPageParams = mockWorkflowPageParams, +}: { + detailsEntries?: EventDetailsEntries; + workflowPageParams?: WorkflowPageParams; +} = {}) { + const user = userEvent.setup(); + + const renderResult = render( + + ); + + return { user, ...renderResult }; +} diff --git a/src/views/workflow-history-v2/workflow-history-details-row/helpers/__tests__/get-parsed-details-row-items.test.ts b/src/views/workflow-history-v2/workflow-history-details-row/helpers/__tests__/get-parsed-details-row-items.test.ts new file mode 100644 index 000000000..b9ceade3f --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-details-row/helpers/__tests__/get-parsed-details-row-items.test.ts @@ -0,0 +1,357 @@ +import { type EventDetailsEntries } from '@/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.types'; + +import * as workflowHistoryDetailsRowParsersConfigModule from '../../../config/workflow-history-details-row-parsers.config'; +import { type DetailsRowItemParser } from '../../workflow-history-details-row.types'; +import getParsedDetailsRowItems from '../get-parsed-details-row-items'; + +// Mock the parser config +jest.mock('../../../config/workflow-history-details-row-parsers.config', () => { + const mockIcon = jest.fn(); + const mockCustomRenderValue = jest.fn(); + const mockCustomTooltipContent = jest.fn(); + + return { + __esModule: true, + default: [ + { + name: 'Json Parser', + matcher: (path: string, _value: unknown) => + path === 'input' || path === 'result', + icon: null, + customRenderValue: mockCustomRenderValue, + customTooltipContent: mockCustomTooltipContent, + invertTooltipColors: true, + }, + { + name: 'Timeout Parser', + matcher: (path: string) => path.endsWith('TimeoutSeconds'), + icon: mockIcon, + }, + { + name: 'Heartbeat Parser', + matcher: (path: string) => path === 'lastHeartbeatTime', + icon: mockIcon, + }, + { + name: 'Hidden Field Parser', + matcher: (path: string) => path === 'hiddenField', + icon: null, + hide: jest.fn((_, value) => value === 'hide-me'), + }, + { + name: 'Attempt Parser', + matcher: (path: string) => path === 'attempt', + icon: mockIcon, + customTooltipContent: jest.fn(() => 'retries'), + omitWrapping: true, + }, + ] satisfies Array, + }; +}); + +describe(getParsedDetailsRowItems.name, () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return empty array when detailsEntries is empty', () => { + const detailsEntries: EventDetailsEntries = []; + + const result = getParsedDetailsRowItems(detailsEntries); + + expect(result).toEqual([]); + }); + + it('should skip group entries', () => { + const detailsEntries: EventDetailsEntries = [ + { + key: 'groupField', + path: 'groupField', + renderConfig: { + name: 'Test Config', + key: 'test', + }, + isGroup: true, + groupEntries: [], + }, + { + key: 'regularField', + path: 'regularField', + value: 'test-value', + renderConfig: { + name: 'Test Config', + key: 'test', + }, + isGroup: false, + }, + ]; + + const result = getParsedDetailsRowItems(detailsEntries); + + expect(result).toHaveLength(1); + expect(result[0].path).toBe('regularField'); + }); + + it('should use parser config when matcher matches', () => { + const detailsEntries: EventDetailsEntries = [ + { + key: 'input', + path: 'input', + value: { data: 'test' }, + renderConfig: { + name: 'Test Config', + key: 'test', + }, + isGroup: false, + }, + ]; + + const result = getParsedDetailsRowItems(detailsEntries); + + expect(result).toHaveLength(1); + expect(result[0].path).toBe('input'); + expect(result[0].value).toEqual({ data: 'test' }); + }); + + it('should exclude entries when hide function returns true', () => { + const detailsEntries: EventDetailsEntries = [ + { + key: 'hiddenField', + path: 'hiddenField', + value: 'hide-me', + renderConfig: { + name: 'Test Config', + key: 'test', + }, + isGroup: false, + }, + { + key: 'hiddenField', + path: 'hiddenField', + value: 'show-me', + renderConfig: { + name: 'Test Config', + key: 'test', + }, + isGroup: false, + }, + ]; + + const result = getParsedDetailsRowItems(detailsEntries); + + expect(result).toHaveLength(1); + expect(result[0].value).toBe('show-me'); + }); + + it('should use customRenderValue from parser config when available', () => { + const detailsEntries: EventDetailsEntries = [ + { + key: 'input', + path: 'input', + value: { data: 'test' }, + renderConfig: { + name: 'Test Config', + key: 'test', + }, + isGroup: false, + }, + ]; + + const result = getParsedDetailsRowItems(detailsEntries); + + expect(result).toHaveLength(1); + expect(result[0].renderValue).toBe( + workflowHistoryDetailsRowParsersConfigModule.default[0].customRenderValue + ); + }); + + it('should use renderConfig.valueComponent when no customRenderValue is available', () => { + const mockValueComponent = jest.fn(); + const detailsEntries: EventDetailsEntries = [ + { + key: 'regularField', + path: 'regularField', + value: 'test-value', + renderConfig: { + name: 'Test Config', + key: 'test', + valueComponent: mockValueComponent, + }, + isGroup: false, + }, + ]; + + const result = getParsedDetailsRowItems(detailsEntries); + + expect(result).toHaveLength(1); + expect(result[0].renderValue).toBeDefined(); + }); + + it('should use icon from parser config when available', () => { + const detailsEntries: EventDetailsEntries = [ + { + key: 'lastHeartbeatTime', + path: 'lastHeartbeatTime', + value: 1234567890, + renderConfig: { + name: 'Test Config', + key: 'test', + }, + isGroup: false, + }, + ]; + + const result = getParsedDetailsRowItems(detailsEntries); + + expect(result).toHaveLength(1); + expect(result[0].icon).toBe( + workflowHistoryDetailsRowParsersConfigModule.default[2].icon + ); + }); + + it('should use null icon when parser config has no icon', () => { + const detailsEntries: EventDetailsEntries = [ + { + key: 'input', + path: 'input', + value: { data: 'test' }, + renderConfig: { + name: 'Test Config', + key: 'test', + }, + isGroup: false, + }, + ]; + + const result = getParsedDetailsRowItems(detailsEntries); + + expect(result).toHaveLength(1); + expect(result[0].icon).toBeNull(); + }); + + it('should use customTooltipContent from parser config when available', () => { + const detailsEntries: EventDetailsEntries = [ + { + key: 'input', + path: 'input', + value: { data: 'test' }, + renderConfig: { + name: 'Test Config', + key: 'test', + }, + isGroup: false, + }, + ]; + + const result = getParsedDetailsRowItems(detailsEntries); + + expect(result).toHaveLength(1); + expect(result[0].renderTooltip).toBe( + workflowHistoryDetailsRowParsersConfigModule.default[0] + .customTooltipContent + ); + }); + + it('should use default tooltip function that returns label when no customTooltipContent is available', () => { + const detailsEntries: EventDetailsEntries = [ + { + key: 'regularField', + path: 'regularField', + value: 'test-value', + renderConfig: { + name: 'Test Config', + key: 'test', + }, + isGroup: false, + }, + ]; + + const result = getParsedDetailsRowItems(detailsEntries); + + expect(result).toHaveLength(1); + expect(result[0].renderTooltip).toBeDefined(); + // renderTooltip should be a function that returns the label + expect(typeof result[0].renderTooltip).toBe('function'); + }); + + it('should use getLabel from renderConfig when available', () => { + const detailsEntries: EventDetailsEntries = [ + { + key: 'regularField', + path: 'regularField', + value: 'test-value', + renderConfig: { + name: 'Test Config', + key: 'test', + getLabel: ({ path }) => `Label: ${path}`, + }, + isGroup: false, + }, + ]; + + const result = getParsedDetailsRowItems(detailsEntries); + + expect(result).toHaveLength(1); + expect(result[0].label).toBe('Label: regularField'); + }); + + it('should use path as label when getLabel is not available', () => { + const detailsEntries: EventDetailsEntries = [ + { + key: 'regularField', + path: 'regularField', + value: 'test-value', + renderConfig: { + name: 'Test Config', + key: 'test', + }, + isGroup: false, + }, + ]; + + const result = getParsedDetailsRowItems(detailsEntries); + + expect(result).toHaveLength(1); + expect(result[0].label).toBe('regularField'); + }); + + it('should include invertTooltipColors from parser config when available', () => { + const detailsEntries: EventDetailsEntries = [ + { + key: 'input', + path: 'input', + value: { data: 'test' }, + renderConfig: { + name: 'Test Config', + key: 'test', + }, + isGroup: false, + }, + ]; + + const result = getParsedDetailsRowItems(detailsEntries); + + expect(result).toHaveLength(1); + expect(result[0].invertTooltipColors).toBe(true); + }); + + it('should include omitWrapping from parser config when available', () => { + const detailsEntries: EventDetailsEntries = [ + { + key: 'attempt', + path: 'attempt', + value: 2, + renderConfig: { + name: 'Test Config', + key: 'test', + }, + isGroup: false, + }, + ]; + + const result = getParsedDetailsRowItems(detailsEntries); + + expect(result).toHaveLength(1); + expect(result[0].omitWrapping).toBe(true); + }); +}); diff --git a/src/views/workflow-history-v2/workflow-history-details-row/helpers/get-parsed-details-row-items.ts b/src/views/workflow-history-v2/workflow-history-details-row/helpers/get-parsed-details-row-items.ts new file mode 100644 index 000000000..dd5193bfa --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-details-row/helpers/get-parsed-details-row-items.ts @@ -0,0 +1,58 @@ +import { createElement, type ComponentType } from 'react'; + +import workflowHistoryDetailsRowParsersConfig from '../../config/workflow-history-details-row-parsers.config'; +import { type EventDetailsEntries } from '../../workflow-history-event-details/workflow-history-event-details.types'; +import { + type DetailsRowValueComponentProps, + type DetailsRowItem, +} from '../workflow-history-details-row.types'; + +export default function getParsedDetailsRowItems( + detailsEntries: EventDetailsEntries +): Array { + return detailsEntries.reduce>((acc, detailsConfig) => { + if (detailsConfig.isGroup) return acc; + + const { key, path, value, renderConfig } = detailsConfig; + + const parserConfig = workflowHistoryDetailsRowParsersConfig.find((config) => + config.matcher(path, value) + ); + + if (parserConfig?.hide?.(path, value)) { + return acc; + } + + const label = renderConfig?.getLabel?.({ key, path, value }) ?? path; + + let renderValue: ComponentType; + if (parserConfig?.customRenderValue) { + renderValue = parserConfig.customRenderValue; + } else if (renderConfig?.valueComponent) { + const detailsRenderValue = renderConfig.valueComponent; + renderValue = ({ value, label, isNegative, ...workflowPageParams }) => + createElement(detailsRenderValue, { + entryKey: key, + entryPath: path, + entryValue: value, + isNegative, + ...workflowPageParams, + }); + } else { + renderValue = ({ value }) => String(value); + } + + acc.push({ + path, + label, + value, + icon: parserConfig?.icon ?? null, + renderValue, + renderTooltip: parserConfig?.customTooltipContent ?? (() => label), + invertTooltipColors: parserConfig?.invertTooltipColors, + omitWrapping: parserConfig?.omitWrapping, + }); + + return acc; + }, []); +} diff --git a/src/views/workflow-history-v2/workflow-history-details-row/workflow-history-details-row.styles.ts b/src/views/workflow-history-v2/workflow-history-details-row/workflow-history-details-row.styles.ts new file mode 100644 index 000000000..cb9ad43e2 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-details-row/workflow-history-details-row.styles.ts @@ -0,0 +1,69 @@ +import { styled as createStyled, type Theme } from 'baseui'; +import { type PopoverOverrides } from 'baseui/popover'; +import { type StyleObject } from 'styletron-react'; + +export const styled = { + DetailsRowContainer: createStyled('div', ({ $theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: $theme.sizing.scale300, + ...$theme.typography.LabelXSmall, + overflow: 'hidden', + })), + DetailsFieldContainer: createStyled< + 'div', + { $isNegative: boolean; $omitWrapping: boolean } + >( + 'div', + ({ + $theme, + $isNegative, + $omitWrapping, + }: { + $theme: Theme; + $isNegative: boolean; + $omitWrapping: boolean; + }) => ({ + display: 'flex', + alignItems: 'center', + gap: $theme.sizing.scale100, + color: $isNegative + ? $theme.colors.contentNegative + : $theme.colors.contentPrimary, + height: $theme.sizing.scale700, + ...($omitWrapping + ? {} + : { + padding: `${$theme.sizing.scale0} ${$theme.sizing.scale100}`, + backgroundColor: $isNegative + ? $theme.colors.backgroundNegativeLight + : $theme.colors.backgroundSecondary, + borderRadius: $theme.borders.radius200, + }), + }) + ), +}; + +export const overrides = { + popover: { + Inner: { + style: { + maxWidth: '500px', + }, + }, + }, + popoverInverted: { + Arrow: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + backgroundColor: $theme.colors.backgroundPrimary, + }), + }, + Inner: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + backgroundColor: $theme.colors.backgroundPrimary, + color: $theme.colors.contentPrimary, + maxWidth: '500px', + }), + }, + } satisfies PopoverOverrides, +}; diff --git a/src/views/workflow-history-v2/workflow-history-details-row/workflow-history-details-row.tsx b/src/views/workflow-history-v2/workflow-history-details-row/workflow-history-details-row.tsx new file mode 100644 index 000000000..72a434f21 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-details-row/workflow-history-details-row.tsx @@ -0,0 +1,67 @@ +import React, { useMemo } from 'react'; + +import { StatefulTooltip } from 'baseui/tooltip'; + +import getParsedDetailsRowItems from './helpers/get-parsed-details-row-items'; +import { overrides, styled } from './workflow-history-details-row.styles'; +import { type Props } from './workflow-history-details-row.types'; + +export default function WorkflowHistoryDetailsRow({ + detailsEntries, + ...workflowPageParams +}: Props) { + const rowItems = useMemo(() => { + if (detailsEntries.length === 0) { + return []; + } + + return getParsedDetailsRowItems(detailsEntries); + }, [detailsEntries]); + + if (rowItems.length === 0) return
; + + return ( + + {rowItems.map((item) => { + const isNegative = detailsEntries.some( + (detail) => detail.path === item.path && detail.isNegative + ); + + return ( + + } + ignoreBoundary + placement="bottom" + showArrow + overrides={ + item.invertTooltipColors + ? overrides.popoverInverted + : overrides.popover + } + > + + {item.icon && } + + + + ); + })} + + ); +} diff --git a/src/views/workflow-history-v2/workflow-history-details-row/workflow-history-details-row.types.ts b/src/views/workflow-history-v2/workflow-history-details-row/workflow-history-details-row.types.ts new file mode 100644 index 000000000..8ffc313a3 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-details-row/workflow-history-details-row.types.ts @@ -0,0 +1,72 @@ +import { type ComponentType } from 'react'; + +import { type IconProps } from 'baseui/icon'; + +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 DetailsRowValueComponentProps = { + label: string; + value: any; + isNegative?: boolean; +} & WorkflowPageParams; + +/** + * Configuration object for parsing and rendering workflow history details row items. + * Parsers are matched against event details entries to determine how they should be displayed. + */ +export type DetailsRowItemParser = { + /** Human-readable name for this parser configuration */ + name: string; + /** + * Function that determines if this parser applies to a given path/value pair. + * Returns true if the parser should handle this entry. + */ + matcher: (path: string, value: unknown) => boolean; + /** + * Optional function that conditionally hides entries even if the matcher matches. + * Returns true to hide the entry, false to show it. + */ + hide?: (path: string, value: unknown) => boolean; + /** + * React component for the icon to display next to the value, or null for no icon. + * The icon component should accept size and color props compatible with BaseUI IconProps. + */ + icon: React.ComponentType<{ + size?: IconProps['size']; + color?: IconProps['color']; + }> | null; + /** + * Optional React component to use for rendering the value instead of the default string conversion. + * Receives DetailsRowValueComponentProps. + */ + customRenderValue?: ComponentType; + /** + * Optional React component to use for rendering tooltip content instead of the default label. + * Receives DetailsRowValueComponentProps. + */ + customTooltipContent?: ComponentType; + /** Optional flag to invert the tooltip color scheme (default: dark tooltip in light mode). */ + invertTooltipColors?: boolean; + /** Optional flag to remove padding and background from the details row item */ + omitWrapping?: boolean; +}; + +export type DetailsRowItem = { + path: string; + label: string; + value: any; + icon: React.ComponentType<{ + size?: IconProps['size']; + color?: IconProps['color']; + }> | null; + renderValue: ComponentType; + renderTooltip: ComponentType; + invertTooltipColors?: boolean; + omitWrapping?: boolean; +}; + +export type Props = { + detailsEntries: EventDetailsEntries; +} & WorkflowPageParams; diff --git a/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.styles.ts b/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.styles.ts index cd7bb060a..1534eab32 100644 --- a/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.styles.ts +++ b/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.styles.ts @@ -35,14 +35,22 @@ export const styled = { gap: $theme.sizing.scale300, alignItems: 'center', })), - SummarizedDetailsContainer: createStyled('div', { - overflow: 'hidden', - }), + SummarizedDetailsContainer: createStyled( + 'div', + ({ $theme }: { $theme: Theme }) => ({ + overflow: 'hidden', + [$theme.mediaQuery.medium]: { + margin: `-${$theme.sizing.scale200} 0`, + }, + }) + ), ActionsContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ display: 'flex', gap: $theme.sizing.scale300, alignItems: 'center', - margin: `-${$theme.sizing.scale200} 0`, + [$theme.mediaQuery.medium]: { + margin: `-${$theme.sizing.scale200} 0`, + }, })), GroupDetailsGridContainer: createStyled('div', { display: 'grid', 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 4ccd94c13..9feee2f87 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 @@ -10,6 +10,7 @@ import WorkflowHistoryTimelineResetButton from '@/views/workflow-history/workflo import workflowHistoryEventFilteringTypeColorsConfig from '../config/workflow-history-event-filtering-type-colors.config'; import generateHistoryGroupDetails from '../helpers/generate-history-group-details'; +import WorkflowHistoryDetailsRow from '../workflow-history-details-row/workflow-history-details-row'; import WorkflowHistoryEventGroupDuration from '../workflow-history-event-group-duration/workflow-history-event-group-duration'; import WorkflowHistoryGroupDetails from '../workflow-history-group-details/workflow-history-group-details'; @@ -72,9 +73,15 @@ export default function WorkflowHistoryEventGroup({ [events, getIsEventExpanded, toggleIsEventExpanded] ); + const groupSummaryDetails = useMemo( + () => + summaryDetailsEntries.flatMap(([_, { eventDetails }]) => eventDetails), + [summaryDetailsEntries] + ); + const groupDetailsEntriesWithSummary = useMemo( () => [ - ...(summaryDetailsEntries.length > 0 + ...(groupSummaryDetails.length > 0 && groupDetailsEntries.length > 1 ? [ getSummaryTabContentEntry({ groupId: eventGroup.firstEventId ?? 'unknown', @@ -86,7 +93,12 @@ export default function WorkflowHistoryEventGroup({ : []), ...groupDetailsEntries, ], - [eventGroup.firstEventId, groupDetailsEntries, summaryDetailsEntries] + [ + eventGroup.firstEventId, + groupDetailsEntries, + summaryDetailsEntries, + groupSummaryDetails.length, + ] ); return ( @@ -124,7 +136,10 @@ export default function WorkflowHistoryEventGroup({ />
- Placeholder for event details + {resetToDecisionEventId && ( From ea0f3b783fbf15ddfae243c97c4cd5023225aa5f Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Thu, 4 Dec 2025 13:48:12 +0100 Subject: [PATCH 08/11] fix type error in test Signed-off-by: Adhitya Mamallan --- .../workflow-history-details-row.test.tsx | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/views/workflow-history-v2/workflow-history-details-row/__tests__/workflow-history-details-row.test.tsx b/src/views/workflow-history-v2/workflow-history-details-row/__tests__/workflow-history-details-row.test.tsx index 9d3a791b6..1d16a19c4 100644 --- a/src/views/workflow-history-v2/workflow-history-details-row/__tests__/workflow-history-details-row.test.tsx +++ b/src/views/workflow-history-v2/workflow-history-details-row/__tests__/workflow-history-details-row.test.tsx @@ -4,29 +4,36 @@ import type { WorkflowPageParams } from '@/views/workflow-page/workflow-page.typ import { type EventDetailsEntries } from '../../workflow-history-event-details/workflow-history-event-details.types'; import WorkflowHistoryDetailsRow from '../workflow-history-details-row'; +import { type DetailsRowItem } from '../workflow-history-details-row.types'; jest.mock('../helpers/get-parsed-details-row-items', () => jest.fn((detailsEntries: EventDetailsEntries) => - detailsEntries - .filter((entry) => !entry.isGroup) - .map((entry, index) => ({ - path: entry.path, - label: entry.path, - value: entry.value, - icon: ({ size }: any) => ( - - ), - renderValue: ({ value, isNegative }: any) => ( - - {value} - - ), - renderTooltip: ({ label }: any) => ( - {label} - ), - invertTooltipColors: index === 1, // Second item has inverted tooltip - omitWrapping: index === 2, // Third item omits wrapping - })) + detailsEntries.reduce>((acc, entry) => { + if (!entry.isGroup) { + acc.push({ + path: entry.path, + label: entry.path, + value: entry.value, + icon: ({ size }: any) => ( + + ), + renderValue: ({ value, isNegative }: any) => ( + + {value} + + ), + renderTooltip: ({ label }: any) => ( + {label} + ), + invertTooltipColors: acc.length === 1, // Second item has inverted tooltip + omitWrapping: acc.length === 2, // Third item omits wrapping + }); + } + return acc; + }, []) ) ); From 7d80bdea7c8f5c970352f644b8bf5c4a4984fe8a Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 8 Dec 2025 11:17:01 +0100 Subject: [PATCH 09/11] Fix files and add tests Signed-off-by: Adhitya Mamallan --- ...workflow-history-details-row-json.test.tsx | 23 +++++++++++ ...-history-details-row-tooltip-json.test.tsx | 38 +++++++++++++++++++ .../workflow-history-event-group.tsx | 11 +----- 3 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 src/views/workflow-history-v2/workflow-history-details-row-json/__tests__/workflow-history-details-row-json.test.tsx create mode 100644 src/views/workflow-history-v2/workflow-history-details-row-tooltip-json/__tests__/workflow-history-details-row-tooltip-json.test.tsx diff --git a/src/views/workflow-history-v2/workflow-history-details-row-json/__tests__/workflow-history-details-row-json.test.tsx b/src/views/workflow-history-v2/workflow-history-details-row-json/__tests__/workflow-history-details-row-json.test.tsx new file mode 100644 index 000000000..23154f9b1 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-details-row-json/__tests__/workflow-history-details-row-json.test.tsx @@ -0,0 +1,23 @@ +import { render, screen } from '@/test-utils/rtl'; + +import WorkflowHistoryDetailsRowJson from '../workflow-history-details-row-json'; + +describe(WorkflowHistoryDetailsRowJson.name, () => { + it('renders the stringified JSON value', () => { + render( + + ); + + expect( + screen.getByText('{"key":"value","nested":{"number":123}}') + ).toBeInTheDocument(); + }); +}); diff --git a/src/views/workflow-history-v2/workflow-history-details-row-tooltip-json/__tests__/workflow-history-details-row-tooltip-json.test.tsx b/src/views/workflow-history-v2/workflow-history-details-row-tooltip-json/__tests__/workflow-history-details-row-tooltip-json.test.tsx new file mode 100644 index 000000000..cb0d55bc2 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-details-row-tooltip-json/__tests__/workflow-history-details-row-tooltip-json.test.tsx @@ -0,0 +1,38 @@ +import { render, screen } from '@/test-utils/rtl'; + +import WorkflowHistoryDetailsRowTooltipJson from '../workflow-history-details-row-tooltip-json'; + +jest.mock( + '@/views/workflow-history/workflow-history-event-details-json/workflow-history-event-details-json', + () => + jest.fn(({ entryValue, isNegative }) => ( +
+ Event Details Json: {JSON.stringify(entryValue)} + {isNegative && ' (negative)'} +
+ )) +); + +describe(WorkflowHistoryDetailsRowTooltipJson.name, () => { + it('renders the label and passes value to WorkflowHistoryEventDetailsJson', () => { + render( + + ); + + expect(screen.getByText('test-label')).toBeInTheDocument(); + expect(screen.getByTestId('event-details-json')).toBeInTheDocument(); + expect( + screen.getByText( + /Event Details Json: \{"key":"value","nested":\{"number":123\}\}/ + ) + ).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 9feee2f87..ce2c89526 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 @@ -85,20 +85,13 @@ export default function WorkflowHistoryEventGroup({ ? [ getSummaryTabContentEntry({ groupId: eventGroup.firstEventId ?? 'unknown', - summaryDetails: summaryDetailsEntries.flatMap( - ([_eventId, { eventDetails }]) => eventDetails - ), + summaryDetails: groupSummaryDetails, }), ] : []), ...groupDetailsEntries, ], - [ - eventGroup.firstEventId, - groupDetailsEntries, - summaryDetailsEntries, - groupSummaryDetails.length, - ] + [eventGroup.firstEventId, groupDetailsEntries, groupSummaryDetails] ); return ( From e08dedc679565143a2e518ea7fa61a8416553301 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 8 Dec 2025 11:35:55 +0100 Subject: [PATCH 10/11] address copilot comments Signed-off-by: Adhitya Mamallan --- .../workflow-history-details-row-parsers.config.ts | 2 +- .../workflow-history-details-row.tsx | 14 +++++++++++--- .../workflow-history-event-group.tsx | 4 +++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/views/workflow-history-v2/config/workflow-history-details-row-parsers.config.ts b/src/views/workflow-history-v2/config/workflow-history-details-row-parsers.config.ts index 84c04edd4..3da86085e 100644 --- a/src/views/workflow-history-v2/config/workflow-history-details-row-parsers.config.ts +++ b/src/views/workflow-history-v2/config/workflow-history-details-row-parsers.config.ts @@ -33,7 +33,7 @@ const workflowHistoryDetailsRowParsersConfig: Array = [ icon: MdHourglassBottom, }, { - name: '"attempt" greater than 1, as "retries"', + name: '"attempt" greater than 0, as "retries"', matcher: (name) => name === 'attempt', hide: (_, value) => typeof value === 'number' && value <= 0, icon: MdReplay, diff --git a/src/views/workflow-history-v2/workflow-history-details-row/workflow-history-details-row.tsx b/src/views/workflow-history-v2/workflow-history-details-row/workflow-history-details-row.tsx index 72a434f21..bae6c4654 100644 --- a/src/views/workflow-history-v2/workflow-history-details-row/workflow-history-details-row.tsx +++ b/src/views/workflow-history-v2/workflow-history-details-row/workflow-history-details-row.tsx @@ -18,14 +18,22 @@ export default function WorkflowHistoryDetailsRow({ return getParsedDetailsRowItems(detailsEntries); }, [detailsEntries]); + const negativePathsSet = useMemo( + () => + new Set( + detailsEntries + .filter((entry) => entry.isNegative) + .map((entry) => entry.path) + ), + [detailsEntries] + ); + if (rowItems.length === 0) return
; return ( {rowItems.map((item) => { - const isNegative = detailsEntries.some( - (detail) => detail.path === item.path && detail.isNegative - ); + const isNegative = negativePathsSet.has(item.path); return ( - summaryDetailsEntries.flatMap(([_, { eventDetails }]) => eventDetails), + summaryDetailsEntries.flatMap( + ([_eventId, { eventDetails }]) => eventDetails + ), [summaryDetailsEntries] ); From 350bac65278033a15309209734fe6e19041e2482 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Wed, 10 Dec 2025 12:48:00 +0100 Subject: [PATCH 11/11] remove negative margins from summary items container Signed-off-by: Adhitya Mamallan --- .../workflow-history-event-group.styles.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.styles.ts b/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.styles.ts index 1534eab32..b2d9ce5d6 100644 --- a/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.styles.ts +++ b/src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.styles.ts @@ -35,15 +35,9 @@ export const styled = { gap: $theme.sizing.scale300, alignItems: 'center', })), - SummarizedDetailsContainer: createStyled( - 'div', - ({ $theme }: { $theme: Theme }) => ({ - overflow: 'hidden', - [$theme.mediaQuery.medium]: { - margin: `-${$theme.sizing.scale200} 0`, - }, - }) - ), + SummarizedDetailsContainer: createStyled('div', { + overflow: 'hidden', + }), ActionsContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ display: 'flex', gap: $theme.sizing.scale300,