diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 48feb67d51f..4a9df0739be 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -1029,7 +1029,9 @@ test.describe('Glossary tests', () => { await selectActiveGlossary(page, glossary2.data.displayName); - const termRes = page.waitForResponse('/api/v1/glossaryTerms?*'); + const termRes = page.waitForResponse( + '/api/v1/glossaryTerms?directChildrenOf=*&fields=childrenCount%2Cowners%2Creviewers&limit=1000' + ); // verify the term is moved to the destination glossary await page.getByTestId('expand-collapse-all-button').click(); await termRes; @@ -1242,7 +1244,9 @@ test.describe('Glossary tests', () => { await selectActiveGlossaryTerm(page, glossaryTerm1.data.displayName); await page.getByTestId('terms').click(); - const termRes = page.waitForResponse('/api/v1/glossaryTerms?*'); + const termRes = page.waitForResponse( + '/api/v1/glossaryTerms?directChildrenOf=*&fields=childrenCount%2Cowners%2Creviewers&limit=1000' + ); await page.getByTestId('expand-collapse-all-button').click(); await termRes; @@ -1436,7 +1440,9 @@ test.describe('Glossary tests', () => { await sidebarClick(page, SidebarItem.GLOSSARY); await selectActiveGlossary(page, glossary1.data.displayName); - const termRes = page.waitForResponse('/api/v1/glossaryTerms?*'); + const termRes = page.waitForResponse( + '/api/v1/glossaryTerms?directChildrenOf=*&fields=childrenCount%2Cowners%2Creviewers&limit=1000' + ); await page.getByTestId('expand-collapse-all-button').click(); await termRes; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx index 1e31d11b2fa..43861fa8c0f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx @@ -63,7 +63,7 @@ import { } from '../../../constants/Glossary.contant'; import { TABLE_CONSTANTS } from '../../../constants/Teams.constants'; import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum'; -import { EntityType, TabSpecificField } from '../../../enums/entity.enum'; +import { EntityType } from '../../../enums/entity.enum'; import { ResolveTask } from '../../../generated/api/feed/resolveTask'; import { EntityReference, @@ -83,7 +83,6 @@ import { getAllFeeds, updateTask } from '../../../rest/feedsAPI'; import { getFirstLevelGlossaryTermsPaginated, getGlossaryTermChildrenLazy, - getGlossaryTerms, GlossaryTermWithChildren, patchGlossaryTerm, searchGlossaryTermsPaginated, @@ -96,8 +95,8 @@ import { } from '../../../utils/EntityUtils'; import Fqn from '../../../utils/Fqn'; import { - buildTree, findExpandableKeysForArray, + getAllExpandableKeys, glossaryTermTableColumnsWidth, permissionForApproveOrReject, StatusClass, @@ -177,13 +176,7 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { >(undefined); const [searchTerm, setSearchTerm] = useState(''); const [searchInput, setSearchInput] = useState(''); - const [searchPaging, setSearchPaging] = useState<{ - offset: number; - total?: number; - hasMore: boolean; - }>({ offset: 0, total: undefined, hasMore: true }); const [isExpandingAll, setIsExpandingAll] = useState(false); - const [toggleExpandBtn, setToggleExpandBtn] = useState(false); const { ref: infiniteScrollRef, inView } = useInView({ threshold: 0.1, @@ -199,6 +192,7 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { const fetchChildTerms = async (parentFQN: string) => { setLoadingChildren((prev) => ({ ...prev, [parentFQN]: true })); + try { const { data } = await getGlossaryTermChildrenLazy(parentFQN, 1000); // Get all children @@ -245,11 +239,7 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { const fetchAllTerms = async (loadMore = false) => { if (!loadMore) { setIsTableLoading(true); - if (searchTerm) { - setSearchPaging({ offset: 0, total: undefined, hasMore: true }); - } else { - handlePagingChange((prev) => ({ ...prev, after: undefined })); - } + handlePagingChange((prev) => ({ ...prev, after: undefined })); } else { setIsLoadingMore(true); } @@ -260,7 +250,7 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { // Use search API if search term is present if (searchTerm) { - const currentOffset = loadMore ? searchPaging.offset : 0; + const offset = loadMore && paging.after ? parseInt(paging.after) : 0; const response = await searchGlossaryTermsPaginated( searchTerm, undefined, @@ -268,23 +258,11 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { undefined, undefined, PAGE_SIZE_LARGE, - currentOffset, + offset, 'children,relatedTerms,reviewers,owners,tags,usageCount,domains,extension,childrenCount' ); data = response.data; pagingResponse = response.paging; - - // Update search pagination state - const newOffset = currentOffset + PAGE_SIZE_LARGE; - const hasMore = - data.length === PAGE_SIZE_LARGE && - (pagingResponse?.total === undefined || - newOffset < pagingResponse?.total); - setSearchPaging({ - offset: newOffset, - total: pagingResponse?.total, - hasMore, - }); } else { // Use regular listing API when no search term const response = await getFirstLevelGlossaryTermsPaginated( @@ -294,13 +272,6 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { ); data = response.data; pagingResponse = response.paging; - - // Update regular paging state for next page - handlePagingChange((prev) => ({ - ...prev, - after: pagingResponse?.after, - total: pagingResponse?.total || prev.total, - })); } if (!data || !Array.isArray(data)) { @@ -320,6 +291,13 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { // Start with all terms collapsed setExpandedRowKeys([]); } + + // Update paging state for next page + handlePagingChange((prev) => ({ + ...prev, + after: pagingResponse?.after, + total: pagingResponse?.total || prev.total, + })); } catch (error) { showErrorToast(error as AxiosError); } finally { @@ -328,32 +306,6 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { } }; - const fetchExpadedTree = async () => { - setIsTableLoading(true); - setIsExpandingAll(true); - const key = isGlossary ? 'glossary' : 'parent'; - const { data } = await getGlossaryTerms({ - [key]: activeGlossary?.id || '', - limit: API_RES_MAX_SIZE, - fields: [ - TabSpecificField.OWNERS, - TabSpecificField.PARENT, - TabSpecificField.CHILDREN, - ], - }); - setGlossaryChildTerms(buildTree(data) as ModifiedGlossary[]); - const keys = data.reduce((prev, curr) => { - if (curr.children?.length) { - prev.push(curr.fullyQualifiedName ?? ''); - } - - return prev; - }, [] as string[]); - - setExpandedRowKeys(keys); - setIsTableLoading(false); - setIsExpandingAll(false); - }; const fetchAllTasks = useCallback(async () => { if (!activeGlossary?.fullyQualifiedName) { return; @@ -403,24 +355,14 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { useEffect(() => { const currentFQN = activeGlossary?.fullyQualifiedName; - if ( - currentFQN && - !isLoadingMore && - currentFQN !== previousGlossaryFQN && - !toggleExpandBtn - ) { + if (currentFQN && !isLoadingMore && currentFQN !== previousGlossaryFQN) { // Clear existing terms when switching glossaries setGlossaryChildTerms([]); handlePagingChange((prev) => ({ ...prev, after: undefined })); setPreviousGlossaryFQN(currentFQN); fetchAllTerms(); } - }, [ - activeGlossary?.fullyQualifiedName, - isLoadingMore, - previousGlossaryFQN, - toggleExpandBtn, - ]); + }, [activeGlossary?.fullyQualifiedName, isLoadingMore, previousGlossaryFQN]); // Clear terms when component unmounts useEffect(() => { @@ -458,45 +400,21 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { }, []); useEffect(() => { - // For search mode, check searchPaging.hasMore; for regular mode, check paging.after - const canLoadMore = searchTerm - ? searchPaging.hasMore - : paging.after !== undefined; - if ( inView && - canLoadMore && + paging.after !== undefined && !isLoadingMore && - !isTableLoading && - !toggleExpandBtn + !isTableLoading ) { fetchAllTerms(true); } - }, [ - inView, - paging.after, - searchPaging.hasMore, - searchTerm, - isLoadingMore, - isTableLoading, - toggleExpandBtn, - ]); + }, [inView, paging.after, isLoadingMore, isTableLoading]); // Monitor for DOM changes to detect when the table becomes scrollable useEffect(() => { const observer = new MutationObserver(() => { const scrollContainer = findScrollContainer(); - // Check if we can load more based on search vs regular mode - const canLoadMore = searchTerm - ? searchPaging.hasMore - : paging.after !== undefined; - - if ( - scrollContainer && - canLoadMore && - !isLoadingMore && - !toggleExpandBtn - ) { + if (scrollContainer && paging.after !== undefined && !isLoadingMore) { const { scrollHeight, clientHeight } = scrollContainer; // If content doesn't fill the viewport, load more if (scrollHeight <= clientHeight + 10) { @@ -517,30 +435,17 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { } return () => observer.disconnect(); - }, [ - paging.after, - searchPaging.hasMore, - searchTerm, - isLoadingMore, - findScrollContainer, - toggleExpandBtn, - ]); + }, [paging.after, isLoadingMore, findScrollContainer]); // Additional scroll handler for parent container useEffect(() => { const handleScroll = (event: Event) => { const scrollContainer = event.target as HTMLElement; - // Check if we can load more based on search vs regular mode - const canLoadMore = searchTerm - ? searchPaging.hasMore - : paging.after !== undefined; - if ( scrollContainer && - canLoadMore && + paging.after !== undefined && !isLoadingMore && - !isTableLoading && - !toggleExpandBtn + !isTableLoading ) { const { scrollTop, scrollHeight, clientHeight } = scrollContainer; // Load more when user is 200px from the bottom @@ -566,8 +471,6 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { return undefined; }, [ paging.after, - searchPaging.hasMore, - searchTerm, isLoadingMore, isTableLoading, findScrollContainer, @@ -923,13 +826,157 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { }; const toggleExpandAll = useCallback(async () => { - setToggleExpandBtn((prev) => !prev); + if (isExpandingAll) { + return; // Prevent multiple simultaneous expand operations + } + if (expandedRowKeys.length === expandableKeys.length) { // Collapse all - immediate UI update setExpandedRowKeys([]); - fetchAllTerms(); } else { - fetchExpadedTree(); + setIsExpandingAll(true); + + try { + // Recursive function to load all children at all levels + const loadAllChildrenRecursively = async ( + terms: ModifiedGlossary[], + depth = 0, + maxDepth = 10 + ): Promise => { + if (depth >= maxDepth) { + return terms; // Prevent infinite recursion + } + + const BATCH_SIZE = 5; + const termsToLoad = terms.filter( + (term) => + term.childrenCount && + term.childrenCount > 0 && + (!term.children || term.children.length === 0) + ); + + if (termsToLoad.length === 0) { + // If no terms need loading at this level, check children + const updatedTerms = await Promise.all( + terms.map(async (term) => { + if (term.children && term.children.length > 0) { + const updatedChildren = await loadAllChildrenRecursively( + term.children as ModifiedGlossary[], + depth + 1, + maxDepth + ); + + return { + ...term, + children: updatedChildren as ModifiedGlossaryTerm[], + }; + } + + return term; + }) + ); + + return updatedTerms; + } + + // Load data for terms at this level + const batches: typeof termsToLoad[] = []; + for (let i = 0; i < termsToLoad.length; i += BATCH_SIZE) { + batches.push(termsToLoad.slice(i, i + BATCH_SIZE)); + } + + const childDataMap: Record = {}; + + for (const batch of batches) { + await Promise.all( + batch.map(async (term) => { + if (term.fullyQualifiedName) { + setLoadingChildren((prev) => ({ + ...prev, + [term.fullyQualifiedName as string]: true, + })); + try { + const { data } = await getGlossaryTermChildrenLazy( + term.fullyQualifiedName, + 1000 // Get all children at once + ); + childDataMap[term.fullyQualifiedName] = data; + } catch (error) { + showErrorToast(error as AxiosError); + } finally { + setLoadingChildren((prev) => ({ + ...prev, + [term.fullyQualifiedName as string]: false, + })); + } + } + }) + ); + // Small delay between batches to keep UI responsive + await new Promise((resolve) => setTimeout(resolve, 50)); + } + + // Update terms with loaded children + const termsWithChildren = terms.map((term) => { + const termFQN = term.fullyQualifiedName; + if (termFQN && childDataMap[termFQN]) { + return { + ...term, + children: childDataMap[termFQN] as ModifiedGlossaryTerm[], + }; + } + + return term; + }); + + // Recursively load children for the newly loaded terms + const fullyLoadedTerms = await Promise.all( + termsWithChildren.map(async (term) => { + if (term.children && term.children.length > 0) { + const updatedChildren = await loadAllChildrenRecursively( + term.children as ModifiedGlossary[], + depth + 1, + maxDepth + ); + + return { + ...term, + children: updatedChildren as ModifiedGlossaryTerm[], + }; + } + + return term; + }) + ); + + return fullyLoadedTerms; + }; + + // Load all children recursively starting from current terms + const currentTerms = glossaryChildTerms; + if (!Array.isArray(currentTerms)) { + setIsExpandingAll(false); + + return; + } + + const fullyExpandedTerms = await loadAllChildrenRecursively( + currentTerms + ); + + // Update the glossary child terms with fully expanded tree + setGlossaryChildTerms(fullyExpandedTerms); + + // Get all expandable keys from the fully loaded tree + const allExpandableKeys = getAllExpandableKeys(fullyExpandedTerms); + + // Set all keys as expanded + setExpandedRowKeys(allExpandableKeys); + } catch (error) { + showErrorToast(error as AxiosError); + } finally { + setIsExpandingAll(false); + } } }, [ glossaryTerms, @@ -939,8 +986,10 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { setLoadingChildren, expandedRowKeys, expandableKeys, + setIsExpandingAll, setExpandedRowKeys, showErrorToast, + isExpandingAll, ]); const isAllExpanded = useMemo(() => { @@ -1062,6 +1111,7 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => {