Skip to content
Prev Previous commit
Next Next commit
More changes
Signed-off-by: Adhitya Mamallan <adhitya.mamallan@uber.com>
  • Loading branch information
adhityamamallan committed Dec 5, 2025
commit 0d7153299dd8632422995541b6d1ea6a67cabef8
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof WorkflowHistoryPanelDetailsEntry>(
'../../workflow-history-panel-details-entry/workflow-history-panel-details-entry',
() =>
jest.fn(
({
entryPath,
entryValue,
isNegative,
}: {
entryPath: string;
entryValue: any;
isNegative?: boolean;
}) => (
<div data-testid="group-details-json">
JSON: {entryPath} = {JSON.stringify(entryValue)}
{isNegative && ' (negative)'}
</div>
)
)
jest.fn(({ detail }) => (
<div data-testid="panel-details-entry">
Panel Entry: {detail.path} ={' '}
{JSON.stringify(detail.isGroup ? detail.groupEntries : detail.value)}
{detail.isNegative && ' (negative)'}
</div>
))
);

jest.mock(
Expand All @@ -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();
});

Expand All @@ -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)')
Expand Down Expand Up @@ -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)')
Expand Down Expand Up @@ -148,19 +142,21 @@ 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(
screen.getByText('Event Details Group (1 entries)')
).toBeInTheDocument();
});

it('passes correct props to WorkflowHistoryGroupDetailsJson', () => {
it('passes correct props to WorkflowHistoryPanelDetailsEntry', () => {
const eventDetails: EventDetailsEntries = [
{
key: 'key1',
Expand All @@ -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();
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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);
Expand All @@ -42,24 +43,10 @@ export default function WorkflowHistoryEventDetails({
{panelDetails.map((detail) => {
return (
<styled.PanelContainer key={detail.path}>
{!detail.isGroup ? (
<WorkflowHistoryEventDetailsEntry
entryKey={detail.key}
entryPath={detail.path}
entryValue={detail.value}
isNegative={detail.isNegative}
renderConfig={detail.renderConfig}
{...workflowPageParams}
/>
) : (
<WorkflowHistoryEventDetailsGroup
entries={restDetails}
decodedPageUrlParams={{
...workflowPageParams,
workflowTab: 'history',
}}
/>
)}
<WorkflowHistoryPanelDetailsEntry
detail={detail}
{...workflowPageParams}
/>
</styled.PanelContainer>
);
})}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
})
),
};
Original file line number Diff line number Diff line change
@@ -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 (
<ValueComponent
entryKey={entryKey}
entryPath={entryPath}
entryValue={entryValue}
isNegative={isNegative}
{...decodedPageUrlParams}
entryKey={detail.key}
entryPath={detail.path}
entryValue={detail.value}
isNegative={detail.isNegative}
{...workflowPageParams}
/>
);
}

return String(entryValue);
return (
<styled.PanelContainer $isNegative={detail.isNegative}>
<styled.PanelLabel $isNegative={detail.isNegative}>
{detail.path}
</styled.PanelLabel>
<styled.PanelValue $isNegative={detail.isNegative}>
{detail.isGroup ? (
<WorkflowHistoryEventDetailsGroup
entries={detail.groupEntries}
parentGroupPath={detail.path}
decodedPageUrlParams={{
...workflowPageParams,
workflowTab: 'history',
}}
/>
) : (
detail.value
)}
</styled.PanelValue>
</styled.PanelContainer>
);
}
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand All @@ -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',
Expand All @@ -50,20 +71,32 @@ 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:
'(input|result|details|failureDetails|Error|lastCompletionResult|heartbeatDetails|lastFailureDetails)$',
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)$',
getLabel: ({ key }) => key.replace(/InSeconds|Seconds|$/, ''), // remove seconds suffix from label as formatted duration can be minutes/hours etc.
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:
Expand All @@ -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:
Expand All @@ -90,6 +127,9 @@ const workflowHistoryEventDetailsConfig = [
});
},
},
/**
* Renames the "attempt" field label to "retryAttempt" for better clarity.
*/
{
name: 'Retry config attempt as retryAttempt',
key: 'attempt',
Expand Down