diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataQualityAndProfiler.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataQualityAndProfiler.spec.ts index 9e86a78af20..c9ff2d03c1b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataQualityAndProfiler.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataQualityAndProfiler.spec.ts @@ -1197,3 +1197,101 @@ test('TestCase filters', PLAYWRIGHT_INGESTION_TAG_OBJ, async ({ page }) => { await afterAction(); } }); + +test( + 'Pagination functionality in test cases list', + PLAYWRIGHT_INGESTION_TAG_OBJ, + async ({ page }) => { + const { apiContext, afterAction } = await getApiContext(page); + const paginationTable = new TableClass(); + + try { + await paginationTable.create(apiContext); + await paginationTable.createTestSuiteAndPipelines(apiContext); + + // Create multiple test cases to ensure pagination is always visible + const testCaseCount = 25; // Create enough test cases to trigger pagination + + for (let i = 0; i < testCaseCount; i++) { + await paginationTable.createTestCase(apiContext, { + name: `pagination-test-case-${i + 1}-${uuid()}`, + testDefinition: 'tableRowCountToBeBetween', + parameterValues: [ + { name: 'minValue', value: 10 + i }, + { name: 'maxValue', value: 100 + i }, + ], + }); + } + + await sidebarClick(page, SidebarItem.DATA_QUALITY); + await page.waitForLoadState('networkidle'); + const getTestCaseListData = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*' + ); + await page.click('[data-testid="by-test-cases"]'); + await getTestCaseListData; + + await page.getByTestId('loader').waitFor({ state: 'detached' }); + + await test.step('Verify pagination controls are visible', async () => { + await expect(page.locator('[data-testid="pagination"]')).toBeVisible(); + await expect(page.locator('[data-testid="previous"]')).toBeVisible(); + await expect(page.locator('[data-testid="next"]')).toBeVisible(); + await expect( + page.locator('[data-testid="page-indicator"]') + ).toBeVisible(); + }); + + await test.step('Verify first page state', async () => { + await expect(page.locator('[data-testid="previous"]')).toBeDisabled(); + await expect(page.locator('[data-testid="next"]')).not.toBeDisabled(); + await expect( + page.locator('[data-testid="page-indicator"]') + ).toContainText('1 of'); + }); + + await test.step('Navigate to next page', async () => { + const nextPageResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*' + ); + await page.click('[data-testid="next"]'); + await nextPageResponse; + + await expect( + page.locator('[data-testid="previous"]') + ).not.toBeDisabled(); + await expect( + page.locator('[data-testid="page-indicator"]') + ).toContainText('2 of'); + }); + + await test.step('Navigate back to previous page', async () => { + const prevPageResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*' + ); + await page.click('[data-testid="previous"]'); + await prevPageResponse; + + await expect(page.locator('[data-testid="previous"]')).toBeDisabled(); + await expect( + page.locator('[data-testid="page-indicator"]') + ).toContainText('1 of'); + }); + + await test.step('Test page size dropdown', async () => { + await expect( + page.locator('[data-testid="page-size-selection-dropdown"]') + ).toBeVisible(); + + await page.click('[data-testid="page-size-selection-dropdown"]'); + + // Verify dropdown options are visible + await expect(page.locator('.ant-dropdown-menu')).toBeVisible(); + await expect(page.locator('.ant-dropdown-menu-item')).toHaveCount(3); + }); + } finally { + await paginationTable.delete(apiContext); + await afterAction(); + } + } +); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQuality.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQuality.interface.ts index 56930c269c7..bfa43083bf1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQuality.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQuality.interface.ts @@ -49,6 +49,8 @@ export type TestCaseSearchParams = { export type DataQualityPageParams = TestCaseSearchParams & { owner?: string; tags?: string[]; + currentPage?: number; + pageSize?: number; }; export interface IncidentTypeAreaChartWidgetProps { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestCases/TestCases.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestCases/TestCases.component.tsx index 08f2edf4a86..0c9908aa4a4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestCases/TestCases.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestCases/TestCases.component.tsx @@ -134,7 +134,12 @@ export const TestCases = () => { value?: TestCaseSearchParams[K] ) => { navigate({ - search: QueryString.stringify({ ...params, [key]: value || undefined }), + search: QueryString.stringify( + { ...params, [key]: value || undefined }, + { + arrayFormat: 'brackets', + } + ), }); }; @@ -150,44 +155,53 @@ export const TestCases = () => { } }; - const fetchTestCases = async ( - currentPage = INITIAL_PAGING_VALUE, - filters?: string[], - apiParams?: ListTestCaseParamsBySearch - ) => { - const updatedParams = getTestCaseFiltersValue( - params, - filters ?? selectedFilter - ); + const fetchTestCases = useCallback( + async ( + currentPage = INITIAL_PAGING_VALUE, + filters?: string[], + apiParams?: ListTestCaseParamsBySearch + ) => { + const updatedParams = getTestCaseFiltersValue( + params, + filters ?? selectedFilter + ); - setIsLoading(true); - try { - const { data, paging } = await getListTestCaseBySearch({ - ...updatedParams, - ...sortOptions, - ...apiParams, - testCaseStatus: isEmpty(params?.testCaseStatus) - ? undefined - : params?.testCaseStatus, - limit: pageSize, - includeAllTests: true, - fields: [ - TabSpecificField.TEST_CASE_RESULT, - TabSpecificField.TESTSUITE, - TabSpecificField.INCIDENT_ID, - ], - q: searchValue ? `*${searchValue}*` : undefined, - offset: (currentPage - 1) * pageSize, - }); - setTestCase(data); - handlePagingChange(paging); - handlePageChange(currentPage); - } catch (error) { - showErrorToast(error as AxiosError); - } finally { - setIsLoading(false); - } - }; + setIsLoading(true); + try { + const { data, paging } = await getListTestCaseBySearch({ + ...updatedParams, + ...sortOptions, + ...apiParams, + testCaseStatus: isEmpty(params?.testCaseStatus) + ? undefined + : params?.testCaseStatus, + limit: pageSize, + includeAllTests: true, + fields: [ + TabSpecificField.TEST_CASE_RESULT, + TabSpecificField.TESTSUITE, + TabSpecificField.INCIDENT_ID, + ], + q: searchValue ? `*${searchValue}*` : undefined, + offset: (currentPage - 1) * pageSize, + }); + setTestCase(data); + handlePagingChange(paging); + } catch (error) { + showErrorToast(error as AxiosError); + } finally { + setIsLoading(false); + } + }, + [ + params, + selectedFilter, + sortOptions, + pageSize, + searchValue, + handlePagingChange, + ] + ); const sortTestCase = async (apiParams?: TestCaseSearchParams) => { const updatedValue = uniq([...selectedFilter, ...Object.keys(params)]); @@ -213,9 +227,13 @@ export const TestCases = () => { }); }; - const handlePagingClick = ({ currentPage }: PagingHandlerParams) => { - fetchTestCases(currentPage); - }; + const handlePagingClick = useCallback( + ({ currentPage }: PagingHandlerParams) => { + handlePageChange(currentPage); + fetchTestCases(currentPage); + }, + [handlePageChange, fetchTestCases] + ); const handleFilterChange: FormProps['onValuesChange'] = (value?: TestCaseSearchParams) => { @@ -438,10 +456,10 @@ export const TestCases = () => { getInitialOptions(key, true); } setSelectedFilter(updatedValue); - fetchTestCases(INITIAL_PAGING_VALUE, updatedValue); + fetchTestCases(currentPage, updatedValue); form.setFieldsValue(params); } else { - fetchTestCases(INITIAL_PAGING_VALUE); + fetchTestCases(currentPage); } }; @@ -454,7 +472,7 @@ export const TestCases = () => { } else { setIsLoading(false); } - }, [tab, testCasePermission, pageSize, params]); + }, [tab, testCasePermission, pageSize, params, currentPage]); const pagingData = useMemo( () => ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestSuite/TestSuiteList/TestSuites.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestSuite/TestSuiteList/TestSuites.component.tsx index b3c0202b085..4576d5ff4e9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestSuite/TestSuiteList/TestSuites.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestSuite/TestSuiteList/TestSuites.component.tsx @@ -229,7 +229,7 @@ export const TestSuites = () => { fetchTestSuites(currentPage, { limit: pageSize }); handlePageChange(currentPage); }, - [pageSize, paging] + [pageSize, handlePageChange] ); const handleSearchParam = ( @@ -253,13 +253,13 @@ export const TestSuites = () => { useEffect(() => { if (testSuitePermission?.ViewAll || testSuitePermission?.ViewBasic) { - fetchTestSuites(INITIAL_PAGING_VALUE, { + fetchTestSuites(currentPage, { limit: pageSize, }); } else { setIsLoading(false); } - }, [testSuitePermission, pageSize, searchValue, owner, tab]); + }, [testSuitePermission, pageSize, searchValue, owner, tab, currentPage]); if (!testSuitePermission?.ViewAll && !testSuitePermission?.ViewBasic) { return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useTableFilters.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useTableFilters.ts index 65da215fc13..c2c65d40fe5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/useTableFilters.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useTableFilters.ts @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { isArray, isEmpty, isNil } from 'lodash'; +import { isArray, isEmpty, isNil, isString } from 'lodash'; import qs, { ParsedQs } from 'qs'; import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -38,10 +38,14 @@ export const useTableFilters = (initialFilters: T) => { if (typeof initialValue === 'boolean') { parsedFilters[key as keyof T] = (paramValue === 'true') as T[keyof T]; } else if (isArray(initialValue)) { - parsedFilters[key as keyof T] = - typeof paramValue === 'string' - ? (paramValue.split(',').map((val) => val.trim()) as T[keyof T]) - : (paramValue as T[keyof T]); + // Handle both array format (from brackets) and comma-separated string format + if (isArray(paramValue)) { + parsedFilters[key as keyof T] = paramValue as T[keyof T]; + } else if (isString(paramValue)) { + parsedFilters[key as keyof T] = paramValue + .split(',') + .map((val) => val.trim()) as T[keyof T]; + } } else { parsedFilters[key as keyof T] = paramValue as T[keyof T]; } @@ -68,14 +72,15 @@ export const useTableFilters = (initialFilters: T) => { if (isNil(value) || (isArray(value) && isEmpty(value))) { delete mergedQueryParams[key]; - } else if (isArray(value)) { - mergedQueryParams[key] = value.join(','); } + // Remove the array to string conversion to preserve array format + // The qs.stringify function will handle arrays properly }); navigate( { search: qs.stringify(mergedQueryParams, { addQueryPrefix: true, + arrayFormat: 'brackets', // This will format arrays as key[0]=value&key[1]=value }), }, { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityProvider.tsx index 1f1fe917bf6..4bf13a0e680 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityProvider.tsx @@ -11,7 +11,7 @@ * limitations under the License. */ import { AxiosError } from 'axios'; -import { pick } from 'lodash'; +import { isEmpty, pick } from 'lodash'; import QueryString from 'qs'; import { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { DataQualityPageParams } from '../../components/DataQuality/DataQuality.interface'; @@ -51,6 +51,18 @@ const DataQualityProvider = ({ children }: { children: React.ReactNode }) => { return params as DataQualityPageParams; }, [location.search]); + // Extract only filter-related parameters, excluding pagination + const filterParams = useMemo(() => { + const { currentPage, pageSize, ...filters } = params; + + return filters; + }, [params]); + + // Create a stable key for filter changes to prevent unnecessary re-renders + const filterKey = useMemo(() => { + return !isEmpty(filterParams) ? JSON.stringify(filterParams) : null; + }, [filterParams]); + const [testCaseSummary, setTestCaseSummary] = useState(INITIAL_TEST_SUMMARY); const [isTestCaseSummaryLoading, setIsTestCaseSummaryLoading] = @@ -121,11 +133,11 @@ const DataQualityProvider = ({ children }: { children: React.ReactNode }) => { useEffect(() => { if (testCasePermission?.ViewAll || testCasePermission?.ViewBasic) { - fetchTestSummary(params); + fetchTestSummary(filterParams); } else { setIsTestCaseSummaryLoading(false); } - }, [params]); + }, [filterKey]); return (