Skip to content

Commit c012698

Browse files
Group history fields (#694)
* init commit for grouping * Clean up UI for grouping and fix issues * Add unit tests * remove unused fixture * Temp commit * Fix implementation * Code cleanup * Remove need for label, and rename components * remove label from fixture too
1 parent ba1352c commit c012698

17 files changed

+729
-274
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { type WorkflowHistoryEventDetailsEntries } from '../workflow-history-event-details/workflow-history-event-details.types';
2+
3+
export const mockWorkflowHistoryDetailsEntries: WorkflowHistoryEventDetailsEntries =
4+
[
5+
{
6+
key: 'version',
7+
path: 'version',
8+
isGroup: false,
9+
value: 1,
10+
renderConfig: null,
11+
},
12+
{
13+
key: 'taskId',
14+
path: 'taskId',
15+
isGroup: false,
16+
value: '1234567890',
17+
renderConfig: null,
18+
},
19+
{
20+
key: 'eventId',
21+
path: 'eventId',
22+
isGroup: false,
23+
value: 1,
24+
renderConfig: null,
25+
},
26+
{
27+
key: 'timestamp',
28+
path: 'timestamp',
29+
isGroup: false,
30+
value: new Date('2024-10-14T12:34:18.721Z'),
31+
renderConfig: null,
32+
},
33+
{
34+
key: 'fields',
35+
path: 'header.fields',
36+
isGroup: true,
37+
renderConfig: null,
38+
groupEntries: [
39+
{
40+
key: 'data',
41+
path: 'header.fields.mockField1.data',
42+
isGroup: false,
43+
value: 'mock-data-1',
44+
renderConfig: null,
45+
},
46+
{
47+
key: 'data',
48+
path: 'header.fields.mockField2.data',
49+
isGroup: false,
50+
value: 'mock-data-2',
51+
renderConfig: null,
52+
},
53+
{
54+
key: 'data',
55+
path: 'header.fields.mockField3.data',
56+
isGroup: true,
57+
renderConfig: null,
58+
groupEntries: [
59+
{
60+
key: 'subField',
61+
path: 'header.fields.mockField3.data.subField',
62+
isGroup: false,
63+
value: 'mock-data-3.1',
64+
renderConfig: null,
65+
},
66+
{
67+
key: 'subTimestamp',
68+
path: 'header.fields.mockField3.data.subTimestamp',
69+
isGroup: false,
70+
value: new Date('2024-10-14T11:34:18.721Z'),
71+
renderConfig: null,
72+
},
73+
],
74+
},
75+
],
76+
},
77+
];
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { type WorkflowPageTabsParams } from '@/views/workflow-page/workflow-page-tabs/workflow-page-tabs.types';
2+
3+
export const workflowPageUrlParams: WorkflowPageTabsParams = {
4+
cluster: 'testCluster',
5+
domain: 'testDomain',
6+
workflowId: 'testWorkflowId',
7+
runId: 'testRunId',
8+
workflowTab: 'history',
9+
};
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React from 'react';
2+
3+
import { render } from '@/test-utils/rtl';
4+
5+
import { workflowPageUrlParams } from '../../__fixtures__/workflow-page-url-params';
6+
import { type WorkflowHistoryEventDetailsValueComponentProps } from '../../workflow-history-event-details/workflow-history-event-details.types';
7+
import WorkflowHistoryEventDetailsEntry from '../workflow-history-event-details-entry';
8+
import { type Props } from '../workflow-history-event-details-entry.types';
9+
10+
describe(WorkflowHistoryEventDetailsEntry.name, () => {
11+
it('renders the custom ValueComponent when provided', () => {
12+
const CustomComponent = ({
13+
entryKey,
14+
entryPath,
15+
entryValue,
16+
}: WorkflowHistoryEventDetailsValueComponentProps) => (
17+
<div>
18+
{entryKey} - {entryPath} - {entryValue}
19+
</div>
20+
);
21+
22+
const props: Props = {
23+
entryKey: 'key1',
24+
entryPath: 'path1',
25+
entryValue: 'value1',
26+
renderConfig: {
27+
name: 'Mock render config with custom component',
28+
customMatcher: () => true,
29+
valueComponent: CustomComponent,
30+
},
31+
...workflowPageUrlParams,
32+
};
33+
34+
const { getByText } = render(
35+
<WorkflowHistoryEventDetailsEntry {...props} />
36+
);
37+
38+
expect(getByText('key1 - path1 - value1')).toBeInTheDocument();
39+
});
40+
41+
it('renders the entryValue as a string when ValueComponent is not provided', () => {
42+
const props: Props = {
43+
entryKey: 'key2',
44+
entryPath: 'path2',
45+
entryValue: 'value2',
46+
renderConfig: {
47+
name: 'Mock render config without custom component',
48+
customMatcher: () => true,
49+
},
50+
...workflowPageUrlParams,
51+
};
52+
53+
const { getByText } = render(
54+
<WorkflowHistoryEventDetailsEntry {...props} />
55+
);
56+
57+
expect(getByText('value2')).toBeInTheDocument();
58+
});
59+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { type Props } from './workflow-history-event-details-entry.types';
2+
3+
export default function WorkflowHistoryEventDetailsEntry({
4+
entryKey,
5+
entryPath,
6+
entryValue,
7+
renderConfig,
8+
...decodedPageUrlParams
9+
}: Props) {
10+
const ValueComponent = renderConfig?.valueComponent;
11+
12+
if (ValueComponent !== undefined) {
13+
return (
14+
<ValueComponent
15+
entryKey={entryKey}
16+
entryPath={entryPath}
17+
entryValue={entryValue}
18+
{...decodedPageUrlParams}
19+
/>
20+
);
21+
}
22+
23+
return String(entryValue);
24+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {
2+
type WorkflowHistoryEventDetailsConfig,
3+
type WorkflowHistoryEventDetailsValueComponentProps,
4+
} from '../workflow-history-event-details/workflow-history-event-details.types';
5+
6+
export type Props = WorkflowHistoryEventDetailsValueComponentProps & {
7+
renderConfig: WorkflowHistoryEventDetailsConfig | null;
8+
};
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import React from 'react';
2+
3+
import { render, screen, within } from '@/test-utils/rtl';
4+
5+
import { mockWorkflowHistoryDetailsEntries } from '../../__fixtures__/mock-workflow-history-details-entries';
6+
import { workflowPageUrlParams } from '../../__fixtures__/workflow-page-url-params';
7+
import WorkflowHistoryEventDetailsGroup from '../workflow-history-event-details-group';
8+
9+
jest.mock('../helpers/get-details-field-label', () =>
10+
jest.fn().mockImplementation(({ label }) => label)
11+
);
12+
13+
jest.mock(
14+
'../../workflow-history-event-details-entry/workflow-history-event-details-entry',
15+
() => jest.fn(({ entryValue }) => <div>{String(entryValue)}</div>)
16+
);
17+
18+
describe(WorkflowHistoryEventDetailsGroup.name, () => {
19+
it('renders without crashing', () => {
20+
render(
21+
<WorkflowHistoryEventDetailsGroup
22+
entries={mockWorkflowHistoryDetailsEntries}
23+
decodedPageUrlParams={workflowPageUrlParams}
24+
/>
25+
);
26+
});
27+
28+
it('renders the correct number of divs', () => {
29+
render(
30+
<WorkflowHistoryEventDetailsGroup
31+
entries={mockWorkflowHistoryDetailsEntries}
32+
decodedPageUrlParams={workflowPageUrlParams}
33+
/>
34+
);
35+
36+
const detailsRows = screen.getAllByTestId('details-row');
37+
// The mock details object has 10 key-value pairs in total, including children
38+
expect(detailsRows).toHaveLength(10);
39+
});
40+
41+
it('stops recursion if an object has a value component defined in its render config', () => {
42+
render(
43+
<WorkflowHistoryEventDetailsGroup
44+
entries={mockWorkflowHistoryDetailsEntries}
45+
decodedPageUrlParams={workflowPageUrlParams}
46+
/>
47+
);
48+
49+
const detailsRows = screen.getAllByTestId('details-row');
50+
51+
const firstDate = detailsRows[3];
52+
expect(
53+
within(firstDate).getByText(/Mon Oct 14 2024 12:34:18/)
54+
).toBeDefined();
55+
56+
const secondDate = detailsRows[9];
57+
expect(
58+
within(secondDate).getByText(/Mon Oct 14 2024 11:34:18/)
59+
).toBeDefined();
60+
});
61+
62+
it('renders nested details correctly', () => {
63+
render(
64+
<WorkflowHistoryEventDetailsGroup
65+
entries={mockWorkflowHistoryDetailsEntries}
66+
decodedPageUrlParams={workflowPageUrlParams}
67+
/>
68+
);
69+
70+
const detailsRows = screen.getAllByTestId('details-row');
71+
72+
const headerSubRows = within(detailsRows[4]).getAllByTestId('details-row');
73+
expect(headerSubRows).toHaveLength(5);
74+
75+
const field3SubRows = within(headerSubRows[2]).getAllByTestId(
76+
'details-row'
77+
);
78+
expect(field3SubRows).toHaveLength(2);
79+
});
80+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {
2+
type WorkflowHistoryEventDetailsGroupEntry,
3+
type WorkflowHistoryEventDetailsEntry,
4+
} from '../../../workflow-history-event-details/workflow-history-event-details.types';
5+
import getDetailsFieldLabel from '../get-details-field-label';
6+
7+
const singleEntry: WorkflowHistoryEventDetailsEntry = {
8+
key: 'testKey',
9+
path: 'testKey',
10+
isGroup: false,
11+
value: 'testValue',
12+
renderConfig: {
13+
name: 'Mock render config without custom label',
14+
customMatcher: () => true,
15+
},
16+
};
17+
18+
const groupEntry: WorkflowHistoryEventDetailsGroupEntry = {
19+
key: 'testKey',
20+
path: 'testKey',
21+
isGroup: true,
22+
groupEntries: [
23+
{
24+
key: 'testKey1',
25+
path: 'testKey.testKey1',
26+
isGroup: false,
27+
value: 'testValue1',
28+
renderConfig: null,
29+
},
30+
{
31+
key: 'testKey2',
32+
path: 'testKey.testKey2',
33+
isGroup: false,
34+
value: 'testValue2',
35+
renderConfig: null,
36+
},
37+
],
38+
renderConfig: {
39+
name: 'Mock render config without custom label',
40+
customMatcher: () => true,
41+
},
42+
};
43+
44+
describe('getDetailsFieldLabel', () => {
45+
it('should return the label from renderConfig if getLabel is defined', () => {
46+
const mockGetLabel = jest.fn().mockReturnValue('Custom Label');
47+
48+
const entry = {
49+
...singleEntry,
50+
renderConfig: {
51+
name: 'Mock render config with custom label',
52+
customMatcher: () => true,
53+
getLabel: mockGetLabel,
54+
},
55+
};
56+
57+
expect(getDetailsFieldLabel(entry)).toBe('Custom Label');
58+
59+
expect(mockGetLabel).toHaveBeenCalledWith({
60+
key: 'testKey',
61+
path: 'testKey',
62+
value: 'testValue',
63+
});
64+
});
65+
66+
it('should return the default label if renderConfig.getLabel is not defined', () => {
67+
expect(getDetailsFieldLabel(singleEntry)).toBe('testKey');
68+
});
69+
70+
it('should append the number of entries to label for group entries', () => {
71+
const entry = {
72+
...groupEntry,
73+
renderConfig: {
74+
name: 'Mock render config with custom label',
75+
customMatcher: () => true,
76+
getLabel: jest.fn().mockReturnValue('Custom Label'),
77+
},
78+
};
79+
80+
expect(getDetailsFieldLabel(entry)).toBe('Custom Label (2)');
81+
});
82+
83+
it('should append the number of entries to default label for group entries', () => {
84+
expect(getDetailsFieldLabel(groupEntry)).toBe('testKey (2)');
85+
});
86+
87+
it('should subtract the parent path correctly for a child path', () => {
88+
expect(getDetailsFieldLabel(groupEntry.groupEntries[0], 'testKey')).toBe(
89+
'testKey1'
90+
);
91+
});
92+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {
2+
type WorkflowHistoryEventDetailsGroupEntry,
3+
type WorkflowHistoryEventDetailsEntry,
4+
} from '../../workflow-history-event-details/workflow-history-event-details.types';
5+
6+
export default function getDetailsFieldLabel(
7+
entry:
8+
| WorkflowHistoryEventDetailsEntry
9+
| WorkflowHistoryEventDetailsGroupEntry,
10+
parentGroupPath: string = ''
11+
) {
12+
const defaultLabel =
13+
parentGroupPath && entry.path.startsWith(parentGroupPath + '.')
14+
? entry.path.slice(parentGroupPath.length + 1)
15+
: entry.path;
16+
17+
const mainLabel = entry.renderConfig?.getLabel
18+
? entry.renderConfig.getLabel({
19+
key: entry.key,
20+
path: entry.path,
21+
value: entry.isGroup ? undefined : entry.value,
22+
})
23+
: defaultLabel;
24+
25+
const groupSuffix = entry.isGroup
26+
? ' ' + `(${entry.groupEntries.length})`
27+
: '';
28+
29+
return mainLabel + groupSuffix;
30+
}

0 commit comments

Comments
 (0)