Skip to content

Commit 299bdea

Browse files
feat: History V2 - event details with summary (#1104)
* Implement Event Details components for Workflow History V2 * Pass groupId to WorkflowHistoryEventGroup to generate a unique key for summary details * Fix group expansion logic to expand/collapse all events in a group instead of just the first one, to make toggling between grouped and ungrouped views more intuitive * Change props type of WorkflowHistoryEventDetailsGroup (v1) to accept WorkflowPageParams instead of WorkflowPageTabsParams (which passes the workflow tab as extra, even though it's always going to be "history") Signed-off-by: Adhitya Mamallan <[email protected]>
1 parent afe927a commit 299bdea

19 files changed

+1894
-49
lines changed

src/views/workflow-history-v2/helpers/__tests__/generate-history-group-details.test.ts

Lines changed: 468 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import formatPendingWorkflowHistoryEvent from '@/utils/data-formatters/format-pending-workflow-history-event';
2+
import formatWorkflowHistoryEvent from '@/utils/data-formatters/format-workflow-history-event';
3+
import isPendingHistoryEvent from '@/views/workflow-history/workflow-history-event-details/helpers/is-pending-history-event';
4+
import { type HistoryEventsGroup } from '@/views/workflow-history/workflow-history.types';
5+
6+
import generateHistoryEventDetails from '../helpers/generate-history-event-details';
7+
import { type EventDetailsTabContent } from '../workflow-history-group-details/workflow-history-group-details.types';
8+
9+
export default function generateHistoryGroupDetails(
10+
eventGroup: HistoryEventsGroup
11+
) {
12+
const groupDetailsEntries: Array<[string, EventDetailsTabContent]> = [],
13+
summaryDetailsEntries: Array<[string, EventDetailsTabContent]> = [];
14+
15+
eventGroup.events.forEach((event, index) => {
16+
const eventId = event.eventId ?? event.computedEventId;
17+
18+
const eventMetadata = eventGroup.eventsMetadata[index];
19+
if (!eventMetadata) return;
20+
21+
const result = isPendingHistoryEvent(event)
22+
? formatPendingWorkflowHistoryEvent(event)
23+
: formatWorkflowHistoryEvent(event);
24+
25+
const eventDetails = result
26+
? generateHistoryEventDetails({
27+
details: {
28+
...result,
29+
...eventMetadata.additionalDetails,
30+
},
31+
negativeFields: eventMetadata.negativeFields,
32+
})
33+
: [];
34+
35+
groupDetailsEntries.push([
36+
eventId,
37+
{
38+
eventLabel: eventMetadata.label,
39+
eventDetails,
40+
} satisfies EventDetailsTabContent,
41+
]);
42+
43+
const eventSummaryDetails = eventDetails.filter((detail) =>
44+
eventMetadata.summaryFields?.includes(detail.path)
45+
);
46+
47+
if (eventSummaryDetails.length > 0) {
48+
summaryDetailsEntries.push([
49+
eventId,
50+
{
51+
eventLabel: eventMetadata.label,
52+
eventDetails: eventSummaryDetails,
53+
} satisfies EventDetailsTabContent,
54+
]);
55+
}
56+
});
57+
58+
return {
59+
groupDetailsEntries,
60+
summaryDetailsEntries,
61+
};
62+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { render, screen } from '@/test-utils/rtl';
2+
3+
import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types';
4+
5+
import type WorkflowHistoryPanelDetailsEntry from '../../workflow-history-panel-details-entry/workflow-history-panel-details-entry';
6+
import WorkflowHistoryEventDetails from '../workflow-history-event-details';
7+
import { type EventDetailsEntries } from '../workflow-history-event-details.types';
8+
9+
jest.mock<typeof WorkflowHistoryPanelDetailsEntry>(
10+
'../../workflow-history-panel-details-entry/workflow-history-panel-details-entry',
11+
() =>
12+
jest.fn(({ detail }) => (
13+
<div data-testid="panel-details-entry">
14+
Panel Entry: {detail.path} ={' '}
15+
{JSON.stringify(detail.isGroup ? detail.groupEntries : detail.value)}
16+
{detail.isNegative && ' (negative)'}
17+
</div>
18+
))
19+
);
20+
21+
jest.mock(
22+
'@/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group',
23+
() =>
24+
jest.fn(({ entries }: { entries: EventDetailsEntries }) => (
25+
<div data-testid="event-details-group">
26+
Event Details Group ({entries.length} entries)
27+
</div>
28+
))
29+
);
30+
31+
describe(WorkflowHistoryEventDetails.name, () => {
32+
it('renders "No Details" when eventDetails is empty', () => {
33+
setup({ eventDetails: [] });
34+
35+
expect(screen.getByText('No Details')).toBeInTheDocument();
36+
expect(screen.queryByTestId('panel-details-entry')).not.toBeInTheDocument();
37+
expect(screen.queryByTestId('event-details-group')).not.toBeInTheDocument();
38+
});
39+
40+
it('renders only rest details when no entries have showInPanels', () => {
41+
const eventDetails: EventDetailsEntries = [
42+
{
43+
key: 'key1',
44+
path: 'path1',
45+
value: 'value1',
46+
isGroup: false,
47+
renderConfig: {
48+
name: 'Test Config',
49+
key: 'key1',
50+
},
51+
},
52+
{
53+
key: 'key2',
54+
path: 'path2',
55+
value: 'value2',
56+
isGroup: false,
57+
renderConfig: null,
58+
},
59+
];
60+
61+
setup({ eventDetails });
62+
63+
expect(screen.queryByTestId('panel-details-entry')).not.toBeInTheDocument();
64+
expect(screen.getByTestId('event-details-group')).toBeInTheDocument();
65+
expect(
66+
screen.getByText('Event Details Group (2 entries)')
67+
).toBeInTheDocument();
68+
});
69+
70+
it('renders panel details when entries have showInPanels flag', () => {
71+
const eventDetails: EventDetailsEntries = [
72+
{
73+
key: 'key1',
74+
path: 'path1',
75+
value: 'value1',
76+
isGroup: false,
77+
renderConfig: {
78+
name: 'Panel Config',
79+
key: 'key1',
80+
showInPanels: true,
81+
},
82+
},
83+
{
84+
key: 'key2',
85+
path: 'path2',
86+
value: 'value2',
87+
isGroup: false,
88+
renderConfig: {
89+
name: 'Rest Config',
90+
key: 'key2',
91+
},
92+
},
93+
];
94+
95+
setup({ eventDetails });
96+
97+
expect(screen.getByTestId('panel-details-entry')).toBeInTheDocument();
98+
expect(
99+
screen.getByText(/Panel Entry: path1 = "value1"/)
100+
).toBeInTheDocument();
101+
expect(screen.getByTestId('event-details-group')).toBeInTheDocument();
102+
expect(
103+
screen.getByText('Event Details Group (1 entries)')
104+
).toBeInTheDocument();
105+
});
106+
107+
it('renders multiple panel details', () => {
108+
const eventDetails: EventDetailsEntries = [
109+
{
110+
key: 'key1',
111+
path: 'path1',
112+
value: 'value1',
113+
isGroup: false,
114+
renderConfig: {
115+
name: 'Panel Config 1',
116+
key: 'key1',
117+
showInPanels: true,
118+
},
119+
},
120+
{
121+
key: 'key2',
122+
path: 'path2',
123+
value: { nested: 'value2' },
124+
isGroup: false,
125+
renderConfig: {
126+
name: 'Panel Config 2',
127+
key: 'key2',
128+
showInPanels: true,
129+
},
130+
},
131+
{
132+
key: 'key3',
133+
path: 'path3',
134+
value: 'value3',
135+
isGroup: false,
136+
renderConfig: {
137+
name: 'Rest Config',
138+
key: 'key3',
139+
},
140+
},
141+
];
142+
143+
setup({ eventDetails });
144+
145+
const panelEntries = screen.getAllByTestId('panel-details-entry');
146+
expect(panelEntries).toHaveLength(2);
147+
expect(
148+
screen.getByText(/Panel Entry: path1 = "value1"/)
149+
).toBeInTheDocument();
150+
expect(
151+
screen.getByText(/Panel Entry: path2 = \{"nested":"value2"\}/)
152+
).toBeInTheDocument();
153+
expect(screen.getByTestId('event-details-group')).toBeInTheDocument();
154+
expect(
155+
screen.getByText('Event Details Group (1 entries)')
156+
).toBeInTheDocument();
157+
});
158+
159+
it('correctly renders panel details entry', () => {
160+
const eventDetails: EventDetailsEntries = [
161+
{
162+
key: 'key1',
163+
path: 'path1',
164+
value: 'value1',
165+
isGroup: false,
166+
isNegative: true,
167+
renderConfig: {
168+
name: 'Panel Config',
169+
key: 'key1',
170+
showInPanels: true,
171+
},
172+
},
173+
];
174+
175+
setup({ eventDetails });
176+
177+
expect(
178+
screen.getByText(/Panel Entry: path1 = "value1"/)
179+
).toBeInTheDocument();
180+
expect(screen.getByText(/\(negative\)/)).toBeInTheDocument();
181+
});
182+
});
183+
184+
function setup({
185+
eventDetails,
186+
workflowPageParams = {
187+
domain: 'test-domain',
188+
cluster: 'test-cluster',
189+
workflowId: 'test-workflow-id',
190+
runId: 'test-run-id',
191+
},
192+
}: {
193+
eventDetails: EventDetailsEntries;
194+
workflowPageParams?: WorkflowPageParams;
195+
}) {
196+
render(
197+
<WorkflowHistoryEventDetails
198+
eventDetails={eventDetails}
199+
workflowPageParams={workflowPageParams}
200+
/>
201+
);
202+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { styled as createStyled, type Theme } from 'baseui';
2+
3+
export const styled = {
4+
EmptyDetails: createStyled('div', ({ $theme }: { $theme: Theme }) => ({
5+
...$theme.typography.LabelXSmall,
6+
color: $theme.colors.contentTertiary,
7+
textAlign: 'center',
8+
padding: `${$theme.sizing.scale700} 0`,
9+
})),
10+
EventDetailsContainer: createStyled('div', {
11+
display: 'flex',
12+
flexDirection: 'column',
13+
}),
14+
PanelDetails: createStyled('div', ({ $theme }: { $theme: Theme }) => ({
15+
display: 'flex',
16+
flexDirection: 'column',
17+
[$theme.mediaQuery.medium]: {
18+
flexDirection: 'row',
19+
},
20+
gap: $theme.sizing.scale500,
21+
paddingBottom: $theme.sizing.scale500,
22+
alignItems: 'stretch',
23+
})),
24+
PanelContainer: createStyled('div', {
25+
flex: 1,
26+
}),
27+
RestDetails: createStyled('div', ({ $theme }: { $theme: Theme }) => ({
28+
paddingLeft: $theme.sizing.scale100,
29+
paddingRight: $theme.sizing.scale100,
30+
})),
31+
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useMemo } from 'react';
2+
3+
import partition from 'lodash/partition';
4+
5+
import WorkflowHistoryEventDetailsGroup from '@/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group';
6+
7+
import WorkflowHistoryPanelDetailsEntry from '../workflow-history-panel-details-entry/workflow-history-panel-details-entry';
8+
9+
import { styled } from './workflow-history-event-details.styles';
10+
import { type Props } from './workflow-history-event-details.types';
11+
12+
export default function WorkflowHistoryEventDetails({
13+
eventDetails,
14+
workflowPageParams,
15+
}: Props) {
16+
const [panelDetails, restDetails] = useMemo(
17+
() =>
18+
partition(eventDetails, (detail) => detail.renderConfig?.showInPanels),
19+
[eventDetails]
20+
);
21+
22+
if (eventDetails.length === 0) {
23+
return <styled.EmptyDetails>No Details</styled.EmptyDetails>;
24+
}
25+
26+
return (
27+
<styled.EventDetailsContainer>
28+
{panelDetails.length > 0 && (
29+
<styled.PanelDetails>
30+
{panelDetails.map((detail) => {
31+
return (
32+
<styled.PanelContainer key={detail.path}>
33+
<WorkflowHistoryPanelDetailsEntry
34+
detail={detail}
35+
{...workflowPageParams}
36+
/>
37+
</styled.PanelContainer>
38+
);
39+
})}
40+
</styled.PanelDetails>
41+
)}
42+
<styled.RestDetails>
43+
<WorkflowHistoryEventDetailsGroup
44+
entries={restDetails}
45+
decodedPageUrlParams={workflowPageParams}
46+
/>
47+
</styled.RestDetails>
48+
</styled.EventDetailsContainer>
49+
);
50+
}

src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types';
22

3+
export type Props = {
4+
eventDetails: EventDetailsEntries;
5+
workflowPageParams: WorkflowPageParams;
6+
};
7+
38
export type EventDetailsFuncArgs = {
49
path: string;
510
key: string;

0 commit comments

Comments
 (0)