Extract components and hooks from oversized page files (563/561/520 LOC) into colocated _components/ and _hooks/ subdirectories. All three page.tsx files are now thin orchestrators under 300 LOC each (dsfa: 216, audit-llm: 121, quality: 163). Zero behavior changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
129 lines
5.8 KiB
TypeScript
129 lines
5.8 KiB
TypeScript
'use client'
|
|
|
|
import { StatCard } from './StatCard'
|
|
import { formatNumber, type ComplianceReport } from './types'
|
|
|
|
export function ComplianceTab({ complianceReport }: { complianceReport: ComplianceReport }) {
|
|
return (
|
|
<div>
|
|
{/* Summary Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
|
<StatCard label="Requests" value={formatNumber(complianceReport.total_requests)} />
|
|
<StatCard
|
|
label="PII-Vorfaelle"
|
|
value={formatNumber(complianceReport.pii_incidents)}
|
|
highlight={complianceReport.pii_incidents > 0}
|
|
/>
|
|
<StatCard
|
|
label="PII-Rate"
|
|
value={`${(complianceReport.pii_rate * 100).toFixed(1)}%`}
|
|
highlight={complianceReport.pii_rate > 0.05}
|
|
/>
|
|
<StatCard label="Redaction-Rate" value={`${(complianceReport.redaction_rate * 100).toFixed(1)}%`} />
|
|
</div>
|
|
|
|
{complianceReport.policy_violations > 0 && (
|
|
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-xl">
|
|
<div className="flex items-center gap-2 text-red-700 font-semibold">
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
{complianceReport.policy_violations} Policy-Verletzungen im Zeitraum
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* PII Categories */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-5">
|
|
<h3 className="font-semibold text-gray-900 mb-4">PII-Kategorien</h3>
|
|
{Object.entries(complianceReport.top_pii_categories || {}).length === 0 ? (
|
|
<p className="text-gray-400 text-sm">Keine PII erkannt</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{Object.entries(complianceReport.top_pii_categories).sort((a, b) => b[1] - a[1]).map(([cat, count]) => (
|
|
<div key={cat} className="flex items-center justify-between py-1">
|
|
<span className="text-sm text-gray-700">{cat}</span>
|
|
<span className="px-2 py-0.5 bg-red-50 text-red-700 rounded text-xs font-mono">{count}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Namespace Breakdown */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-5">
|
|
<h3 className="font-semibold text-gray-900 mb-4">Namespace-Analyse</h3>
|
|
{Object.entries(complianceReport.namespace_breakdown || {}).length === 0 ? (
|
|
<p className="text-gray-400 text-sm">Keine Namespace-Daten</p>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-gray-100">
|
|
<th className="text-left py-2 text-gray-500 font-medium">Namespace</th>
|
|
<th className="text-right py-2 text-gray-500 font-medium">Requests</th>
|
|
<th className="text-right py-2 text-gray-500 font-medium">PII</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{Object.entries(complianceReport.namespace_breakdown).map(([ns, data]) => (
|
|
<tr key={ns} className="border-b border-gray-50">
|
|
<td className="py-2 font-mono text-xs">{ns}</td>
|
|
<td className="py-2 text-right">{formatNumber(data.requests)}</td>
|
|
<td className="py-2 text-right">
|
|
{data.pii_incidents > 0 ? (
|
|
<span className="text-red-600 font-medium">{data.pii_incidents}</span>
|
|
) : (
|
|
<span className="text-gray-400">0</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* User Breakdown */}
|
|
{Object.entries(complianceReport.user_breakdown || {}).length > 0 && (
|
|
<div className="mt-6 bg-white rounded-xl border border-gray-200 p-5">
|
|
<h3 className="font-semibold text-gray-900 mb-4">Top-Nutzer</h3>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-gray-100">
|
|
<th className="text-left py-2 text-gray-500 font-medium">User-ID</th>
|
|
<th className="text-right py-2 text-gray-500 font-medium">Requests</th>
|
|
<th className="text-right py-2 text-gray-500 font-medium">PII-Vorfaelle</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{Object.entries(complianceReport.user_breakdown)
|
|
.sort((a, b) => b[1].requests - a[1].requests)
|
|
.slice(0, 10)
|
|
.map(([userId, data]) => (
|
|
<tr key={userId} className="border-b border-gray-50">
|
|
<td className="py-2 font-mono text-xs">{userId}</td>
|
|
<td className="py-2 text-right">{formatNumber(data.requests)}</td>
|
|
<td className="py-2 text-right">
|
|
{data.pii_incidents > 0 ? (
|
|
<span className="text-red-600 font-medium">{data.pii_incidents}</span>
|
|
) : (
|
|
<span className="text-gray-400">0</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|