Skip to content

Commit 6d608ab

Browse files
Add single-line summary for history events
Signed-off-by: Adhitya Mamallan <[email protected]>
1 parent 349fc3b commit 6d608ab

15 files changed

+886
-8
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
MdHourglassBottom,
3+
MdOutlineMonitorHeart,
4+
MdReplay,
5+
} from 'react-icons/md';
6+
7+
import { type DetailsRowItemParser } from '../workflow-history-details-row/workflow-history-details-row.types';
8+
import WorkflowHistoryDetailsRowJson from '../workflow-history-details-row-json/workflow-history-details-row-json';
9+
import WorkflowHistoryDetailsRowTooltipJson from '../workflow-history-details-row-tooltip-json/workflow-history-details-row-tooltip-json';
10+
11+
const workflowHistoryDetailsRowParsersConfig: Array<DetailsRowItemParser> = [
12+
{
13+
name: 'Heartbeat time',
14+
matcher: (name) => name === 'lastHeartbeatTime',
15+
icon: MdOutlineMonitorHeart,
16+
},
17+
{
18+
name: 'Json as PrettyJson',
19+
matcher: (name, value) =>
20+
value !== null &&
21+
new RegExp(
22+
'(input|result|details|failureDetails|Error|lastCompletionResult|heartbeatDetails|lastFailureDetails)$'
23+
).test(name),
24+
icon: null,
25+
customRenderValue: WorkflowHistoryDetailsRowJson,
26+
customTooltipContent: WorkflowHistoryDetailsRowTooltipJson,
27+
invertTooltipColors: true,
28+
},
29+
{
30+
name: 'Timeouts with timer icon',
31+
matcher: (name) =>
32+
new RegExp('(TimeoutSeconds|BackoffSeconds|InSeconds)$').test(name),
33+
icon: MdHourglassBottom,
34+
},
35+
{
36+
name: '"attempt" greater than 1, as "retries"',
37+
matcher: (name) => name === 'attempt',
38+
hide: (_, value) => typeof value === 'number' && value <= 0,
39+
icon: MdReplay,
40+
customTooltipContent: () => 'retries',
41+
},
42+
];
43+
44+
export default workflowHistoryDetailsRowParsersConfig;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { styled as createStyled } from 'baseui';
2+
3+
export const styled = {
4+
JsonViewContainer: createStyled<'div', { $isNegative: boolean }>(
5+
'div',
6+
({ $theme, $isNegative }) => ({
7+
color: $isNegative ? $theme.colors.contentNegative : '#A964F7',
8+
maxWidth: '360px',
9+
overflow: 'hidden',
10+
whiteSpace: 'nowrap',
11+
textOverflow: 'ellipsis',
12+
...$theme.typography.MonoParagraphXSmall,
13+
})
14+
),
15+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import losslessJsonStringify from '@/utils/lossless-json-stringify';
2+
3+
import { type DetailsRowValueComponentProps } from '../workflow-history-details-row/workflow-history-details-row.types';
4+
5+
import { styled } from './workflow-history-details-row-json.styles';
6+
7+
export default function WorkflowHistoryDetailsRowJson({
8+
value,
9+
isNegative,
10+
}: DetailsRowValueComponentProps) {
11+
return (
12+
<styled.JsonViewContainer $isNegative={isNegative ?? false}>
13+
{losslessJsonStringify(value)}
14+
</styled.JsonViewContainer>
15+
);
16+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { type DetailsRowValueComponentProps } from '../workflow-history-details-row/workflow-history-details-row.types';
2+
3+
export type Props = DetailsRowValueComponentProps;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { styled as createStyled } from 'baseui';
2+
3+
export const styled = {
4+
JsonPreviewContainer: createStyled('div', ({ $theme }) => ({
5+
display: 'flex',
6+
flexDirection: 'column',
7+
alignItems: 'flex-start',
8+
gap: $theme.sizing.scale200,
9+
})),
10+
JsonPreviewLabel: createStyled('div', ({ $theme }) => ({
11+
...$theme.typography.LabelXSmall,
12+
})),
13+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import WorkflowHistoryEventDetailsJson from '@/views/workflow-history/workflow-history-event-details-json/workflow-history-event-details-json';
2+
3+
import { type DetailsRowValueComponentProps } from '../workflow-history-details-row/workflow-history-details-row.types';
4+
5+
import { styled } from './workflow-history-details-row-tooltip-json.styles';
6+
7+
export default function WorkflowHistoryDetailsRowTooltipJson({
8+
value,
9+
label,
10+
isNegative,
11+
}: DetailsRowValueComponentProps) {
12+
return (
13+
<styled.JsonPreviewContainer>
14+
<styled.JsonPreviewLabel>{label}</styled.JsonPreviewLabel>
15+
<WorkflowHistoryEventDetailsJson
16+
entryValue={value}
17+
isNegative={isNegative}
18+
/>
19+
</styled.JsonPreviewContainer>
20+
);
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { type DetailsRowValueComponentProps } from '../workflow-history-details-row/workflow-history-details-row.types';
2+
3+
export type Props = DetailsRowValueComponentProps;
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { render, screen, userEvent } from '@/test-utils/rtl';
2+
3+
import type { WorkflowPageParams } from '@/views/workflow-page/workflow-page.types';
4+
5+
import { type EventDetailsEntries } from '../../workflow-history-event-details/workflow-history-event-details.types';
6+
import WorkflowHistoryDetailsRow from '../workflow-history-details-row';
7+
8+
jest.mock('../helpers/get-parsed-details-row-items', () =>
9+
jest.fn((detailsEntries: EventDetailsEntries) =>
10+
detailsEntries
11+
.filter((entry) => !entry.isGroup)
12+
.map((entry, index) => ({
13+
path: entry.path,
14+
label: entry.path,
15+
value: entry.value,
16+
icon: ({ size }: any) => (
17+
<span data-testid={`icon-${entry.path}`} data-size={size} />
18+
),
19+
renderValue: ({ value, isNegative }: any) => (
20+
<span data-testid={`field-${entry.path}`} data-negative={isNegative}>
21+
{value}
22+
</span>
23+
),
24+
renderTooltip: ({ label }: any) => (
25+
<span data-testid={`tooltip-${entry.path}`}>{label}</span>
26+
),
27+
invertTooltipColors: index === 1, // Second item has inverted tooltip
28+
omitWrapping: index === 2, // Third item omits wrapping
29+
}))
30+
)
31+
);
32+
33+
const mockWorkflowPageParams: WorkflowPageParams = {
34+
cluster: 'test-cluster',
35+
domain: 'test-domain',
36+
workflowId: 'test-workflow',
37+
runId: 'test-run',
38+
};
39+
40+
const mockDetailsEntries: EventDetailsEntries = [
41+
{
42+
key: 'field1',
43+
path: 'field1',
44+
isGroup: false,
45+
value: 'value1',
46+
isNegative: false,
47+
renderConfig: null,
48+
},
49+
{
50+
key: 'field2',
51+
path: 'field2',
52+
isGroup: false,
53+
value: 'value2',
54+
isNegative: true,
55+
renderConfig: null,
56+
},
57+
{
58+
key: 'field3',
59+
path: 'field3',
60+
isGroup: false,
61+
value: 'value3',
62+
isNegative: false,
63+
renderConfig: null,
64+
},
65+
];
66+
67+
describe(WorkflowHistoryDetailsRow.name, () => {
68+
beforeEach(() => {
69+
jest.clearAllMocks();
70+
});
71+
72+
it('should render details row items when detailsEntries has items', () => {
73+
setup();
74+
75+
expect(screen.getByTestId('field-field1')).toBeInTheDocument();
76+
expect(screen.getByTestId('field-field2')).toBeInTheDocument();
77+
expect(screen.getByTestId('field-field3')).toBeInTheDocument();
78+
expect(screen.getByText('value1')).toBeInTheDocument();
79+
expect(screen.getByText('value2')).toBeInTheDocument();
80+
expect(screen.getByText('value3')).toBeInTheDocument();
81+
});
82+
83+
it('should mark negative fields correctly', () => {
84+
setup();
85+
86+
const negativeField = screen.getByTestId('field-field2');
87+
expect(negativeField).toHaveAttribute('data-negative', 'true');
88+
89+
const positiveField = screen.getByTestId('field-field1');
90+
expect(positiveField).toHaveAttribute('data-negative', 'false');
91+
});
92+
93+
it('should render icons when provided in item config', () => {
94+
setup();
95+
96+
expect(screen.getByTestId('icon-field1')).toBeInTheDocument();
97+
expect(screen.getByTestId('icon-field2')).toBeInTheDocument();
98+
expect(screen.getByTestId('icon-field3')).toBeInTheDocument();
99+
});
100+
101+
it('should render tooltip content on hover', async () => {
102+
const { user } = setup();
103+
104+
const field1 = screen.getByTestId('field-field1');
105+
await user.hover(field1);
106+
107+
expect(await screen.findByTestId('tooltip-field1')).toBeInTheDocument();
108+
expect(screen.getByText('field1')).toBeInTheDocument();
109+
});
110+
});
111+
112+
function setup({
113+
detailsEntries = mockDetailsEntries,
114+
workflowPageParams = mockWorkflowPageParams,
115+
}: {
116+
detailsEntries?: EventDetailsEntries;
117+
workflowPageParams?: WorkflowPageParams;
118+
} = {}) {
119+
const user = userEvent.setup();
120+
121+
const renderResult = render(
122+
<WorkflowHistoryDetailsRow
123+
detailsEntries={detailsEntries}
124+
{...workflowPageParams}
125+
/>
126+
);
127+
128+
return { user, ...renderResult };
129+
}

0 commit comments

Comments
 (0)