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();
}
});
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 & {
owner?: string;
tags?: string[];
currentPage?: number;
pageSize?: number;
};
export interface IncidentTypeAreaChartWidgetProps {

View File

@ -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<TestCaseSearchParams>['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(
() => ({

View File

@ -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 (

View File

@ -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 = <T extends FilterState>(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 = <T extends FilterState>(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
}),
},
{

View File

@ -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<TestSummary>(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 (
<DataQualityContext.Provider value={dataQualityContextValue}>