|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from 'react'; |
|
|
import { Activity, AlertCircle, BarChart3, Database, GitBranch, Shield, Clock, TrendingUp } from 'lucide-react'; |
|
|
|
|
|
interface DashboardData { |
|
|
status: string; |
|
|
timestamp: string; |
|
|
system: { |
|
|
uptime_seconds: number; |
|
|
uptime_human: string; |
|
|
error_rate: number; |
|
|
total_requests: number; |
|
|
error_threshold: number; |
|
|
status: string; |
|
|
}; |
|
|
pipeline: { |
|
|
total_jobs_processed: number; |
|
|
completed_jobs: number; |
|
|
failed_jobs: number; |
|
|
processing_jobs: number; |
|
|
success_rate: number; |
|
|
}; |
|
|
models: { |
|
|
total_registered: number; |
|
|
performance: Record<string, ModelPerformance>; |
|
|
}; |
|
|
synthesis: { |
|
|
total_syntheses: number; |
|
|
avg_confidence: number; |
|
|
requiring_review: number; |
|
|
avg_processing_time_ms: number; |
|
|
}; |
|
|
cache: { |
|
|
total_entries: number; |
|
|
hits: number; |
|
|
misses: number; |
|
|
hit_rate: number; |
|
|
evictions: number; |
|
|
memory_usage_mb: number; |
|
|
avg_retrieval_time_ms: number; |
|
|
cache_efficiency: number; |
|
|
}; |
|
|
alerts: { |
|
|
active_count: number; |
|
|
critical_count: number; |
|
|
recent: Alert[]; |
|
|
}; |
|
|
compliance: { |
|
|
hipaa_compliant: boolean; |
|
|
gdpr_compliant: boolean; |
|
|
audit_logging_active: boolean; |
|
|
phi_removal_active: boolean; |
|
|
encryption_enabled: boolean; |
|
|
}; |
|
|
components: Record<string, string>; |
|
|
} |
|
|
|
|
|
interface ModelPerformance { |
|
|
version: string; |
|
|
total_inferences: number; |
|
|
avg_latency_ms: number; |
|
|
error_rate: number; |
|
|
last_used: string; |
|
|
} |
|
|
|
|
|
interface Alert { |
|
|
alert_id: string; |
|
|
level: string; |
|
|
message: string; |
|
|
category: string; |
|
|
timestamp: string; |
|
|
resolved: boolean; |
|
|
details: Record<string, any>; |
|
|
} |
|
|
|
|
|
const AdminDashboard: React.FC = () => { |
|
|
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null); |
|
|
const [loading, setLoading] = useState(true); |
|
|
const [error, setError] = useState<string | null>(null); |
|
|
const [activeTab, setActiveTab] = useState<'overview' | 'models' | 'cache' | 'alerts'>('overview'); |
|
|
const [autoRefresh, setAutoRefresh] = useState(true); |
|
|
|
|
|
|
|
|
const fetchDashboard = async () => { |
|
|
try { |
|
|
const response = await fetch('/health/dashboard'); |
|
|
if (!response.ok) { |
|
|
throw new Error('Failed to fetch dashboard data'); |
|
|
} |
|
|
const data = await response.json(); |
|
|
setDashboardData(data); |
|
|
setError(null); |
|
|
} catch (err) { |
|
|
setError(err instanceof Error ? err.message : 'Unknown error'); |
|
|
} finally { |
|
|
setLoading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
fetchDashboard(); |
|
|
|
|
|
if (autoRefresh) { |
|
|
const interval = setInterval(fetchDashboard, 10000); |
|
|
return () => clearInterval(interval); |
|
|
} |
|
|
}, [autoRefresh]); |
|
|
|
|
|
|
|
|
const StatusBadge: React.FC<{ status: string }> = ({ status }) => { |
|
|
const colors = { |
|
|
operational: 'bg-green-100 text-green-800', |
|
|
healthy: 'bg-green-100 text-green-800', |
|
|
degraded: 'bg-yellow-100 text-yellow-800', |
|
|
critical: 'bg-red-100 text-red-800', |
|
|
ready: 'bg-blue-100 text-blue-800', |
|
|
active: 'bg-blue-100 text-blue-800' |
|
|
}; |
|
|
|
|
|
const color = colors[status.toLowerCase() as keyof typeof colors] || 'bg-gray-100 text-gray-800'; |
|
|
|
|
|
return ( |
|
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${color}`}> |
|
|
{status.toUpperCase()} |
|
|
</span> |
|
|
); |
|
|
}; |
|
|
|
|
|
|
|
|
const MetricCard: React.FC<{ |
|
|
title: string; |
|
|
value: string | number; |
|
|
subtitle?: string; |
|
|
icon: React.ReactNode; |
|
|
status?: 'good' | 'warning' | 'critical'; |
|
|
}> = ({ title, value, subtitle, icon, status }) => { |
|
|
const statusColors = { |
|
|
good: 'border-green-200 bg-green-50', |
|
|
warning: 'border-yellow-200 bg-yellow-50', |
|
|
critical: 'border-red-200 bg-red-50' |
|
|
}; |
|
|
|
|
|
const borderColor = status ? statusColors[status] : 'border-gray-200 bg-white'; |
|
|
|
|
|
return ( |
|
|
<div className={`p-4 rounded-lg border-2 ${borderColor}`}> |
|
|
<div className="flex items-center justify-between mb-2"> |
|
|
<span className="text-sm font-medium text-gray-600">{title}</span> |
|
|
<div className="text-gray-400">{icon}</div> |
|
|
</div> |
|
|
<div className="text-2xl font-bold text-gray-900">{value}</div> |
|
|
{subtitle && <div className="text-xs text-gray-500 mt-1">{subtitle}</div>} |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
|
|
|
const resolveAlert = async (alertId: string) => { |
|
|
try { |
|
|
const response = await fetch(`/admin/alerts/${alertId}/resolve`, { |
|
|
method: 'POST' |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
fetchDashboard(); |
|
|
} |
|
|
} catch (err) { |
|
|
console.error('Failed to resolve alert:', err); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const clearCache = async () => { |
|
|
if (!confirm('Are you sure you want to clear all cache entries? This may temporarily impact performance.')) { |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const response = await fetch('/admin/cache/clear', { |
|
|
method: 'POST' |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
alert('Cache cleared successfully'); |
|
|
fetchDashboard(); |
|
|
} |
|
|
} catch (err) { |
|
|
alert('Failed to clear cache'); |
|
|
} |
|
|
}; |
|
|
|
|
|
if (loading) { |
|
|
return ( |
|
|
<div className="flex items-center justify-center min-h-screen"> |
|
|
<div className="text-center"> |
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div> |
|
|
<p className="text-gray-600">Loading dashboard...</p> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
if (error) { |
|
|
return ( |
|
|
<div className="flex items-center justify-center min-h-screen"> |
|
|
<div className="text-center"> |
|
|
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" /> |
|
|
<p className="text-gray-600">Error: {error}</p> |
|
|
<button |
|
|
onClick={fetchDashboard} |
|
|
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700" |
|
|
> |
|
|
Retry |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
if (!dashboardData) { |
|
|
return null; |
|
|
} |
|
|
|
|
|
return ( |
|
|
<div className="min-h-screen bg-gray-50"> |
|
|
{/* Header */} |
|
|
<div className="bg-white border-b border-gray-200"> |
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4"> |
|
|
<div className="flex items-center justify-between"> |
|
|
<div> |
|
|
<h1 className="text-2xl font-bold text-gray-900">Medical AI Platform - Admin Dashboard</h1> |
|
|
<p className="text-sm text-gray-500 mt-1"> |
|
|
Real-time monitoring and system management |
|
|
</p> |
|
|
</div> |
|
|
<div className="flex items-center space-x-4"> |
|
|
<StatusBadge status={dashboardData.status} /> |
|
|
<label className="flex items-center space-x-2 text-sm text-gray-600"> |
|
|
<input |
|
|
type="checkbox" |
|
|
checked={autoRefresh} |
|
|
onChange={(e) => setAutoRefresh(e.target.checked)} |
|
|
className="rounded" |
|
|
/> |
|
|
<span>Auto-refresh</span> |
|
|
</label> |
|
|
<button |
|
|
onClick={fetchDashboard} |
|
|
className="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm" |
|
|
> |
|
|
Refresh Now |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{/* Tabs */} |
|
|
<div className="mt-4 flex space-x-4 border-b border-gray-200"> |
|
|
{['overview', 'models', 'cache', 'alerts'].map((tab) => ( |
|
|
<button |
|
|
key={tab} |
|
|
onClick={() => setActiveTab(tab as any)} |
|
|
className={`px-4 py-2 font-medium text-sm border-b-2 transition-colors ${ |
|
|
activeTab === tab |
|
|
? 'border-blue-600 text-blue-600' |
|
|
: 'border-transparent text-gray-500 hover:text-gray-700' |
|
|
}`} |
|
|
> |
|
|
{tab.charAt(0).toUpperCase() + tab.slice(1)} |
|
|
</button> |
|
|
))} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{/* Content */} |
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> |
|
|
{/* Overview Tab */} |
|
|
{activeTab === 'overview' && ( |
|
|
<div className="space-y-6"> |
|
|
{/* System Status Grid */} |
|
|
<div> |
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">System Status</h2> |
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> |
|
|
<MetricCard |
|
|
title="Uptime" |
|
|
value={dashboardData.system.uptime_human} |
|
|
subtitle={`${dashboardData.system.uptime_seconds.toLocaleString()}s`} |
|
|
icon={<Clock className="h-5 w-5" />} |
|
|
status="good" |
|
|
/> |
|
|
<MetricCard |
|
|
title="Error Rate" |
|
|
value={`${(dashboardData.system.error_rate * 100).toFixed(2)}%`} |
|
|
subtitle={`Threshold: ${(dashboardData.system.error_threshold * 100).toFixed(0)}%`} |
|
|
icon={<AlertCircle className="h-5 w-5" />} |
|
|
status={dashboardData.system.error_rate > dashboardData.system.error_threshold ? 'critical' : 'good'} |
|
|
/> |
|
|
<MetricCard |
|
|
title="Total Requests" |
|
|
value={dashboardData.system.total_requests.toLocaleString()} |
|
|
icon={<Activity className="h-5 w-5" />} |
|
|
/> |
|
|
<MetricCard |
|
|
title="Active Alerts" |
|
|
value={dashboardData.alerts.active_count} |
|
|
subtitle={`${dashboardData.alerts.critical_count} critical`} |
|
|
icon={<AlertCircle className="h-5 w-5" />} |
|
|
status={dashboardData.alerts.critical_count > 0 ? 'critical' : dashboardData.alerts.active_count > 0 ? 'warning' : 'good'} |
|
|
/> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{/* Pipeline Statistics */} |
|
|
<div> |
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">Pipeline Statistics</h2> |
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> |
|
|
<MetricCard |
|
|
title="Total Jobs" |
|
|
value={dashboardData.pipeline.total_jobs_processed.toLocaleString()} |
|
|
icon={<BarChart3 className="h-5 w-5" />} |
|
|
/> |
|
|
<MetricCard |
|
|
title="Completed" |
|
|
value={dashboardData.pipeline.completed_jobs.toLocaleString()} |
|
|
icon={<TrendingUp className="h-5 w-5" />} |
|
|
status="good" |
|
|
/> |
|
|
<MetricCard |
|
|
title="Failed" |
|
|
value={dashboardData.pipeline.failed_jobs.toLocaleString()} |
|
|
icon={<AlertCircle className="h-5 w-5" />} |
|
|
status={dashboardData.pipeline.failed_jobs > 0 ? 'warning' : 'good'} |
|
|
/> |
|
|
<MetricCard |
|
|
title="Success Rate" |
|
|
value={`${(dashboardData.pipeline.success_rate * 100).toFixed(1)}%`} |
|
|
icon={<Activity className="h-5 w-5" />} |
|
|
status={dashboardData.pipeline.success_rate > 0.95 ? 'good' : dashboardData.pipeline.success_rate > 0.85 ? 'warning' : 'critical'} |
|
|
/> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{/* Cache & Synthesis */} |
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> |
|
|
{/* Cache Statistics */} |
|
|
<div className="bg-white p-6 rounded-lg border border-gray-200"> |
|
|
<div className="flex items-center justify-between mb-4"> |
|
|
<h3 className="text-lg font-semibold text-gray-900">Cache Performance</h3> |
|
|
<Database className="h-5 w-5 text-gray-400" /> |
|
|
</div> |
|
|
<div className="space-y-3"> |
|
|
<div className="flex justify-between"> |
|
|
<span className="text-sm text-gray-600">Hit Rate</span> |
|
|
<span className="text-sm font-medium">{(dashboardData.cache.hit_rate * 100).toFixed(1)}%</span> |
|
|
</div> |
|
|
<div className="flex justify-between"> |
|
|
<span className="text-sm text-gray-600">Entries</span> |
|
|
<span className="text-sm font-medium">{dashboardData.cache.total_entries.toLocaleString()}</span> |
|
|
</div> |
|
|
<div className="flex justify-between"> |
|
|
<span className="text-sm text-gray-600">Memory Usage</span> |
|
|
<span className="text-sm font-medium">{dashboardData.cache.memory_usage_mb.toFixed(1)} MB</span> |
|
|
</div> |
|
|
<div className="flex justify-between"> |
|
|
<span className="text-sm text-gray-600">Avg Retrieval</span> |
|
|
<span className="text-sm font-medium">{dashboardData.cache.avg_retrieval_time_ms.toFixed(2)} ms</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{/* Synthesis Statistics */} |
|
|
<div className="bg-white p-6 rounded-lg border border-gray-200"> |
|
|
<div className="flex items-center justify-between mb-4"> |
|
|
<h3 className="text-lg font-semibold text-gray-900">Clinical Synthesis</h3> |
|
|
<GitBranch className="h-5 w-5 text-gray-400" /> |
|
|
</div> |
|
|
<div className="space-y-3"> |
|
|
<div className="flex justify-between"> |
|
|
<span className="text-sm text-gray-600">Total Syntheses</span> |
|
|
<span className="text-sm font-medium">{dashboardData.synthesis.total_syntheses.toLocaleString()}</span> |
|
|
</div> |
|
|
<div className="flex justify-between"> |
|
|
<span className="text-sm text-gray-600">Avg Confidence</span> |
|
|
<span className="text-sm font-medium">{(dashboardData.synthesis.avg_confidence * 100).toFixed(1)}%</span> |
|
|
</div> |
|
|
<div className="flex justify-between"> |
|
|
<span className="text-sm text-gray-600">Requiring Review</span> |
|
|
<span className="text-sm font-medium">{dashboardData.synthesis.requiring_review.toLocaleString()}</span> |
|
|
</div> |
|
|
<div className="flex justify-between"> |
|
|
<span className="text-sm text-gray-600">Avg Processing</span> |
|
|
<span className="text-sm font-medium">{dashboardData.synthesis.avg_processing_time_ms.toFixed(0)} ms</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{/* Compliance Status */} |
|
|
<div className="bg-white p-6 rounded-lg border border-gray-200"> |
|
|
<div className="flex items-center justify-between mb-4"> |
|
|
<h3 className="text-lg font-semibold text-gray-900">Compliance Status</h3> |
|
|
<Shield className="h-5 w-5 text-gray-400" /> |
|
|
</div> |
|
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-4"> |
|
|
{Object.entries(dashboardData.compliance).map(([key, value]) => ( |
|
|
<div key={key} className="text-center"> |
|
|
<div className={`text-2xl mb-1 ${value ? 'text-green-600' : 'text-red-600'}`}> |
|
|
{value ? '✓' : '✗'} |
|
|
</div> |
|
|
<div className="text-xs text-gray-600"> |
|
|
{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} |
|
|
</div> |
|
|
</div> |
|
|
))} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{/* Models Tab */} |
|
|
{activeTab === 'models' && ( |
|
|
<div> |
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">Model Performance</h2> |
|
|
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden"> |
|
|
<table className="min-w-full divide-y divide-gray-200"> |
|
|
<thead className="bg-gray-50"> |
|
|
<tr> |
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Model ID</th> |
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Version</th> |
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Inferences</th> |
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Avg Latency</th> |
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Error Rate</th> |
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Last Used</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody className="bg-white divide-y divide-gray-200"> |
|
|
{Object.entries(dashboardData.models.performance).map(([modelId, perf]) => ( |
|
|
<tr key={modelId}> |
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{modelId}</td> |
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{perf.version}</td> |
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{perf.total_inferences.toLocaleString()}</td> |
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{perf.avg_latency_ms.toFixed(1)} ms</td> |
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{(perf.error_rate * 100).toFixed(2)}%</td> |
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{perf.last_used}</td> |
|
|
</tr> |
|
|
))} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{/* Cache Tab */} |
|
|
{activeTab === 'cache' && ( |
|
|
<div> |
|
|
<div className="flex items-center justify-between mb-4"> |
|
|
<h2 className="text-lg font-semibold text-gray-900">Cache Management</h2> |
|
|
<button |
|
|
onClick={clearCache} |
|
|
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm" |
|
|
> |
|
|
Clear Cache |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6"> |
|
|
<MetricCard |
|
|
title="Cache Entries" |
|
|
value={dashboardData.cache.total_entries.toLocaleString()} |
|
|
icon={<Database className="h-5 w-5" />} |
|
|
/> |
|
|
<MetricCard |
|
|
title="Hit Rate" |
|
|
value={`${(dashboardData.cache.hit_rate * 100).toFixed(1)}%`} |
|
|
subtitle={`${dashboardData.cache.hits} hits, ${dashboardData.cache.misses} misses`} |
|
|
icon={<Activity className="h-5 w-5" />} |
|
|
status={dashboardData.cache.hit_rate > 0.7 ? 'good' : dashboardData.cache.hit_rate > 0.4 ? 'warning' : 'critical'} |
|
|
/> |
|
|
<MetricCard |
|
|
title="Memory Usage" |
|
|
value={`${dashboardData.cache.memory_usage_mb.toFixed(1)} MB`} |
|
|
subtitle={`${dashboardData.cache.evictions} evictions`} |
|
|
icon={<BarChart3 className="h-5 w-5" />} |
|
|
/> |
|
|
</div> |
|
|
|
|
|
<div className="bg-white p-6 rounded-lg border border-gray-200"> |
|
|
<h3 className="text-md font-semibold text-gray-900 mb-4">Cache Performance Analysis</h3> |
|
|
<div className="space-y-2"> |
|
|
{dashboardData.cache.hit_rate < 0.5 && ( |
|
|
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded"> |
|
|
<p className="text-sm text-yellow-800"> |
|
|
⚠ Low cache hit rate ({(dashboardData.cache.hit_rate * 100).toFixed(1)}%). |
|
|
Consider increasing cache size or TTL. |
|
|
</p> |
|
|
</div> |
|
|
)} |
|
|
{dashboardData.cache.hit_rate >= 0.8 && ( |
|
|
<div className="p-3 bg-green-50 border border-green-200 rounded"> |
|
|
<p className="text-sm text-green-800"> |
|
|
✓ Excellent cache hit rate ({(dashboardData.cache.hit_rate * 100).toFixed(1)}%). |
|
|
Cache performing optimally. |
|
|
</p> |
|
|
</div> |
|
|
)} |
|
|
<div className="p-3 bg-blue-50 border border-blue-200 rounded"> |
|
|
<p className="text-sm text-blue-800"> |
|
|
Cache efficiency: {dashboardData.cache.cache_efficiency.toFixed(1)}% |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{/* Alerts Tab */} |
|
|
{activeTab === 'alerts' && ( |
|
|
<div> |
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-4"> |
|
|
Active Alerts ({dashboardData.alerts.active_count}) |
|
|
</h2> |
|
|
{dashboardData.alerts.recent.length === 0 ? ( |
|
|
<div className="bg-white p-12 rounded-lg border border-gray-200 text-center"> |
|
|
<AlertCircle className="h-12 w-12 text-gray-300 mx-auto mb-4" /> |
|
|
<p className="text-gray-500">No active alerts</p> |
|
|
</div> |
|
|
) : ( |
|
|
<div className="space-y-3"> |
|
|
{dashboardData.alerts.recent.map((alert) => ( |
|
|
<div |
|
|
key={alert.alert_id} |
|
|
className={`p-4 rounded-lg border-2 ${ |
|
|
alert.level === 'critical' |
|
|
? 'border-red-200 bg-red-50' |
|
|
: alert.level === 'error' |
|
|
? 'border-orange-200 bg-orange-50' |
|
|
: alert.level === 'warning' |
|
|
? 'border-yellow-200 bg-yellow-50' |
|
|
: 'border-blue-200 bg-blue-50' |
|
|
}`} |
|
|
> |
|
|
<div className="flex items-start justify-between"> |
|
|
<div className="flex-1"> |
|
|
<div className="flex items-center space-x-2 mb-2"> |
|
|
<span className={`px-2 py-1 rounded text-xs font-medium ${ |
|
|
alert.level === 'critical' |
|
|
? 'bg-red-200 text-red-800' |
|
|
: alert.level === 'error' |
|
|
? 'bg-orange-200 text-orange-800' |
|
|
: alert.level === 'warning' |
|
|
? 'bg-yellow-200 text-yellow-800' |
|
|
: 'bg-blue-200 text-blue-800' |
|
|
}`}> |
|
|
{alert.level.toUpperCase()} |
|
|
</span> |
|
|
<span className="text-xs text-gray-500">{alert.category}</span> |
|
|
<span className="text-xs text-gray-400"> |
|
|
{new Date(alert.timestamp).toLocaleString()} |
|
|
</span> |
|
|
</div> |
|
|
<p className="text-sm font-medium text-gray-900">{alert.message}</p> |
|
|
{Object.keys(alert.details).length > 0 && ( |
|
|
<pre className="mt-2 text-xs text-gray-600 bg-white/50 p-2 rounded"> |
|
|
{JSON.stringify(alert.details, null, 2)} |
|
|
</pre> |
|
|
)} |
|
|
</div> |
|
|
{!alert.resolved && ( |
|
|
<button |
|
|
onClick={() => resolveAlert(alert.alert_id)} |
|
|
className="ml-4 px-3 py-1 bg-white border border-gray-300 text-gray-700 rounded hover:bg-gray-50 text-sm" |
|
|
> |
|
|
Resolve |
|
|
</button> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
))} |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default AdminDashboard; |
|
|
|