Skip to content

Commit 0e312d9

Browse files
feat: Add buttons to toggle between legacy and new views in Workflow History (#1133)
* Add buttons next to "Workflow History" to toggle between legacy and new History views * Add tooltips on these buttons, explaining the changes and providing links to share feedback Signed-off-by: Adhitya Mamallan <adhitya.mamallan@uber.com>
1 parent 5f45bfb commit 0e312d9

16 files changed

Lines changed: 633 additions & 4 deletions
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { MdOutlineChat } from 'react-icons/md';
2+
3+
import { type ViewToggleTooltipContentConfig } from '../workflow-history-view-toggle-button/workflow-history-view-toggle-button.types';
4+
5+
const workflowHistorySwitchToV1ButtonTooltipContentConfig: ViewToggleTooltipContentConfig =
6+
{
7+
content: [
8+
`The new History view provides a more compact overview with a table-based
9+
format designed for information density and easier scanning, helping you
10+
find important events faster.`,
11+
`Please feel free to share any feedback if you encounter anything
12+
that seems suboptimal in the new History view.`,
13+
],
14+
linkButtons: [
15+
{
16+
label: 'Provide feedback',
17+
startEnhancer: MdOutlineChat,
18+
href: 'https://cloud-native.slack.com/archives/C09J2FQ7XU3',
19+
},
20+
],
21+
};
22+
23+
export default workflowHistorySwitchToV1ButtonTooltipContentConfig;

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ jest.mock(
2323
))
2424
);
2525

26+
jest.mock(
27+
'../../workflow-history-switch-to-v1-button/workflow-history-switch-to-v1-button',
28+
() => jest.fn(() => <button>Switch to V1</button>)
29+
);
30+
2631
describe(WorkflowHistoryHeader.name, () => {
2732
it('should render the header with title', () => {
2833
setup();
@@ -34,6 +39,11 @@ describe(WorkflowHistoryHeader.name, () => {
3439
expect(screen.getByText('Export JSON')).toBeInTheDocument();
3540
});
3641

42+
it('should render switch to V1 button', () => {
43+
setup();
44+
expect(screen.getByText('Switch to V1')).toBeInTheDocument();
45+
});
46+
3747
it('should render segmented control with grouped and ungrouped segments', () => {
3848
setup();
3949
expect(screen.getByText('Grouped')).toBeInTheDocument();

src/views/workflow-history-v2/workflow-history-header/workflow-history-header.styles.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@ export const styled = {
5151
},
5252
})
5353
),
54+
Heading: createStyled(
55+
'div',
56+
({ $theme }: { $theme: Theme }): StyleObject => ({
57+
display: 'flex',
58+
gap: $theme.sizing.scale600,
59+
...$theme.typography.HeadingXSmall,
60+
flexDirection: 'column',
61+
[$theme.mediaQuery.medium]: {
62+
alignItems: 'center',
63+
flexDirection: 'row',
64+
},
65+
})
66+
),
5467
Actions: createStyled(
5568
'div',
5669
({ $theme }: { $theme: Theme }): StyleObject => ({

src/views/workflow-history-v2/workflow-history-header/workflow-history-header.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import { Button } from 'baseui/button';
44
import { Filter } from 'baseui/icon';
55
import { StatefulPopover } from 'baseui/popover';
66
import { SegmentedControl, Segment } from 'baseui/segmented-control';
7-
import { HeadingXSmall } from 'baseui/typography';
87
import { useInView } from 'react-intersection-observer';
98

109
import PageSection from '@/components/page-section/page-section';
1110
import WorkflowHistoryExportJsonButton from '@/views/workflow-history/workflow-history-export-json-button/workflow-history-export-json-button';
1211
import WorkflowHistoryFiltersMenu from '@/views/workflow-history-v2/workflow-history-filters-menu/workflow-history-filters-menu';
1312

13+
import WorkflowHistorySwitchToV1Button from '../workflow-history-switch-to-v1-button/workflow-history-switch-to-v1-button';
14+
1415
import { overrides, styled } from './workflow-history-header.styles';
1516
import { type Props } from './workflow-history-header.types';
1617

@@ -49,7 +50,10 @@ export default function WorkflowHistoryHeader({
4950
>
5051
<PageSection>
5152
<styled.Header>
52-
<HeadingXSmall>Workflow history</HeadingXSmall>
53+
<styled.Heading>
54+
Workflow history
55+
<WorkflowHistorySwitchToV1Button />
56+
</styled.Heading>
5357
<styled.Actions>
5458
<WorkflowHistoryExportJsonButton {...wfHistoryRequestArgs} />
5559
<SegmentedControl
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React from 'react';
2+
3+
import { HttpResponse } from 'msw';
4+
5+
import { render, screen, userEvent, waitFor } from '@/test-utils/rtl';
6+
7+
import { WorkflowHistoryContext } from '@/views/workflow-history/workflow-history-context-provider/workflow-history-context-provider';
8+
9+
import WorkflowHistorySwitchToV1Button from '../workflow-history-switch-to-v1-button';
10+
11+
jest.mock(
12+
'../../workflow-history-view-toggle-button/workflow-history-view-toggle-button',
13+
() =>
14+
jest.fn(({ onClick, label }) => <button onClick={onClick}>{label}</button>)
15+
);
16+
17+
describe(WorkflowHistorySwitchToV1Button.name, () => {
18+
it('should render button when config is OPT_IN', async () => {
19+
setup({ configValue: 'OPT_IN' });
20+
21+
expect(
22+
await screen.findByText('Switch to the legacy History view')
23+
).toBeInTheDocument();
24+
});
25+
26+
it('should render button when config is OPT_OUT', async () => {
27+
setup({ configValue: 'OPT_OUT' });
28+
29+
expect(
30+
await screen.findByText('Switch to the legacy History view')
31+
).toBeInTheDocument();
32+
});
33+
34+
it('should not render when config is DISABLED', async () => {
35+
setup({ configValue: 'DISABLED' });
36+
37+
await waitFor(() => {
38+
expect(
39+
screen.queryByText('Switch to the legacy History view')
40+
).not.toBeInTheDocument();
41+
});
42+
});
43+
44+
it('should not render when config is ENABLED', async () => {
45+
setup({ configValue: 'ENABLED' });
46+
47+
await waitFor(() => {
48+
expect(
49+
screen.queryByText('Switch to the legacy History view')
50+
).not.toBeInTheDocument();
51+
});
52+
});
53+
54+
it('should call setIsWorkflowHistoryV2Selected with false when button is clicked', async () => {
55+
const { user, mockSetIsWorkflowHistoryV2Selected } = setup({
56+
configValue: 'OPT_OUT',
57+
});
58+
59+
const button = await screen.findByText('Switch to the legacy History view');
60+
await user.click(button);
61+
62+
expect(mockSetIsWorkflowHistoryV2Selected).toHaveBeenCalledTimes(1);
63+
expect(mockSetIsWorkflowHistoryV2Selected).toHaveBeenCalledWith(false);
64+
});
65+
});
66+
67+
function setup({ configValue }: { configValue: string }) {
68+
const user = userEvent.setup();
69+
const mockSetIsWorkflowHistoryV2Selected = jest.fn();
70+
71+
const contextValue = {
72+
ungroupedViewUserPreference: null,
73+
setUngroupedViewUserPreference: jest.fn(),
74+
isWorkflowHistoryV2Selected: true,
75+
setIsWorkflowHistoryV2Selected: mockSetIsWorkflowHistoryV2Selected,
76+
};
77+
78+
const renderResult = render(
79+
<WorkflowHistoryContext.Provider value={contextValue}>
80+
<WorkflowHistorySwitchToV1Button />
81+
</WorkflowHistoryContext.Provider>,
82+
{
83+
endpointsMocks: [
84+
{
85+
path: '/api/config',
86+
httpMethod: 'GET',
87+
mockOnce: false,
88+
httpResolver: async () => {
89+
return HttpResponse.json(configValue);
90+
},
91+
},
92+
],
93+
}
94+
);
95+
96+
return {
97+
user,
98+
mockSetIsWorkflowHistoryV2Selected,
99+
...renderResult,
100+
};
101+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useContext } from 'react';
2+
3+
import useConfigValue from '@/hooks/use-config-value/use-config-value';
4+
import { WorkflowHistoryContext } from '@/views/workflow-history/workflow-history-context-provider/workflow-history-context-provider';
5+
6+
import workflowHistorySwitchToV1ButtonTooltipContentConfig from '../config/workflow-history-switch-to-v1-button-tooltip-content.config';
7+
import WorkflowHistoryViewToggleButton from '../workflow-history-view-toggle-button/workflow-history-view-toggle-button';
8+
9+
export default function WorkflowHistorySwitchToV1Button() {
10+
const { data: historyPageV2Config } = useConfigValue(
11+
'HISTORY_PAGE_V2_ENABLED'
12+
);
13+
14+
const { setIsWorkflowHistoryV2Selected } = useContext(WorkflowHistoryContext);
15+
16+
if (historyPageV2Config === 'DISABLED' || historyPageV2Config === 'ENABLED') {
17+
return null;
18+
}
19+
20+
return (
21+
<WorkflowHistoryViewToggleButton
22+
kind="secondary"
23+
label="Switch to the legacy History view"
24+
onClick={() => setIsWorkflowHistoryV2Selected(false)}
25+
tooltipContent={workflowHistorySwitchToV1ButtonTooltipContentConfig}
26+
/>
27+
);
28+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import React from 'react';
2+
3+
import { render, screen, userEvent, waitFor } from '@/test-utils/rtl';
4+
5+
import WorkflowHistoryViewToggleButton from '../workflow-history-view-toggle-button';
6+
import { type Props } from '../workflow-history-view-toggle-button.types';
7+
8+
describe(WorkflowHistoryViewToggleButton.name, () => {
9+
it('should render button with label', () => {
10+
setup();
11+
12+
expect(screen.getByText('Test Button')).toBeInTheDocument();
13+
});
14+
15+
it('should call onClick when button is clicked', async () => {
16+
const { user, mockOnClick } = setup();
17+
18+
const button = screen.getByText('Test Button');
19+
await user.click(button);
20+
21+
expect(mockOnClick).toHaveBeenCalledTimes(1);
22+
});
23+
24+
it('should render tooltip content when popover is opened on hover', async () => {
25+
const { user } = setup();
26+
27+
const button = screen.getByText('Test Button');
28+
await user.hover(button);
29+
30+
await waitFor(() => {
31+
expect(screen.getByText('First paragraph')).toBeInTheDocument();
32+
expect(screen.getByText('Second paragraph')).toBeInTheDocument();
33+
});
34+
});
35+
36+
it('should render link buttons when provided', async () => {
37+
const { user } = setup({
38+
tooltipContent: {
39+
content: ['Test content'],
40+
linkButtons: [
41+
{
42+
label: 'Learn More',
43+
href: '/docs',
44+
startEnhancer: null,
45+
},
46+
{
47+
label: 'FAQ',
48+
href: '/faq',
49+
startEnhancer: null,
50+
},
51+
],
52+
},
53+
});
54+
55+
const button = screen.getByText('Test Button');
56+
await user.hover(button);
57+
58+
await waitFor(() => {
59+
expect(screen.getByText('Learn More')).toBeInTheDocument();
60+
expect(screen.getByText('FAQ')).toBeInTheDocument();
61+
});
62+
});
63+
64+
it('should not render link buttons when not provided', async () => {
65+
const { user } = setup({
66+
tooltipContent: {
67+
content: ['Test content'],
68+
},
69+
});
70+
71+
const button = screen.getByText('Test Button');
72+
await user.hover(button);
73+
74+
await waitFor(() => {
75+
expect(screen.getByText('Test content')).toBeInTheDocument();
76+
});
77+
78+
expect(screen.queryByText('Learn More')).not.toBeInTheDocument();
79+
});
80+
81+
it('should render with primary kind', () => {
82+
setup({ kind: 'primary' });
83+
84+
expect(screen.getByText('Test Button')).toBeInTheDocument();
85+
});
86+
87+
it('should render with secondary kind', () => {
88+
setup({ kind: 'secondary' });
89+
90+
expect(screen.getByText('Test Button')).toBeInTheDocument();
91+
});
92+
});
93+
94+
function setup(props: Partial<Props> = {}) {
95+
const user = userEvent.setup();
96+
const mockOnClick = jest.fn();
97+
98+
const defaultProps: Props = {
99+
kind: 'primary',
100+
label: 'Test Button',
101+
onClick: mockOnClick,
102+
tooltipContent: {
103+
content: ['First paragraph', 'Second paragraph'],
104+
},
105+
...props,
106+
};
107+
108+
const renderResult = render(
109+
<WorkflowHistoryViewToggleButton {...defaultProps} />
110+
);
111+
112+
return {
113+
user,
114+
mockOnClick,
115+
...renderResult,
116+
};
117+
}

0 commit comments

Comments
 (0)