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 d234f94315
commit f376135fa5
3 changed files with 25 additions and 4 deletions

View File

@ -18,6 +18,7 @@ import type { ResultDbMaterializationStatus } from '../../domain/result-db-mater
const assessorApi = new AssessorApiClient();
const PAGE_SIZE = 50;
const MAX_QUERY_PAGE_SIZE = 10000;
export const DEFAULT_RESULT_DB_FILTERS: UnifiedResultQuery = { deviation: true };
type BackendStatus = 'connected' | 'checking' | 'unavailable';
@ -34,6 +35,7 @@ interface ResultsBrowserProps {
export function ResultsBrowser({ backendStatus, projectId, scanRunIds, assessmentIds, workRecordIds, onOpenInWork }: ResultsBrowserProps) {
const [filters, setFilters] = useState<UnifiedResultQuery>(() => ({ ...DEFAULT_RESULT_DB_FILTERS }));
const [limit, setLimit] = useState(PAGE_SIZE);
const [loadAll, setLoadAll] = useState(false);
const [facetResult, setFacetResult] = useState<UnifiedResultFacetResult | null>(null);
const [resultDbStatus, setResultDbStatus] = useState<ResultDbMaterializationStatus | null>(null);
const [queryResult, setQueryResult] = useState<UnifiedResultQueryResult | null>(null);
@ -59,12 +61,22 @@ export function ResultsBrowser({ backendStatus, projectId, scanRunIds, assessmen
scanRunIds,
assessmentIds
};
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 });
// Read after the rows: serving them guarantees the stored Result DB is current.
setResultDbStatus(await assessorApi.getResultDbStatus().catch(() => null));
} catch (reason) {
@ -72,13 +84,14 @@ export function ResultsBrowser({ backendStatus, projectId, scanRunIds, assessmen
} finally {
setLoading(false);
}
}, [backendStatus, projectId, scanRunIds, assessmentIds, hasTimeslotScope, filters, limit]);
}, [backendStatus, projectId, scanRunIds, assessmentIds, hasTimeslotScope, filters, limit, loadAll]);
useEffect(() => { void load(); }, [load]);
const updateFilters = (next: UnifiedResultQuery) => {
setExpandedId(null);
setLimit(PAGE_SIZE);
setLoadAll(false);
setFilters(next);
};
const exportFilteredResults = async (format: 'jsonl' | 'excel' | 'csv') => {
@ -166,7 +179,10 @@ export function ResultsBrowser({ backendStatus, projectId, scanRunIds, assessmen
expanded={expandedId === item.resultProjectionId}
onToggle={() => setExpandedId(current => current === item.resultProjectionId ? null : item.resultProjectionId)}/>)}
</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>;

View File

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

View File

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