Skip to content

Commit dc34a02

Browse files
Add single line summary for history events in Workflow History UI (#996)
* Use summaryFields in event metadata to display a one-line event summary in Workflow History * Create a parser config that runs on top of history event details to get summary fields to render * Render summary fields with tooltips (if not disabled) * Create custom one-line JSON component which shows the full JSON in a custom tooltip * Update grouped event card header styling to accommodate event summary * Update grouped event card props to accept entire eventMetadata (and update tests) * Hide summary when panel is expanded * Update ungrouped view grid template to add space for event summary * Hide summary when panel is expanded
1 parent ff4f053 commit dc34a02

21 files changed

+1018
-24
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { MdOutlineMonitorHeart, MdOutlineTimer } from 'react-icons/md';
2+
3+
import { type WorkflowHistoryEventSummaryFieldParser } from '../workflow-history-event-summary/workflow-history-event-summary.types';
4+
import WorkflowHistoryEventSummaryJson from '../workflow-history-event-summary-json/workflow-history-event-summary-json';
5+
6+
const workflowHistoryEventSummaryFieldParsersConfig: Array<WorkflowHistoryEventSummaryFieldParser> =
7+
[
8+
{
9+
name: 'Heartbeat time',
10+
matcher: (name) => name === 'lastHeartbeatTime',
11+
icon: MdOutlineMonitorHeart,
12+
},
13+
{
14+
name: 'Json as PrettyJson',
15+
matcher: (name, value) =>
16+
value !== null &&
17+
new RegExp(
18+
'(input|result|details|failureDetails|Error|lastCompletionResult|heartbeatDetails)$'
19+
).test(name),
20+
icon: null,
21+
customRenderValue: WorkflowHistoryEventSummaryJson,
22+
hideDefaultTooltip: true,
23+
},
24+
{
25+
name: 'Timeouts with timer icon',
26+
matcher: (name) =>
27+
new RegExp('(TimeoutSeconds|BackoffSeconds|InSeconds)$').test(name),
28+
icon: MdOutlineTimer,
29+
},
30+
];
31+
32+
export default workflowHistoryEventSummaryFieldParsersConfig;

src/views/workflow-history/helpers/get-history-group-from-events/__tests__/get-activity-group-from-events.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,8 +410,8 @@ describe('getActivityGroupFromEvents', () => {
410410
(metadata) => metadata.label === 'Started'
411411
);
412412
expect(startedEventMetadata?.summaryFields).toEqual([
413-
'lastHeartbeatTime',
414413
'heartbeatDetails',
414+
'lastHeartbeatTime',
415415
]);
416416

417417
// The completed event should also have summaryFields

src/views/workflow-history/helpers/get-history-group-from-events/get-activity-group-from-events.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ export default function getActivityGroupFromEvents(
159159
'scheduleToCloseTimeoutSeconds',
160160
],
161161
activityTaskStartedEventAttributes: [
162-
'lastHeartbeatTime',
163162
'heartbeatDetails',
163+
'lastHeartbeatTime',
164164
],
165165
activityTaskCompletedEventAttributes: ['result'],
166166
activityTaskFailedEventAttributes: ['details', 'reason'],

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type WorkflowPageTabsParams } from '@/views/workflow-page/workflow-page-tabs/workflow-page-tabs.types';
1+
import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types';
22

33
import type {
44
ExtendedHistoryEvent,
@@ -23,7 +23,7 @@ export type WorkflowHistoryEventDetailsValueComponentProps = {
2323
entryPath: string;
2424
entryValue: any;
2525
isNegative?: boolean;
26-
} & WorkflowPageTabsParams;
26+
} & WorkflowPageParams;
2727

2828
export type WorkflowHistoryEventDetailsConfig = {
2929
name: string;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React from 'react';
2+
3+
import { render, screen, userEvent, waitFor } from '@/test-utils/rtl';
4+
5+
import WorkflowHistoryEventSummaryJson from '../workflow-history-event-summary-json';
6+
7+
jest.mock(
8+
'../../workflow-history-event-details-json/workflow-history-event-details-json',
9+
() =>
10+
jest.fn(({ entryValue, isNegative }) => (
11+
<div data-testid="mock-details-json" data-negative={isNegative ?? false}>
12+
{JSON.stringify(entryValue)}
13+
</div>
14+
))
15+
);
16+
17+
describe(WorkflowHistoryEventSummaryJson.name, () => {
18+
it('renders correctly with default props', () => {
19+
setup();
20+
21+
expect(screen.getByText(/{"test":"data"}/)).toBeInTheDocument();
22+
});
23+
24+
it('renders the label correctly in tooltip', async () => {
25+
const { user } = setup();
26+
27+
const jsonContainer = screen.getByText(/{"test":"data"}/);
28+
await user.hover(jsonContainer);
29+
30+
expect(await screen.findByText('Test Field')).toBeInTheDocument();
31+
});
32+
33+
it('passes isNegative prop correctly to details component', async () => {
34+
const { user } = setup({ isNegative: true });
35+
36+
const jsonContainer = screen.getByText(/{"test":"data"}/);
37+
await user.hover(jsonContainer);
38+
39+
const detailsJson = await screen.findByTestId('mock-details-json');
40+
expect(detailsJson).toHaveAttribute('data-negative', 'true');
41+
});
42+
43+
it('shows tooltip content on hover', async () => {
44+
const { user } = setup();
45+
46+
const jsonContainer = screen.getByText(/{"test":"data"}/);
47+
await user.hover(jsonContainer);
48+
49+
expect(await screen.findByText('Test Field')).toBeInTheDocument();
50+
expect(await screen.findByTestId('mock-details-json')).toBeInTheDocument();
51+
});
52+
53+
it('hides tooltip on unhover', async () => {
54+
const { user } = setup();
55+
56+
const jsonContainer = screen.getByText(/{"test":"data"}/);
57+
await user.hover(jsonContainer);
58+
59+
expect(await screen.findByText('Test Field')).toBeInTheDocument();
60+
61+
await user.unhover(jsonContainer);
62+
63+
await waitFor(() => {
64+
expect(screen.queryByText('Test Field')).not.toBeInTheDocument();
65+
});
66+
});
67+
});
68+
69+
function setup({ isNegative = false }: { isNegative?: boolean } = {}) {
70+
const user = userEvent.setup();
71+
72+
const renderResult = render(
73+
<WorkflowHistoryEventSummaryJson
74+
label="Test Field"
75+
value={{ test: 'data' }}
76+
isNegative={isNegative}
77+
domain="test-domain"
78+
cluster="test-cluster"
79+
workflowId="test-workflow-id"
80+
runId="test-run-id"
81+
/>
82+
);
83+
84+
return { user, ...renderResult };
85+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { styled as createStyled, type Theme } from 'baseui';
2+
import { type PopoverOverrides } from 'baseui/popover';
3+
import { type StyleObject } from 'styletron-react';
4+
5+
export const styled = {
6+
JsonViewContainer: createStyled<'div', { $isNegative: boolean }>(
7+
'div',
8+
({ $theme, $isNegative }) => ({
9+
padding: `${$theme.sizing.scale0} ${$theme.sizing.scale300}`,
10+
color: $isNegative ? $theme.colors.contentNegative : '#A964F7',
11+
backgroundColor: $isNegative
12+
? $theme.colors.backgroundNegativeLight
13+
: $theme.colors.backgroundSecondary,
14+
borderRadius: $theme.borders.radius300,
15+
maxHeight: $theme.sizing.scale800,
16+
maxWidth: '360px',
17+
overflow: 'hidden',
18+
whiteSpace: 'nowrap',
19+
textOverflow: 'ellipsis',
20+
...$theme.typography.MonoParagraphXSmall,
21+
})
22+
),
23+
JsonPreviewContainer: createStyled('div', ({ $theme }) => ({
24+
display: 'flex',
25+
flexDirection: 'column',
26+
alignItems: 'flex-start',
27+
gap: $theme.sizing.scale200,
28+
})),
29+
JsonPreviewLabel: createStyled('div', ({ $theme }) => ({
30+
...$theme.typography.LabelXSmall,
31+
})),
32+
};
33+
34+
export const overrides = {
35+
popover: {
36+
Arrow: {
37+
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
38+
backgroundColor: $theme.colors.backgroundPrimary,
39+
}),
40+
},
41+
Inner: {
42+
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
43+
backgroundColor: $theme.colors.backgroundPrimary,
44+
color: $theme.colors.contentPrimary,
45+
maxWidth: '500px',
46+
}),
47+
},
48+
} satisfies PopoverOverrides,
49+
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { StatefulTooltip } from 'baseui/tooltip';
2+
3+
import losslessJsonStringify from '@/utils/lossless-json-stringify';
4+
5+
import WorkflowHistoryEventDetailsJson from '../workflow-history-event-details-json/workflow-history-event-details-json';
6+
import { type EventSummaryValueComponentProps } from '../workflow-history-event-summary/workflow-history-event-summary.types';
7+
8+
import {
9+
overrides,
10+
styled,
11+
} from './workflow-history-event-summary-json.styles';
12+
13+
export default function WorkflowHistoryEventSummaryJson({
14+
value,
15+
label,
16+
isNegative,
17+
}: EventSummaryValueComponentProps) {
18+
return (
19+
<StatefulTooltip
20+
content={
21+
<styled.JsonPreviewContainer>
22+
<styled.JsonPreviewLabel>{label}</styled.JsonPreviewLabel>
23+
<WorkflowHistoryEventDetailsJson
24+
entryValue={value}
25+
isNegative={isNegative}
26+
/>
27+
</styled.JsonPreviewContainer>
28+
}
29+
ignoreBoundary
30+
placement="bottom"
31+
showArrow
32+
overrides={overrides.popover}
33+
>
34+
<styled.JsonViewContainer $isNegative={isNegative ?? false}>
35+
{losslessJsonStringify(value)}
36+
</styled.JsonViewContainer>
37+
</StatefulTooltip>
38+
);
39+
}

0 commit comments

Comments
 (0)