Skip to content

Commit 1d42160

Browse files
Add ungrouped event table to Workflow History page (#926)
- Add an ungrouped event table to Workflow History page that sorts events by Event ID, and adds the pending events at the end - Add some unit test coverage for Workflow History component
1 parent 94153bf commit 1d42160

20 files changed

+1386
-148
lines changed

src/views/workflow-history/__tests__/workflow-history.test.tsx

Lines changed: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ import {
1212
waitForElementToBeRemoved,
1313
} from '@/test-utils/rtl';
1414

15+
import { type HistoryEvent } from '@/__generated__/proto-ts/uber/cadence/api/v1/HistoryEvent';
1516
import * as usePageFiltersModule from '@/components/page-filters/hooks/use-page-filters';
1617
import { type Props as PageFiltersToggleProps } from '@/components/page-filters/page-filters-toggle/page-filters-toggle.types';
18+
import { type PageQueryParamValues } from '@/hooks/use-page-query-params/use-page-query-params.types';
1719
import { type GetWorkflowHistoryResponse } from '@/route-handlers/get-workflow-history/get-workflow-history.types';
1820
import { mockDescribeWorkflowResponse } from '@/views/workflow-page/__fixtures__/describe-workflow-response';
21+
import type workflowPageQueryParamsConfig from '@/views/workflow-page/config/workflow-page-query-params.config';
1922

2023
import { completedActivityTaskEvents } from '../__fixtures__/workflow-history-activity-events';
2124
import { completedDecisionTaskEvents } from '../__fixtures__/workflow-history-decision-events';
@@ -32,7 +35,13 @@ jest.mock(
3235

3336
jest.mock(
3437
'../workflow-history-timeline-group/workflow-history-timeline-group',
35-
() => jest.fn(() => <div>Timeline group card</div>)
38+
() =>
39+
jest.fn(({ onReset, resetToDecisionEventId }) => (
40+
<div>
41+
Timeline group card
42+
{resetToDecisionEventId && <button onClick={onReset}>Reset</button>}
43+
</div>
44+
))
3645
);
3746

3847
jest.mock(
@@ -69,6 +78,37 @@ jest.mock(
6978
() => jest.fn(() => <div>keep loading events</div>)
7079
);
7180

81+
jest.mock(
82+
'../workflow-history-expand-all-events-button/workflow-history-expand-all-events-button',
83+
() =>
84+
jest.fn(({ isExpandAllEvents, toggleIsExpandAllEvents }) => (
85+
<button onClick={toggleIsExpandAllEvents}>
86+
{isExpandAllEvents ? 'Collapse All' : 'Expand All'}
87+
</button>
88+
))
89+
);
90+
91+
jest.mock(
92+
'../workflow-history-export-json-button/workflow-history-export-json-button',
93+
() => jest.fn(() => <button>Export JSON</button>)
94+
);
95+
96+
jest.mock(
97+
'../workflow-history-ungrouped-table/workflow-history-ungrouped-table',
98+
() => jest.fn(() => <div>Ungrouped Table</div>)
99+
);
100+
101+
jest.mock(
102+
'@/views/workflow-actions/workflow-actions-modal/workflow-actions-modal',
103+
() =>
104+
jest.fn(({ onClose }) => (
105+
<div>
106+
<div>Workflow Actions</div>
107+
<button onClick={onClose}>Close</button>
108+
</div>
109+
))
110+
);
111+
72112
describe('WorkflowHistory', () => {
73113
it('renders page correctly', async () => {
74114
setup({});
@@ -180,6 +220,57 @@ describe('WorkflowHistory', () => {
180220
expect(screen.queryByText('keep loading events')).not.toBeInTheDocument();
181221
});
182222
});
223+
224+
it('should show no results when filtered events are empty', async () => {
225+
setup({ emptyEvents: true });
226+
expect(await screen.findByText('No Results')).toBeInTheDocument();
227+
});
228+
229+
it('should render expand all events button', async () => {
230+
setup({});
231+
expect(await screen.findByText('Expand All')).toBeInTheDocument();
232+
});
233+
234+
it('should render export JSON button', async () => {
235+
setup({});
236+
expect(await screen.findByText('Export JSON')).toBeInTheDocument();
237+
});
238+
239+
it('should show "Ungroup" button in grouped view and call setQueryParams when clicked', async () => {
240+
const { user, mockSetQueryParams } = await setup({
241+
pageQueryParamsValues: { ungroupedHistoryViewEnabled: false },
242+
});
243+
244+
const ungroupButton = await screen.findByText('Ungroup');
245+
expect(ungroupButton).toBeInTheDocument();
246+
247+
await user.click(ungroupButton);
248+
expect(mockSetQueryParams).toHaveBeenCalledWith({
249+
ungroupedHistoryViewEnabled: 'true',
250+
});
251+
});
252+
253+
it('should show "Group" button when in ungrouped view', async () => {
254+
await setup({
255+
pageQueryParamsValues: { ungroupedHistoryViewEnabled: true },
256+
});
257+
258+
expect(await screen.findByText('Group')).toBeInTheDocument();
259+
});
260+
261+
it('should show ungrouped table when ungrouped view is enabled', async () => {
262+
setup({ pageQueryParamsValues: { ungroupedHistoryViewEnabled: true } });
263+
expect(await screen.findByText('Ungrouped Table')).toBeInTheDocument();
264+
});
265+
266+
it('should show workflow actions modal when resetToDecisionEventId is set', async () => {
267+
const { user } = await setup({ withResetModal: true });
268+
269+
const resetButton = await screen.findByText('Reset');
270+
await user.click(resetButton);
271+
272+
expect(screen.getByText('Workflow Actions')).toBeInTheDocument();
273+
});
183274
});
184275

185276
async function setup({
@@ -188,19 +279,26 @@ async function setup({
188279
resolveLoadMoreManually,
189280
pageQueryParamsValues = {},
190281
hasNextPage,
282+
emptyEvents,
283+
withResetModal,
191284
}: {
192285
error?: boolean;
193286
summaryError?: boolean;
194287
resolveLoadMoreManually?: boolean;
195-
pageQueryParamsValues?: Record<string, string>;
288+
pageQueryParamsValues?: Partial<
289+
PageQueryParamValues<typeof workflowPageQueryParamsConfig>
290+
>;
196291
hasNextPage?: boolean;
292+
emptyEvents?: boolean;
293+
withResetModal?: boolean;
197294
}) {
198295
const user = userEvent.setup();
199296

297+
const mockSetQueryParams = jest.fn();
200298
if (pageQueryParamsValues) {
201299
jest.spyOn(usePageFiltersModule, 'default').mockReturnValue({
202300
queryParams: pageQueryParamsValues,
203-
setQueryParams: jest.fn(),
301+
setQueryParams: mockSetQueryParams,
204302
activeFiltersCount: 0,
205303
resetAllFilters: jest.fn(),
206304
});
@@ -255,10 +353,17 @@ async function setup({
255353
);
256354
}
257355

356+
let events: Array<HistoryEvent> = completedActivityTaskEvents;
357+
if (emptyEvents) {
358+
events = [];
359+
} else if (withResetModal) {
360+
events = completedDecisionTaskEvents;
361+
}
362+
258363
return HttpResponse.json(
259364
{
260365
history: {
261-
events: completedActivityTaskEvents,
366+
events,
262367
},
263368
archived: false,
264369
nextPageToken: hasNextPage ? 'mock-next-page-token' : '',
@@ -302,5 +407,11 @@ async function setup({
302407
screen.queryAllByText('Suspense placeholder')
303408
);
304409

305-
return { user, getRequestResolver, getRequestRejector, ...renderResult };
410+
return {
411+
user,
412+
getRequestResolver,
413+
getRequestRejector,
414+
...renderResult,
415+
mockSetQueryParams,
416+
};
306417
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const WORKFLOW_HISTORY_PAGE_SIZE_CONFIG = 200;
2+
3+
export default WORKFLOW_HISTORY_PAGE_SIZE_CONFIG;
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { type Timestamp } from '@/__generated__/proto-ts/google/protobuf/Timestamp';
2+
3+
import {
4+
pendingActivityTaskStartEvent,
5+
pendingDecisionTaskStartEvent,
6+
} from '../../__fixtures__/workflow-history-pending-events';
7+
import { startWorkflowExecutionEvent } from '../../__fixtures__/workflow-history-single-events';
8+
import { type WorkflowEventStatus } from '../../workflow-history-event-status-badge/workflow-history-event-status-badge.types';
9+
import compareUngroupedEvents from '../compare-ungrouped-events';
10+
11+
describe(compareUngroupedEvents.name, () => {
12+
it('orders non-pending events by event ID', () => {
13+
const eventA = {
14+
id: '1',
15+
label: 'Event A',
16+
status: 'COMPLETED' as WorkflowEventStatus,
17+
statusLabel: 'Completed',
18+
event: startWorkflowExecutionEvent,
19+
};
20+
const eventB = {
21+
id: '2',
22+
label: 'Event B',
23+
status: 'COMPLETED' as WorkflowEventStatus,
24+
statusLabel: 'Completed',
25+
event: startWorkflowExecutionEvent,
26+
};
27+
28+
expect(compareUngroupedEvents(eventA, eventB)).toBe(-1);
29+
expect(compareUngroupedEvents(eventB, eventA)).toBe(1);
30+
expect(compareUngroupedEvents(eventA, eventA)).toBe(0);
31+
});
32+
33+
it('puts non-pending events before pending events', () => {
34+
const nonPendingEvent = {
35+
id: '2',
36+
label: 'Non-pending Event',
37+
status: 'COMPLETED' as WorkflowEventStatus,
38+
statusLabel: 'Completed',
39+
event: startWorkflowExecutionEvent,
40+
};
41+
const pendingEvent = {
42+
id: '1',
43+
label: 'Pending Event',
44+
status: 'WAITING' as WorkflowEventStatus,
45+
statusLabel: 'Waiting',
46+
event: pendingActivityTaskStartEvent,
47+
};
48+
49+
expect(compareUngroupedEvents(nonPendingEvent, pendingEvent)).toBe(-1);
50+
expect(compareUngroupedEvents(pendingEvent, nonPendingEvent)).toBe(1);
51+
});
52+
53+
it('orders pending events by event time', () => {
54+
const eventTimeA: Timestamp = { seconds: '1000', nanos: 0 };
55+
const eventTimeB: Timestamp = { seconds: '2000', nanos: 0 };
56+
57+
const pendingEventA = {
58+
id: '1',
59+
label: 'Pending Event A',
60+
status: 'WAITING' as WorkflowEventStatus,
61+
statusLabel: 'Waiting',
62+
event: {
63+
...pendingActivityTaskStartEvent,
64+
eventTime: eventTimeA,
65+
},
66+
};
67+
const pendingEventB = {
68+
id: '2',
69+
label: 'Pending Event B',
70+
status: 'WAITING' as WorkflowEventStatus,
71+
statusLabel: 'Waiting',
72+
event: {
73+
...pendingDecisionTaskStartEvent,
74+
eventTime: eventTimeB,
75+
},
76+
};
77+
78+
expect(compareUngroupedEvents(pendingEventA, pendingEventB)).toBe(-1000000);
79+
expect(compareUngroupedEvents(pendingEventB, pendingEventA)).toBe(1000000);
80+
expect(compareUngroupedEvents(pendingEventA, pendingEventA)).toBe(0);
81+
});
82+
83+
it('returns 0 when pending events have no event time', () => {
84+
const pendingEventA = {
85+
id: '1',
86+
label: 'Pending Event A',
87+
status: 'WAITING' as WorkflowEventStatus,
88+
statusLabel: 'Waiting',
89+
event: {
90+
...pendingActivityTaskStartEvent,
91+
eventTime: null,
92+
},
93+
};
94+
const pendingEventB = {
95+
id: '2',
96+
label: 'Pending Event B',
97+
status: 'WAITING' as WorkflowEventStatus,
98+
statusLabel: 'Waiting',
99+
event: {
100+
...pendingDecisionTaskStartEvent,
101+
eventTime: null,
102+
},
103+
};
104+
105+
expect(compareUngroupedEvents(pendingEventA, pendingEventB)).toBe(0);
106+
});
107+
});

src/views/workflow-history/helpers/__tests__/get-visible-groups-has-missing-events.test.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { type VisibleHistoryGroupRanges } from '../../workflow-history.types';
12
import getVisibleGroupsHasMissingEvents from '../get-visible-groups-has-missing-events';
23

34
describe('getVisibleGroupsHasMissingEvents', () => {
@@ -9,57 +10,67 @@ describe('getVisibleGroupsHasMissingEvents', () => {
910
];
1011

1112
it('should return true if any event in the main visible range has missing events', () => {
12-
const visibleRanges = {
13+
const visibleRanges: VisibleHistoryGroupRanges = {
1314
startIndex: 0,
1415
endIndex: 2,
1516
compactStartIndex: 2,
1617
compactEndIndex: 3,
18+
ungroupedStartIndex: -1,
19+
ungroupedEndIndex: -1,
1720
};
1821
expect(getVisibleGroupsHasMissingEvents(groupEntries, visibleRanges)).toBe(
1922
true
2023
);
2124
});
2225

2326
it('should return true if any event in the compact visible range has missing events', () => {
24-
const visibleRanges = {
27+
const visibleRanges: VisibleHistoryGroupRanges = {
2528
startIndex: 0,
2629
endIndex: 1,
2730
compactStartIndex: 2,
2831
compactEndIndex: 3,
32+
ungroupedStartIndex: -1,
33+
ungroupedEndIndex: -1,
2934
};
3035
expect(getVisibleGroupsHasMissingEvents(groupEntries, visibleRanges)).toBe(
3136
true
3237
);
3338
});
3439

3540
it('should return false if no events in the visible range have missing events', () => {
36-
const visibleRanges = {
41+
const visibleRanges: VisibleHistoryGroupRanges = {
3742
startIndex: 0,
3843
endIndex: 0,
3944
compactStartIndex: 2,
4045
compactEndIndex: 2,
46+
ungroupedStartIndex: -1,
47+
ungroupedEndIndex: -1,
4148
};
4249
expect(getVisibleGroupsHasMissingEvents(groupEntries, visibleRanges)).toBe(
4350
false
4451
);
4552
});
4653

4754
it('should handle an empty groupEntries array and return false', () => {
48-
const visibleRanges = {
55+
const visibleRanges: VisibleHistoryGroupRanges = {
4956
startIndex: 0,
5057
endIndex: 0,
5158
compactStartIndex: 0,
5259
compactEndIndex: 0,
60+
ungroupedStartIndex: -1,
61+
ungroupedEndIndex: -1,
5362
};
5463
expect(getVisibleGroupsHasMissingEvents([], visibleRanges)).toBe(false);
5564
});
5665

5766
it('should handle out of range numbers and return false', () => {
58-
const visibleRanges = {
67+
const visibleRanges: VisibleHistoryGroupRanges = {
5968
startIndex: -1,
6069
endIndex: -1,
6170
compactStartIndex: 100,
6271
compactEndIndex: 200,
72+
ungroupedStartIndex: -1,
73+
ungroupedEndIndex: -1,
6374
};
6475
expect(getVisibleGroupsHasMissingEvents(groupEntries, visibleRanges)).toBe(
6576
false

0 commit comments

Comments
 (0)