fix(ui): add pagination in sub domains table (#22844)

* add pagination on subdomains table

* add tests

* fix domain tests

* fix flaky test

* moved config outsude
This commit is contained in:
Karan Hotchandani 2025-08-11 16:33:48 +05:30 committed by GitHub
parent b725bee3c7
commit a239cfb884
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 290 additions and 42 deletions

View File

@ -0,0 +1,161 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, test } from '@playwright/test';
import { SidebarItem } from '../../constant/sidebar';
import { Domain } from '../../support/domain/Domain';
import { SubDomain } from '../../support/domain/SubDomain';
import {
createNewPage,
getApiContext,
redirectToHomePage,
} from '../../utils/common';
import { createSubDomain, selectDomain } from '../../utils/domain';
import { sidebarClick } from '../../utils/sidebar';
test.use({ storageState: 'playwright/.auth/admin.json' });
const domain = new Domain();
const subDomains: SubDomain[] = [];
const SUBDOMAIN_COUNT = 60;
test.describe('SubDomain Pagination', () => {
test.slow(true);
test.beforeAll('Setup domain and subdomains', async ({ browser }) => {
const { page, apiContext, afterAction } = await createNewPage(browser);
await domain.create(apiContext);
await redirectToHomePage(page);
const createPromises = [];
for (let i = 1; i <= SUBDOMAIN_COUNT; i++) {
const subDomain = new SubDomain(
domain,
`TestSubDomain${i.toString().padStart(2, '0')}`
);
subDomains.push(subDomain);
createPromises.push(subDomain.create(apiContext));
}
await Promise.all(createPromises);
await afterAction();
});
test.afterAll('Cleanup', async ({ browser }) => {
const { apiContext, afterAction } = await createNewPage(browser);
// Delete all subdomains in parallel
const deletePromises = subDomains.map((subDomain) =>
subDomain.delete(apiContext)
);
await Promise.all(deletePromises);
await domain.delete(apiContext);
await afterAction();
});
test.beforeEach('Navigate to domain page', async ({ page }) => {
await redirectToHomePage(page);
await sidebarClick(page, SidebarItem.DOMAIN);
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
});
test('Verify subdomain count and pagination functionality', async ({
page,
}) => {
await selectDomain(page, domain.data);
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
await test.step('Verify subdomain count in tab label', async () => {
const subDomainsTab = page.getByTestId('subdomains');
await expect(subDomainsTab).toBeVisible();
await expect(subDomainsTab).toContainText('60');
});
await test.step(
'Navigate to subdomains tab and verify initial data load',
async () => {
const subDomainRes = page.waitForResponse(
'/api/v1/search/query?q=*&from=0&size=50&index=domain_search_index&deleted=false&track_total_hits=true'
);
await page.getByTestId('subdomains').click();
await subDomainRes;
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
await expect(page.locator('table')).toBeVisible();
await expect(page.locator('[data-testid="pagination"]')).toBeVisible();
}
);
await test.step('Test pagination navigation', async () => {
// Verify current page shows page 1
const tableRows = page.locator('table tbody tr');
await expect(tableRows).toHaveCount(50);
const nextPageResponse = page.waitForResponse('/api/v1/search/query?*');
await page.locator('[data-testid="next"]').click();
await nextPageResponse;
await expect(tableRows).toHaveCount(10);
const prevPageResponse = page.waitForResponse('/api/v1/search/query?*');
await page.locator('[data-testid="previous"]').click();
await prevPageResponse;
await expect(tableRows).toHaveCount(50);
});
await test.step(
'Create new subdomain and verify count updates',
async () => {
const subDomain = new SubDomain(domain);
await createSubDomain(page, subDomain.data);
await redirectToHomePage(page);
await sidebarClick(page, SidebarItem.DOMAIN);
await page.waitForLoadState('networkidle');
await selectDomain(page, domain.data);
const subDomainsTab = page.getByTestId('subdomains');
await expect(subDomainsTab).toContainText('61');
const { apiContext, afterAction } = await getApiContext(page);
const response = await apiContext.get(
'/api/v1/domains/name/' +
encodeURIComponent(`"${domain.data.name}"."NewTestSubDomain"`)
);
const subDomainData = await response.json();
await apiContext.delete(
`/api/v1/domains/${subDomainData.id}?hardDelete=true`
);
await afterAction();
}
);
});
});

View File

@ -48,7 +48,7 @@ export class SubDomain {
responseData: ResponseDataType = {} as ResponseDataType;
constructor(domain: Domain | SubDomain, name?: string) {
this.data.parent = domain.data.name;
this.data.parent = domain.data.fullyQualifiedName;
this.data.name = name ?? this.data.name;
// eslint-disable-next-line no-useless-escape
this.data.fullyQualifiedName = `\"${this.data.parent}\".\"${this.data.name}\"`;

View File

@ -137,14 +137,23 @@ export const selectSubDomain = async (
if (!isSelected) {
const subDomainRes = page.waitForResponse(
'/api/v1/search/query?q=*&from=0&size=50&index=domain_search_index&deleted=false'
'/api/v1/search/query?q=*&from=0&size=0&index=domain_search_index&deleted=false&track_total_hits=true'
);
await menuItem.click();
await subDomainRes;
await page.waitForLoadState('networkidle');
}
const subDomainRes = page.waitForResponse(
'/api/v1/search/query?q=*&from=0&size=50&index=domain_search_index&deleted=false&track_total_hits=true'
);
await page.getByTestId('subdomains').getByText('Sub Domains').click();
await subDomainRes;
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
await page.getByTestId(subDomain.name).click();
await page.waitForLoadState('networkidle');
};

View File

@ -44,11 +44,7 @@ import { AssetsTabRef } from '../../../components/Glossary/GlossaryTerms/tabs/As
import { AssetsOfEntity } from '../../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface';
import EntityNameModal from '../../../components/Modals/EntityNameModal/EntityNameModal.component';
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import {
DE_ACTIVE_COLOR,
ERROR_MESSAGE,
PAGE_SIZE_LARGE,
} from '../../../constants/constants';
import { DE_ACTIVE_COLOR, ERROR_MESSAGE } from '../../../constants/constants';
import { EntityField } from '../../../constants/Feeds.constants';
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
import {
@ -71,7 +67,6 @@ import { addDataProducts } from '../../../rest/dataProductAPI';
import { addDomains } from '../../../rest/domainAPI';
import { searchData } from '../../../rest/miscAPI';
import { searchQuery } from '../../../rest/searchAPI';
import { formatDomainsResponse } from '../../../utils/APIUtils';
import { getIsErrorMatch } from '../../../utils/CommonUtils';
import {
checkIfExpandViewSupported,
@ -152,9 +147,7 @@ const DomainDetailsPage = ({
useState<EntityDetailsObjectInterface>();
const [assetCount, setAssetCount] = useState<number>(0);
const [dataProductsCount, setDataProductsCount] = useState<number>(0);
const [subDomains, setSubDomains] = useState<Domain[]>([]);
const [isSubDomainsLoading, setIsSubDomainsLoading] =
useState<boolean>(false);
const [subDomainsCount, setSubDomainsCount] = useState<number>(0);
const encodedFqn = getEncodedFqn(
escapeESReservedCharacters(domain.fullyQualifiedName)
);
@ -245,32 +238,31 @@ const DomainDetailsPage = ({
: []),
];
const fetchSubDomains = useCallback(async () => {
const fetchSubDomainsCount = useCallback(async () => {
if (!isVersionsView) {
try {
setIsSubDomainsLoading(true);
const res = await searchData(
'',
1,
PAGE_SIZE_LARGE,
0,
0,
`(parent.fullyQualifiedName:"${encodedFqn}")`,
'',
'',
SearchIndex.DOMAIN
SearchIndex.DOMAIN,
false,
true
);
const data = formatDomainsResponse(res.data.hits.hits);
setSubDomains(data);
const totalCount = res.data.hits.total.value ?? 0;
setSubDomainsCount(totalCount);
} catch (error) {
setSubDomains([]);
setSubDomainsCount(0);
showErrorToast(
error as AxiosError,
t('server.entity-fetch-error', {
entity: t('label.sub-domain-lowercase'),
})
);
} finally {
setIsSubDomainsLoading(false);
}
}
}, [isVersionsView, encodedFqn]);
@ -284,7 +276,7 @@ const DomainDetailsPage = ({
try {
await addDomains(data as CreateDomain);
fetchSubDomains();
fetchSubDomainsCount();
} catch (error) {
showErrorToast(
getIsErrorMatch(error as AxiosError, ERROR_MESSAGE.alreadyExist)
@ -302,7 +294,7 @@ const DomainDetailsPage = ({
setShowAddSubDomainModal(false);
}
},
[domain, fetchSubDomains]
[domain, fetchSubDomainsCount]
);
const addDataProduct = useCallback(
@ -559,12 +551,11 @@ const DomainDetailsPage = ({
domain,
isVersionsView,
domainPermission,
subDomains,
subDomainsCount,
dataProductsCount,
assetCount,
activeTab,
onAddDataProduct,
isSubDomainsLoading,
queryFilter,
assetTabRef,
dataProductsTabRef,
@ -593,8 +584,7 @@ const DomainDetailsPage = ({
assetCount,
dataProductsCount,
activeTab,
subDomains,
isSubDomainsLoading,
subDomainsCount,
queryFilter,
customizedPage?.tabs,
]);
@ -606,8 +596,8 @@ const DomainDetailsPage = ({
}, [domain.fullyQualifiedName]);
useEffect(() => {
fetchSubDomains();
}, [domainFqn]);
fetchSubDomainsCount();
}, [domainFqn, fetchSubDomainsCount]);
const iconData = useMemo(() => {
if (domain.style?.iconURL) {

View File

@ -11,15 +11,26 @@
* limitations under the License.
*/
import { ColumnsType } from 'antd/lib/table';
import { AxiosError } from 'axios';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { PAGE_SIZE_LARGE } from '../../../constants/constants';
import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum';
import { SearchIndex } from '../../../enums/search.enum';
import { Domain } from '../../../generated/entity/domains/domain';
import { usePaging } from '../../../hooks/paging/usePaging';
import { searchData } from '../../../rest/miscAPI';
import { formatDomainsResponse } from '../../../utils/APIUtils';
import { getEntityName } from '../../../utils/EntityUtils';
import { getDomainDetailsPath } from '../../../utils/RouterUtils';
import {
escapeESReservedCharacters,
getEncodedFqn,
} from '../../../utils/StringsUtils';
import { ownerTableObject } from '../../../utils/TableColumn.util';
import { showErrorToast } from '../../../utils/ToastUtils';
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
import Loader from '../../common/Loader/Loader';
import RichTextEditorPreviewerNew from '../../common/RichTextEditor/RichTextEditorPreviewNew';
@ -27,12 +38,93 @@ import Table from '../../common/Table/Table';
import { SubDomainsTableProps } from './SubDomainsTable.interface';
const SubDomainsTable = ({
subDomains = [],
isLoading = false,
domainFqn,
permissions,
onAddSubDomain,
}: SubDomainsTableProps) => {
const [subDomains, setSubDomains] = useState<Domain[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const { t } = useTranslation();
const encodedFqn = getEncodedFqn(escapeESReservedCharacters(domainFqn));
const {
currentPage,
pageSize,
paging,
handlePagingChange,
handlePageChange,
showPagination,
} = usePaging(PAGE_SIZE_LARGE);
const fetchSubDomains = useCallback(async () => {
try {
setIsLoading(true);
const res = await searchData(
'',
currentPage,
pageSize,
`(parent.fullyQualifiedName:"${encodedFqn}")`,
'',
'',
SearchIndex.DOMAIN,
false,
true
);
const data = formatDomainsResponse(res.data.hits.hits);
const totalCount = res.data.hits.total.value ?? 0;
setSubDomains(data);
handlePagingChange({
total: totalCount,
});
} catch (error) {
setSubDomains([]);
handlePagingChange({ total: 0 });
showErrorToast(
error as AxiosError,
t('server.entity-fetch-error', {
entity: t('label.sub-domain-lowercase'),
})
);
} finally {
setIsLoading(false);
}
}, [encodedFqn, t, currentPage, pageSize, handlePagingChange]);
const handlePagingClick = useCallback(
(params: { currentPage: number }) => {
handlePageChange(params.currentPage);
},
[handlePageChange]
);
const customPaginationProps = useMemo(
() => ({
currentPage,
pageSize,
paging,
pagingHandler: handlePagingClick,
showPagination,
isNumberBased: true,
isLoading,
}),
[
currentPage,
pageSize,
paging,
handlePagingClick,
showPagination,
isLoading,
]
);
useEffect(() => {
if (domainFqn) {
fetchSubDomains();
}
}, [domainFqn, fetchSubDomains]);
const columns: ColumnsType<Domain> = useMemo(() => {
const data = [
@ -99,6 +191,7 @@ const SubDomainsTable = ({
<Table
columns={columns}
containerClassName="m-md"
customPaginationProps={customPaginationProps}
dataSource={subDomains}
pagination={false}
rowKey="fullyQualifiedName"

View File

@ -12,11 +12,9 @@
*/
import { OperationPermission } from '../../../context/PermissionProvider/PermissionProvider.interface';
import { Domain } from '../../../generated/entity/domains/domain';
export interface SubDomainsTableProps {
subDomains?: Domain[];
isLoading?: boolean;
domainFqn: string;
permissions: OperationPermission;
onAddSubDomain: () => void;
}

View File

@ -35,13 +35,12 @@ export interface DomainDetailPageTabProps {
domain: Domain;
isVersionsView: boolean;
domainPermission: OperationPermission;
subDomains: Domain[];
subDomainsCount: number;
dataProductsCount: number;
assetCount: number;
activeTab: EntityTabs;
onAddDataProduct: () => void;
onAddSubDomain: (subDomain: CreateDomain) => Promise<void>;
isSubDomainsLoading: boolean;
queryFilter?: string | Record<string, unknown>;
assetTabRef: React.RefObject<AssetsTabRef>;
dataProductsTabRef: React.RefObject<DataProductsTabRef>;

View File

@ -357,12 +357,11 @@ export const getDomainDetailTabs = ({
domain,
isVersionsView,
domainPermission,
subDomains,
subDomainsCount,
dataProductsCount,
assetCount,
activeTab,
onAddDataProduct,
isSubDomainsLoading,
queryFilter,
assetTabRef,
dataProductsTabRef,
@ -389,7 +388,7 @@ export const getDomainDetailTabs = ({
{
label: (
<TabsLabel
count={subDomains.length ?? 0}
count={subDomainsCount ?? 0}
id={EntityTabs.SUBDOMAINS}
isActive={activeTab === EntityTabs.SUBDOMAINS}
name={t('label.sub-domain-plural')}
@ -398,9 +397,8 @@ export const getDomainDetailTabs = ({
key: EntityTabs.SUBDOMAINS,
children: (
<SubDomainsTable
isLoading={isSubDomainsLoading}
domainFqn={domain.fullyQualifiedName ?? ''}
permissions={domainPermission}
subDomains={subDomains}
onAddSubDomain={() => setShowAddSubDomainModal(true)}
/>
),