mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-26 09:22:14 +00:00
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:
parent
c2a3027962
commit
e8bd7ea8a0
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -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 {
|
||||||
|
@ -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(
|
||||||
() => ({
|
() => ({
|
||||||
|
@ -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 (
|
||||||
|
@ -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
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -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}>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user