feat(results-browser): implement load all functionality for results db

This commit is contained in:
Binh 2026-09-03 10:25:03 +02:00
parent 614ac1b147
commit 1c01118956
3 changed files with 25 additions and 4 deletions

View File

@ -19,6 +19,7 @@ import type { ResultDbMaterializationStatus } from '../../domain/result-db-mater
const assessorApi = new AssessorApiClient(); const assessorApi = new AssessorApiClient();
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
const MAX_QUERY_PAGE_SIZE = 10000;
export const DEFAULT_RESULT_DB_FILTERS: UnifiedResultQuery = { deviation: true }; export const DEFAULT_RESULT_DB_FILTERS: UnifiedResultQuery = { deviation: true };
type BackendStatus = 'connected' | 'checking' | 'unavailable'; type BackendStatus = 'connected' | 'checking' | 'unavailable';
@ -35,6 +36,7 @@ interface ResultsBrowserProps {
export function ResultsBrowser({ backendStatus, projectId, scanRunIds, assessmentIds, workRecordIds, onOpenInWork }: ResultsBrowserProps) { export function ResultsBrowser({ backendStatus, projectId, scanRunIds, assessmentIds, workRecordIds, onOpenInWork }: ResultsBrowserProps) {
const [filters, setFilters] = useState<UnifiedResultQuery>(() => ({ ...DEFAULT_RESULT_DB_FILTERS })); const [filters, setFilters] = useState<UnifiedResultQuery>(() => ({ ...DEFAULT_RESULT_DB_FILTERS }));
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 [resultDbStatus, setResultDbStatus] = useState<ResultDbMaterializationStatus | null>(null); const [resultDbStatus, setResultDbStatus] = useState<ResultDbMaterializationStatus | null>(null);
const [queryResult, setQueryResult] = useState<UnifiedResultQueryResult | null>(null); const [queryResult, setQueryResult] = useState<UnifiedResultQueryResult | null>(null);
@ -60,12 +62,22 @@ export function ResultsBrowser({ backendStatus, projectId, scanRunIds, assessmen
scanRunIds, scanRunIds,
assessmentIds assessmentIds
}; };
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 });
// Read after the rows: serving them guarantees the stored Result DB is current. // Read after the rows: serving them guarantees the stored Result DB is current.
setResultDbStatus(await assessorApi.getResultDbStatus().catch(() => null)); setResultDbStatus(await assessorApi.getResultDbStatus().catch(() => null));
} catch (reason) { } catch (reason) {
@ -73,13 +85,14 @@ export function ResultsBrowser({ backendStatus, projectId, scanRunIds, assessmen
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [backendStatus, projectId, scanRunIds, assessmentIds, hasTimeslotScope, filters, limit]); }, [backendStatus, projectId, scanRunIds, assessmentIds, hasTimeslotScope, 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 exportFilteredResults = async (format: 'jsonl' | 'excel' | 'csv') => { const exportFilteredResults = async (format: 'jsonl' | 'excel' | 'csv') => {
@ -167,7 +180,10 @@ export function ResultsBrowser({ backendStatus, projectId, scanRunIds, assessmen
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

@ -984,6 +984,7 @@ button.client-overall-ring:focus-visible { outline: var(--bw-emphasis) solid var
.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

@ -25,6 +25,10 @@ describe('Result DB timeslot scope', () => {
expect(browserSource).toContain("'Export JSONL'"); expect(browserSource).toContain("'Export JSONL'");
expect(browserSource).toContain('queryAllUnifiedResults'); expect(browserSource).toContain('queryAllUnifiedResults');
expect(browserSource).toContain('buildUnifiedResultJsonl(scopedQuery, results, generatedAt)'); expect(browserSource).toContain('buildUnifiedResultJsonl(scopedQuery, results, generatedAt)');
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.sourceKind === 'policy-observation'"); expect(browserSource).toContain("item.sourceKind === 'policy-observation'");
expect(browserSource).toContain('item.policyValues'); expect(browserSource).toContain('item.policyValues');