fix(ui): Data quality tab table pagination issue (#22103)

* fix data quality tab pagination issue

* added test

* fix re-render issue on pagination param update

* update test to create multiple test case data

* Enhance query parameter handling in TestCases and useTableFilters components

- Updated TestCases component to use brackets format for array query parameters.
- Modified useTableFilters to handle both array format and comma-separated strings for filter values.
- Removed unnecessary array to string conversion to preserve array format in query parameters.

---------

Co-authored-by: Sriharsha Chintalapani <harshach@users.noreply.github.com>
Co-authored-by: Shailesh Parmar <shailesh.parmar.webdev@gmail.com>
This commit is contained in:
Shrushti Polekar 2025-07-08 10:33:09 +05:30 committed by GitHub
parent c2a3027962
commit e8bd7ea8a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 192 additions and 57 deletions

View File

@ -1197,3 +1197,101 @@ test('TestCase filters', PLAYWRIGHT_INGESTION_TAG_OBJ, async ({ page }) => {
await afterAction(); 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();
}
}
);

View File

@ -49,6 +49,8 @@ export type TestCaseSearchParams = {
export type DataQualityPageParams = TestCaseSearchParams & { export type DataQualityPageParams = TestCaseSearchParams & {
owner?: string; owner?: string;
tags?: string[]; tags?: string[];
currentPage?: number;
pageSize?: number;
}; };
export interface IncidentTypeAreaChartWidgetProps { export interface IncidentTypeAreaChartWidgetProps {

View File

@ -134,7 +134,12 @@ export const TestCases = () => {
value?: TestCaseSearchParams[K] value?: TestCaseSearchParams[K]
) => { ) => {
navigate({ 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 ( const fetchTestCases = useCallback(
currentPage = INITIAL_PAGING_VALUE, async (
filters?: string[], currentPage = INITIAL_PAGING_VALUE,
apiParams?: ListTestCaseParamsBySearch filters?: string[],
) => { apiParams?: ListTestCaseParamsBySearch
const updatedParams = getTestCaseFiltersValue( ) => {
params, const updatedParams = getTestCaseFiltersValue(
filters ?? selectedFilter params,
); filters ?? selectedFilter
);
setIsLoading(true); setIsLoading(true);
try { try {
const { data, paging } = await getListTestCaseBySearch({ const { data, paging } = await getListTestCaseBySearch({
...updatedParams, ...updatedParams,
...sortOptions, ...sortOptions,
...apiParams, ...apiParams,
testCaseStatus: isEmpty(params?.testCaseStatus) testCaseStatus: isEmpty(params?.testCaseStatus)
? undefined ? undefined
: params?.testCaseStatus, : params?.testCaseStatus,
limit: pageSize, limit: pageSize,
includeAllTests: true, includeAllTests: true,
fields: [ fields: [
TabSpecificField.TEST_CASE_RESULT, TabSpecificField.TEST_CASE_RESULT,
TabSpecificField.TESTSUITE, TabSpecificField.TESTSUITE,
TabSpecificField.INCIDENT_ID, TabSpecificField.INCIDENT_ID,
], ],
q: searchValue ? `*${searchValue}*` : undefined, q: searchValue ? `*${searchValue}*` : undefined,
offset: (currentPage - 1) * pageSize, offset: (currentPage - 1) * pageSize,
}); });
setTestCase(data); setTestCase(data);
handlePagingChange(paging); handlePagingChange(paging);
handlePageChange(currentPage); } catch (error) {
} catch (error) { showErrorToast(error as AxiosError);
showErrorToast(error as AxiosError); } finally {
} finally { setIsLoading(false);
setIsLoading(false); }
} },
}; [
params,
selectedFilter,
sortOptions,
pageSize,
searchValue,
handlePagingChange,
]
);
const sortTestCase = async (apiParams?: TestCaseSearchParams) => { const sortTestCase = async (apiParams?: TestCaseSearchParams) => {
const updatedValue = uniq([...selectedFilter, ...Object.keys(params)]); const updatedValue = uniq([...selectedFilter, ...Object.keys(params)]);
@ -213,9 +227,13 @@ export const TestCases = () => {
}); });
}; };
const handlePagingClick = ({ currentPage }: PagingHandlerParams) => { const handlePagingClick = useCallback(
fetchTestCases(currentPage); ({ currentPage }: PagingHandlerParams) => {
}; handlePageChange(currentPage);
fetchTestCases(currentPage);
},
[handlePageChange, fetchTestCases]
);
const handleFilterChange: FormProps<TestCaseSearchParams>['onValuesChange'] = const handleFilterChange: FormProps<TestCaseSearchParams>['onValuesChange'] =
(value?: TestCaseSearchParams) => { (value?: TestCaseSearchParams) => {
@ -438,10 +456,10 @@ export const TestCases = () => {
getInitialOptions(key, true); getInitialOptions(key, true);
} }
setSelectedFilter(updatedValue); setSelectedFilter(updatedValue);
fetchTestCases(INITIAL_PAGING_VALUE, updatedValue); fetchTestCases(currentPage, updatedValue);
form.setFieldsValue(params); form.setFieldsValue(params);
} else { } else {
fetchTestCases(INITIAL_PAGING_VALUE); fetchTestCases(currentPage);
} }
}; };
@ -454,7 +472,7 @@ export const TestCases = () => {
} else { } else {
setIsLoading(false); setIsLoading(false);
} }
}, [tab, testCasePermission, pageSize, params]); }, [tab, testCasePermission, pageSize, params, currentPage]);
const pagingData = useMemo( const pagingData = useMemo(
() => ({ () => ({

View File

@ -229,7 +229,7 @@ export const TestSuites = () => {
fetchTestSuites(currentPage, { limit: pageSize }); fetchTestSuites(currentPage, { limit: pageSize });
handlePageChange(currentPage); handlePageChange(currentPage);
}, },
[pageSize, paging] [pageSize, handlePageChange]
); );
const handleSearchParam = ( const handleSearchParam = (
@ -253,13 +253,13 @@ export const TestSuites = () => {
useEffect(() => { useEffect(() => {
if (testSuitePermission?.ViewAll || testSuitePermission?.ViewBasic) { if (testSuitePermission?.ViewAll || testSuitePermission?.ViewBasic) {
fetchTestSuites(INITIAL_PAGING_VALUE, { fetchTestSuites(currentPage, {
limit: pageSize, limit: pageSize,
}); });
} else { } else {
setIsLoading(false); setIsLoading(false);
} }
}, [testSuitePermission, pageSize, searchValue, owner, tab]); }, [testSuitePermission, pageSize, searchValue, owner, tab, currentPage]);
if (!testSuitePermission?.ViewAll && !testSuitePermission?.ViewBasic) { if (!testSuitePermission?.ViewAll && !testSuitePermission?.ViewBasic) {
return ( return (

View File

@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { isArray, isEmpty, isNil } from 'lodash'; import { isArray, isEmpty, isNil, isString } from 'lodash';
import qs, { ParsedQs } from 'qs'; import qs, { ParsedQs } from 'qs';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -38,10 +38,14 @@ export const useTableFilters = <T extends FilterState>(initialFilters: T) => {
if (typeof initialValue === 'boolean') { if (typeof initialValue === 'boolean') {
parsedFilters[key as keyof T] = (paramValue === 'true') as T[keyof T]; parsedFilters[key as keyof T] = (paramValue === 'true') as T[keyof T];
} else if (isArray(initialValue)) { } else if (isArray(initialValue)) {
parsedFilters[key as keyof T] = // Handle both array format (from brackets) and comma-separated string format
typeof paramValue === 'string' if (isArray(paramValue)) {
? (paramValue.split(',').map((val) => val.trim()) as T[keyof T]) parsedFilters[key as keyof T] = paramValue as T[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 { } else {
parsedFilters[key as keyof T] = paramValue as T[keyof T]; parsedFilters[key as keyof T] = paramValue as T[keyof T];
} }
@ -68,14 +72,15 @@ export const useTableFilters = <T extends FilterState>(initialFilters: T) => {
if (isNil(value) || (isArray(value) && isEmpty(value))) { if (isNil(value) || (isArray(value) && isEmpty(value))) {
delete mergedQueryParams[key]; 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( navigate(
{ {
search: qs.stringify(mergedQueryParams, { search: qs.stringify(mergedQueryParams, {
addQueryPrefix: true, addQueryPrefix: true,
arrayFormat: 'brackets', // This will format arrays as key[0]=value&key[1]=value
}), }),
}, },
{ {

View File

@ -11,7 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { pick } from 'lodash'; import { isEmpty, pick } from 'lodash';
import QueryString from 'qs'; import QueryString from 'qs';
import { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { DataQualityPageParams } from '../../components/DataQuality/DataQuality.interface'; import { DataQualityPageParams } from '../../components/DataQuality/DataQuality.interface';
@ -51,6 +51,18 @@ const DataQualityProvider = ({ children }: { children: React.ReactNode }) => {
return params as DataQualityPageParams; return params as DataQualityPageParams;
}, [location.search]); }, [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] = const [testCaseSummary, setTestCaseSummary] =
useState<TestSummary>(INITIAL_TEST_SUMMARY); useState<TestSummary>(INITIAL_TEST_SUMMARY);
const [isTestCaseSummaryLoading, setIsTestCaseSummaryLoading] = const [isTestCaseSummaryLoading, setIsTestCaseSummaryLoading] =
@ -121,11 +133,11 @@ const DataQualityProvider = ({ children }: { children: React.ReactNode }) => {
useEffect(() => { useEffect(() => {
if (testCasePermission?.ViewAll || testCasePermission?.ViewBasic) { if (testCasePermission?.ViewAll || testCasePermission?.ViewBasic) {
fetchTestSummary(params); fetchTestSummary(filterParams);
} else { } else {
setIsTestCaseSummaryLoading(false); setIsTestCaseSummaryLoading(false);
} }
}, [params]); }, [filterKey]);
return ( return (
<DataQualityContext.Provider value={dataQualityContextValue}> <DataQualityContext.Provider value={dataQualityContextValue}>