Fix(ui): glossary term pagination issues (#23375)

* fixed glossary term pagination issues

* fixed expand all recursive api calls

* removed unused code

* fixed expand all button

* fixed glossary tests
This commit is contained in:
Dhruv Parmar 2025-09-17 06:01:52 +05:30 committed by GitHub
parent c2fc241228
commit 73eb1fd0d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 132 additions and 202 deletions

View File

@ -1029,9 +1029,7 @@ test.describe('Glossary tests', () => {
await selectActiveGlossary(page, glossary2.data.displayName);
const termRes = page.waitForResponse(
'/api/v1/glossaryTerms?directChildrenOf=*&fields=childrenCount%2Cowners%2Creviewers&limit=1000'
);
const termRes = page.waitForResponse('/api/v1/glossaryTerms?*');
// verify the term is moved to the destination glossary
await page.getByTestId('expand-collapse-all-button').click();
await termRes;
@ -1244,9 +1242,7 @@ test.describe('Glossary tests', () => {
await selectActiveGlossaryTerm(page, glossaryTerm1.data.displayName);
await page.getByTestId('terms').click();
const termRes = page.waitForResponse(
'/api/v1/glossaryTerms?directChildrenOf=*&fields=childrenCount%2Cowners%2Creviewers&limit=1000'
);
const termRes = page.waitForResponse('/api/v1/glossaryTerms?*');
await page.getByTestId('expand-collapse-all-button').click();
await termRes;
@ -1440,9 +1436,7 @@ test.describe('Glossary tests', () => {
await sidebarClick(page, SidebarItem.GLOSSARY);
await selectActiveGlossary(page, glossary1.data.displayName);
const termRes = page.waitForResponse(
'/api/v1/glossaryTerms?directChildrenOf=*&fields=childrenCount%2Cowners%2Creviewers&limit=1000'
);
const termRes = page.waitForResponse('/api/v1/glossaryTerms?*');
await page.getByTestId('expand-collapse-all-button').click();
await termRes;

View File

@ -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 } from '../../../enums/entity.enum';
import { EntityType, TabSpecificField } from '../../../enums/entity.enum';
import { ResolveTask } from '../../../generated/api/feed/resolveTask';
import {
EntityReference,
@ -83,6 +83,7 @@ import { getAllFeeds, updateTask } from '../../../rest/feedsAPI';
import {
getFirstLevelGlossaryTermsPaginated,
getGlossaryTermChildrenLazy,
getGlossaryTerms,
GlossaryTermWithChildren,
patchGlossaryTerm,
searchGlossaryTermsPaginated,
@ -95,8 +96,8 @@ import {
} from '../../../utils/EntityUtils';
import Fqn from '../../../utils/Fqn';
import {
buildTree,
findExpandableKeysForArray,
getAllExpandableKeys,
glossaryTermTableColumnsWidth,
permissionForApproveOrReject,
StatusClass,
@ -176,7 +177,13 @@ 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,
@ -192,7 +199,6 @@ 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
@ -239,7 +245,11 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => {
const fetchAllTerms = async (loadMore = false) => {
if (!loadMore) {
setIsTableLoading(true);
handlePagingChange((prev) => ({ ...prev, after: undefined }));
if (searchTerm) {
setSearchPaging({ offset: 0, total: undefined, hasMore: true });
} else {
handlePagingChange((prev) => ({ ...prev, after: undefined }));
}
} else {
setIsLoadingMore(true);
}
@ -250,7 +260,7 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => {
// Use search API if search term is present
if (searchTerm) {
const offset = loadMore && paging.after ? parseInt(paging.after) : 0;
const currentOffset = loadMore ? searchPaging.offset : 0;
const response = await searchGlossaryTermsPaginated(
searchTerm,
undefined,
@ -258,11 +268,23 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => {
undefined,
undefined,
PAGE_SIZE_LARGE,
offset,
currentOffset,
'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(
@ -272,6 +294,13 @@ 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)) {
@ -291,13 +320,6 @@ 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 {
@ -306,6 +328,32 @@ 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;
@ -355,14 +403,24 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => {
useEffect(() => {
const currentFQN = activeGlossary?.fullyQualifiedName;
if (currentFQN && !isLoadingMore && currentFQN !== previousGlossaryFQN) {
if (
currentFQN &&
!isLoadingMore &&
currentFQN !== previousGlossaryFQN &&
!toggleExpandBtn
) {
// Clear existing terms when switching glossaries
setGlossaryChildTerms([]);
handlePagingChange((prev) => ({ ...prev, after: undefined }));
setPreviousGlossaryFQN(currentFQN);
fetchAllTerms();
}
}, [activeGlossary?.fullyQualifiedName, isLoadingMore, previousGlossaryFQN]);
}, [
activeGlossary?.fullyQualifiedName,
isLoadingMore,
previousGlossaryFQN,
toggleExpandBtn,
]);
// Clear terms when component unmounts
useEffect(() => {
@ -400,21 +458,45 @@ 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 &&
paging.after !== undefined &&
canLoadMore &&
!isLoadingMore &&
!isTableLoading
!isTableLoading &&
!toggleExpandBtn
) {
fetchAllTerms(true);
}
}, [inView, paging.after, isLoadingMore, isTableLoading]);
}, [
inView,
paging.after,
searchPaging.hasMore,
searchTerm,
isLoadingMore,
isTableLoading,
toggleExpandBtn,
]);
// Monitor for DOM changes to detect when the table becomes scrollable
useEffect(() => {
const observer = new MutationObserver(() => {
const scrollContainer = findScrollContainer();
if (scrollContainer && paging.after !== undefined && !isLoadingMore) {
// 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
) {
const { scrollHeight, clientHeight } = scrollContainer;
// If content doesn't fill the viewport, load more
if (scrollHeight <= clientHeight + 10) {
@ -435,17 +517,30 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => {
}
return () => observer.disconnect();
}, [paging.after, isLoadingMore, findScrollContainer]);
}, [
paging.after,
searchPaging.hasMore,
searchTerm,
isLoadingMore,
findScrollContainer,
toggleExpandBtn,
]);
// 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 &&
paging.after !== undefined &&
canLoadMore &&
!isLoadingMore &&
!isTableLoading
!isTableLoading &&
!toggleExpandBtn
) {
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
// Load more when user is 200px from the bottom
@ -471,6 +566,8 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => {
return undefined;
}, [
paging.after,
searchPaging.hasMore,
searchTerm,
isLoadingMore,
isTableLoading,
findScrollContainer,
@ -826,157 +923,13 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => {
};
const toggleExpandAll = useCallback(async () => {
if (isExpandingAll) {
return; // Prevent multiple simultaneous expand operations
}
setToggleExpandBtn((prev) => !prev);
if (expandedRowKeys.length === expandableKeys.length) {
// Collapse all - immediate UI update
setExpandedRowKeys([]);
fetchAllTerms();
} else {
setIsExpandingAll(true);
try {
// Recursive function to load all children at all levels
const loadAllChildrenRecursively = async (
terms: ModifiedGlossary[],
depth = 0,
maxDepth = 10
): Promise<ModifiedGlossary[]> => {
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<string, GlossaryTermWithChildren[]> = {};
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);
}
fetchExpadedTree();
}
}, [
glossaryTerms,
@ -986,10 +939,8 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => {
setLoadingChildren,
expandedRowKeys,
expandableKeys,
setIsExpandingAll,
setExpandedRowKeys,
showErrorToast,
isExpandingAll,
]);
const isAllExpanded = useMemo(() => {
@ -1111,7 +1062,6 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => {
<Button
className="text-primary remove-button-background-hover"
data-testid="expand-collapse-all-button"
disabled={isExpandingAll}
size="small"
type="text"
onClick={toggleExpandAll}>
@ -1316,6 +1266,10 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => {
// Trigger new fetch when search term changes
useEffect(() => {
if (activeGlossary) {
// Reset search pagination when search term changes
if (searchTerm) {
setSearchPaging({ offset: 0, total: undefined, hasMore: true });
}
fetchAllTerms();
}
}, [searchTerm, activeGlossary]);
@ -1382,7 +1336,9 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => {
onHeaderRow={onTableHeader}
onRow={onTableRow}
/>
{paging.after !== undefined && (
{/* Show infinite scroll trigger if there are more results */}
{((!searchTerm && paging.after !== undefined) ||
(searchTerm && searchPaging.hasMore)) && (
<div
className="m-t-md m-b-md text-center p-y-lg"
ref={infiniteScrollRef}

View File

@ -356,26 +356,6 @@ export type GlossaryTermWithChildren = Omit<GlossaryTerm, 'children'> & {
children?: GlossaryTerm[];
};
export const getFirstLevelGlossaryTerms = async (parentFQN: string) => {
const apiUrl = `/glossaryTerms`;
const { data } = await APIClient.get<
PagingResponse<GlossaryTermWithChildren[]>
>(apiUrl, {
params: {
directChildrenOf: parentFQN,
fields: [
TabSpecificField.CHILDREN_COUNT,
TabSpecificField.OWNERS,
TabSpecificField.REVIEWERS,
],
limit: 100000,
},
});
return data;
};
export const getFirstLevelGlossaryTermsPaginated = async (
parentFQN: string,
pageSize = 50,