fix: Faceted Counts — NULL-Werte einbeziehen + AbortController fuer Race Conditions
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 32s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 2s
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 32s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 2s
Backend: Facets zaehlen jetzt Controls OHNE Wert (z.B. "Ohne Nachweis") als __none__. Filter unterstuetzen __none__ fuer verification_method, category, evidence_type. Counts addieren sich immer zum Total. Frontend: "Ohne X" Optionen in Dropdowns. AbortController verhindert dass aeltere API-Antworten neuere ueberschreiben. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,10 @@ export default function ControlLibraryPage() {
|
|||||||
similarity_score: number; match_rank: number; match_method: string
|
similarity_score: number; match_rank: number; match_method: string
|
||||||
}>>([])
|
}>>([])
|
||||||
|
|
||||||
|
// Abort controllers for cancelling stale requests
|
||||||
|
const metaAbortRef = useRef<AbortController | null>(null)
|
||||||
|
const controlsAbortRef = useRef<AbortController | null>(null)
|
||||||
|
|
||||||
// Debounce search
|
// Debounce search
|
||||||
const searchTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const searchTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -135,17 +139,25 @@ export default function ControlLibraryPage() {
|
|||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Load faceted metadata (reloads when filters change)
|
// Load faceted metadata (reloads when filters change, cancels stale requests)
|
||||||
const loadMeta = useCallback(async () => {
|
const loadMeta = useCallback(async () => {
|
||||||
|
if (metaAbortRef.current) metaAbortRef.current.abort()
|
||||||
|
const controller = new AbortController()
|
||||||
|
metaAbortRef.current = controller
|
||||||
try {
|
try {
|
||||||
const qs = buildParams()
|
const qs = buildParams()
|
||||||
const res = await fetch(`${BACKEND_URL}?endpoint=controls-meta${qs ? `&${qs}` : ''}`)
|
const res = await fetch(`${BACKEND_URL}?endpoint=controls-meta${qs ? `&${qs}` : ''}`, { signal: controller.signal })
|
||||||
if (res.ok) setMeta(await res.json())
|
if (res.ok && !controller.signal.aborted) setMeta(await res.json())
|
||||||
} catch { /* ignore */ }
|
} catch (e) {
|
||||||
|
if (e instanceof DOMException && e.name === 'AbortError') return
|
||||||
|
}
|
||||||
}, [buildParams])
|
}, [buildParams])
|
||||||
|
|
||||||
// Load controls page
|
// Load controls page (cancels stale requests)
|
||||||
const loadControls = useCallback(async () => {
|
const loadControls = useCallback(async () => {
|
||||||
|
if (controlsAbortRef.current) controlsAbortRef.current.abort()
|
||||||
|
const controller = new AbortController()
|
||||||
|
controlsAbortRef.current = controller
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
@@ -164,19 +176,22 @@ export default function ControlLibraryPage() {
|
|||||||
const countQs = buildParams()
|
const countQs = buildParams()
|
||||||
|
|
||||||
const [ctrlRes, countRes] = await Promise.all([
|
const [ctrlRes, countRes] = await Promise.all([
|
||||||
fetch(`${BACKEND_URL}?endpoint=controls&${qs}`),
|
fetch(`${BACKEND_URL}?endpoint=controls&${qs}`, { signal: controller.signal }),
|
||||||
fetch(`${BACKEND_URL}?endpoint=controls-count&${countQs}`),
|
fetch(`${BACKEND_URL}?endpoint=controls-count&${countQs}`, { signal: controller.signal }),
|
||||||
])
|
])
|
||||||
|
|
||||||
if (ctrlRes.ok) setControls(await ctrlRes.json())
|
if (!controller.signal.aborted) {
|
||||||
if (countRes.ok) {
|
if (ctrlRes.ok) setControls(await ctrlRes.json())
|
||||||
const data = await countRes.json()
|
if (countRes.ok) {
|
||||||
setTotalCount(data.total || 0)
|
const data = await countRes.json()
|
||||||
|
setTotalCount(data.total || 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err instanceof DOMException && err.name === 'AbortError') return
|
||||||
setError(err instanceof Error ? err.message : 'Fehler beim Laden')
|
setError(err instanceof Error ? err.message : 'Fehler beim Laden')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
if (!controller.signal.aborted) setLoading(false)
|
||||||
}
|
}
|
||||||
}, [buildParams, sortBy, currentPage])
|
}, [buildParams, sortBy, currentPage])
|
||||||
|
|
||||||
@@ -701,6 +716,7 @@ export default function ControlLibraryPage() {
|
|||||||
{Object.entries(VERIFICATION_METHODS).map(([k, v]) => (
|
{Object.entries(VERIFICATION_METHODS).map(([k, v]) => (
|
||||||
<option key={k} value={k}>{v.label}{meta?.verification_method_counts?.[k] ? ` (${meta.verification_method_counts[k]})` : ''}</option>
|
<option key={k} value={k}>{v.label}{meta?.verification_method_counts?.[k] ? ` (${meta.verification_method_counts[k]})` : ''}</option>
|
||||||
))}
|
))}
|
||||||
|
{meta?.verification_method_counts?.['__none__'] ? <option value="__none__">Ohne Nachweis ({meta.verification_method_counts['__none__']})</option> : null}
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={categoryFilter}
|
value={categoryFilter}
|
||||||
@@ -711,6 +727,7 @@ export default function ControlLibraryPage() {
|
|||||||
{CATEGORY_OPTIONS.map(c => (
|
{CATEGORY_OPTIONS.map(c => (
|
||||||
<option key={c.value} value={c.value}>{c.label}{meta?.category_counts?.[c.value] ? ` (${meta.category_counts[c.value]})` : ''}</option>
|
<option key={c.value} value={c.value}>{c.label}{meta?.category_counts?.[c.value] ? ` (${meta.category_counts[c.value]})` : ''}</option>
|
||||||
))}
|
))}
|
||||||
|
{meta?.category_counts?.['__none__'] ? <option value="__none__">Ohne Kategorie ({meta.category_counts['__none__']})</option> : null}
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={evidenceTypeFilter}
|
value={evidenceTypeFilter}
|
||||||
@@ -721,6 +738,7 @@ export default function ControlLibraryPage() {
|
|||||||
{EVIDENCE_TYPE_OPTIONS.map(c => (
|
{EVIDENCE_TYPE_OPTIONS.map(c => (
|
||||||
<option key={c.value} value={c.value}>{c.label}{meta?.evidence_type_counts?.[c.value] ? ` (${meta.evidence_type_counts[c.value]})` : ''}</option>
|
<option key={c.value} value={c.value}>{c.label}{meta?.evidence_type_counts?.[c.value] ? ` (${meta.evidence_type_counts[c.value]})` : ''}</option>
|
||||||
))}
|
))}
|
||||||
|
{meta?.evidence_type_counts?.['__none__'] ? <option value="__none__">Ohne Nachweisart ({meta.evidence_type_counts['__none__']})</option> : null}
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={audienceFilter}
|
value={audienceFilter}
|
||||||
|
|||||||
@@ -347,14 +347,23 @@ async def list_controls(
|
|||||||
query += " AND release_state = :rs"
|
query += " AND release_state = :rs"
|
||||||
params["rs"] = release_state
|
params["rs"] = release_state
|
||||||
if verification_method:
|
if verification_method:
|
||||||
query += " AND verification_method = :vm"
|
if verification_method == "__none__":
|
||||||
params["vm"] = verification_method
|
query += " AND verification_method IS NULL"
|
||||||
|
else:
|
||||||
|
query += " AND verification_method = :vm"
|
||||||
|
params["vm"] = verification_method
|
||||||
if category:
|
if category:
|
||||||
query += " AND category = :cat"
|
if category == "__none__":
|
||||||
params["cat"] = category
|
query += " AND category IS NULL"
|
||||||
|
else:
|
||||||
|
query += " AND category = :cat"
|
||||||
|
params["cat"] = category
|
||||||
if evidence_type:
|
if evidence_type:
|
||||||
query += " AND evidence_type = :et"
|
if evidence_type == "__none__":
|
||||||
params["et"] = evidence_type
|
query += " AND evidence_type IS NULL"
|
||||||
|
else:
|
||||||
|
query += " AND evidence_type = :et"
|
||||||
|
params["et"] = evidence_type
|
||||||
if target_audience:
|
if target_audience:
|
||||||
query += " AND target_audience LIKE :ta_pattern"
|
query += " AND target_audience LIKE :ta_pattern"
|
||||||
params["ta_pattern"] = f'%"{target_audience}"%'
|
params["ta_pattern"] = f'%"{target_audience}"%'
|
||||||
@@ -434,14 +443,23 @@ async def count_controls(
|
|||||||
query += " AND release_state = :rs"
|
query += " AND release_state = :rs"
|
||||||
params["rs"] = release_state
|
params["rs"] = release_state
|
||||||
if verification_method:
|
if verification_method:
|
||||||
query += " AND verification_method = :vm"
|
if verification_method == "__none__":
|
||||||
params["vm"] = verification_method
|
query += " AND verification_method IS NULL"
|
||||||
|
else:
|
||||||
|
query += " AND verification_method = :vm"
|
||||||
|
params["vm"] = verification_method
|
||||||
if category:
|
if category:
|
||||||
query += " AND category = :cat"
|
if category == "__none__":
|
||||||
params["cat"] = category
|
query += " AND category IS NULL"
|
||||||
|
else:
|
||||||
|
query += " AND category = :cat"
|
||||||
|
params["cat"] = category
|
||||||
if evidence_type:
|
if evidence_type:
|
||||||
query += " AND evidence_type = :et"
|
if evidence_type == "__none__":
|
||||||
params["et"] = evidence_type
|
query += " AND evidence_type IS NULL"
|
||||||
|
else:
|
||||||
|
query += " AND evidence_type = :et"
|
||||||
|
params["et"] = evidence_type
|
||||||
if target_audience:
|
if target_audience:
|
||||||
query += " AND target_audience LIKE :ta_pattern"
|
query += " AND target_audience LIKE :ta_pattern"
|
||||||
params["ta_pattern"] = f'%"{target_audience}"%'
|
params["ta_pattern"] = f'%"{target_audience}"%'
|
||||||
@@ -506,14 +524,23 @@ async def controls_meta(
|
|||||||
clauses.append("release_state = :rs")
|
clauses.append("release_state = :rs")
|
||||||
p["rs"] = release_state
|
p["rs"] = release_state
|
||||||
if verification_method and skip != "verification_method":
|
if verification_method and skip != "verification_method":
|
||||||
clauses.append("verification_method = :vm")
|
if verification_method == "__none__":
|
||||||
p["vm"] = verification_method
|
clauses.append("verification_method IS NULL")
|
||||||
|
else:
|
||||||
|
clauses.append("verification_method = :vm")
|
||||||
|
p["vm"] = verification_method
|
||||||
if category and skip != "category":
|
if category and skip != "category":
|
||||||
clauses.append("category = :cat")
|
if category == "__none__":
|
||||||
p["cat"] = category
|
clauses.append("category IS NULL")
|
||||||
|
else:
|
||||||
|
clauses.append("category = :cat")
|
||||||
|
p["cat"] = category
|
||||||
if evidence_type and skip != "evidence_type":
|
if evidence_type and skip != "evidence_type":
|
||||||
clauses.append("evidence_type = :et")
|
if evidence_type == "__none__":
|
||||||
p["et"] = evidence_type
|
clauses.append("evidence_type IS NULL")
|
||||||
|
else:
|
||||||
|
clauses.append("evidence_type = :et")
|
||||||
|
p["et"] = evidence_type
|
||||||
if target_audience and skip != "target_audience":
|
if target_audience and skip != "target_audience":
|
||||||
clauses.append("target_audience LIKE :ta_pattern")
|
clauses.append("target_audience LIKE :ta_pattern")
|
||||||
p["ta_pattern"] = f'%"{target_audience}"%'
|
p["ta_pattern"] = f'%"{target_audience}"%'
|
||||||
@@ -598,28 +625,28 @@ async def controls_meta(
|
|||||||
GROUP BY severity ORDER BY severity
|
GROUP BY severity ORDER BY severity
|
||||||
"""), p_sev).fetchall()
|
"""), p_sev).fetchall()
|
||||||
|
|
||||||
# Verification method facet
|
# Verification method facet (include NULLs as __none__)
|
||||||
w_vm, p_vm = _build_where(skip="verification_method")
|
w_vm, p_vm = _build_where(skip="verification_method")
|
||||||
vm_counts = db.execute(text(f"""
|
vm_counts = db.execute(text(f"""
|
||||||
SELECT verification_method, count(*) as cnt
|
SELECT COALESCE(verification_method, '__none__') as vm, count(*) as cnt
|
||||||
FROM canonical_controls WHERE {w_vm} AND verification_method IS NOT NULL
|
FROM canonical_controls WHERE {w_vm}
|
||||||
GROUP BY verification_method ORDER BY verification_method
|
GROUP BY vm ORDER BY vm
|
||||||
"""), p_vm).fetchall()
|
"""), p_vm).fetchall()
|
||||||
|
|
||||||
# Category facet
|
# Category facet (include NULLs as __none__)
|
||||||
w_cat, p_cat = _build_where(skip="category")
|
w_cat, p_cat = _build_where(skip="category")
|
||||||
cat_counts = db.execute(text(f"""
|
cat_counts = db.execute(text(f"""
|
||||||
SELECT category, count(*) as cnt
|
SELECT COALESCE(category, '__none__') as cat, count(*) as cnt
|
||||||
FROM canonical_controls WHERE {w_cat} AND category IS NOT NULL
|
FROM canonical_controls WHERE {w_cat}
|
||||||
GROUP BY category ORDER BY cnt DESC
|
GROUP BY cat ORDER BY cnt DESC
|
||||||
"""), p_cat).fetchall()
|
"""), p_cat).fetchall()
|
||||||
|
|
||||||
# Evidence type facet
|
# Evidence type facet (include NULLs as __none__)
|
||||||
w_et, p_et = _build_where(skip="evidence_type")
|
w_et, p_et = _build_where(skip="evidence_type")
|
||||||
et_counts = db.execute(text(f"""
|
et_counts = db.execute(text(f"""
|
||||||
SELECT evidence_type, count(*) as cnt
|
SELECT COALESCE(evidence_type, '__none__') as et, count(*) as cnt
|
||||||
FROM canonical_controls WHERE {w_et} AND evidence_type IS NOT NULL
|
FROM canonical_controls WHERE {w_et}
|
||||||
GROUP BY evidence_type ORDER BY evidence_type
|
GROUP BY et ORDER BY et
|
||||||
"""), p_et).fetchall()
|
"""), p_et).fetchall()
|
||||||
|
|
||||||
# Release state facet
|
# Release state facet
|
||||||
|
|||||||
Reference in New Issue
Block a user