diff --git a/src/views/domain-page/config/domain-page-tabs-error.config.ts b/src/views/domain-page/config/domain-page-tabs-error.config.ts index f0dd9734f..ca259fcf3 100644 --- a/src/views/domain-page/config/domain-page-tabs-error.config.ts +++ b/src/views/domain-page/config/domain-page-tabs-error.config.ts @@ -1,9 +1,10 @@ -import getDomainWorkflowsErrorConfig from '@/views/domain-workflows/helpers/get-domain-workflows-error-config'; - import { type DomainPageTabsErrorConfig } from '../domain-page-tabs-error/domain-page-tabs-error.types'; const domainPageTabsErrorConfig: DomainPageTabsErrorConfig = { - workflows: getDomainWorkflowsErrorConfig, + workflows: () => ({ + message: 'Failed to load workflows', + actions: [{ kind: 'retry', label: 'Retry' }], + }), metadata: () => ({ message: 'Failed to load metadata', actions: [{ kind: 'retry', label: 'Retry' }], diff --git a/src/views/domain-page/domain-page-content/domain-page-content.styles.ts b/src/views/domain-page/domain-page-content/domain-page-content.styles.ts new file mode 100644 index 000000000..a2c71ac18 --- /dev/null +++ b/src/views/domain-page/domain-page-content/domain-page-content.styles.ts @@ -0,0 +1,11 @@ +import { withStyle } from 'baseui'; + +import PageSection from '@/components/page-section/page-section'; + +export const styled = { + PageSection: withStyle(PageSection, () => ({ + display: 'flex', + flexDirection: 'column', + flex: 1, + })), +}; diff --git a/src/views/domain-page/domain-page-content/domain-page-content.tsx b/src/views/domain-page/domain-page-content/domain-page-content.tsx index 54f075ad9..6a193899b 100644 --- a/src/views/domain-page/domain-page-content/domain-page-content.tsx +++ b/src/views/domain-page/domain-page-content/domain-page-content.tsx @@ -1,3 +1,4 @@ +'use client'; import React from 'react'; import { notFound } from 'next/navigation'; @@ -6,6 +7,7 @@ import decodeUrlParams from '@/utils/decode-url-params'; import domainPageTabsContentConfig from '../config/domain-page-tabs-content.config'; +import { styled } from './domain-page-content.styles'; import { type DomainPageContentParams, type Props, @@ -22,11 +24,11 @@ export default function DomainPageContent(props: Props) { } return ( -
+ -
+ ); } diff --git a/src/views/domain-page/domain-page-metadata/domain-page-metadata.tsx b/src/views/domain-page/domain-page-metadata/domain-page-metadata.tsx index b187636af..033b63c6d 100644 --- a/src/views/domain-page/domain-page-metadata/domain-page-metadata.tsx +++ b/src/views/domain-page/domain-page-metadata/domain-page-metadata.tsx @@ -4,7 +4,6 @@ import React from 'react'; import { useSuspenseQuery } from '@tanstack/react-query'; import ListTable from '@/components/list-table/list-table'; -import PageSection from '@/components/page-section/page-section'; import request from '@/utils/request'; import domainPageMetadataTableConfig from '../config/domain-page-metadata-table.config'; @@ -23,13 +22,11 @@ export default function DomainPageMetadata(props: DomainPageTabContentProps) { }); return ( - - - - - + + + ); } diff --git a/src/views/domain-page/domain-page-settings/domain-page-settings.tsx b/src/views/domain-page/domain-page-settings/domain-page-settings.tsx index 24eea3a52..b04421049 100644 --- a/src/views/domain-page/domain-page-settings/domain-page-settings.tsx +++ b/src/views/domain-page/domain-page-settings/domain-page-settings.tsx @@ -8,7 +8,6 @@ import { } from '@tanstack/react-query'; import { toaster, ToasterContainer, PLACEMENT } from 'baseui/toast'; -import PageSection from '@/components/page-section/page-section'; import updateDomain from '@/server-actions/update-domain/update-domain'; import request from '@/utils/request'; import SettingsForm from '@/views/shared/settings-form/settings-form'; @@ -68,27 +67,25 @@ export default function DomainPageSettings(props: DomainPageTabContentProps) { autoHideDuration={SETTINGS_UPDATE_TOAST_DURATION_MS} overrides={overrides.toast} > - - - - await saveSettings.mutateAsync(data).then(() => { - queryClient.invalidateQueries({ - queryKey: ['describeDomain', props], - }); - toaster.positive('Successfully updated domain settings'); - }) - } - submitButtonText="Save settings" - onSubmitError={(e) => - toaster.negative('Error updating domain settings: ' + e.message) - } - /> - - + + + await saveSettings.mutateAsync(data).then(() => { + queryClient.invalidateQueries({ + queryKey: ['describeDomain', props], + }); + toaster.positive('Successfully updated domain settings'); + }) + } + submitButtonText="Save settings" + onSubmitError={(e) => + toaster.negative('Error updating domain settings: ' + e.message) + } + /> + ); } diff --git a/src/views/domain-workflows/domain-workflows-filters/domain-workflows-filters.tsx b/src/views/domain-workflows/domain-workflows-filters/domain-workflows-filters.tsx index 91c614897..4eb2151bb 100644 --- a/src/views/domain-workflows/domain-workflows-filters/domain-workflows-filters.tsx +++ b/src/views/domain-workflows/domain-workflows-filters/domain-workflows-filters.tsx @@ -1,6 +1,5 @@ 'use client'; import PageFilters from '@/components/page-filters/page-filters'; -import PageSection from '@/components/page-section/page-section'; import domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config'; import domainWorkflowsFiltersConfig from '../config/domain-workflows-filters.config'; @@ -9,15 +8,13 @@ import { styled } from './domain-workflows-filters.styles'; export default function DomainWorkflowsFilters() { return ( - - - - - + + + ); } diff --git a/src/views/domain-workflows/domain-workflows-table/__tests__/domain-workflows-table.test.tsx b/src/views/domain-workflows/domain-workflows-table/__tests__/domain-workflows-table.test.tsx index 086d7cd78..bfe08180b 100644 --- a/src/views/domain-workflows/domain-workflows-table/__tests__/domain-workflows-table.test.tsx +++ b/src/views/domain-workflows/domain-workflows-table/__tests__/domain-workflows-table.test.tsx @@ -1,14 +1,24 @@ -import { Suspense } from 'react'; +import { HttpResponse } from 'msw'; -import { render, screen, act, fireEvent } from '@/test-utils/rtl'; +import { render, screen, userEvent } from '@/test-utils/rtl'; import { type ListWorkflowsResponse } from '@/route-handlers/list-workflows/list-workflows.types'; -import * as requestModule from '@/utils/request'; +import type { Props as MSWMocksHandlersProps } from '../../../../test-utils/msw-mock-handlers/msw-mock-handlers.types'; import { mockDomainWorkflowsQueryParamsValues } from '../../__fixtures__/domain-workflows-query-params'; import { type Props as EndMessageProps } from '../../domain-workflows-table-end-message/domain-workflows-table-end-message.types'; import DomainWorkflowsTable from '../domain-workflows-table'; +jest.mock('@/components/error-panel/error-panel', () => + jest.fn(({ message }: { message: string }) =>
{message}
) +); + +jest.mock('../helpers/get-workflows-error-panel-props', () => + jest.fn().mockImplementation(({ error }) => ({ + message: error ? 'Error loading workflows' : 'No workflows found', + })) +); + jest.mock( '../../domain-workflows-table-end-message/domain-workflows-table-end-message', () => @@ -20,11 +30,11 @@ jest.mock( ); jest.mock('query-string', () => ({ - stringifyUrl: jest.fn(() => 'mock-stringified-api-url'), + stringifyUrl: jest.fn( + () => '/api/domains/mock-domain/mock-cluster/workflows' + ), })); -jest.mock('@/utils/request'); - const mockSetQueryParams = jest.fn(); jest.mock('@/hooks/use-page-query-params/use-page-query-params', () => jest.fn(() => [mockDomainWorkflowsQueryParamsValues, mockSetQueryParams]) @@ -32,7 +42,7 @@ jest.mock('@/hooks/use-page-query-params/use-page-query-params', () => describe(DomainWorkflowsTable.name, () => { it('renders workflows without error', async () => { - await setup({}); + const { user } = setup({}); expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); Array(10).forEach((_, index) => { @@ -41,9 +51,7 @@ describe(DomainWorkflowsTable.name, () => { ).toBeInTheDocument(); }); - act(() => { - fireEvent.click(screen.getByTestId('mock-end-message')); - }); + await user.click(screen.getByTestId('mock-end-message')); expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); Array(10).forEach((_, index) => { @@ -53,23 +61,22 @@ describe(DomainWorkflowsTable.name, () => { }); }); - it('does not render if the initial call fails', async () => { - let renderErrorMessage; - try { - await act(async () => { - await setup({ errorCase: 'initial-fetch-error' }); - }); - } catch (error) { - if (error instanceof Error) { - renderErrorMessage = error.message; - } - } - - expect(renderErrorMessage).toEqual('Request failed'); + it('renders error panel if the initial call fails', async () => { + setup({ errorCase: 'initial-fetch-error' }); + + expect( + await screen.findByText('Error loading workflows') + ).toBeInTheDocument(); + }); + + it('renders error panel if no workflows are found', async () => { + setup({ errorCase: 'no-workflows' }); + + expect(await screen.findByText('No workflows found')).toBeInTheDocument(); }); it('renders workflows and allows the user to try again if there is an error', async () => { - await setup({ errorCase: 'subsequent-fetch-error' }); + const { user } = setup({ errorCase: 'subsequent-fetch-error' }); expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); Array(10).forEach((_, index) => { @@ -78,17 +85,13 @@ describe(DomainWorkflowsTable.name, () => { ).toBeInTheDocument(); }); - act(() => { - fireEvent.click(screen.getByTestId('mock-end-message')); - }); + await user.click(screen.getByTestId('mock-end-message')); expect( await screen.findByText('Mock end message: Error') ).toBeInTheDocument(); - act(() => { - fireEvent.click(screen.getByTestId('mock-end-message')); - }); + await user.click(screen.getByTestId('mock-end-message')); expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); Array(10).forEach((_, index) => { @@ -99,41 +102,57 @@ describe(DomainWorkflowsTable.name, () => { }); }); -async function setup({ +function setup({ errorCase, }: { - errorCase?: 'initial-fetch-error' | 'subsequent-fetch-error'; + errorCase?: 'initial-fetch-error' | 'subsequent-fetch-error' | 'no-workflows'; }) { - // TODO: @adhitya.mamallan - This is not type-safe, explore using a library such as nock or msw - const requestMock = jest.spyOn(requestModule, 'default') as jest.Mock; const pages = generateWorkflowPages(2); + let currentEventIndex = 0; + const user = userEvent.setup(); + + render(, { + endpointsMocks: [ + { + path: '/api/domains/:domain/:cluster/workflows', + httpMethod: 'GET', + mockOnce: false, + httpResolver: async () => { + const index = currentEventIndex; + currentEventIndex++; + + switch (errorCase) { + case 'no-workflows': + return HttpResponse.json({ workflows: [], nextPage: undefined }); + case 'initial-fetch-error': + return HttpResponse.json( + { message: 'Request failed' }, + { status: 500 } + ); + case 'subsequent-fetch-error': + if (index === 0) { + return HttpResponse.json(pages[0]); + } else if (index === 1) { + return HttpResponse.json( + { message: 'Request failed' }, + { status: 500 } + ); + } else { + return HttpResponse.json(pages[1]); + } + default: + if (index === 0) { + return HttpResponse.json(pages[0]); + } else { + return HttpResponse.json(pages[1]); + } + } + }, + }, + ] as MSWMocksHandlersProps['endpointsMocks'], + }); - if (errorCase === 'subsequent-fetch-error') { - requestMock - .mockResolvedValueOnce({ - json: () => Promise.resolve(pages[0]), - }) - .mockRejectedValueOnce(new Error('Request failed')) - .mockResolvedValueOnce({ - json: () => Promise.resolve(pages[1]), - }); - } else if (errorCase === 'initial-fetch-error') { - requestMock.mockRejectedValueOnce(new Error('Request failed')); - } else { - requestMock - .mockResolvedValueOnce({ - json: () => Promise.resolve(pages[0]), - }) - .mockResolvedValueOnce({ - json: () => Promise.resolve(pages[1]), - }); - } - - render( - - - - ); + return { user }; } // TODO @adhitya.mamallan - Explore using fakerjs.dev for cases like this diff --git a/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.constants.ts b/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.constants.ts index 6f84f51d2..718f14d0f 100644 --- a/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.constants.ts +++ b/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.constants.ts @@ -1,3 +1 @@ export const PAGE_SIZE = 10; - -export const NO_WORKFLOWS_ERROR_MESSAGE = 'Domain has no workflows'; diff --git a/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.styles.ts b/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.styles.ts index 9f63ba324..d536b2fdf 100644 --- a/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.styles.ts +++ b/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.styles.ts @@ -4,4 +4,10 @@ export const styled = { TableContainer: createStyled('div', { overflowX: 'auto', }), + ErrorPanelContainer: createStyled('div', ({ $theme }) => ({ + padding: `${$theme.sizing.scale1200} 0px`, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + })), }; diff --git a/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.tsx b/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.tsx index 817c3989d..3ac33ecea 100644 --- a/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.tsx +++ b/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.tsx @@ -1,10 +1,11 @@ 'use client'; import React, { useMemo } from 'react'; -import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import { useInfiniteQuery } from '@tanstack/react-query'; import queryString from 'query-string'; -import PageSection from '@/components/page-section/page-section'; +import ErrorPanel from '@/components/error-panel/error-panel'; +import SectionLoadingIndicator from '@/components/section-loading-indicator/section-loading-indicator'; import Table from '@/components/table/table'; import usePageQueryParams from '@/hooks/use-page-query-params/use-page-query-params'; import { @@ -18,12 +19,10 @@ import domainWorkflowsTableConfig from '../config/domain-workflows-table.config' import DomainWorkflowsTableEndMessage from '../domain-workflows-table-end-message/domain-workflows-table-end-message'; import getNextSortOrder from '../helpers/get-next-sort-order'; -import { - NO_WORKFLOWS_ERROR_MESSAGE, - PAGE_SIZE, -} from './domain-workflows-table.constants'; +import { PAGE_SIZE } from './domain-workflows-table.constants'; import { styled } from './domain-workflows-table.styles'; import { type Props } from './domain-workflows-table.types'; +import getWorkflowsErrorPanelProps from './helpers/get-workflows-error-panel-props'; export default function DomainWorkflowsTable(props: Props) { const [queryParams, setQueryParams] = usePageQueryParams( @@ -47,7 +46,8 @@ export default function DomainWorkflowsTable(props: Props) { hasNextPage, fetchNextPage, isFetchingNextPage, - } = useSuspenseInfiniteQuery({ + refetch, + } = useInfiniteQuery({ queryKey: ['listWorkflows', { ...props, ...requestQueryParams }], queryFn: async ({ pageParam }) => request( @@ -66,51 +66,58 @@ export default function DomainWorkflowsTable(props: Props) { }, }); - const workflows = useMemo( - () => data.pages.flatMap((page) => page.workflows ?? []), - [data] - ); + const workflows = useMemo(() => { + if (!data) return []; + return data.pages.flatMap((page) => page.workflows ?? []); + }, [data]); + + if (isLoading) { + return ; + } + + if (workflows.length === 0) { + const errorPanelProps = getWorkflowsErrorPanelProps({ + error, + areSearchParamsAbsent: + !queryParams.search && + !queryParams.status && + !queryParams.timeRangeStart && + !queryParams.timeRangeEnd, + }); - if ( - !queryParams.search && - !queryParams.status && - !queryParams.timeRangeStart && - !queryParams.timeRangeEnd && - workflows.length === 0 - ) { - throw new Error(NO_WORKFLOWS_ERROR_MESSAGE); + if (errorPanelProps) { + return ; + } } return ( - - - 0} - onSort={(column) => { - setQueryParams({ - sortColumn: column, - sortOrder: getNextSortOrder({ - currentColumn: queryParams.sortColumn, - nextColumn: column, - currentSortOrder: queryParams.sortOrder, - }), - }); - }} - sortColumn={queryParams.sortColumn} - sortOrder={queryParams.sortOrder} - endMessage={ - 0} - error={error} - fetchNextPage={fetchNextPage} - hasNextPage={hasNextPage} - isFetchingNextPage={isFetchingNextPage} - /> - } - /> - - + +
0} + onSort={(column) => { + setQueryParams({ + sortColumn: column, + sortOrder: getNextSortOrder({ + currentColumn: queryParams.sortColumn, + nextColumn: column, + currentSortOrder: queryParams.sortOrder, + }), + }); + }} + sortColumn={queryParams.sortColumn} + sortOrder={queryParams.sortOrder} + endMessage={ + 0} + error={error} + fetchNextPage={fetchNextPage} + hasNextPage={hasNextPage} + isFetchingNextPage={isFetchingNextPage} + /> + } + /> + ); } diff --git a/src/views/domain-workflows/domain-workflows-table/helpers/__tests__/get-workflows-error-panel-props.test.ts b/src/views/domain-workflows/domain-workflows-table/helpers/__tests__/get-workflows-error-panel-props.test.ts new file mode 100644 index 000000000..df7331398 --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-table/helpers/__tests__/get-workflows-error-panel-props.test.ts @@ -0,0 +1,43 @@ +import getWorkflowsErrorPanelProps from '../get-workflows-error-panel-props'; + +describe(getWorkflowsErrorPanelProps.name, () => { + it('returns default error panel props for regular error', () => { + expect( + getWorkflowsErrorPanelProps({ + error: new Error('Test error'), + areSearchParamsAbsent: false, + }) + ).toEqual({ + message: 'Failed to fetch workflows', + actions: [{ kind: 'retry', label: 'Retry' }], + }); + }); + + it('returns "not found" error panel props when search params are absent', () => { + expect( + getWorkflowsErrorPanelProps({ + error: null, + areSearchParamsAbsent: true, + }) + ).toEqual({ + message: 'No workflows found for this domain', + omitLogging: true, + actions: [ + { + kind: 'link-external', + label: 'Get started on workflows', + link: 'https://cadenceworkflow.io/docs/concepts/workflows', + }, + ], + }); + }); + + it('returns undefined in all other cases', () => { + expect( + getWorkflowsErrorPanelProps({ + error: null, + areSearchParamsAbsent: false, + }) + ).toBeUndefined(); + }); +}); diff --git a/src/views/domain-workflows/domain-workflows-table/helpers/get-workflows-error-panel-props.ts b/src/views/domain-workflows/domain-workflows-table/helpers/get-workflows-error-panel-props.ts new file mode 100644 index 000000000..ea10e3c17 --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-table/helpers/get-workflows-error-panel-props.ts @@ -0,0 +1,32 @@ +import { type Props as ErrorPanelProps } from '@/components/error-panel/error-panel.types'; + +export default function getWorkflowsErrorPanelProps({ + error, + areSearchParamsAbsent, +}: { + error: Error | null; + areSearchParamsAbsent: boolean; +}): ErrorPanelProps | undefined { + if (error) { + return { + message: 'Failed to fetch workflows', + actions: [{ kind: 'retry', label: 'Retry' }], + }; + } + + if (areSearchParamsAbsent) { + return { + message: 'No workflows found for this domain', + actions: [ + { + kind: 'link-external', + label: 'Get started on workflows', + link: 'https://cadenceworkflow.io/docs/concepts/workflows', + }, + ], + omitLogging: true, + }; + } + + return undefined; +} diff --git a/src/views/domain-workflows/domain-workflows.tsx b/src/views/domain-workflows/domain-workflows.tsx index c10cf2152..01c176bd3 100644 --- a/src/views/domain-workflows/domain-workflows.tsx +++ b/src/views/domain-workflows/domain-workflows.tsx @@ -1,6 +1,5 @@ -import React, { Suspense } from 'react'; +import React from 'react'; -import SectionLoadingIndicator from '@/components/section-loading-indicator/section-loading-indicator'; import { type DomainPageTabContentProps } from '@/views/domain-page/domain-page-content/domain-page-content.types'; import DomainWorkflowsFilters from './domain-workflows-filters/domain-workflows-filters'; @@ -10,9 +9,7 @@ export default function DomainWorkflows(props: DomainPageTabContentProps) { return ( <> - }> - - + ); } diff --git a/src/views/domain-workflows/helpers/__tests__/get-domain-workflows-error-config.test.ts b/src/views/domain-workflows/helpers/__tests__/get-domain-workflows-error-config.test.ts deleted file mode 100644 index d608cf6b1..000000000 --- a/src/views/domain-workflows/helpers/__tests__/get-domain-workflows-error-config.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NO_WORKFLOWS_ERROR_MESSAGE } from '../../domain-workflows-table/domain-workflows-table.constants'; -import getDomainWorkflowsErrorConfig from '../get-domain-workflows-error-config'; - -describe(getDomainWorkflowsErrorConfig.name, () => { - it('returns default error config for regular error', () => { - expect(getDomainWorkflowsErrorConfig(new Error('Test error'))).toEqual({ - message: 'Failed to load workflows', - actions: [{ kind: 'retry', label: 'Retry' }], - }); - }); - - it('returns "not found" error config for workflows not found error', () => { - expect( - getDomainWorkflowsErrorConfig(new Error(NO_WORKFLOWS_ERROR_MESSAGE)) - ).toEqual({ - message: 'No workflows found for this domain', - omitLogging: true, - actions: [ - { - kind: 'link-external', - label: 'Get started on workflows', - link: 'https://cadenceworkflow.io/docs/concepts/workflows', - }, - ], - }); - }); -}); diff --git a/src/views/domain-workflows/helpers/get-domain-workflows-error-config.ts b/src/views/domain-workflows/helpers/get-domain-workflows-error-config.ts deleted file mode 100644 index a70042b3f..000000000 --- a/src/views/domain-workflows/helpers/get-domain-workflows-error-config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { type DomainPageTabErrorConfig } from '@/views/domain-page/domain-page-tabs-error/domain-page-tabs-error.types'; - -import { NO_WORKFLOWS_ERROR_MESSAGE } from '../domain-workflows-table/domain-workflows-table.constants'; - -export default function getDomainWorkflowsErrorConfig( - err: Error -): DomainPageTabErrorConfig { - if (err.message === NO_WORKFLOWS_ERROR_MESSAGE) { - return { - message: 'No workflows found for this domain', - actions: [ - { - kind: 'link-external', - label: 'Get started on workflows', - link: 'https://cadenceworkflow.io/docs/concepts/workflows', - }, - ], - omitLogging: true, - }; - } - - return { - message: 'Failed to load workflows', - actions: [{ kind: 'retry', label: 'Retry' }], - }; -}