mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-04 05:16:47 +00:00
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:
parent
b725bee3c7
commit
a239cfb884
@ -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();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
@ -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}\"`;
|
||||
|
@ -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');
|
||||
};
|
||||
|
@ -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) {
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>;
|
||||
|
@ -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)}
|
||||
/>
|
||||
),
|
||||
|
Loading…
x
Reference in New Issue
Block a user