diff --git a/src/views/domain-workflows/domain-workflows-header/__tests__/domain-workflows-header.test.tsx b/src/views/domain-workflows/domain-workflows-header/__tests__/domain-workflows-header.test.tsx index fc587dcde..807b34adc 100644 --- a/src/views/domain-workflows/domain-workflows-header/__tests__/domain-workflows-header.test.tsx +++ b/src/views/domain-workflows/domain-workflows-header/__tests__/domain-workflows-header.test.tsx @@ -41,16 +41,26 @@ jest.mock('@/components/page-filters/hooks/use-page-filters', () => })) ); +jest.mock('../../hooks/use-list-workflows', () => + jest.fn(() => ({ + refetch: jest.fn(), + })) +); + describe(DomainWorkflowsHeader.name, () => { it('renders segmented control', async () => { - render(); + render( + + ); expect(await screen.findByText('Search')).toBeInTheDocument(); expect(await screen.findByText('Query')).toBeInTheDocument(); }); it('renders page search and filters button when input type is search', async () => { - render(); + render( + + ); expect(await screen.findByText('Filter search')).toBeInTheDocument(); expect(await screen.findByText('Filter toggle')).toBeInTheDocument(); @@ -58,7 +68,9 @@ describe(DomainWorkflowsHeader.name, () => { it('renders page filters when filter toggle is clicked', async () => { const user = userEvent.setup(); - render(); + render( + + ); const filterToggle = await screen.findByText('Filter toggle'); await user.click(filterToggle); @@ -77,14 +89,18 @@ describe(DomainWorkflowsHeader.name, () => { setQueryParams: mockSetQueryParams, }); - render(); + render( + + ); expect(await screen.findByText('Query')).toBeInTheDocument(); }); it('toggles input type when segmented control is used', async () => { const user = userEvent.setup(); - render(); + render( + + ); const queryButton = await screen.findByText('Search'); await user.click(queryButton); diff --git a/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.tsx b/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.tsx index 847fe3b29..da3d9ae5d 100644 --- a/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.tsx +++ b/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.tsx @@ -11,10 +11,12 @@ import domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page- import domainWorkflowsFiltersConfig from '../config/domain-workflows-filters.config'; import DomainWorkflowsQueryInput from '../domain-workflows-query-input/domain-workflows-query-input'; +import useListWorkflows from '../hooks/use-list-workflows'; import { overrides, styled } from './domain-workflows-header.styles'; +import { type Props } from './domain-workflows-header.types'; -export default function DomainWorkflowsHeader() { +export default function DomainWorkflowsHeader({ domain, cluster }: Props) { const [areFiltersShown, setAreFiltersShown] = useState(false); const { resetAllFilters, activeFiltersCount, queryParams, setQueryParams } = @@ -23,6 +25,8 @@ export default function DomainWorkflowsHeader() { pageQueryParamsConfig: domainPageQueryParamsConfig, }); + const { refetch } = useListWorkflows({ domain, cluster }); + return ( @@ -54,6 +58,7 @@ export default function DomainWorkflowsHeader() { setQueryParams({ query: v })} + refetchQuery={refetch} /> ) : ( <> diff --git a/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.types.ts b/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.types.ts index 1caec1506..5ab04ad92 100644 --- a/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.types.ts +++ b/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.types.ts @@ -2,3 +2,8 @@ import { type ListWorkflowsRequestQueryParams } from '@/route-handlers/list-work export type DomainWorkflowsHeaderInputType = ListWorkflowsRequestQueryParams['inputType']; + +export type Props = { + domain: string; + cluster: string; +}; diff --git a/src/views/domain-workflows/domain-workflows-query-input/__tests__/domain-workflows-query-input.test.tsx b/src/views/domain-workflows/domain-workflows-query-input/__tests__/domain-workflows-query-input.test.tsx index 99bb0a7a7..0f614fd96 100644 --- a/src/views/domain-workflows/domain-workflows-query-input/__tests__/domain-workflows-query-input.test.tsx +++ b/src/views/domain-workflows/domain-workflows-query-input/__tests__/domain-workflows-query-input.test.tsx @@ -29,17 +29,27 @@ describe(DomainWorkflowsQueryInput.name, () => { expect(mockSetValue).toHaveBeenCalledWith('mock_query'); }); + + it('calls refetchQuery when the Rerun Query button is clicked', async () => { + const { mockRefetch, user } = setup({ startValue: 'test_query' }); + + await user.click(await screen.findByText('Rerun Query')); + + expect(mockRefetch).toHaveBeenCalled(); + }); }); function setup({ startValue }: { startValue?: string }) { const mockSetValue = jest.fn(); + const mockRefetch = jest.fn(); const user = userEvent.setup(); render( ); - return { mockSetValue, user }; + return { mockSetValue, mockRefetch, user }; } diff --git a/src/views/domain-workflows/domain-workflows-query-input/domain-workflows-query-input.tsx b/src/views/domain-workflows/domain-workflows-query-input/domain-workflows-query-input.tsx index 3066aadd5..0827ce66a 100644 --- a/src/views/domain-workflows/domain-workflows-query-input/domain-workflows-query-input.tsx +++ b/src/views/domain-workflows/domain-workflows-query-input/domain-workflows-query-input.tsx @@ -7,7 +7,11 @@ import { MdPlayArrow, MdCode, MdRefresh } from 'react-icons/md'; import { overrides } from './domain-workflows-query-input.styles'; import { type Props } from './domain-workflows-query-input.types'; -export default function DomainWorkflowsQueryInput({ value, setValue }: Props) { +export default function DomainWorkflowsQueryInput({ + value, + setValue, + refetchQuery, +}: Props) { const [queryText, setQueryText] = useState(''); useEffect(() => { @@ -30,7 +34,9 @@ export default function DomainWorkflowsQueryInput({ value, setValue }: Props) { clearOnEscape /> - ) : ( -
No workflows
- ) - ) -); - -jest.mock('query-string', () => ({ - stringifyUrl: jest.fn( - () => '/api/domains/mock-domain/mock-cluster/workflows' - ), -})); - -const mockSetQueryParams = jest.fn(); -jest.mock('@/hooks/use-page-query-params/use-page-query-params', () => - jest.fn(() => [mockDomainWorkflowsQueryParamsValues, mockSetQueryParams]) -); - -describe(DomainWorkflowsTableQuery.name, () => { - it('renders workflows without error', async () => { - const { user } = setup({}); - expect(await screen.findByText('Loading...')).toBeInTheDocument(); - - expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); - Array(10).forEach((_, index) => { - expect( - screen.getByText(`mock-workflow-id-0-${index}`) - ).toBeInTheDocument(); - }); - - await user.click(screen.getByTestId('mock-end-message')); - - expect(await screen.findByText('No workflows')).toBeInTheDocument(); - Array(10).forEach((_, index) => { - expect( - screen.getByText(`mock-workflow-id-1-${index}`) - ).toBeInTheDocument(); - }); - }); - - 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 empty table if no workflows are found', async () => { - setup({ errorCase: 'no-workflows' }); - - expect(await screen.findByText('No workflows')).toBeInTheDocument(); - }); - - it('renders workflows and allows the user to try again if there is an error', async () => { - const { user } = setup({ errorCase: 'subsequent-fetch-error' }); - - expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); - Array(10).forEach((_, index) => { - expect( - screen.getByText(`mock-workflow-id-0-${index}`) - ).toBeInTheDocument(); - }); - - await user.click(screen.getByTestId('mock-end-message')); - - expect( - await screen.findByText('Mock end message: Error') - ).toBeInTheDocument(); - - await user.click(screen.getByTestId('mock-end-message')); - - expect(await screen.findByText('No workflows')).toBeInTheDocument(); - Array(10).forEach((_, index) => { - expect( - screen.getByText(`mock-workflow-id-1-${index}`) - ).toBeInTheDocument(); - }); - }); -}); - -function setup({ - errorCase, -}: { - errorCase?: 'initial-fetch-error' | 'subsequent-fetch-error' | 'no-workflows'; -}) { - 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'], - } - ); - - return { user }; -} - -// TODO @adhitya.mamallan - Explore using fakerjs.dev for cases like this -function generateWorkflowPages(count: number): Array { - const pages = Array.from( - { length: count }, - (_, pageIndex): ListWorkflowsResponse => ({ - workflows: Array.from({ length: 10 }, (_, index) => ({ - workflowID: `mock-workflow-id-${pageIndex}-${index}`, - runID: `mock-run-id-${pageIndex}-${index}`, - workflowName: `mock-workflow-name-${pageIndex}-${index}`, - status: 'WORKFLOW_EXECUTION_CLOSE_STATUS_COMPLETED', - startTime: 1684800000000, - closeTime: count > 5 ? 1684886400000 : undefined, - })), - nextPage: `${pageIndex + 1}`, - }) - ); - - pages[pages.length - 1].nextPage = ''; - return pages; -} diff --git a/src/views/domain-workflows/domain-workflows-table-query/domain-workflows-table-query.styles.ts b/src/views/domain-workflows/domain-workflows-table-query/domain-workflows-table-query.styles.ts deleted file mode 100644 index ee5a55dcc..000000000 --- a/src/views/domain-workflows/domain-workflows-table-query/domain-workflows-table-query.styles.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { styled as createStyled } from 'baseui'; - -export const styled = { - 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-query/domain-workflows-table-query.tsx b/src/views/domain-workflows/domain-workflows-table-query/domain-workflows-table-query.tsx deleted file mode 100644 index ba8388303..000000000 --- a/src/views/domain-workflows/domain-workflows-table-query/domain-workflows-table-query.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client'; -import React from 'react'; - -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 domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config'; - -import domainWorkflowsTableConfig from '../config/domain-workflows-table.config'; -import { type Props } from '../domain-workflows-table/domain-workflows-table.types'; -import DomainWorkflowsTableEndMessage from '../domain-workflows-table-end-message/domain-workflows-table-end-message'; -import useListWorkflows from '../hooks/use-list-workflows'; - -import { styled } from './domain-workflows-table-query.styles'; -import getQueryErrorPanelProps from './helpers/get-query-error-panel-props'; - -export default function DomainWorkflowsTableQuery({ domain, cluster }: Props) { - const [queryParams] = usePageQueryParams(domainPageQueryParamsConfig); - - const { - workflows, - error, - isLoading, - hasNextPage, - fetchNextPage, - isFetchingNextPage, - refetch, - } = useListWorkflows({ - domain, - cluster, - inputType: queryParams.inputType, - query: queryParams.query, - }); - - if (isLoading) { - return ; - } - - if (error && workflows.length === 0) { - return ( - - - - ); - } - - return ( - 0} - columns={domainWorkflowsTableConfig.map((columnConfig) => ({ - ...columnConfig, - sortable: false, - }))} - // TODO @adhitya.mamallan - remove this after 3.5 is merged - onSort={() => {}} - endMessage={ - 0} - error={error} - fetchNextPage={fetchNextPage} - hasNextPage={hasNextPage} - isFetchingNextPage={isFetchingNextPage} - /> - } - /> - ); -} diff --git a/src/views/domain-workflows/domain-workflows-table-query/helpers/__tests__/get-query-error-panel-props.test.ts b/src/views/domain-workflows/domain-workflows-table-query/helpers/__tests__/get-query-error-panel-props.test.ts deleted file mode 100644 index eb33113dc..000000000 --- a/src/views/domain-workflows/domain-workflows-table-query/helpers/__tests__/get-query-error-panel-props.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { RequestError } from '@/utils/request/request-error'; - -import getQueryErrorPanelProps from '../get-query-error-panel-props'; - -describe(getQueryErrorPanelProps.name, () => { - it('returns default error panel props for regular error', () => { - expect( - getQueryErrorPanelProps({ - error: new RequestError('Test error', 500), - }) - ).toEqual({ - message: 'Failed to fetch workflows', - actions: [{ kind: 'retry', label: 'Retry' }], - }); - }); - - it('returns query error message directly for bad request error', () => { - expect( - getQueryErrorPanelProps({ - error: new RequestError('Test error', 400), - }) - ).toEqual({ - message: 'Error in query: Test error', - }); - }); -}); diff --git a/src/views/domain-workflows/domain-workflows-table-query/helpers/get-query-error-panel-props.ts b/src/views/domain-workflows/domain-workflows-table-query/helpers/get-query-error-panel-props.ts deleted file mode 100644 index 97b75a2a4..000000000 --- a/src/views/domain-workflows/domain-workflows-table-query/helpers/get-query-error-panel-props.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { type Props as ErrorPanelProps } from '@/components/error-panel/error-panel.types'; -import { type RequestError } from '@/utils/request/request-error'; - -export default function getQueryErrorPanelProps({ - error, -}: { - error: RequestError; -}): ErrorPanelProps { - if (error.status === 400) { - return { - message: 'Error in query: ' + error.message, - }; - } - - return { - message: 'Failed to fetch workflows', - actions: [{ kind: 'retry', label: 'Retry' }], - }; -} diff --git a/src/views/domain-workflows/domain-workflows-table-search/__tests__/domain-workflows-table-search.test.tsx b/src/views/domain-workflows/domain-workflows-table-search/__tests__/domain-workflows-table-search.test.tsx deleted file mode 100644 index 052e73889..000000000 --- a/src/views/domain-workflows/domain-workflows-table-search/__tests__/domain-workflows-table-search.test.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { HttpResponse } from 'msw'; - -import { render, screen, userEvent } from '@/test-utils/rtl'; - -import { type ListWorkflowsResponse } from '@/route-handlers/list-workflows/list-workflows.types'; - -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 DomainWorkflowsTableSearch from '../domain-workflows-table-search'; - -jest.mock('@/components/error-panel/error-panel', () => - jest.fn(({ message }: { message: string }) =>
{message}
) -); - -jest.mock( - '@/components/section-loading-indicator/section-loading-indicator', - () => jest.fn(() =>
Loading...
) -); - -jest.mock('../helpers/get-search-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', - () => - jest.fn((props: EndMessageProps) => - props.hasNextPage ? ( - - ) : ( -
No workflows
- ) - ) -); - -jest.mock('query-string', () => ({ - stringifyUrl: jest.fn( - () => '/api/domains/mock-domain/mock-cluster/workflows' - ), -})); - -const mockSetQueryParams = jest.fn(); -jest.mock('@/hooks/use-page-query-params/use-page-query-params', () => - jest.fn(() => [mockDomainWorkflowsQueryParamsValues, mockSetQueryParams]) -); - -describe(DomainWorkflowsTableSearch.name, () => { - it('renders workflows without error', async () => { - const { user } = setup({}); - expect(await screen.findByText('Loading...')).toBeInTheDocument(); - - expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); - Array(10).forEach((_, index) => { - expect( - screen.getByText(`mock-workflow-id-0-${index}`) - ).toBeInTheDocument(); - }); - - await user.click(screen.getByTestId('mock-end-message')); - - expect(await screen.findByText('No workflows')).toBeInTheDocument(); - Array(10).forEach((_, index) => { - expect( - screen.getByText(`mock-workflow-id-1-${index}`) - ).toBeInTheDocument(); - }); - }); - - 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 () => { - const { user } = setup({ errorCase: 'subsequent-fetch-error' }); - - expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); - Array(10).forEach((_, index) => { - expect( - screen.getByText(`mock-workflow-id-0-${index}`) - ).toBeInTheDocument(); - }); - - await user.click(screen.getByTestId('mock-end-message')); - - expect( - await screen.findByText('Mock end message: Error') - ).toBeInTheDocument(); - - await user.click(screen.getByTestId('mock-end-message')); - - expect(await screen.findByText('No workflows')).toBeInTheDocument(); - Array(10).forEach((_, index) => { - expect( - screen.getByText(`mock-workflow-id-1-${index}`) - ).toBeInTheDocument(); - }); - }); -}); - -function setup({ - errorCase, -}: { - errorCase?: 'initial-fetch-error' | 'subsequent-fetch-error' | 'no-workflows'; -}) { - 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'], - } - ); - - return { user }; -} - -// TODO @adhitya.mamallan - Explore using fakerjs.dev for cases like this -function generateWorkflowPages(count: number): Array { - const pages = Array.from( - { length: count }, - (_, pageIndex): ListWorkflowsResponse => ({ - workflows: Array.from({ length: 10 }, (_, index) => ({ - workflowID: `mock-workflow-id-${pageIndex}-${index}`, - runID: `mock-run-id-${pageIndex}-${index}`, - workflowName: `mock-workflow-name-${pageIndex}-${index}`, - status: 'WORKFLOW_EXECUTION_CLOSE_STATUS_COMPLETED', - startTime: 1684800000000, - closeTime: count > 5 ? 1684886400000 : undefined, - })), - nextPage: `${pageIndex + 1}`, - }) - ); - - pages[pages.length - 1].nextPage = ''; - return pages; -} diff --git a/src/views/domain-workflows/domain-workflows-table-search/domain-workflows-table-search.styles.ts b/src/views/domain-workflows/domain-workflows-table-search/domain-workflows-table-search.styles.ts deleted file mode 100644 index ee5a55dcc..000000000 --- a/src/views/domain-workflows/domain-workflows-table-search/domain-workflows-table-search.styles.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { styled as createStyled } from 'baseui'; - -export const styled = { - 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-search/domain-workflows-table-search.tsx b/src/views/domain-workflows/domain-workflows-table-search/domain-workflows-table-search.tsx deleted file mode 100644 index 26de7737a..000000000 --- a/src/views/domain-workflows/domain-workflows-table-search/domain-workflows-table-search.tsx +++ /dev/null @@ -1,95 +0,0 @@ -'use client'; -import React from 'react'; - -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 domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config'; - -import domainWorkflowsTableConfig from '../config/domain-workflows-table.config'; -import { type Props } from '../domain-workflows-table/domain-workflows-table.types'; -import DomainWorkflowsTableEndMessage from '../domain-workflows-table-end-message/domain-workflows-table-end-message'; -import getNextSortOrder from '../helpers/get-next-sort-order'; -import useListWorkflows from '../hooks/use-list-workflows'; - -import { styled } from './domain-workflows-table-search.styles'; -import getSearchErrorPanelProps from './helpers/get-search-error-panel-props'; - -export default function DomainWorkflowsTableSearch({ domain, cluster }: Props) { - const [queryParams, setQueryParams] = usePageQueryParams( - domainPageQueryParamsConfig - ); - - const { - workflows, - error, - isLoading, - hasNextPage, - fetchNextPage, - isFetchingNextPage, - refetch, - } = useListWorkflows({ - domain, - cluster, - inputType: queryParams.inputType, - search: queryParams.search, - status: queryParams.status, - sortColumn: queryParams.sortColumn, - sortOrder: queryParams.sortOrder, - timeRangeStart: queryParams.timeRangeStart?.toISOString(), - timeRangeEnd: queryParams.timeRangeEnd?.toISOString(), - }); - - if (isLoading) { - return ; - } - - if (workflows.length === 0) { - const errorPanelProps = getSearchErrorPanelProps({ - error, - areSearchParamsAbsent: - !queryParams.search && - !queryParams.status && - !queryParams.timeRangeStart && - !queryParams.timeRangeEnd, - }); - - 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} - /> - } - /> - ); -} 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 c2d91e004..88b21ee1d 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,45 +1,224 @@ -import { render, screen } from '@/test-utils/rtl'; +import { HttpResponse } from 'msw'; + +import { render, screen, userEvent, waitFor } from '@/test-utils/rtl'; import * as usePageQueryParamsModule from '@/hooks/use-page-query-params/use-page-query-params'; +import { type ListWorkflowsResponse } from '@/route-handlers/list-workflows/list-workflows.types'; +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 DomainWorkflowsHeaderInputType } from '../../domain-workflows-header/domain-workflows-header.types'; +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( - '../../domain-workflows-table-search/domain-workflows-table-search', - () => jest.fn(() =>
Search results 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, + inputType, + }: { + error: Error; + inputType: DomainWorkflowsHeaderInputType; + }) => { + if (inputType === 'query') { + return { + message: error ? error.message : undefined, + }; + } + return { + message: error ? 'Error loading workflows' : 'No workflows found', + }; + } + ) ); jest.mock( - '../../domain-workflows-table-query/domain-workflows-table-query', - () => jest.fn(() =>
Query results table
) + '../../domain-workflows-table-end-message/domain-workflows-table-end-message', + () => + jest.fn((props: EndMessageProps) => ( + + )) ); +jest.mock('query-string', () => ({ + stringifyUrl: jest.fn( + () => '/api/domains/mock-domain/mock-cluster/workflows' + ), +})); + +const mockSetQueryParams = jest.fn(); jest.mock('@/hooks/use-page-query-params/use-page-query-params', () => - jest.fn(() => [mockDomainWorkflowsQueryParamsValues, jest.fn()]) + jest.fn(() => [mockDomainWorkflowsQueryParamsValues, mockSetQueryParams]) ); describe(DomainWorkflowsTable.name, () => { - it('renders search table by default', async () => { - render( - - ); + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders workflows without error', async () => { + const { user } = setup({}); + + expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); + Array(10).forEach((_, index) => { + expect( + screen.getByText(`mock-workflow-id-0-${index}`) + ).toBeInTheDocument(); + }); + + await user.click(screen.getByTestId('mock-end-message')); + + expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); + Array(10).forEach((_, index) => { + expect( + screen.getByText(`mock-workflow-id-1-${index}`) + ).toBeInTheDocument(); + }); + }); + + 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 in search mode if no workflows are found', async () => { + setup({ errorCase: 'no-workflows' }); - expect(await screen.findByText('Search results table')).toBeInTheDocument(); + expect(await screen.findByText('No workflows found')).toBeInTheDocument(); }); - it('renders query table if inputType is query', async () => { + it('renders empty table in query mode if no workflows are found', async () => { jest .spyOn(usePageQueryParamsModule, 'default') .mockReturnValue([ { ...mockDomainWorkflowsQueryParamsValues, inputType: 'query' }, - jest.fn(), + mockSetQueryParams, ]); - render( - - ); + setup({ errorCase: 'no-workflows' }); + const progressbar = await screen.findByRole('progressbar'); - expect(await screen.findByText('Query results table')).toBeInTheDocument(); + await waitFor(() => { + expect(progressbar).not.toBeInTheDocument(); + }); + + expect(screen.queryByText('No workflows found')).not.toBeInTheDocument(); + }); + + it('renders workflows and allows the user to try again if there is an error', async () => { + const { user } = setup({ errorCase: 'subsequent-fetch-error' }); + + expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); + Array(10).forEach((_, index) => { + expect( + screen.getByText(`mock-workflow-id-0-${index}`) + ).toBeInTheDocument(); + }); + + await user.click(screen.getByTestId('mock-end-message')); + + expect( + await screen.findByText('Mock end message: Error') + ).toBeInTheDocument(); + + await user.click(screen.getByTestId('mock-end-message')); + + expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); + Array(10).forEach((_, index) => { + expect( + screen.getByText(`mock-workflow-id-1-${index}`) + ).toBeInTheDocument(); + }); }); }); + +function setup({ + errorCase, +}: { + errorCase?: 'initial-fetch-error' | 'subsequent-fetch-error' | 'no-workflows'; +}) { + const pages = generateWorkflowPages(2); + let currentEventIndex = 0; + const user = userEvent.setup(); + + const { debug } = 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'], + } + ); + + return { user, debug }; +} + +// TODO @adhitya.mamallan - Explore using fakerjs.dev for cases like this +function generateWorkflowPages(count: number): Array { + const pages = Array.from( + { length: count }, + (_, pageIndex): ListWorkflowsResponse => ({ + workflows: Array.from({ length: 10 }, (_, index) => ({ + workflowID: `mock-workflow-id-${pageIndex}-${index}`, + runID: `mock-run-id-${pageIndex}-${index}`, + workflowName: `mock-workflow-name-${pageIndex}-${index}`, + status: 'WORKFLOW_EXECUTION_CLOSE_STATUS_COMPLETED', + startTime: 1684800000000, + closeTime: count > 5 ? 1684886400000 : undefined, + })), + nextPage: `${pageIndex + 1}`, + }) + ); + + pages[pages.length - 1].nextPage = ''; + return pages; +} 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 c7d01db28..4e9b3b1f5 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,25 +1,100 @@ 'use client'; import React from 'react'; +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 domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config'; -import DomainWorkflowsTableQuery from '../domain-workflows-table-query/domain-workflows-table-query'; -import DomainWorkflowsTableSearch from '../domain-workflows-table-search/domain-workflows-table-search'; +import domainWorkflowsTableConfig from '../config/domain-workflows-table.config'; +import { type Props } from '../domain-workflows-table/domain-workflows-table.types'; +import DomainWorkflowsTableEndMessage from '../domain-workflows-table-end-message/domain-workflows-table-end-message'; +import getNextSortOrder from '../helpers/get-next-sort-order'; +import useListWorkflows from '../hooks/use-list-workflows'; 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 [{ inputType }] = usePageQueryParams(domainPageQueryParamsConfig); +export default function DomainWorkflowsTable({ domain, cluster }: Props) { + const [queryParams, setQueryParams] = usePageQueryParams( + domainPageQueryParamsConfig + ); + + const inputType = queryParams.inputType; + + const { + workflows, + error, + isLoading, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + refetch, + } = useListWorkflows({ domain, cluster }); + + if (isLoading) { + return ; + } + + if (workflows.length === 0) { + const errorPanelProps = getWorkflowsErrorPanelProps({ + inputType, + error, + areSearchParamsAbsent: + !queryParams.search && + !queryParams.status && + !queryParams.timeRangeStart && + !queryParams.timeRangeEnd, + }); + + if (errorPanelProps) { + return ( + + + + ); + } + } return ( - {inputType === 'query' ? ( - - ) : ( - - )} +
0} + endMessage={ + 0} + error={error} + fetchNextPage={fetchNextPage} + hasNextPage={hasNextPage} + isFetchingNextPage={isFetchingNextPage} + /> + } + {...(inputType === 'query' + ? { + columns: domainWorkflowsTableConfig.map((columnConfig) => ({ + ...columnConfig, + sortable: false, + })), + onSort: () => {}, + } + : { + columns: domainWorkflowsTableConfig, + onSort: (column) => { + setQueryParams({ + sortColumn: column, + sortOrder: getNextSortOrder({ + currentColumn: queryParams.sortColumn, + nextColumn: column, + currentSortOrder: queryParams.sortOrder, + }), + }); + }, + sortColumn: queryParams.sortColumn, + sortOrder: queryParams.sortOrder, + })} + /> ); } diff --git a/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.types.ts b/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.types.ts index 9ac077ebc..2adbd04c7 100644 --- a/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.types.ts +++ b/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.types.ts @@ -1,5 +1,4 @@ export type Props = { domain: string; cluster: string; - // TODO @adhitya.mamallan - when working on Archival, add a flag here }; diff --git a/src/views/domain-workflows/domain-workflows-table-search/helpers/__tests__/get-search-error-panel-props.test.ts b/src/views/domain-workflows/domain-workflows-table/helpers/__tests__/get-workflows-error-panel-props.test.ts similarity index 60% rename from src/views/domain-workflows/domain-workflows-table-search/helpers/__tests__/get-search-error-panel-props.test.ts rename to src/views/domain-workflows/domain-workflows-table/helpers/__tests__/get-workflows-error-panel-props.test.ts index cb45d4098..8956fd76a 100644 --- a/src/views/domain-workflows/domain-workflows-table-search/helpers/__tests__/get-search-error-panel-props.test.ts +++ b/src/views/domain-workflows/domain-workflows-table/helpers/__tests__/get-workflows-error-panel-props.test.ts @@ -1,11 +1,12 @@ import { RequestError } from '@/utils/request/request-error'; -import getSearchErrorPanelProps from '../get-search-error-panel-props'; +import getWorkflowsErrorPanelProps from '../get-workflows-error-panel-props'; -describe(getSearchErrorPanelProps.name, () => { +describe(getWorkflowsErrorPanelProps.name, () => { it('returns default error panel props for regular error', () => { expect( - getSearchErrorPanelProps({ + getWorkflowsErrorPanelProps({ + inputType: 'search', error: new RequestError('Test error', 500), areSearchParamsAbsent: false, }) @@ -15,9 +16,22 @@ describe(getSearchErrorPanelProps.name, () => { }); }); + it('returns error message directly for bad request error for queries', () => { + expect( + getWorkflowsErrorPanelProps({ + inputType: 'query', + error: new RequestError('Incorrect query', 400), + areSearchParamsAbsent: false, + }) + ).toEqual({ + message: 'Error in query: Incorrect query', + }); + }); + it('returns "not found" error panel props when search params are absent', () => { expect( - getSearchErrorPanelProps({ + getWorkflowsErrorPanelProps({ + inputType: 'search', error: null, areSearchParamsAbsent: true, }) @@ -36,7 +50,8 @@ describe(getSearchErrorPanelProps.name, () => { it('returns undefined in all other cases', () => { expect( - getSearchErrorPanelProps({ + getWorkflowsErrorPanelProps({ + inputType: 'search', error: null, areSearchParamsAbsent: false, }) diff --git a/src/views/domain-workflows/domain-workflows-table-search/helpers/get-search-error-panel-props.ts b/src/views/domain-workflows/domain-workflows-table/helpers/get-workflows-error-panel-props.ts similarity index 67% rename from src/views/domain-workflows/domain-workflows-table-search/helpers/get-search-error-panel-props.ts rename to src/views/domain-workflows/domain-workflows-table/helpers/get-workflows-error-panel-props.ts index ebed47328..972d9306c 100644 --- a/src/views/domain-workflows/domain-workflows-table-search/helpers/get-search-error-panel-props.ts +++ b/src/views/domain-workflows/domain-workflows-table/helpers/get-workflows-error-panel-props.ts @@ -1,14 +1,24 @@ import { type Props as ErrorPanelProps } from '@/components/error-panel/error-panel.types'; import { type RequestError } from '@/utils/request/request-error'; -export default function getSearchErrorPanelProps({ +import { type DomainWorkflowsHeaderInputType } from '../../domain-workflows-header/domain-workflows-header.types'; + +export default function getWorkflowsErrorPanelProps({ + inputType, error, areSearchParamsAbsent, }: { + inputType: DomainWorkflowsHeaderInputType; error: RequestError | null; areSearchParamsAbsent: boolean; }): ErrorPanelProps | undefined { if (error) { + if (inputType === 'query' && error.status === 400) { + return { + message: 'Error in query: ' + error.message, + }; + } + return { message: 'Failed to fetch workflows', actions: [{ kind: 'retry', label: 'Retry' }], diff --git a/src/views/domain-workflows/domain-workflows.tsx b/src/views/domain-workflows/domain-workflows.tsx index dbf5c5e7d..6888bbd36 100644 --- a/src/views/domain-workflows/domain-workflows.tsx +++ b/src/views/domain-workflows/domain-workflows.tsx @@ -8,7 +8,7 @@ import DomainWorkflowsTable from './domain-workflows-table/domain-workflows-tabl export default function DomainWorkflows(props: DomainPageTabContentProps) { return ( <> - + ); diff --git a/src/views/domain-workflows/domain-workflows.types.ts b/src/views/domain-workflows/domain-workflows.types.ts index 055124e9e..b85ce1255 100644 --- a/src/views/domain-workflows/domain-workflows.types.ts +++ b/src/views/domain-workflows/domain-workflows.types.ts @@ -1,7 +1,5 @@ -import { - type RouteParams as ListWorkflowsRouteParams, - type ListWorkflowsRequestQueryParams, -} from '@/route-handlers/list-workflows/list-workflows.types'; +import { type RouteParams as ListWorkflowsRouteParams } from '@/route-handlers/list-workflows/list-workflows.types'; -export type UseListWorkflowsParams = ListWorkflowsRouteParams & - Omit & { pageSize?: number }; +export type UseListWorkflowsParams = ListWorkflowsRouteParams & { + pageSize?: number; +}; diff --git a/src/views/domain-workflows/hooks/use-list-workflows.ts b/src/views/domain-workflows/hooks/use-list-workflows.ts index 7f942a7bc..564929824 100644 --- a/src/views/domain-workflows/hooks/use-list-workflows.ts +++ b/src/views/domain-workflows/hooks/use-list-workflows.ts @@ -4,12 +4,14 @@ import { useMemo } from 'react'; import { useInfiniteQuery } from '@tanstack/react-query'; import queryString from 'query-string'; +import usePageQueryParams from '@/hooks/use-page-query-params/use-page-query-params'; import { type ListWorkflowsResponse, type ListWorkflowsRequestQueryParams, } from '@/route-handlers/list-workflows/list-workflows.types'; import request from '@/utils/request'; import { type RequestError } from '@/utils/request/request-error'; +import domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config'; import DOMAIN_WORKFLOWS_PAGE_SIZE from '../config/domain-workflows-page-size.config'; import { type UseListWorkflowsParams } from '../domain-workflows.types'; @@ -18,8 +20,25 @@ export default function useListWorkflows({ domain, cluster, pageSize = DOMAIN_WORKFLOWS_PAGE_SIZE, - ...requestQueryParams }: UseListWorkflowsParams) { + const [queryParams] = usePageQueryParams(domainPageQueryParamsConfig); + + const requestQueryParams = { + inputType: queryParams.inputType, + ...(queryParams.inputType === 'query' + ? { + query: queryParams.query, + } + : { + search: queryParams.search, + status: queryParams.status, + sortColumn: queryParams.sortColumn, + sortOrder: queryParams.sortOrder, + timeRangeStart: queryParams.timeRangeStart?.toISOString(), + timeRangeEnd: queryParams.timeRangeEnd?.toISOString(), + }), + }; + const queryResult = useInfiniteQuery({ queryKey: [ 'listWorkflows',