feat(control-library): add document source dropdown filter
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 44s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 25s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 6s

Add "Dokumentenursprung" filter dropdown to the control library page.
Extracts unique source_citation.source values from controls, sorted by
frequency. Includes "Ohne Quelle" option for controls without source info.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-15 09:03:21 +01:00
parent dd09fa7a46
commit f066cf1a03

View File

@@ -34,6 +34,7 @@ export default function ControlLibraryPage() {
const [verificationFilter, setVerificationFilter] = useState<string>('')
const [categoryFilter, setCategoryFilter] = useState<string>('')
const [audienceFilter, setAudienceFilter] = useState<string>('')
const [sourceFilter, setSourceFilter] = useState<string>('')
// CRUD state
const [mode, setMode] = useState<'list' | 'detail' | 'create' | 'edit'>('list')
@@ -78,6 +79,22 @@ export default function ControlLibraryPage() {
return Array.from(set).sort()
}, [controls])
// Derived: unique document sources (sorted by frequency)
const documentSources = useMemo(() => {
const counts = new Map<string, number>()
let noSource = 0
for (const c of controls) {
const src = c.source_citation?.source
if (src) {
counts.set(src, (counts.get(src) || 0) + 1)
} else {
noSource++
}
}
const sorted = Array.from(counts.entries()).sort((a, b) => b[1] - a[1])
return { sources: sorted, noSourceCount: noSource }
}, [controls])
// Filtered controls
const filteredControls = useMemo(() => {
return controls.filter(c => {
@@ -87,6 +104,14 @@ export default function ControlLibraryPage() {
if (verificationFilter && c.verification_method !== verificationFilter) return false
if (categoryFilter && c.category !== categoryFilter) return false
if (audienceFilter && c.target_audience !== audienceFilter) return false
if (sourceFilter) {
const src = c.source_citation?.source || ''
if (sourceFilter === '__none__') {
if (src) return false
} else {
if (src !== sourceFilter) return false
}
}
if (searchQuery) {
const q = searchQuery.toLowerCase()
return (
@@ -98,10 +123,10 @@ export default function ControlLibraryPage() {
}
return true
})
}, [controls, severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, audienceFilter, searchQuery])
}, [controls, severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, audienceFilter, sourceFilter, searchQuery])
// Reset page when filters change
useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, audienceFilter, searchQuery])
useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, audienceFilter, sourceFilter, searchQuery])
// Pagination
const totalPages = Math.max(1, Math.ceil(filteredControls.length / PAGE_SIZE))
@@ -441,6 +466,17 @@ export default function ControlLibraryPage() {
<option key={k} value={k}>{v.label}</option>
))}
</select>
<select
value={sourceFilter}
onChange={e => setSourceFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500 max-w-[220px]"
>
<option value="">Dokumentenursprung</option>
<option value="__none__">Ohne Quelle ({documentSources.noSourceCount})</option>
{documentSources.sources.map(([src, count]) => (
<option key={src} value={src}>{src} ({count})</option>
))}
</select>
</div>
</div>