Skip to content

Commit 5f45bfb

Browse files
feat: Workflow History V2 - show Failed and Pending events in nav bar (#1128)
* Create WorkflowHistoryNavigationBarEventsMenu to show list of events (paginated if >10) * Use the events menu component in the nav bar to show failed and pending events * Filter out failed and pending events in WorkflowHistoryV2 to pass to nav bar * Use React state to handle scrolling to events in both grouped & ungrouped views Signed-off-by: Adhitya Mamallan <adhitya.mamallan@uber.com>
1 parent 76ef2c1 commit 5f45bfb

14 files changed

Lines changed: 856 additions & 24 deletions

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

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,29 @@ import { type HistoryEvent } from '@/__generated__/proto-ts/uber/cadence/api/v1/
1616
import * as usePageFiltersModule from '@/components/page-filters/hooks/use-page-filters';
1717
import { type PageQueryParamValues } from '@/hooks/use-page-query-params/use-page-query-params.types';
1818
import { type GetWorkflowHistoryResponse } from '@/route-handlers/get-workflow-history/get-workflow-history.types';
19+
import {
20+
type PendingActivityTaskStartEvent,
21+
type PendingDecisionTaskStartEvent,
22+
} from '@/views/workflow-history/workflow-history.types';
1923
import { mockDescribeWorkflowResponse } from '@/views/workflow-page/__fixtures__/describe-workflow-response';
2024
import type workflowPageQueryParamsConfig from '@/views/workflow-page/config/workflow-page-query-params.config';
2125

22-
import { completedActivityTaskEvents } from '../../workflow-history/__fixtures__/workflow-history-activity-events';
23-
import { completedDecisionTaskEvents } from '../../workflow-history/__fixtures__/workflow-history-decision-events';
26+
import {
27+
completedActivityTaskEvents,
28+
failedActivityTaskEvents,
29+
scheduleActivityTaskEvent,
30+
} from '../../workflow-history/__fixtures__/workflow-history-activity-events';
31+
import {
32+
completedDecisionTaskEvents,
33+
failedDecisionTaskEvents,
34+
scheduleDecisionTaskEvent,
35+
} from '../../workflow-history/__fixtures__/workflow-history-decision-events';
36+
import {
37+
pendingActivityTaskStartEvent,
38+
pendingDecisionTaskStartEvent,
39+
} from '../../workflow-history/__fixtures__/workflow-history-pending-events';
2440
import { WorkflowHistoryContext } from '../../workflow-history/workflow-history-context-provider/workflow-history-context-provider';
41+
import { type Props as NavbarProps } from '../workflow-history-navigation-bar/workflow-history-navigation-bar.types';
2542
import WorkflowHistoryV2 from '../workflow-history-v2';
2643

2744
jest.mock('@/hooks/use-page-query-params/use-page-query-params', () =>
@@ -111,9 +128,22 @@ jest.mock(
111128
jest.mock(
112129
'../workflow-history-navigation-bar/workflow-history-navigation-bar',
113130
() =>
114-
jest.fn(() => (
115-
<div data-testid="workflow-history-navigation-bar">Navigation Bar</div>
116-
))
131+
jest.fn(
132+
({ failedEventsMenuItems, pendingEventsMenuItems }: NavbarProps) => (
133+
<div data-testid="workflow-history-navigation-bar">
134+
{failedEventsMenuItems && failedEventsMenuItems.length > 0 && (
135+
<div data-testid="failed-events-menu-items-count">
136+
{failedEventsMenuItems.length} failed events
137+
</div>
138+
)}
139+
{pendingEventsMenuItems && pendingEventsMenuItems.length > 0 && (
140+
<div data-testid="pending-events-menu-items-count">
141+
{pendingEventsMenuItems.length} pending events
142+
</div>
143+
)}
144+
</div>
145+
)
146+
)
117147
);
118148

119149
jest.mock('@/utils/decode-url-params', () => jest.fn((params) => params));
@@ -408,6 +438,30 @@ describe(WorkflowHistoryV2.name, () => {
408438
await screen.findByTestId('ungrouped-selected-event-id')
409439
).toHaveTextContent('test-event-id');
410440
});
441+
442+
it('passes failed events menu items to navigation bar when failed events exist', async () => {
443+
await setup({
444+
historyEvents: [...failedActivityTaskEvents, ...failedDecisionTaskEvents],
445+
});
446+
447+
const failedItemsCounter = await screen.findByTestId(
448+
'failed-events-menu-items-count'
449+
);
450+
expect(failedItemsCounter).toHaveTextContent('2 failed events');
451+
});
452+
453+
it('passes pending events menu items to navigation bar when pending events exist', async () => {
454+
await setup({
455+
historyEvents: [scheduleActivityTaskEvent, scheduleDecisionTaskEvent],
456+
pendingActivities: [pendingActivityTaskStartEvent],
457+
pendingDecision: pendingDecisionTaskStartEvent,
458+
});
459+
460+
const pendingItemsCounter = await screen.findByTestId(
461+
'pending-events-menu-items-count'
462+
);
463+
expect(pendingItemsCounter).toHaveTextContent('2 pending events');
464+
});
411465
});
412466

413467
async function setup({
@@ -417,6 +471,9 @@ async function setup({
417471
pageQueryParamsValues = {},
418472
hasNextPage,
419473
ungroupedViewPreference,
474+
historyEvents = completedActivityTaskEvents,
475+
pendingActivities,
476+
pendingDecision,
420477
}: {
421478
error?: boolean;
422479
summaryError?: boolean;
@@ -426,7 +483,10 @@ async function setup({
426483
>;
427484
hasNextPage?: boolean;
428485
ungroupedViewPreference?: boolean | null;
429-
}) {
486+
historyEvents?: Array<HistoryEvent>;
487+
pendingActivities?: Array<PendingActivityTaskStartEvent>;
488+
pendingDecision?: PendingDecisionTaskStartEvent | null;
489+
} = {}) {
430490
const user = userEvent.setup();
431491

432492
const mockSetQueryParams = jest.fn();
@@ -497,12 +557,10 @@ async function setup({
497557
);
498558
}
499559

500-
const events: Array<HistoryEvent> = completedActivityTaskEvents;
501-
502560
return HttpResponse.json(
503561
{
504562
history: {
505-
events,
563+
events: historyEvents,
506564
},
507565
archived: false,
508566
nextPageToken: hasNextPage ? 'mock-next-page-token' : '',
@@ -526,7 +584,18 @@ async function setup({
526584
},
527585
}
528586
: {
529-
jsonResponse: mockDescribeWorkflowResponse,
587+
httpResolver: () => {
588+
const describeResponse = {
589+
...mockDescribeWorkflowResponse,
590+
...(pendingActivities && pendingActivities.length > 0
591+
? {
592+
pendingActivities,
593+
}
594+
: {}),
595+
...(pendingDecision ? { pendingDecision } : {}),
596+
};
597+
return HttpResponse.json(describeResponse, { status: 200 });
598+
},
530599
}),
531600
},
532601
],
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import {
2+
mockActivityEventGroup,
3+
mockDecisionEventGroup,
4+
} from '@/views/workflow-history/__fixtures__/workflow-history-event-groups';
5+
import { type HistoryEventsGroup } from '@/views/workflow-history/workflow-history.types';
6+
7+
import { type EventGroupEntry } from '../../workflow-history-v2.types';
8+
import getNavigationBarEventsMenuItems from '../get-navigation-bar-events-menu-items';
9+
10+
jest.mock(
11+
'../../workflow-history-event-group/helpers/get-event-group-filtering-type',
12+
() =>
13+
jest.fn((group: HistoryEventsGroup) => {
14+
if (group.groupType === 'Activity') return 'ACTIVITY';
15+
if (group.groupType === 'Decision') return 'DECISION';
16+
return 'WORKFLOW';
17+
})
18+
);
19+
20+
describe(getNavigationBarEventsMenuItems.name, () => {
21+
it('should return an empty array when eventGroupsEntries is empty', () => {
22+
const eventGroupsEntries: Array<EventGroupEntry> = [];
23+
24+
const result = getNavigationBarEventsMenuItems(
25+
eventGroupsEntries,
26+
() => true
27+
);
28+
29+
expect(result).toEqual([]);
30+
});
31+
32+
it('should skip groups with no events', () => {
33+
const groupWithoutEvents: HistoryEventsGroup = {
34+
...mockActivityEventGroup,
35+
events: [],
36+
};
37+
const eventGroupsEntries: Array<EventGroupEntry> = [
38+
['group1', groupWithoutEvents],
39+
];
40+
41+
const result = getNavigationBarEventsMenuItems(
42+
eventGroupsEntries,
43+
() => true
44+
);
45+
46+
expect(result).toEqual([]);
47+
});
48+
49+
it('should skip groups filtered out by filterFn', () => {
50+
const eventGroupsEntries: Array<EventGroupEntry> = [
51+
['group1', mockActivityEventGroup],
52+
['group2', mockDecisionEventGroup],
53+
];
54+
const filterFn = jest.fn((group: HistoryEventsGroup) => {
55+
return group.groupType === 'Decision';
56+
});
57+
58+
const result = getNavigationBarEventsMenuItems(
59+
eventGroupsEntries,
60+
filterFn
61+
);
62+
63+
expect(result).toHaveLength(1);
64+
expect(result[0]).toEqual({
65+
eventId: '4', // last eventId from completedDecisionTaskEvents
66+
label: 'Mock decision',
67+
type: 'DECISION',
68+
});
69+
});
70+
71+
it('should include groups that pass filterFn', () => {
72+
const eventGroupsEntries: Array<EventGroupEntry> = [
73+
['group1', mockActivityEventGroup],
74+
['group2', mockDecisionEventGroup],
75+
];
76+
77+
const result = getNavigationBarEventsMenuItems(
78+
eventGroupsEntries,
79+
() => true
80+
);
81+
82+
expect(result).toHaveLength(2);
83+
expect(result[0]).toEqual({
84+
eventId: '10', // last eventId from completedActivityTaskEvents
85+
label: 'Mock event',
86+
type: 'ACTIVITY',
87+
});
88+
expect(result[1]).toEqual({
89+
eventId: '4', // last eventId from completedDecisionTaskEvents
90+
label: 'Mock decision',
91+
type: 'DECISION',
92+
});
93+
});
94+
95+
it('should use shortLabel when available, otherwise use label', () => {
96+
const groupWithShortLabel: HistoryEventsGroup = {
97+
...mockActivityEventGroup,
98+
label: 'Long Label',
99+
shortLabel: 'Short',
100+
};
101+
const groupWithoutShortLabel: HistoryEventsGroup = {
102+
...mockDecisionEventGroup,
103+
label: 'Decision Label',
104+
};
105+
const eventGroupsEntries: Array<EventGroupEntry> = [
106+
['group1', groupWithShortLabel],
107+
['group2', groupWithoutShortLabel],
108+
];
109+
110+
const result = getNavigationBarEventsMenuItems(
111+
eventGroupsEntries,
112+
() => true
113+
);
114+
115+
expect(result).toHaveLength(2);
116+
expect(result[0].label).toBe('Short');
117+
expect(result[1].label).toBe('Decision Label');
118+
});
119+
120+
it('should use the last event eventId from each group', () => {
121+
const eventGroupsEntries: Array<EventGroupEntry> = [
122+
['group1', mockActivityEventGroup],
123+
];
124+
125+
const result = getNavigationBarEventsMenuItems(
126+
eventGroupsEntries,
127+
() => true
128+
);
129+
130+
expect(result).toHaveLength(1);
131+
// completedActivityTaskEvents has events with ids: '7', '9', '10'
132+
// Last eventId should be '10'
133+
expect(result[0].eventId).toBe('10');
134+
});
135+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { type HistoryEventsGroup } from '@/views/workflow-history/workflow-history.types';
2+
3+
import getEventGroupFilteringType from '../workflow-history-event-group/helpers/get-event-group-filtering-type';
4+
import { type NavigationBarEventsMenuItem } from '../workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.types';
5+
import { type EventGroupEntry } from '../workflow-history-v2.types';
6+
7+
export default function getNavigationBarEventsMenuItems(
8+
eventGroupsEntries: Array<EventGroupEntry>,
9+
filterFn: (group: HistoryEventsGroup) => boolean
10+
): Array<NavigationBarEventsMenuItem> {
11+
return eventGroupsEntries.reduce<Array<NavigationBarEventsMenuItem>>(
12+
(acc, [_, group]) => {
13+
const lastEventId = group.events.at(-1)?.eventId;
14+
if (!lastEventId) return acc;
15+
16+
if (!filterFn(group)) return acc;
17+
18+
acc.push({
19+
eventId: lastEventId,
20+
label: group.shortLabel ?? group.label,
21+
type: getEventGroupFilteringType(group),
22+
});
23+
24+
return acc;
25+
},
26+
[]
27+
);
28+
}

0 commit comments

Comments
 (0)