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
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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user