feat(results-browser): implement load all functionality for results db
Deploy / Test, build & deploy (push) Successful in 1m59s Details

This commit is contained in:
Binh 2026-09-03 10:25:03 +02:00
parent a5331de771
commit 21b6165b8c
3 changed files with 25 additions and 4 deletions

View File

@ -6,6 +6,7 @@ import { TriangleIcon } from '../../components/TriangleIcon';
const assessorApi = new AssessorApiClient(); const assessorApi = new AssessorApiClient();
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
const MAX_QUERY_PAGE_SIZE = 10000;
type BackendStatus = 'connected' | 'checking' | 'unavailable'; type BackendStatus = 'connected' | 'checking' | 'unavailable';
interface ResultsBrowserProps { interface ResultsBrowserProps {
@ -17,6 +18,7 @@ interface ResultsBrowserProps {
export function ResultsBrowser({ backendStatus, projectId, scanRunIds }: ResultsBrowserProps) { export function ResultsBrowser({ backendStatus, projectId, scanRunIds }: ResultsBrowserProps) {
const [filters, setFilters] = useState<UnifiedResultQuery>({}); const [filters, setFilters] = useState<UnifiedResultQuery>({});
const [limit, setLimit] = useState(PAGE_SIZE); const [limit, setLimit] = useState(PAGE_SIZE);
const [loadAll, setLoadAll] = useState(false);
const [facetResult, setFacetResult] = useState<UnifiedResultFacetResult | null>(null); const [facetResult, setFacetResult] = useState<UnifiedResultFacetResult | null>(null);
const [queryResult, setQueryResult] = useState<UnifiedResultQueryResult | null>(null); const [queryResult, setQueryResult] = useState<UnifiedResultQueryResult | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -38,24 +40,35 @@ export function ResultsBrowser({ backendStatus, projectId, scanRunIds }: Results
projectId, projectId,
scanRunIds scanRunIds
}; };
const [facets, results] = await Promise.all([ const [facets, firstPage] = await Promise.all([
assessorApi.queryUnifiedResultFacets(scopedQuery), assessorApi.queryUnifiedResultFacets(scopedQuery),
assessorApi.queryUnifiedResults({ ...scopedQuery, offset: 0, limit }) 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); setFacetResult(facets);
setQueryResult(results); setQueryResult({ ...firstPage, items });
} catch (reason) { } catch (reason) {
setError(reason instanceof Error ? reason.message : 'Results could not be loaded.'); setError(reason instanceof Error ? reason.message : 'Results could not be loaded.');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [backendStatus, projectId, scanRunIds, filters, limit]); }, [backendStatus, projectId, scanRunIds, filters, limit, loadAll]);
useEffect(() => { void load(); }, [load]); useEffect(() => { void load(); }, [load]);
const updateFilters = (next: UnifiedResultQuery) => { const updateFilters = (next: UnifiedResultQuery) => {
setExpandedId(null); setExpandedId(null);
setLimit(PAGE_SIZE); setLimit(PAGE_SIZE);
setLoadAll(false);
setFilters(next); setFilters(next);
}; };
const processingFacet = facetResult?.facets.find(facet => facet.id === 'processingStatus'); const processingFacet = facetResult?.facets.find(facet => facet.id === 'processingStatus');
@ -100,7 +113,10 @@ export function ResultsBrowser({ backendStatus, projectId, scanRunIds }: Results
expanded={expandedId === item.resultProjectionId} expanded={expandedId === item.resultProjectionId}
onToggle={() => setExpandedId(current => current === item.resultProjectionId ? null : item.resultProjectionId)}/>)} onToggle={() => setExpandedId(current => current === item.resultProjectionId ? null : item.resultProjectionId)}/>)}
</div>} </div>}
{queryResult && queryResult.total > queryResult.items.length && <button className="btn" type="button" disabled={loading} onClick={() => setLimit(current => current + PAGE_SIZE)}>Load more</button>} {queryResult && queryResult.total > queryResult.items.length && <div className="results-browser-load-actions">
<button className="btn" type="button" disabled={loading} onClick={() => setLimit(current => current + PAGE_SIZE)}>Load more</button>
<button className="btn" type="button" disabled={loading} onClick={() => loadAll ? void load() : setLoadAll(true)}>Load all</button>
</div>}
</div> </div>
</div>} </div>}
</div>; </div>;

View File

@ -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 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-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-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) { @media (max-width: 1150px) {
.results-browser-layout { grid-template-columns: 1fr; } .results-browser-layout { grid-template-columns: 1fr; }
.results-filter-panel { position: static; max-height: none; } .results-filter-panel { position: static; max-height: none; }

View File

@ -15,6 +15,10 @@ describe('Result DB timeslot scope', () => {
expect(browserSource).toMatch(/scanRunIds\r?\n };/); expect(browserSource).toMatch(/scanRunIds\r?\n };/);
expect(browserSource).toContain('queryUnifiedResultFacets(scopedQuery)'); expect(browserSource).toContain('queryUnifiedResultFacets(scopedQuery)');
expect(browserSource).toContain('queryUnifiedResults({ ...scopedQuery, offset: 0, limit })'); 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</button>');
expect(browserSource).toContain('>Load all</button>');
expect(browserSource).toContain("item.sourceKind === 'environment-observation'"); expect(browserSource).toContain("item.sourceKind === 'environment-observation'");
expect(browserSource).toContain('item.environmentNormalizedValue'); expect(browserSource).toContain('item.environmentNormalizedValue');
expect(browserSource).toContain('item.environmentOriginalValue'); expect(browserSource).toContain('item.environmentOriginalValue');