From 52e463a7c8214a5a03358d063110d887c3a8c69a Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 26 Mar 2026 15:00:40 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Faceted=20Search=20=E2=80=94=20Dropdown?= =?UTF-8?q?-Counts=20passen=20sich=20aktiven=20Filtern=20an?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: controls-meta akzeptiert alle Filter-Parameter und berechnet Faceted Counts (jede Dimension zaehlt mit allen ANDEREN Filtern). Neue Facets: severity, verification_method, category, evidence_type, release_state — zusaetzlich zu domains, sources, type_counts. Frontend: loadMeta laedt bei jeder Filteraenderung neu, alle Dropdowns zeigen kontextsensitive Zahlen. Proxy leitet Filter an controls-meta weiter. Co-Authored-By: Claude Opus 4.6 --- .../app/api/sdk/v1/canonical/route.ts | 13 +- .../app/sdk/control-library/page.tsx | 65 ++++--- .../api/canonical_control_routes.py | 169 +++++++++++++++--- .../tests/test_canonical_control_routes.py | 18 +- 4 files changed, 207 insertions(+), 58 deletions(-) diff --git a/admin-compliance/app/api/sdk/v1/canonical/route.ts b/admin-compliance/app/api/sdk/v1/canonical/route.ts index d7ee46c..d5ab588 100644 --- a/admin-compliance/app/api/sdk/v1/canonical/route.ts +++ b/admin-compliance/app/api/sdk/v1/canonical/route.ts @@ -50,9 +50,18 @@ export async function GET(request: NextRequest) { break } - case 'controls-meta': - backendPath = '/api/compliance/v1/canonical/controls-meta' + case 'controls-meta': { + const metaParams = new URLSearchParams() + const metaPassthrough = ['severity', 'domain', 'release_state', 'verification_method', 'category', 'evidence_type', + 'target_audience', 'source', 'search', 'control_type', 'exclude_duplicates'] + for (const key of metaPassthrough) { + const val = searchParams.get(key) + if (val) metaParams.set(key, val) + } + const metaQs = metaParams.toString() + backendPath = `/api/compliance/v1/canonical/controls-meta${metaQs ? `?${metaQs}` : ''}` break + } case 'control': { const controlId = searchParams.get('id') diff --git a/admin-compliance/app/sdk/control-library/page.tsx b/admin-compliance/app/sdk/control-library/page.tsx index 3655f77..0e7bb77 100644 --- a/admin-compliance/app/sdk/control-library/page.tsx +++ b/admin-compliance/app/sdk/control-library/page.tsx @@ -32,6 +32,11 @@ interface ControlsMeta { atomic: number eigenentwicklung: number } + severity_counts?: Record + verification_method_counts?: Record + category_counts?: Record + evidence_type_counts?: Record + release_state_counts?: Record } // ============================================================================= @@ -122,18 +127,23 @@ export default function ControlLibraryPage() { return p.toString() }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, evidenceTypeFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch]) - // Load metadata (domains, sources — once + on refresh) - const loadMeta = useCallback(async () => { + // Load frameworks (once) + const loadFrameworks = useCallback(async () => { try { - const [fwRes, metaRes] = await Promise.all([ - fetch(`${BACKEND_URL}?endpoint=frameworks`), - fetch(`${BACKEND_URL}?endpoint=controls-meta`), - ]) - if (fwRes.ok) setFrameworks(await fwRes.json()) - if (metaRes.ok) setMeta(await metaRes.json()) + const res = await fetch(`${BACKEND_URL}?endpoint=frameworks`) + if (res.ok) setFrameworks(await res.json()) } catch { /* ignore */ } }, []) + // Load faceted metadata (reloads when filters change) + const loadMeta = useCallback(async () => { + try { + const qs = buildParams() + const res = await fetch(`${BACKEND_URL}?endpoint=controls-meta${qs ? `&${qs}` : ''}`) + if (res.ok) setMeta(await res.json()) + } catch { /* ignore */ } + }, [buildParams]) + // Load controls page const loadControls = useCallback(async () => { try { @@ -181,8 +191,11 @@ export default function ControlLibraryPage() { } catch { /* ignore */ } }, []) - // Initial load - useEffect(() => { loadMeta(); loadReviewCount() }, [loadMeta, loadReviewCount]) + // Initial load (frameworks only once) + useEffect(() => { loadFrameworks(); loadReviewCount() }, [loadFrameworks, loadReviewCount]) + + // Load faceted meta when filters change + useEffect(() => { loadMeta() }, [loadMeta]) // Load controls when filters/page/sort change useEffect(() => { loadControls() }, [loadControls]) @@ -195,8 +208,8 @@ export default function ControlLibraryPage() { // Full reload (after CRUD) const fullReload = useCallback(async () => { - await Promise.all([loadControls(), loadMeta(), loadReviewCount()]) - }, [loadControls, loadMeta, loadReviewCount]) + await Promise.all([loadControls(), loadMeta(), loadFrameworks(), loadReviewCount()]) + }, [loadControls, loadMeta, loadFrameworks, loadReviewCount]) // CRUD handlers const handleCreate = async (data: typeof EMPTY_CONTROL) => { @@ -627,7 +640,7 @@ export default function ControlLibraryPage() { />