Skip to content

Commit 2ab43cd

Browse files
Persist workflow search bar when ListWorkflows fails (#724)
* Remove useSuspenseQuery * Fix styling of page section * Add client boundary to content loader
1 parent 1490f1e commit 2ab43cd

File tree

15 files changed

+270
-216
lines changed

15 files changed

+270
-216
lines changed

src/views/domain-page/config/domain-page-tabs-error.config.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import getDomainWorkflowsErrorConfig from '@/views/domain-workflows/helpers/get-domain-workflows-error-config';
2-
31
import { type DomainPageTabsErrorConfig } from '../domain-page-tabs-error/domain-page-tabs-error.types';
42

53
const domainPageTabsErrorConfig: DomainPageTabsErrorConfig = {
6-
workflows: getDomainWorkflowsErrorConfig,
4+
workflows: () => ({
5+
message: 'Failed to load workflows',
6+
actions: [{ kind: 'retry', label: 'Retry' }],
7+
}),
78
metadata: () => ({
89
message: 'Failed to load metadata',
910
actions: [{ kind: 'retry', label: 'Retry' }],
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { withStyle } from 'baseui';
2+
3+
import PageSection from '@/components/page-section/page-section';
4+
5+
export const styled = {
6+
PageSection: withStyle(PageSection, () => ({
7+
display: 'flex',
8+
flexDirection: 'column',
9+
flex: 1,
10+
})),
11+
};

src/views/domain-page/domain-page-content/domain-page-content.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
'use client';
12
import React from 'react';
23

34
import { notFound } from 'next/navigation';
@@ -6,6 +7,7 @@ import decodeUrlParams from '@/utils/decode-url-params';
67

78
import domainPageTabsContentConfig from '../config/domain-page-tabs-content.config';
89

10+
import { styled } from './domain-page-content.styles';
911
import {
1012
type DomainPageContentParams,
1113
type Props,
@@ -22,11 +24,11 @@ export default function DomainPageContent(props: Props) {
2224
}
2325

2426
return (
25-
<section>
27+
<styled.PageSection>
2628
<TabContent
2729
domain={decodedParams.domain}
2830
cluster={decodedParams.cluster}
2931
/>
30-
</section>
32+
</styled.PageSection>
3133
);
3234
}

src/views/domain-page/domain-page-metadata/domain-page-metadata.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import React from 'react';
44
import { useSuspenseQuery } from '@tanstack/react-query';
55

66
import ListTable from '@/components/list-table/list-table';
7-
import PageSection from '@/components/page-section/page-section';
87
import request from '@/utils/request';
98

109
import domainPageMetadataTableConfig from '../config/domain-page-metadata-table.config';
@@ -23,13 +22,11 @@ export default function DomainPageMetadata(props: DomainPageTabContentProps) {
2322
});
2423

2524
return (
26-
<PageSection>
27-
<styled.MetadataContainer>
28-
<ListTable
29-
data={domainInfo}
30-
listTableConfig={domainPageMetadataTableConfig}
31-
/>
32-
</styled.MetadataContainer>
33-
</PageSection>
25+
<styled.MetadataContainer>
26+
<ListTable
27+
data={domainInfo}
28+
listTableConfig={domainPageMetadataTableConfig}
29+
/>
30+
</styled.MetadataContainer>
3431
);
3532
}

src/views/domain-page/domain-page-settings/domain-page-settings.tsx

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
} from '@tanstack/react-query';
99
import { toaster, ToasterContainer, PLACEMENT } from 'baseui/toast';
1010

11-
import PageSection from '@/components/page-section/page-section';
1211
import updateDomain from '@/server-actions/update-domain/update-domain';
1312
import request from '@/utils/request';
1413
import SettingsForm from '@/views/shared/settings-form/settings-form';
@@ -68,27 +67,25 @@ export default function DomainPageSettings(props: DomainPageTabContentProps) {
6867
autoHideDuration={SETTINGS_UPDATE_TOAST_DURATION_MS}
6968
overrides={overrides.toast}
7069
>
71-
<PageSection>
72-
<styled.SettingsContainer>
73-
<SettingsForm
74-
data={domainInfo}
75-
zodSchema={domainPageSettingsFormSchema}
76-
formConfig={domainPageSettingsFormConfig}
77-
onSubmit={async (data) =>
78-
await saveSettings.mutateAsync(data).then(() => {
79-
queryClient.invalidateQueries({
80-
queryKey: ['describeDomain', props],
81-
});
82-
toaster.positive('Successfully updated domain settings');
83-
})
84-
}
85-
submitButtonText="Save settings"
86-
onSubmitError={(e) =>
87-
toaster.negative('Error updating domain settings: ' + e.message)
88-
}
89-
/>
90-
</styled.SettingsContainer>
91-
</PageSection>
70+
<styled.SettingsContainer>
71+
<SettingsForm
72+
data={domainInfo}
73+
zodSchema={domainPageSettingsFormSchema}
74+
formConfig={domainPageSettingsFormConfig}
75+
onSubmit={async (data) =>
76+
await saveSettings.mutateAsync(data).then(() => {
77+
queryClient.invalidateQueries({
78+
queryKey: ['describeDomain', props],
79+
});
80+
toaster.positive('Successfully updated domain settings');
81+
})
82+
}
83+
submitButtonText="Save settings"
84+
onSubmitError={(e) =>
85+
toaster.negative('Error updating domain settings: ' + e.message)
86+
}
87+
/>
88+
</styled.SettingsContainer>
9289
</ToasterContainer>
9390
);
9491
}
Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use client';
22
import PageFilters from '@/components/page-filters/page-filters';
3-
import PageSection from '@/components/page-section/page-section';
43
import domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config';
54

65
import domainWorkflowsFiltersConfig from '../config/domain-workflows-filters.config';
@@ -9,15 +8,13 @@ import { styled } from './domain-workflows-filters.styles';
98

109
export default function DomainWorkflowsFilters() {
1110
return (
12-
<PageSection>
13-
<styled.FiltersContainer>
14-
<PageFilters
15-
searchQueryParamKey="search"
16-
searchPlaceholder="Search for Workflow ID, Run ID, or Workflow Type"
17-
pageFiltersConfig={domainWorkflowsFiltersConfig}
18-
pageQueryParamsConfig={domainPageQueryParamsConfig}
19-
/>
20-
</styled.FiltersContainer>
21-
</PageSection>
11+
<styled.FiltersContainer>
12+
<PageFilters
13+
searchQueryParamKey="search"
14+
searchPlaceholder="Search for Workflow ID, Run ID, or Workflow Type"
15+
pageFiltersConfig={domainWorkflowsFiltersConfig}
16+
pageQueryParamsConfig={domainPageQueryParamsConfig}
17+
/>
18+
</styled.FiltersContainer>
2219
);
2320
}

src/views/domain-workflows/domain-workflows-table/__tests__/domain-workflows-table.test.tsx

Lines changed: 79 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1-
import { Suspense } from 'react';
1+
import { HttpResponse } from 'msw';
22

3-
import { render, screen, act, fireEvent } from '@/test-utils/rtl';
3+
import { render, screen, userEvent } from '@/test-utils/rtl';
44

55
import { type ListWorkflowsResponse } from '@/route-handlers/list-workflows/list-workflows.types';
6-
import * as requestModule from '@/utils/request';
76

7+
import type { Props as MSWMocksHandlersProps } from '../../../../test-utils/msw-mock-handlers/msw-mock-handlers.types';
88
import { mockDomainWorkflowsQueryParamsValues } from '../../__fixtures__/domain-workflows-query-params';
99
import { type Props as EndMessageProps } from '../../domain-workflows-table-end-message/domain-workflows-table-end-message.types';
1010
import DomainWorkflowsTable from '../domain-workflows-table';
1111

12+
jest.mock('@/components/error-panel/error-panel', () =>
13+
jest.fn(({ message }: { message: string }) => <div>{message}</div>)
14+
);
15+
16+
jest.mock('../helpers/get-workflows-error-panel-props', () =>
17+
jest.fn().mockImplementation(({ error }) => ({
18+
message: error ? 'Error loading workflows' : 'No workflows found',
19+
}))
20+
);
21+
1222
jest.mock(
1323
'../../domain-workflows-table-end-message/domain-workflows-table-end-message',
1424
() =>
@@ -20,19 +30,19 @@ jest.mock(
2030
);
2131

2232
jest.mock('query-string', () => ({
23-
stringifyUrl: jest.fn(() => 'mock-stringified-api-url'),
33+
stringifyUrl: jest.fn(
34+
() => '/api/domains/mock-domain/mock-cluster/workflows'
35+
),
2436
}));
2537

26-
jest.mock('@/utils/request');
27-
2838
const mockSetQueryParams = jest.fn();
2939
jest.mock('@/hooks/use-page-query-params/use-page-query-params', () =>
3040
jest.fn(() => [mockDomainWorkflowsQueryParamsValues, mockSetQueryParams])
3141
);
3242

3343
describe(DomainWorkflowsTable.name, () => {
3444
it('renders workflows without error', async () => {
35-
await setup({});
45+
const { user } = setup({});
3646

3747
expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument();
3848
Array(10).forEach((_, index) => {
@@ -41,9 +51,7 @@ describe(DomainWorkflowsTable.name, () => {
4151
).toBeInTheDocument();
4252
});
4353

44-
act(() => {
45-
fireEvent.click(screen.getByTestId('mock-end-message'));
46-
});
54+
await user.click(screen.getByTestId('mock-end-message'));
4755

4856
expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument();
4957
Array(10).forEach((_, index) => {
@@ -53,23 +61,22 @@ describe(DomainWorkflowsTable.name, () => {
5361
});
5462
});
5563

56-
it('does not render if the initial call fails', async () => {
57-
let renderErrorMessage;
58-
try {
59-
await act(async () => {
60-
await setup({ errorCase: 'initial-fetch-error' });
61-
});
62-
} catch (error) {
63-
if (error instanceof Error) {
64-
renderErrorMessage = error.message;
65-
}
66-
}
67-
68-
expect(renderErrorMessage).toEqual('Request failed');
64+
it('renders error panel if the initial call fails', async () => {
65+
setup({ errorCase: 'initial-fetch-error' });
66+
67+
expect(
68+
await screen.findByText('Error loading workflows')
69+
).toBeInTheDocument();
70+
});
71+
72+
it('renders error panel if no workflows are found', async () => {
73+
setup({ errorCase: 'no-workflows' });
74+
75+
expect(await screen.findByText('No workflows found')).toBeInTheDocument();
6976
});
7077

7178
it('renders workflows and allows the user to try again if there is an error', async () => {
72-
await setup({ errorCase: 'subsequent-fetch-error' });
79+
const { user } = setup({ errorCase: 'subsequent-fetch-error' });
7380

7481
expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument();
7582
Array(10).forEach((_, index) => {
@@ -78,17 +85,13 @@ describe(DomainWorkflowsTable.name, () => {
7885
).toBeInTheDocument();
7986
});
8087

81-
act(() => {
82-
fireEvent.click(screen.getByTestId('mock-end-message'));
83-
});
88+
await user.click(screen.getByTestId('mock-end-message'));
8489

8590
expect(
8691
await screen.findByText('Mock end message: Error')
8792
).toBeInTheDocument();
8893

89-
act(() => {
90-
fireEvent.click(screen.getByTestId('mock-end-message'));
91-
});
94+
await user.click(screen.getByTestId('mock-end-message'));
9295

9396
expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument();
9497
Array(10).forEach((_, index) => {
@@ -99,41 +102,57 @@ describe(DomainWorkflowsTable.name, () => {
99102
});
100103
});
101104

102-
async function setup({
105+
function setup({
103106
errorCase,
104107
}: {
105-
errorCase?: 'initial-fetch-error' | 'subsequent-fetch-error';
108+
errorCase?: 'initial-fetch-error' | 'subsequent-fetch-error' | 'no-workflows';
106109
}) {
107-
// TODO: @adhitya.mamallan - This is not type-safe, explore using a library such as nock or msw
108-
const requestMock = jest.spyOn(requestModule, 'default') as jest.Mock;
109110
const pages = generateWorkflowPages(2);
111+
let currentEventIndex = 0;
112+
const user = userEvent.setup();
113+
114+
render(<DomainWorkflowsTable domain="mock-domain" cluster="mock-cluster" />, {
115+
endpointsMocks: [
116+
{
117+
path: '/api/domains/:domain/:cluster/workflows',
118+
httpMethod: 'GET',
119+
mockOnce: false,
120+
httpResolver: async () => {
121+
const index = currentEventIndex;
122+
currentEventIndex++;
123+
124+
switch (errorCase) {
125+
case 'no-workflows':
126+
return HttpResponse.json({ workflows: [], nextPage: undefined });
127+
case 'initial-fetch-error':
128+
return HttpResponse.json(
129+
{ message: 'Request failed' },
130+
{ status: 500 }
131+
);
132+
case 'subsequent-fetch-error':
133+
if (index === 0) {
134+
return HttpResponse.json(pages[0]);
135+
} else if (index === 1) {
136+
return HttpResponse.json(
137+
{ message: 'Request failed' },
138+
{ status: 500 }
139+
);
140+
} else {
141+
return HttpResponse.json(pages[1]);
142+
}
143+
default:
144+
if (index === 0) {
145+
return HttpResponse.json(pages[0]);
146+
} else {
147+
return HttpResponse.json(pages[1]);
148+
}
149+
}
150+
},
151+
},
152+
] as MSWMocksHandlersProps['endpointsMocks'],
153+
});
110154

111-
if (errorCase === 'subsequent-fetch-error') {
112-
requestMock
113-
.mockResolvedValueOnce({
114-
json: () => Promise.resolve(pages[0]),
115-
})
116-
.mockRejectedValueOnce(new Error('Request failed'))
117-
.mockResolvedValueOnce({
118-
json: () => Promise.resolve(pages[1]),
119-
});
120-
} else if (errorCase === 'initial-fetch-error') {
121-
requestMock.mockRejectedValueOnce(new Error('Request failed'));
122-
} else {
123-
requestMock
124-
.mockResolvedValueOnce({
125-
json: () => Promise.resolve(pages[0]),
126-
})
127-
.mockResolvedValueOnce({
128-
json: () => Promise.resolve(pages[1]),
129-
});
130-
}
131-
132-
render(
133-
<Suspense>
134-
<DomainWorkflowsTable domain="mock-domain" cluster="mock-cluster" />
135-
</Suspense>
136-
);
155+
return { user };
137156
}
138157

139158
// TODO @adhitya.mamallan - Explore using fakerjs.dev for cases like this
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
11
export const PAGE_SIZE = 10;
2-
3-
export const NO_WORKFLOWS_ERROR_MESSAGE = 'Domain has no workflows';

src/views/domain-workflows/domain-workflows-table/domain-workflows-table.styles.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,10 @@ export const styled = {
44
TableContainer: createStyled('div', {
55
overflowX: 'auto',
66
}),
7+
ErrorPanelContainer: createStyled('div', ({ $theme }) => ({
8+
padding: `${$theme.sizing.scale1200} 0px`,
9+
display: 'flex',
10+
alignItems: 'center',
11+
justifyContent: 'center',
12+
})),
713
};

0 commit comments

Comments
 (0)