Skip to content

Commit aabebe6

Browse files
feat: Workflow History V2 - Ungrouped Event Table (#1109)
* Implement basic ungrouped table with placeholders for ungrouped events Signed-off-by: Adhitya Mamallan <[email protected]>
1 parent a2a722f commit aabebe6

12 files changed

+721
-8
lines changed

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

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ jest.mock(
8686
))
8787
);
8888

89+
jest.mock(
90+
'../workflow-history-ungrouped-table/workflow-history-ungrouped-table',
91+
() =>
92+
jest.fn(() => (
93+
<div data-testid="workflow-history-ungrouped-table">Ungrouped Table</div>
94+
))
95+
);
96+
8997
jest.mock('@/utils/decode-url-params', () => jest.fn((params) => params));
9098

9199
const mockResetAllFilters = jest.fn();
@@ -182,15 +190,19 @@ describe(WorkflowHistoryV2.name, () => {
182190
expect(
183191
await screen.findByTestId('workflow-history-grouped-table')
184192
).toBeInTheDocument();
185-
expect(screen.queryByText('WIP: ungrouped table')).not.toBeInTheDocument();
193+
expect(
194+
screen.queryByTestId('workflow-history-ungrouped-table')
195+
).not.toBeInTheDocument();
186196
});
187197

188198
it('should render grouped table by default when ungroupedHistoryViewEnabled is not set and user preference is null', async () => {
189199
await setup({ ungroupedViewPreference: null });
190200
expect(
191201
await screen.findByTestId('workflow-history-grouped-table')
192202
).toBeInTheDocument();
193-
expect(screen.queryByText('WIP: ungrouped table')).not.toBeInTheDocument();
203+
expect(
204+
screen.queryByTestId('workflow-history-ungrouped-table')
205+
).not.toBeInTheDocument();
194206
});
195207

196208
it('should render ungrouped table when ungroupedHistoryViewEnabled query param is true', async () => {
@@ -199,7 +211,9 @@ describe(WorkflowHistoryV2.name, () => {
199211
ungroupedHistoryViewEnabled: true,
200212
},
201213
});
202-
expect(await screen.findByText('WIP: ungrouped table')).toBeInTheDocument();
214+
expect(
215+
await screen.findByTestId('workflow-history-ungrouped-table')
216+
).toBeInTheDocument();
203217
expect(
204218
screen.queryByTestId('workflow-history-grouped-table')
205219
).not.toBeInTheDocument();
@@ -214,12 +228,16 @@ describe(WorkflowHistoryV2.name, () => {
214228
expect(
215229
await screen.findByTestId('workflow-history-grouped-table')
216230
).toBeInTheDocument();
217-
expect(screen.queryByText('WIP: ungrouped table')).not.toBeInTheDocument();
231+
expect(
232+
screen.queryByTestId('workflow-history-ungrouped-table')
233+
).not.toBeInTheDocument();
218234
});
219235

220236
it('should render ungrouped table when user preference is true and query param is not set', async () => {
221237
await setup({ ungroupedViewPreference: true });
222-
expect(await screen.findByText('WIP: ungrouped table')).toBeInTheDocument();
238+
expect(
239+
await screen.findByTestId('workflow-history-ungrouped-table')
240+
).toBeInTheDocument();
223241
expect(
224242
screen.queryByTestId('workflow-history-grouped-table')
225243
).not.toBeInTheDocument();
@@ -232,7 +250,9 @@ describe(WorkflowHistoryV2.name, () => {
232250
});
233251

234252
// Should show ungrouped table even though preference is false
235-
expect(await screen.findByText('WIP: ungrouped table')).toBeInTheDocument();
253+
expect(
254+
await screen.findByTestId('workflow-history-ungrouped-table')
255+
).toBeInTheDocument();
236256
});
237257

238258
it('should use user preference when query param is undefined for ungrouped view', async () => {
@@ -242,7 +262,9 @@ describe(WorkflowHistoryV2.name, () => {
242262
});
243263

244264
// Should use preference (true) when query param is undefined
245-
expect(await screen.findByText('WIP: ungrouped table')).toBeInTheDocument();
265+
expect(
266+
await screen.findByTestId('workflow-history-ungrouped-table')
267+
).toBeInTheDocument();
246268
});
247269

248270
it('should call setUngroupedViewUserPreference and setQueryParams when toggle is clicked from grouped to ungrouped', async () => {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { styled as createStyled, type Theme } from 'baseui';
2+
3+
export const styled = {
4+
TempContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({
5+
...$theme.typography.MonoParagraphXSmall,
6+
padding: $theme.sizing.scale300,
7+
...$theme.borders.border100,
8+
})),
9+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { styled } from './workflow-history-ungrouped-event.styles';
2+
import { type Props } from './workflow-history-ungrouped-event.types';
3+
4+
export default function WorkflowHistoryUngroupedEvent({ eventInfo }: Props) {
5+
return (
6+
<styled.TempContainer>{JSON.stringify(eventInfo)}</styled.TempContainer>
7+
);
8+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { type WorkflowPageTabsParams } from '@/views/workflow-page/workflow-page-tabs/workflow-page-tabs.types';
2+
3+
import { type UngroupedEventInfo } from '../workflow-history-ungrouped-table/workflow-history-ungrouped-table.types';
4+
5+
export type Props = {
6+
// Core data props
7+
eventInfo: UngroupedEventInfo;
8+
workflowStartTimeMs: number | null;
9+
decodedPageUrlParams: WorkflowPageTabsParams;
10+
11+
// Expansion state
12+
isExpanded: boolean;
13+
toggleIsExpanded: () => void;
14+
15+
// UI behavior
16+
animateOnEnter?: boolean;
17+
onReset?: () => void;
18+
};
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import React from 'react';
2+
3+
import { VirtuosoMockContext } from 'react-virtuoso';
4+
5+
import { render, screen, userEvent, waitFor } from '@/test-utils/rtl';
6+
7+
import { type RequestError } from '@/utils/request/request-error';
8+
import { mockActivityEventGroup } from '@/views/workflow-history/__fixtures__/workflow-history-event-groups';
9+
import { type HistoryEventsGroup } from '@/views/workflow-history/workflow-history.types';
10+
import { type WorkflowPageTabsParams } from '@/views/workflow-page/workflow-page-tabs/workflow-page-tabs.types';
11+
12+
import WorkflowHistoryUngroupedTable from '../workflow-history-ungrouped-table';
13+
14+
jest.mock(
15+
'@/views/workflow-history/workflow-history-timeline-load-more/workflow-history-timeline-load-more',
16+
() =>
17+
jest.fn(({ error, hasNextPage, isFetchingNextPage, fetchNextPage }) => (
18+
<div data-testid="timeline-load-more">
19+
{error && <div data-testid="load-more-error">Error loading more</div>}
20+
{hasNextPage && <div data-testid="has-next-page">Has more</div>}
21+
{isFetchingNextPage && <div data-testid="is-fetching">Fetching...</div>}
22+
<button onClick={fetchNextPage} data-testid="fetch-more-button">
23+
Fetch More
24+
</button>
25+
</div>
26+
))
27+
);
28+
29+
jest.mock(
30+
'../../workflow-history-ungrouped-event/workflow-history-ungrouped-event',
31+
() =>
32+
jest.fn(
33+
({
34+
eventInfo,
35+
isExpanded,
36+
toggleIsExpanded,
37+
onReset,
38+
animateOnEnter,
39+
}) => (
40+
<div
41+
data-testid="workflow-history-ungrouped-event"
42+
data-expanded={isExpanded}
43+
data-animate-on-enter={animateOnEnter}
44+
data-event-id={eventInfo.id}
45+
>
46+
<button onClick={toggleIsExpanded}>Toggle Event</button>
47+
<div>Event ID: {eventInfo.id}</div>
48+
<div>Label: {eventInfo.label}</div>
49+
{onReset && <button onClick={onReset}>Reset Event</button>}
50+
</div>
51+
)
52+
)
53+
);
54+
55+
describe(WorkflowHistoryUngroupedTable.name, () => {
56+
it('should render all column headers in correct order', () => {
57+
setup();
58+
59+
expect(screen.getByText('ID')).toBeInTheDocument();
60+
expect(screen.getByText('Event group')).toBeInTheDocument();
61+
expect(screen.getByText('Status')).toBeInTheDocument();
62+
expect(screen.getByText('Time')).toBeInTheDocument();
63+
expect(screen.getByText('Duration')).toBeInTheDocument();
64+
expect(screen.getByText('Details')).toBeInTheDocument();
65+
});
66+
67+
it('should render events from event groups', () => {
68+
const mockEventGroups: Array<[string, HistoryEventsGroup]> = [
69+
['group-1', mockActivityEventGroup],
70+
];
71+
setup({ eventGroupsById: mockEventGroups });
72+
73+
const events = screen.getAllByTestId('workflow-history-ungrouped-event');
74+
expect(events.length).toBeGreaterThan(0);
75+
expect(events[0]).toHaveTextContent('Event ID:');
76+
});
77+
78+
it('should render events with correct labels from groups', () => {
79+
const mockEventGroups: Array<[string, HistoryEventsGroup]> = [
80+
['group-1', mockActivityEventGroup],
81+
];
82+
setup({ eventGroupsById: mockEventGroups });
83+
84+
const events = screen.getAllByTestId('workflow-history-ungrouped-event');
85+
expect(events[0]).toHaveTextContent(
86+
`Label: ${mockActivityEventGroup.label}`
87+
);
88+
});
89+
90+
it('should handle event expansion toggle', async () => {
91+
const { user, mockToggleIsEventExpanded } = setup({
92+
eventGroupsById: [['group-1', mockActivityEventGroup]],
93+
});
94+
95+
const toggleButtons = screen.getAllByText('Toggle Event');
96+
await user.click(toggleButtons[0]);
97+
98+
const firstEventId =
99+
mockActivityEventGroup.events[0].eventId ??
100+
mockActivityEventGroup.events[0].computedEventId;
101+
expect(mockToggleIsEventExpanded).toHaveBeenCalledWith(firstEventId);
102+
});
103+
104+
it('should pass isExpanded state to events', () => {
105+
const mockEventGroups: Array<[string, HistoryEventsGroup]> = [
106+
['group-1', mockActivityEventGroup],
107+
];
108+
const firstEventId =
109+
mockActivityEventGroup.events[0].eventId ??
110+
mockActivityEventGroup.events[0].computedEventId;
111+
112+
setup({
113+
eventGroupsById: mockEventGroups,
114+
getIsEventExpanded: jest.fn((id) => id === firstEventId),
115+
});
116+
117+
const events = screen.getAllByTestId('workflow-history-ungrouped-event');
118+
expect(events[0]).toHaveAttribute('data-expanded', 'true');
119+
if (events.length > 1) {
120+
expect(events[1]).toHaveAttribute('data-expanded', 'false');
121+
}
122+
});
123+
124+
it('should pass hasMoreEvents to load more component', () => {
125+
setup({
126+
hasMoreEvents: true,
127+
isFetchingMoreEvents: false,
128+
fetchMoreEvents: jest.fn(),
129+
});
130+
131+
expect(screen.getByTestId('timeline-load-more')).toBeInTheDocument();
132+
expect(screen.getByTestId('has-next-page')).toBeInTheDocument();
133+
});
134+
135+
it('should pass animateOnEnter for selectedEventId', async () => {
136+
const mockEventGroups: Array<[string, HistoryEventsGroup]> = [
137+
['group-1', mockActivityEventGroup],
138+
];
139+
const firstEventId =
140+
mockActivityEventGroup.events[0].eventId ??
141+
mockActivityEventGroup.events[0].computedEventId;
142+
143+
setup({
144+
eventGroupsById: mockEventGroups,
145+
selectedEventId: firstEventId,
146+
});
147+
148+
await waitFor(() => {
149+
const events = screen.getAllByTestId('workflow-history-ungrouped-event');
150+
expect(events[0]).toHaveAttribute('data-animate-on-enter', 'true');
151+
});
152+
});
153+
154+
it('should call resetToDecisionEventId when reset button is clicked on resettable event', async () => {
155+
const mockEventGroups: Array<[string, HistoryEventsGroup]> = [
156+
[
157+
'group-1',
158+
{
159+
...mockActivityEventGroup,
160+
resetToDecisionEventId: mockActivityEventGroup.events[0].eventId,
161+
},
162+
],
163+
];
164+
const { user, mockResetToDecisionEventId } = setup({
165+
eventGroupsById: mockEventGroups,
166+
});
167+
168+
const resetButtons = screen.getAllByText('Reset Event');
169+
await user.click(resetButtons[0]);
170+
171+
const firstEventId =
172+
mockActivityEventGroup.events[0].eventId ??
173+
mockActivityEventGroup.events[0].computedEventId;
174+
expect(mockResetToDecisionEventId).toHaveBeenCalledWith(firstEventId);
175+
});
176+
177+
it('should not show reset button for non-resettable events', () => {
178+
const mockEventGroups: Array<[string, HistoryEventsGroup]> = [
179+
[
180+
'group-1',
181+
{
182+
...mockActivityEventGroup,
183+
resetToDecisionEventId: undefined,
184+
},
185+
],
186+
];
187+
setup({ eventGroupsById: mockEventGroups });
188+
189+
expect(screen.queryByText('Reset Event')).not.toBeInTheDocument();
190+
});
191+
});
192+
193+
function setup({
194+
eventGroupsById = [],
195+
error = null,
196+
hasMoreEvents = false,
197+
isFetchingMoreEvents = false,
198+
fetchMoreEvents = jest.fn(),
199+
setVisibleRange = jest.fn(),
200+
initialStartIndex,
201+
decodedPageUrlParams = {
202+
domain: 'test-domain',
203+
cluster: 'test-cluster',
204+
workflowId: 'test-workflow-id',
205+
runId: 'test-run-id',
206+
workflowTab: 'history',
207+
},
208+
selectedEventId,
209+
getIsEventExpanded = jest.fn(() => false),
210+
toggleIsEventExpanded = jest.fn(),
211+
resetToDecisionEventId = jest.fn(),
212+
}: {
213+
eventGroupsById?: Array<[string, HistoryEventsGroup]>;
214+
error?: RequestError | null;
215+
hasMoreEvents?: boolean;
216+
isFetchingMoreEvents?: boolean;
217+
fetchMoreEvents?: () => void;
218+
setVisibleRange?: ({
219+
startIndex,
220+
endIndex,
221+
}: {
222+
startIndex: number;
223+
endIndex: number;
224+
}) => void;
225+
initialStartIndex?: number;
226+
decodedPageUrlParams?: WorkflowPageTabsParams;
227+
selectedEventId?: string;
228+
getIsEventExpanded?: (eventId: string) => boolean;
229+
toggleIsEventExpanded?: (eventId: string) => void;
230+
resetToDecisionEventId?: (decisionEventId: string) => void;
231+
} = {}) {
232+
const virtuosoRef = { current: null };
233+
const user = userEvent.setup();
234+
235+
render(
236+
<VirtuosoMockContext.Provider
237+
value={{ viewportHeight: 1000, itemHeight: 36 }}
238+
>
239+
<WorkflowHistoryUngroupedTable
240+
eventGroupsById={eventGroupsById}
241+
virtuosoRef={virtuosoRef}
242+
initialStartIndex={initialStartIndex}
243+
setVisibleRange={setVisibleRange}
244+
decodedPageUrlParams={decodedPageUrlParams}
245+
selectedEventId={selectedEventId}
246+
getIsEventExpanded={getIsEventExpanded}
247+
toggleIsEventExpanded={toggleIsEventExpanded}
248+
resetToDecisionEventId={resetToDecisionEventId}
249+
error={error}
250+
hasMoreEvents={hasMoreEvents}
251+
fetchMoreEvents={fetchMoreEvents}
252+
isFetchingMoreEvents={isFetchingMoreEvents}
253+
/>
254+
</VirtuosoMockContext.Provider>
255+
);
256+
257+
return {
258+
user,
259+
virtuosoRef,
260+
mockFetchMoreEvents: fetchMoreEvents,
261+
mockSetVisibleRange: setVisibleRange,
262+
mockToggleIsEventExpanded: toggleIsEventExpanded,
263+
mockResetToDecisionEventId: resetToDecisionEventId,
264+
};
265+
}

0 commit comments

Comments
 (0)