From a79b542d657f8baa33d217a9aef426f2d90ef4c3 Mon Sep 17 00:00:00 2001 From: Chris Collins Date: Thu, 21 Aug 2025 12:18:47 -0400 Subject: [PATCH] fix(ui) Prevent ever searching past the 10k asset boundary (#14437) --- .../src/app/searchV2/SearchPage.tsx | 6 +- .../src/app/searchV2/SearchResults.tsx | 5 +- .../utils/__tests__/searchUtils.test.ts | 106 ++++++++++++++++++ .../src/app/searchV2/utils/searchUtils.ts | 10 ++ 4 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 datahub-web-react/src/app/searchV2/utils/__tests__/searchUtils.test.ts create mode 100644 datahub-web-react/src/app/searchV2/utils/searchUtils.ts diff --git a/datahub-web-react/src/app/searchV2/SearchPage.tsx b/datahub-web-react/src/app/searchV2/SearchPage.tsx index 2cce0385b6..e8a9b15dfa 100644 --- a/datahub-web-react/src/app/searchV2/SearchPage.tsx +++ b/datahub-web-react/src/app/searchV2/SearchPage.tsx @@ -27,6 +27,7 @@ import useGetSearchQueryInputs from '@app/searchV2/useGetSearchQueryInputs'; import { useIsBrowseV2, useIsSearchV2, useSearchVersion } from '@app/searchV2/useSearchAndBrowseVersion'; import { ENTITY_SUB_TYPE_FILTER_FIELDS, UnionType } from '@app/searchV2/utils/constants'; import { navigateToSearchUrl } from '@app/searchV2/utils/navigateToSearchUrl'; +import { getSearchCount } from '@app/searchV2/utils/searchUtils'; import { DownloadSearchResults, DownloadSearchResultsInput } from '@app/searchV2/utils/types'; import { useDownloadScrollAcrossEntitiesSearchResults } from '@app/searchV2/utils/useDownloadScrollAcrossEntitiesSearchResults'; import { scrollToTop } from '@app/shared/searchUtils'; @@ -58,6 +59,7 @@ export const SearchPage = () => { const [numResultsPerPage, setNumResultsPerPage] = useState(SearchCfg.RESULTS_PER_PAGE); const [isSelectMode, setIsSelectMode] = useState(false); const [selectedEntities, setSelectedEntities] = useState([]); + const start = (page - 1) * numResultsPerPage; const { data, @@ -69,8 +71,8 @@ export const SearchPage = () => { input: { types: [], query, - start: (page - 1) * numResultsPerPage, - count: numResultsPerPage, + start, + count: getSearchCount(start, numResultsPerPage), filters: [], orFilters, viewUrn, diff --git a/datahub-web-react/src/app/searchV2/SearchResults.tsx b/datahub-web-react/src/app/searchV2/SearchResults.tsx index 43e98f5826..fb059d5b5e 100644 --- a/datahub-web-react/src/app/searchV2/SearchResults.tsx +++ b/datahub-web-react/src/app/searchV2/SearchResults.tsx @@ -172,9 +172,8 @@ export const SearchResults = ({ const showSearchFiltersV2 = useIsSearchV2(); const showBrowseV2 = useIsBrowseV2(); const pageStart = searchResponse?.start || 0; - const pageSize = searchResponse?.count || 0; const totalResults = searchResponse?.total || 0; - const lastResultIndex = pageStart + pageSize > totalResults ? totalResults : pageStart + pageSize; + const lastResultIndex = pageStart + numResultsPerPage > totalResults ? totalResults : pageStart + numResultsPerPage; const showSeparateSiblings = useIsShowSeparateSiblingsEnabled(); const isShowNavBarRedesign = useShowNavBarRedesign(); const combinedSiblingSearchResults = combineSiblingsInSearchResults( @@ -253,7 +252,7 @@ export const SearchResults = ({ Showing{' '} - {lastResultIndex > 0 ? (page - 1) * pageSize + 1 : 0} -{' '} + {lastResultIndex > 0 ? (page - 1) * numResultsPerPage + 1 : 0} -{' '} {lastResultIndex} {' '} of{' '} diff --git a/datahub-web-react/src/app/searchV2/utils/__tests__/searchUtils.test.ts b/datahub-web-react/src/app/searchV2/utils/__tests__/searchUtils.test.ts new file mode 100644 index 0000000000..a4289c86de --- /dev/null +++ b/datahub-web-react/src/app/searchV2/utils/__tests__/searchUtils.test.ts @@ -0,0 +1,106 @@ +import { MAX_COUNT_VAL } from '@app/searchV2/utils/constants'; +import { getSearchCount } from '@app/searchV2/utils/searchUtils'; + +describe('getSearchCount', () => { + it('should return numResultsPerPage when start + numResultsPerPage is less than MAX_COUNT_VAL', () => { + const start = 0; + const numResultsPerPage = 20; + + const result = getSearchCount(start, numResultsPerPage); + + expect(result).toBe(numResultsPerPage); + }); + + it('should return numResultsPerPage when start + numResultsPerPage equals MAX_COUNT_VAL', () => { + const start = 9980; + const numResultsPerPage = 20; + + const result = getSearchCount(start, numResultsPerPage); + + expect(result).toBe(numResultsPerPage); + }); + + it('should return adjusted count when start + numResultsPerPage exceeds MAX_COUNT_VAL', () => { + const start = 9990; + const numResultsPerPage = 20; + const expectedCount = MAX_COUNT_VAL - start; // 10000 - 9990 = 10 + + const result = getSearchCount(start, numResultsPerPage); + + expect(result).toBe(expectedCount); + }); + + it('should return 0 when start equals MAX_COUNT_VAL', () => { + const start = MAX_COUNT_VAL; + const numResultsPerPage = 20; + + const result = getSearchCount(start, numResultsPerPage); + + expect(result).toBe(0); + }); + + it('should handle edge case when start is close to MAX_COUNT_VAL', () => { + const start = 9999; + const numResultsPerPage = 50; + const expectedCount = MAX_COUNT_VAL - start; // 10000 - 9999 = 1 + + const result = getSearchCount(start, numResultsPerPage); + + expect(result).toBe(expectedCount); + }); + + it('should handle typical pagination scenarios', () => { + // First page + expect(getSearchCount(0, 10)).toBe(10); + + // Second page + expect(getSearchCount(10, 10)).toBe(10); + + // Page 100 + expect(getSearchCount(990, 10)).toBe(10); + + // Page 1000 (near the limit) + expect(getSearchCount(9990, 10)).toBe(10); + }); + + it('should handle large numResultsPerPage values', () => { + const start = 0; + const numResultsPerPage = 1000; + + const result = getSearchCount(start, numResultsPerPage); + + expect(result).toBe(numResultsPerPage); + }); + + it('should handle numResultsPerPage of 1', () => { + const start = 9999; + const numResultsPerPage = 1; + + const result = getSearchCount(start, numResultsPerPage); + + expect(result).toBe(1); + }); + + it('should handle when start is negative (edge case)', () => { + const start = -5; + const numResultsPerPage = 20; + + const result = getSearchCount(start, numResultsPerPage); + + // Should still return numResultsPerPage since -5 + 20 = 15 < MAX_COUNT_VAL + expect(result).toBe(numResultsPerPage); + }); + + it('should use MAX_COUNT_VAL constant correctly', () => { + // Verify our understanding of MAX_COUNT_VAL + expect(MAX_COUNT_VAL).toBe(10000); + + // Test right at the boundary + const start = MAX_COUNT_VAL - 1; + const numResultsPerPage = 5; + + const result = getSearchCount(start, numResultsPerPage); + + expect(result).toBe(1); // MAX_COUNT_VAL - (MAX_COUNT_VAL - 1) = 1 + }); +}); diff --git a/datahub-web-react/src/app/searchV2/utils/searchUtils.ts b/datahub-web-react/src/app/searchV2/utils/searchUtils.ts new file mode 100644 index 0000000000..3182d126ae --- /dev/null +++ b/datahub-web-react/src/app/searchV2/utils/searchUtils.ts @@ -0,0 +1,10 @@ +import { MAX_COUNT_VAL } from '@app/searchV2/utils/constants'; + +// we can't ask for more than the max (10,000) so calculate count to ask for up to but no more than the max +export function getSearchCount(start: number, numResultsPerPage: number) { + let count = numResultsPerPage; + if (start + numResultsPerPage > MAX_COUNT_VAL) { + count = MAX_COUNT_VAL - start; + } + return count; +}