diff --git a/src/features/results-browser/ResultsBrowser.tsx b/src/features/results-browser/ResultsBrowser.tsx index b269326..5386319 100644 --- a/src/features/results-browser/ResultsBrowser.tsx +++ b/src/features/results-browser/ResultsBrowser.tsx @@ -6,6 +6,7 @@ import { TriangleIcon } from '../../components/TriangleIcon'; const assessorApi = new AssessorApiClient(); const PAGE_SIZE = 50; +const MAX_QUERY_PAGE_SIZE = 10000; type BackendStatus = 'connected' | 'checking' | 'unavailable'; interface ResultsBrowserProps { @@ -17,6 +18,7 @@ interface ResultsBrowserProps { export function ResultsBrowser({ backendStatus, projectId, scanRunIds }: ResultsBrowserProps) { const [filters, setFilters] = useState({}); const [limit, setLimit] = useState(PAGE_SIZE); + const [loadAll, setLoadAll] = useState(false); const [facetResult, setFacetResult] = useState(null); const [queryResult, setQueryResult] = useState(null); const [loading, setLoading] = useState(true); @@ -38,24 +40,35 @@ export function ResultsBrowser({ backendStatus, projectId, scanRunIds }: Results projectId, scanRunIds }; - const [facets, results] = await Promise.all([ + const [facets, firstPage] = await Promise.all([ assessorApi.queryUnifiedResultFacets(scopedQuery), assessorApi.queryUnifiedResults({ ...scopedQuery, offset: 0, limit }) ]); + const items = [...firstPage.items]; + while (loadAll && items.length < firstPage.total) { + const nextPage = await assessorApi.queryUnifiedResults({ + ...scopedQuery, + offset: items.length, + limit: MAX_QUERY_PAGE_SIZE + }); + if (!nextPage.items.length) break; + items.push(...nextPage.items); + } setFacetResult(facets); - setQueryResult(results); + setQueryResult({ ...firstPage, items }); } catch (reason) { setError(reason instanceof Error ? reason.message : 'Results could not be loaded.'); } finally { setLoading(false); } - }, [backendStatus, projectId, scanRunIds, filters, limit]); + }, [backendStatus, projectId, scanRunIds, filters, limit, loadAll]); useEffect(() => { void load(); }, [load]); const updateFilters = (next: UnifiedResultQuery) => { setExpandedId(null); setLimit(PAGE_SIZE); + setLoadAll(false); setFilters(next); }; const processingFacet = facetResult?.facets.find(facet => facet.id === 'processingStatus'); @@ -100,7 +113,10 @@ export function ResultsBrowser({ backendStatus, projectId, scanRunIds }: Results expanded={expandedId === item.resultProjectionId} onToggle={() => setExpandedId(current => current === item.resultProjectionId ? null : item.resultProjectionId)}/>)} } - {queryResult && queryResult.total > queryResult.items.length && } + {queryResult && queryResult.total > queryResult.items.length &&
+ + +
} } ; diff --git a/src/styles/datahub.css b/src/styles/datahub.css index a2d1a0a..56c09c4 100644 --- a/src/styles/datahub.css +++ b/src/styles/datahub.css @@ -503,6 +503,7 @@ button.client-info-metric[aria-pressed="true"] { border-color: var(--tile-select .results-environment-value strong { color: var(--heading); } .results-environment-value p { margin: var(--legacy-space-1) 0 var(--legacy-space-3); overflow-wrap: anywhere; white-space: pre-wrap; } .results-processing-alert { margin-bottom: var(--legacy-space-3); } +.results-browser-load-actions { display: flex; gap: var(--legacy-space-2); margin-top: var(--legacy-space-3); } @media (max-width: 1150px) { .results-browser-layout { grid-template-columns: 1fr; } .results-filter-panel { position: static; max-height: none; } diff --git a/tests/features/results-browser-timeslot-scope.test.ts b/tests/features/results-browser-timeslot-scope.test.ts index fcf7189..012c6bc 100644 --- a/tests/features/results-browser-timeslot-scope.test.ts +++ b/tests/features/results-browser-timeslot-scope.test.ts @@ -15,6 +15,10 @@ describe('Result DB timeslot scope', () => { expect(browserSource).toMatch(/scanRunIds\r?\n };/); expect(browserSource).toContain('queryUnifiedResultFacets(scopedQuery)'); expect(browserSource).toContain('queryUnifiedResults({ ...scopedQuery, offset: 0, limit })'); + expect(browserSource).toContain('while (loadAll && items.length < firstPage.total)'); + expect(browserSource).toContain('limit: MAX_QUERY_PAGE_SIZE'); + expect(browserSource).toContain('>Load more'); + expect(browserSource).toContain('>Load all'); expect(browserSource).toContain("item.sourceKind === 'environment-observation'"); expect(browserSource).toContain('item.environmentNormalizedValue'); expect(browserSource).toContain('item.environmentOriginalValue');