Skip to content

Commit 25c8e63

Browse files
feat: Show summarized history event details in event group row (#1106)
* Add WorkflowHistoryRowDetails component that displays summarized details in the event group entry * Add components for the row JSON and the on-hover tooltip JSON Signed-off-by: Adhitya Mamallan <[email protected]>
1 parent d61bc2a commit 25c8e63

17 files changed

+962
-7
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
MdHourglassBottom,
3+
MdOutlineMonitorHeart,
4+
MdReplay,
5+
} from 'react-icons/md';
6+
7+
import { type DetailsRowItemParser } from '../workflow-history-details-row/workflow-history-details-row.types';
8+
import WorkflowHistoryDetailsRowJson from '../workflow-history-details-row-json/workflow-history-details-row-json';
9+
import WorkflowHistoryDetailsRowTooltipJson from '../workflow-history-details-row-tooltip-json/workflow-history-details-row-tooltip-json';
10+
11+
const workflowHistoryDetailsRowParsersConfig: Array<DetailsRowItemParser> = [
12+
{
13+
name: 'Heartbeat time',
14+
matcher: (name) => name === 'lastHeartbeatTime',
15+
icon: MdOutlineMonitorHeart,
16+
},
17+
{
18+
name: 'Json as PrettyJson',
19+
matcher: (name, value) =>
20+
value !== null &&
21+
new RegExp(
22+
'(input|result|details|failureDetails|Error|lastCompletionResult|heartbeatDetails|lastFailureDetails)$'
23+
).test(name),
24+
icon: null,
25+
customRenderValue: WorkflowHistoryDetailsRowJson,
26+
customTooltipContent: WorkflowHistoryDetailsRowTooltipJson,
27+
invertTooltipColors: true,
28+
},
29+
{
30+
name: 'Timeouts with timer icon',
31+
matcher: (name) =>
32+
new RegExp('(TimeoutSeconds|BackoffSeconds|InSeconds)$').test(name),
33+
icon: MdHourglassBottom,
34+
},
35+
{
36+
name: '"attempt" greater than 0, as "retries"',
37+
matcher: (name) => name === 'attempt',
38+
hide: (_, value) => typeof value === 'number' && value <= 0,
39+
icon: MdReplay,
40+
customTooltipContent: () => 'retries',
41+
},
42+
];
43+
44+
export default workflowHistoryDetailsRowParsersConfig;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { render, screen } from '@/test-utils/rtl';
2+
3+
import WorkflowHistoryDetailsRowJson from '../workflow-history-details-row-json';
4+
5+
describe(WorkflowHistoryDetailsRowJson.name, () => {
6+
it('renders the stringified JSON value', () => {
7+
render(
8+
<WorkflowHistoryDetailsRowJson
9+
value={{ key: 'value', nested: { number: 123 } }}
10+
isNegative={false}
11+
label="test-label"
12+
domain="test-domain"
13+
cluster="test-cluster"
14+
workflowId="test-workflow-id"
15+
runId="test-run-id"
16+
/>
17+
);
18+
19+
expect(
20+
screen.getByText('{"key":"value","nested":{"number":123}}')
21+
).toBeInTheDocument();
22+
});
23+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { styled as createStyled } from 'baseui';
2+
3+
export const styled = {
4+
JsonViewContainer: createStyled<'div', { $isNegative: boolean }>(
5+
'div',
6+
({ $theme, $isNegative }) => ({
7+
color: $isNegative ? $theme.colors.contentNegative : '#A964F7',
8+
maxWidth: '360px',
9+
overflow: 'hidden',
10+
whiteSpace: 'nowrap',
11+
textOverflow: 'ellipsis',
12+
...$theme.typography.MonoParagraphXSmall,
13+
})
14+
),
15+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import losslessJsonStringify from '@/utils/lossless-json-stringify';
2+
3+
import { type DetailsRowValueComponentProps } from '../workflow-history-details-row/workflow-history-details-row.types';
4+
5+
import { styled } from './workflow-history-details-row-json.styles';
6+
7+
export default function WorkflowHistoryDetailsRowJson({
8+
value,
9+
isNegative,
10+
}: DetailsRowValueComponentProps) {
11+
return (
12+
<styled.JsonViewContainer $isNegative={isNegative ?? false}>
13+
{losslessJsonStringify(value)}
14+
</styled.JsonViewContainer>
15+
);
16+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { type DetailsRowValueComponentProps } from '../workflow-history-details-row/workflow-history-details-row.types';
2+
3+
export type Props = DetailsRowValueComponentProps;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { render, screen } from '@/test-utils/rtl';
2+
3+
import WorkflowHistoryDetailsRowTooltipJson from '../workflow-history-details-row-tooltip-json';
4+
5+
jest.mock(
6+
'@/views/workflow-history/workflow-history-event-details-json/workflow-history-event-details-json',
7+
() =>
8+
jest.fn(({ entryValue, isNegative }) => (
9+
<div data-testid="event-details-json">
10+
Event Details Json: {JSON.stringify(entryValue)}
11+
{isNegative && ' (negative)'}
12+
</div>
13+
))
14+
);
15+
16+
describe(WorkflowHistoryDetailsRowTooltipJson.name, () => {
17+
it('renders the label and passes value to WorkflowHistoryEventDetailsJson', () => {
18+
render(
19+
<WorkflowHistoryDetailsRowTooltipJson
20+
value={{ key: 'value', nested: { number: 123 } }}
21+
label="test-label"
22+
isNegative={false}
23+
domain="test-domain"
24+
cluster="test-cluster"
25+
workflowId="test-workflow-id"
26+
runId="test-run-id"
27+
/>
28+
);
29+
30+
expect(screen.getByText('test-label')).toBeInTheDocument();
31+
expect(screen.getByTestId('event-details-json')).toBeInTheDocument();
32+
expect(
33+
screen.getByText(
34+
/Event Details Json: \{"key":"value","nested":\{"number":123\}\}/
35+
)
36+
).toBeInTheDocument();
37+
});
38+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { styled as createStyled } from 'baseui';
2+
3+
export const styled = {
4+
JsonPreviewContainer: createStyled('div', ({ $theme }) => ({
5+
display: 'flex',
6+
flexDirection: 'column',
7+
alignItems: 'flex-start',
8+
gap: $theme.sizing.scale200,
9+
})),
10+
JsonPreviewLabel: createStyled('div', ({ $theme }) => ({
11+
...$theme.typography.LabelXSmall,
12+
})),
13+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import WorkflowHistoryEventDetailsJson from '@/views/workflow-history/workflow-history-event-details-json/workflow-history-event-details-json';
2+
3+
import { type DetailsRowValueComponentProps } from '../workflow-history-details-row/workflow-history-details-row.types';
4+
5+
import { styled } from './workflow-history-details-row-tooltip-json.styles';
6+
7+
export default function WorkflowHistoryDetailsRowTooltipJson({
8+
value,
9+
label,
10+
isNegative,
11+
}: DetailsRowValueComponentProps) {
12+
return (
13+
<styled.JsonPreviewContainer>
14+
<styled.JsonPreviewLabel>{label}</styled.JsonPreviewLabel>
15+
<WorkflowHistoryEventDetailsJson
16+
entryValue={value}
17+
isNegative={isNegative}
18+
/>
19+
</styled.JsonPreviewContainer>
20+
);
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { type DetailsRowValueComponentProps } from '../workflow-history-details-row/workflow-history-details-row.types';
2+
3+
export type Props = DetailsRowValueComponentProps;
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { render, screen, userEvent } from '@/test-utils/rtl';
2+
3+
import type { WorkflowPageParams } from '@/views/workflow-page/workflow-page.types';
4+
5+
import { type EventDetailsEntries } from '../../workflow-history-event-details/workflow-history-event-details.types';
6+
import WorkflowHistoryDetailsRow from '../workflow-history-details-row';
7+
import { type DetailsRowItem } from '../workflow-history-details-row.types';
8+
9+
jest.mock('../helpers/get-parsed-details-row-items', () =>
10+
jest.fn((detailsEntries: EventDetailsEntries) =>
11+
detailsEntries.reduce<Array<DetailsRowItem>>((acc, entry) => {
12+
if (!entry.isGroup) {
13+
acc.push({
14+
path: entry.path,
15+
label: entry.path,
16+
value: entry.value,
17+
icon: ({ size }: any) => (
18+
<span data-testid={`icon-${entry.path}`} data-size={size} />
19+
),
20+
renderValue: ({ value, isNegative }: any) => (
21+
<span
22+
data-testid={`field-${entry.path}`}
23+
data-negative={isNegative}
24+
>
25+
{value}
26+
</span>
27+
),
28+
renderTooltip: ({ label }: any) => (
29+
<span data-testid={`tooltip-${entry.path}`}>{label}</span>
30+
),
31+
invertTooltipColors: acc.length === 1, // Second item has inverted tooltip
32+
omitWrapping: acc.length === 2, // Third item omits wrapping
33+
});
34+
}
35+
return acc;
36+
}, [])
37+
)
38+
);
39+
40+
const mockWorkflowPageParams: WorkflowPageParams = {
41+
cluster: 'test-cluster',
42+
domain: 'test-domain',
43+
workflowId: 'test-workflow',
44+
runId: 'test-run',
45+
};
46+
47+
const mockDetailsEntries: EventDetailsEntries = [
48+
{
49+
key: 'field1',
50+
path: 'field1',
51+
isGroup: false,
52+
value: 'value1',
53+
isNegative: false,
54+
renderConfig: null,
55+
},
56+
{
57+
key: 'field2',
58+
path: 'field2',
59+
isGroup: false,
60+
value: 'value2',
61+
isNegative: true,
62+
renderConfig: null,
63+
},
64+
{
65+
key: 'field3',
66+
path: 'field3',
67+
isGroup: false,
68+
value: 'value3',
69+
isNegative: false,
70+
renderConfig: null,
71+
},
72+
];
73+
74+
describe(WorkflowHistoryDetailsRow.name, () => {
75+
beforeEach(() => {
76+
jest.clearAllMocks();
77+
});
78+
79+
it('should render details row items when detailsEntries has items', () => {
80+
setup();
81+
82+
expect(screen.getByTestId('field-field1')).toBeInTheDocument();
83+
expect(screen.getByTestId('field-field2')).toBeInTheDocument();
84+
expect(screen.getByTestId('field-field3')).toBeInTheDocument();
85+
expect(screen.getByText('value1')).toBeInTheDocument();
86+
expect(screen.getByText('value2')).toBeInTheDocument();
87+
expect(screen.getByText('value3')).toBeInTheDocument();
88+
});
89+
90+
it('should mark negative fields correctly', () => {
91+
setup();
92+
93+
const negativeField = screen.getByTestId('field-field2');
94+
expect(negativeField).toHaveAttribute('data-negative', 'true');
95+
96+
const positiveField = screen.getByTestId('field-field1');
97+
expect(positiveField).toHaveAttribute('data-negative', 'false');
98+
});
99+
100+
it('should render icons when provided in item config', () => {
101+
setup();
102+
103+
expect(screen.getByTestId('icon-field1')).toBeInTheDocument();
104+
expect(screen.getByTestId('icon-field2')).toBeInTheDocument();
105+
expect(screen.getByTestId('icon-field3')).toBeInTheDocument();
106+
});
107+
108+
it('should render tooltip content on hover', async () => {
109+
const { user } = setup();
110+
111+
const field1 = screen.getByTestId('field-field1');
112+
await user.hover(field1);
113+
114+
expect(await screen.findByTestId('tooltip-field1')).toBeInTheDocument();
115+
expect(screen.getByText('field1')).toBeInTheDocument();
116+
});
117+
});
118+
119+
function setup({
120+
detailsEntries = mockDetailsEntries,
121+
workflowPageParams = mockWorkflowPageParams,
122+
}: {
123+
detailsEntries?: EventDetailsEntries;
124+
workflowPageParams?: WorkflowPageParams;
125+
} = {}) {
126+
const user = userEvent.setup();
127+
128+
const renderResult = render(
129+
<WorkflowHistoryDetailsRow
130+
detailsEntries={detailsEntries}
131+
{...workflowPageParams}
132+
/>
133+
);
134+
135+
return { user, ...renderResult };
136+
}

0 commit comments

Comments
 (0)