feat(results-browser): implement load all functionality for results db
This commit is contained in:
parent
d234f94315
commit
f376135fa5
|
|
@ -18,6 +18,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';
|
||||||
|
|
@ -34,6 +35,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);
|
||||||
|
|
@ -59,12 +61,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) {
|
||||||
|
|
@ -72,13 +84,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') => {
|
||||||
|
|
@ -166,7 +179,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>;
|
||||||
|
|
|
||||||
|
|
@ -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 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; }
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue