feat(results-browser): implement load all functionality for results db
This commit is contained in:
parent
cdd93fe9e8
commit
13401d4eb4
|
|
@ -15,6 +15,7 @@ import { BackendConnectionDialog } from '../../components/BackendConnectionDialo
|
|||
const assessorApi = new AssessorApiClient();
|
||||
const PAGE_SIZE = 50;
|
||||
export const DEFAULT_RESULT_DB_FILTERS: UnifiedResultQuery = { deviation: true };
|
||||
const MAX_QUERY_PAGE_SIZE = 10000;
|
||||
|
||||
type BackendStatus = 'connected' | 'checking' | 'unavailable';
|
||||
interface ResultsBrowserProps {
|
||||
|
|
@ -27,6 +28,7 @@ interface ResultsBrowserProps {
|
|||
export function ResultsBrowser({ backendStatus, projectId, scanRunIds, assessmentIds }: 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 [queryResult, setQueryResult] = useState<UnifiedResultQueryResult | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -51,24 +53,35 @@ 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 });
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : 'Results could not be loaded.');
|
||||
} 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 () => {
|
||||
|
|
@ -141,7 +154,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>;
|
||||
|
|
|
|||
|
|
@ -854,6 +854,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; }
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in New Issue