From a239cfb88415f6c46369254f9c4dae9227e52df7 Mon Sep 17 00:00:00 2001 From: Karan Hotchandani <33024356+karanh37@users.noreply.github.com> Date: Mon, 11 Aug 2025 16:33:48 +0530 Subject: [PATCH] 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 --- .../e2e/Pages/SubDomainPagination.spec.ts | 161 ++++++++++++++++++ .../ui/playwright/support/domain/SubDomain.ts | 2 +- .../resources/ui/playwright/utils/domain.ts | 11 +- .../DomainDetailsPage.component.tsx | 44 ++--- .../SubDomainsTable.component.tsx | 99 ++++++++++- .../SubDomainsTable.interface.ts | 4 +- .../ui/src/utils/Domain/DomainClassBase.ts | 3 +- .../resources/ui/src/utils/DomainUtils.tsx | 8 +- 8 files changed, 290 insertions(+), 42 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SubDomainPagination.spec.ts diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SubDomainPagination.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SubDomainPagination.spec.ts new file mode 100644 index 00000000000..83c0c046d81 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SubDomainPagination.spec.ts @@ -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(); + } + ); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/domain/SubDomain.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/domain/SubDomain.ts index e1387f38d39..887beb44929 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/domain/SubDomain.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/domain/SubDomain.ts @@ -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}\"`; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts index 3079088b54d..02bfec66c91 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts @@ -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'); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx index 68b04d5020f..4ff7be4efb4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx @@ -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(); const [assetCount, setAssetCount] = useState(0); const [dataProductsCount, setDataProductsCount] = useState(0); - const [subDomains, setSubDomains] = useState([]); - const [isSubDomainsLoading, setIsSubDomainsLoading] = - useState(false); + const [subDomainsCount, setSubDomainsCount] = useState(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) { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx index 6f8db929c16..f8af02d1bd9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx @@ -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([]); + const [isLoading, setIsLoading] = useState(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 = useMemo(() => { const data = [ @@ -99,6 +191,7 @@ const SubDomainsTable = ({ void; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts index 48e23d61439..72f14362d5c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts @@ -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; - isSubDomainsLoading: boolean; queryFilter?: string | Record; assetTabRef: React.RefObject; dataProductsTabRef: React.RefObject; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx index c7345639000..c7954855bc7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx @@ -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: ( setShowAddSubDomainModal(true)} /> ),