refactor(admin): split multi-tenant page.tsx into colocated components
Extract types, constants, helpers, and UI pieces (LoadingSkeleton, EmptyState, StatCard, ComplianceRing, Modal, TenantCard, CreateTenantModal, EditTenantModal, TenantDetailModal) into _components/ and _types.ts to bring page.tsx from 1663 LOC to 432 LOC (under the 500 hard cap). Behavior preserved. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
151
admin-compliance/app/sdk/multi-tenant/_components/TenantCard.tsx
Normal file
151
admin-compliance/app/sdk/multi-tenant/_components/TenantCard.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
AlertTriangle,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Eye,
|
||||
Globe,
|
||||
GraduationCap,
|
||||
Pencil,
|
||||
Shield,
|
||||
Truck,
|
||||
Users,
|
||||
} from 'lucide-react'
|
||||
import type { TenantOverview } from '../_types'
|
||||
import { formatDate, formatDateTime, getRiskBadgeClasses, getStatusBadge } from './helpers'
|
||||
import { ComplianceRing } from './ComplianceRing'
|
||||
|
||||
export function TenantCard({
|
||||
tenant,
|
||||
onEdit,
|
||||
onViewDetails,
|
||||
onSwitchTenant,
|
||||
}: {
|
||||
tenant: TenantOverview
|
||||
onEdit: (t: TenantOverview) => void
|
||||
onViewDetails: (t: TenantOverview) => void
|
||||
onSwitchTenant: (t: TenantOverview) => void
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const statusInfo = getStatusBadge(tenant.status)
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-slate-200 hover:border-indigo-300 hover:shadow-md transition-all overflow-hidden">
|
||||
{/* Card Header */}
|
||||
<div className="p-5">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-base font-semibold text-slate-900 truncate">{tenant.name}</h3>
|
||||
<span className={`inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full ${statusInfo.bg} ${statusInfo.text}`}>
|
||||
{statusInfo.icon}
|
||||
{statusInfo.label}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 font-mono">{tenant.slug}</p>
|
||||
</div>
|
||||
<ComplianceRing score={tenant.compliance_score} size={56} />
|
||||
</div>
|
||||
|
||||
{/* Risk Level */}
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className={`inline-flex items-center gap-1 px-2 py-0.5 text-xs font-semibold rounded-full ${getRiskBadgeClasses(tenant.risk_level)}`}>
|
||||
<Shield className="w-3 h-3" />
|
||||
{tenant.risk_level}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400">
|
||||
{tenant.namespace_count} Namespace{tenant.namespace_count !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400 ml-auto">
|
||||
{formatDate(tenant.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Quick Metrics */}
|
||||
<div className="grid grid-cols-2 gap-2 mb-4">
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-600 bg-slate-50 rounded-lg px-2.5 py-1.5">
|
||||
<AlertTriangle className="w-3.5 h-3.5 text-orange-500" />
|
||||
<span>{tenant.open_incidents} Vorfaelle</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-600 bg-slate-50 rounded-lg px-2.5 py-1.5">
|
||||
<Shield className="w-3.5 h-3.5 text-indigo-500" />
|
||||
<span>{tenant.open_reports} Meldungen</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-600 bg-slate-50 rounded-lg px-2.5 py-1.5">
|
||||
<Users className="w-3.5 h-3.5 text-blue-500" />
|
||||
<span>{tenant.pending_dsrs} DSRs</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-600 bg-slate-50 rounded-lg px-2.5 py-1.5">
|
||||
<GraduationCap className="w-3.5 h-3.5 text-green-500" />
|
||||
<span>{tenant.training_completion_rate}% Training</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Vendor Risk Info */}
|
||||
{tenant.vendor_risk_high > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-orange-600 bg-orange-50 rounded-lg px-2.5 py-1.5 mb-4">
|
||||
<Truck className="w-3.5 h-3.5" />
|
||||
<span>{tenant.vendor_risk_high} Dienstleister mit hohem Risiko</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => onViewDetails(tenant)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-indigo-600 bg-indigo-50 hover:bg-indigo-100 rounded-lg transition-colors"
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
Details
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onEdit(tenant)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-slate-600 bg-slate-50 hover:bg-slate-100 rounded-lg transition-colors"
|
||||
>
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onSwitchTenant(tenant)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors ml-auto"
|
||||
>
|
||||
<Globe className="w-3.5 h-3.5" />
|
||||
Wechseln
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="p-1.5 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-lg transition-colors"
|
||||
>
|
||||
{expanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Section */}
|
||||
{expanded && (
|
||||
<div className="border-t border-slate-100 bg-slate-50/50 px-5 py-4 space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||
<div>
|
||||
<span className="text-slate-400">Max. Benutzer</span>
|
||||
<p className="font-semibold text-slate-700">{tenant.max_users.toLocaleString('de-DE')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400">LLM Kontingent / Monat</span>
|
||||
<p className="font-semibold text-slate-700">{tenant.llm_quota_monthly.toLocaleString('de-DE')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400">Compliance Score</span>
|
||||
<p className="font-semibold text-slate-700">{tenant.compliance_score} / 100</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400">Aktualisiert</span>
|
||||
<p className="font-semibold text-slate-700">{formatDateTime(tenant.updated_at)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user