523 lines
21 KiB
JavaScript
523 lines
21 KiB
JavaScript
import React, { useState, useEffect } from 'react'
|
|
import api from '../services/api'
|
|
import toast from 'react-hot-toast'
|
|
import {
|
|
CogIcon,
|
|
ServerIcon,
|
|
CircleStackIcon,
|
|
ShieldCheckIcon,
|
|
ClockIcon,
|
|
CpuChipIcon,
|
|
ChartBarIcon,
|
|
ExclamationTriangleIcon,
|
|
CheckCircleIcon,
|
|
XCircleIcon
|
|
} from '@heroicons/react/24/outline'
|
|
|
|
const System = () => {
|
|
const [systemInfo, setSystemInfo] = useState(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [lastUpdate, setLastUpdate] = useState(null)
|
|
|
|
useEffect(() => {
|
|
loadSystemInfo()
|
|
const interval = setInterval(loadSystemInfo, 30000) // Update every 30 seconds
|
|
return () => clearInterval(interval)
|
|
}, [])
|
|
|
|
const loadSystemInfo = async () => {
|
|
try {
|
|
if (!loading) setLoading(false) // Don't show loading for refreshes
|
|
const response = await api.get('/management/system-info')
|
|
setSystemInfo(response.data.data)
|
|
setLastUpdate(new Date())
|
|
} catch (error) {
|
|
console.error('Failed to load system info:', error)
|
|
toast.error('Failed to load system information')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const StatusCard = ({ title, icon: Icon, children, status = 'normal' }) => {
|
|
const statusColors = {
|
|
normal: 'border-gray-200',
|
|
warning: 'border-yellow-300 bg-yellow-50',
|
|
critical: 'border-red-300 bg-red-50',
|
|
success: 'border-green-300 bg-green-50'
|
|
}
|
|
|
|
return (
|
|
<div className={`bg-white rounded-lg shadow border-2 p-6 ${statusColors[status]}`}>
|
|
<div className="flex items-center mb-4">
|
|
<Icon className="h-6 w-6 text-blue-600 mr-2" />
|
|
<h3 className="text-lg font-medium text-gray-900">{title}</h3>
|
|
</div>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const StatusIndicator = ({ status, label }) => {
|
|
const configs = {
|
|
valid: { color: 'bg-green-100 text-green-800', icon: CheckCircleIcon },
|
|
warning: { color: 'bg-yellow-100 text-yellow-800', icon: ExclamationTriangleIcon },
|
|
critical: { color: 'bg-red-100 text-red-800', icon: XCircleIcon },
|
|
error: { color: 'bg-red-100 text-red-800', icon: XCircleIcon },
|
|
connected: { color: 'bg-green-100 text-green-800', icon: CheckCircleIcon }
|
|
}
|
|
|
|
const config = configs[status] || configs.error
|
|
const Icon = config.icon
|
|
|
|
return (
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${config.color}`}>
|
|
<Icon className="h-3 w-3 mr-1" />
|
|
{label || status.charAt(0).toUpperCase() + status.slice(1)}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
const ProgressBar = ({ percentage, color = 'blue' }) => {
|
|
const colorClasses = {
|
|
blue: 'bg-blue-600',
|
|
green: 'bg-green-600',
|
|
yellow: 'bg-yellow-600',
|
|
red: 'bg-red-600'
|
|
}
|
|
|
|
const barColor = percentage > 80 ? 'red' : percentage > 60 ? 'yellow' : 'green'
|
|
|
|
return (
|
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className={`h-2 rounded-full transition-all duration-300 ${colorClasses[barColor]}`}
|
|
style={{ width: `${Math.min(percentage, 100)}%` }}
|
|
></div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const ContainerCard = ({ name, metrics }) => {
|
|
const getTypeIcon = (type) => {
|
|
switch (type) {
|
|
case 'database': return '🗄️';
|
|
case 'cache': return '⚡';
|
|
case 'proxy': return '🌐';
|
|
case 'application': return '📱';
|
|
case 'app': return '📱';
|
|
case 'logging': return '📋';
|
|
case 'monitoring': return '📊';
|
|
default: return '📦';
|
|
}
|
|
};
|
|
|
|
const getTypeColor = (type) => {
|
|
switch (type) {
|
|
case 'database': return 'border-l-blue-500';
|
|
case 'cache': return 'border-l-yellow-500';
|
|
case 'proxy': return 'border-l-green-500';
|
|
case 'application': case 'app': return 'border-l-purple-500';
|
|
case 'logging': return 'border-l-orange-500';
|
|
case 'monitoring': return 'border-l-red-500';
|
|
default: return 'border-l-gray-500';
|
|
}
|
|
};
|
|
|
|
const getStatusColor = (status) => {
|
|
switch (status) {
|
|
case 'healthy': case 'responding': return 'text-green-600';
|
|
case 'timeout': case 'warning': return 'text-yellow-600';
|
|
case 'unreachable': case 'error': case 'health_check_failed': return 'text-red-600';
|
|
case 'running': case 'Up': return 'text-green-600';
|
|
default: return 'text-gray-600';
|
|
}
|
|
};
|
|
|
|
const getStatusIcon = (status) => {
|
|
switch (status) {
|
|
case 'healthy': case 'responding': return '✅';
|
|
case 'timeout': return '⏱️';
|
|
case 'unreachable': case 'error': return '❌';
|
|
case 'running': case 'Up': return '🟢';
|
|
default: return '⚪';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={`bg-gray-50 rounded-lg p-4 border-l-4 ${getTypeColor(metrics.type)} border border-gray-200`}>
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h4 className="font-medium text-gray-900 truncate" title={name}>
|
|
<span className="mr-2">{getTypeIcon(metrics.type)}</span>
|
|
{name.replace('drone-detection-', '').replace('uamils-', '')}
|
|
</h4>
|
|
{metrics.type && (
|
|
<span className="text-xs bg-gray-200 text-gray-600 px-2 py-1 rounded-full">
|
|
{metrics.type}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-gray-500">Status</span>
|
|
<span className={`font-medium flex items-center ${getStatusColor(metrics.status)}`}>
|
|
<span className="mr-1">{getStatusIcon(metrics.status)}</span>
|
|
{metrics.status === 'health_check_failed' ? 'Failed' :
|
|
metrics.status.charAt(0).toUpperCase() + metrics.status.slice(1)}
|
|
</span>
|
|
</div>
|
|
|
|
{metrics.error && (
|
|
<div className="bg-red-50 border border-red-200 rounded p-2">
|
|
<div className="text-xs font-medium text-red-800">Error Details:</div>
|
|
<div className="text-xs text-red-700 mt-1">{metrics.error}</div>
|
|
</div>
|
|
)}
|
|
|
|
{metrics.raw && metrics.status === 'healthy' && (
|
|
<div className="bg-green-50 border border-green-200 rounded p-2">
|
|
<div className="text-xs text-green-700">Response: {metrics.raw.trim()}</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Show resource metrics if available */}
|
|
{metrics.cpu && (
|
|
<>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">CPU</span>
|
|
<span className="font-medium">{metrics.cpu}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">Memory</span>
|
|
<span className="font-medium">{metrics.memory?.percentage || metrics.memory}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">Network I/O</span>
|
|
<span className="font-medium text-xs">{metrics.network}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">Disk I/O</span>
|
|
<span className="font-medium text-xs">{metrics.disk}</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{metrics.health && metrics.health !== 'unknown' && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">Health</span>
|
|
<span className="font-medium">{metrics.health}</span>
|
|
</div>
|
|
)}
|
|
|
|
{metrics.ports && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">Ports</span>
|
|
<span className="font-medium text-xs">{metrics.ports}</span>
|
|
</div>
|
|
)}
|
|
|
|
{metrics.source && (
|
|
<div className="text-xs text-blue-600 mt-2 border-t pt-2">
|
|
Source: {metrics.source.replace('_', ' ').toUpperCase()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const SSLCard = ({ domain, ssl }) => (
|
|
<div className="bg-gray-50 rounded-lg p-4 border">
|
|
<h4 className="font-medium text-gray-900 mb-2 truncate" title={domain}>
|
|
{domain}
|
|
</h4>
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-sm text-gray-500">Status</span>
|
|
<StatusIndicator status={ssl.status} />
|
|
</div>
|
|
{ssl.expiresAt && (
|
|
<>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-gray-500">Expires</span>
|
|
<span className="font-medium">
|
|
{new Date(ssl.expiresAt).toLocaleDateString()}
|
|
</span>
|
|
</div>
|
|
{ssl.daysUntilExpiry !== undefined && (
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-gray-500">Days Left</span>
|
|
<span className={`font-medium ${ssl.daysUntilExpiry < 30 ? 'text-red-600' : 'text-green-600'}`}>
|
|
{ssl.daysUntilExpiry}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{ssl.issuer && (
|
|
<div className="text-xs text-gray-400 truncate" title={ssl.issuer}>
|
|
Issuer: {ssl.issuer}
|
|
</div>
|
|
)}
|
|
{ssl.fingerprint && (
|
|
<div className="text-xs text-gray-400 truncate" title={ssl.fingerprint}>
|
|
Fingerprint: {ssl.fingerprint.substring(0, 20)}...
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
{ssl.error && (
|
|
<div className="text-xs text-red-600 bg-red-50 p-2 rounded border">
|
|
<div className="font-medium">Error:</div>
|
|
<div>{ssl.error}</div>
|
|
{ssl.errorCode && (
|
|
<div className="text-gray-500 mt-1">Code: {ssl.errorCode}</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!systemInfo) {
|
|
return (
|
|
<div className="text-center py-12">
|
|
<XCircleIcon className="mx-auto h-12 w-12 text-red-400" />
|
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No system information available</h3>
|
|
<p className="mt-1 text-sm text-gray-500">Unable to load system metrics.</p>
|
|
<div className="mt-6">
|
|
<button
|
|
onClick={loadSystemInfo}
|
|
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-8 flex justify-between items-center">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">System Monitor</h1>
|
|
<p className="text-gray-600">Real-time system health and configuration monitoring</p>
|
|
{lastUpdate && (
|
|
<p className="text-xs text-gray-400 mt-1">
|
|
Last updated: {lastUpdate.toLocaleTimeString()}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={loadSystemInfo}
|
|
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
|
>
|
|
<CogIcon className="h-4 w-4 mr-2" />
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
|
|
{/* Platform Overview */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
<StatusCard title="Platform Status" icon={ServerIcon}>
|
|
<div className="space-y-3">
|
|
<div className="flex justify-between">
|
|
<span className="text-sm text-gray-500">Version</span>
|
|
<span className="text-sm font-medium">{systemInfo.platform.version}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-sm text-gray-500">Environment</span>
|
|
<span className="text-sm font-medium capitalize">{systemInfo.platform.environment}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-sm text-gray-500">Uptime</span>
|
|
<span className="text-sm font-medium">{systemInfo.platform.uptime}</span>
|
|
</div>
|
|
{/* Container Health Summary */}
|
|
<div className="border-t pt-2 mt-3">
|
|
<div className="text-xs text-gray-500 mb-1">Service Health</div>
|
|
<div className="flex space-x-1">
|
|
{Object.entries(systemInfo.containers).map(([name, metrics]) => {
|
|
const isHealthy = metrics.status === 'healthy' || metrics.status === 'responding';
|
|
return (
|
|
<div
|
|
key={name}
|
|
className={`w-3 h-3 rounded-full ${
|
|
isHealthy ? 'bg-green-400' :
|
|
metrics.status === 'timeout' ? 'bg-yellow-400' : 'bg-red-400'
|
|
}`}
|
|
title={`${name}: ${metrics.status}`}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</StatusCard>
|
|
|
|
<StatusCard title="System Resources" icon={CpuChipIcon}>
|
|
{systemInfo.system.error ? (
|
|
<div className="text-center text-red-600">
|
|
<div className="font-medium">System metrics unavailable</div>
|
|
<div className="text-xs mt-1">{systemInfo.system.message}</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
<div>
|
|
<div className="flex justify-between text-sm mb-1">
|
|
<span className="text-gray-500">CPU Usage</span>
|
|
<span className="font-medium">{systemInfo.system.cpu.usage}</span>
|
|
</div>
|
|
<ProgressBar percentage={systemInfo.system.cpu.percentage} />
|
|
</div>
|
|
<div>
|
|
<div className="flex justify-between text-sm mb-1">
|
|
<span className="text-gray-500">Memory</span>
|
|
<span className="font-medium">{systemInfo.system.memory.used} / {systemInfo.system.memory.total}</span>
|
|
</div>
|
|
<ProgressBar percentage={systemInfo.system.memory.percentage} />
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-gray-500">Disk</span>
|
|
<span className="font-medium text-xs">{systemInfo.system.disk}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</StatusCard>
|
|
|
|
<StatusCard title="Statistics" icon={ChartBarIcon}>
|
|
<div className="space-y-3">
|
|
<div className="flex justify-between">
|
|
<span className="text-sm text-gray-500">Tenants</span>
|
|
<span className="text-sm font-medium">{systemInfo.statistics.tenants}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-sm text-gray-500">Total Users</span>
|
|
<span className="text-sm font-medium">{systemInfo.statistics.total_users}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-sm text-gray-500">Access Level</span>
|
|
<span className="text-sm font-medium capitalize">{systemInfo.security.management_access_level}</span>
|
|
</div>
|
|
</div>
|
|
</StatusCard>
|
|
|
|
<StatusCard title="Backup Status" icon={ClockIcon}>
|
|
<div className="text-center">
|
|
<div className="text-sm font-medium text-gray-900 mb-2">
|
|
{systemInfo.security.last_backup !== 'Not configured'
|
|
? new Date(systemInfo.security.last_backup).toLocaleDateString()
|
|
: 'Not configured'
|
|
}
|
|
</div>
|
|
<button className="w-full bg-blue-600 text-white px-3 py-2 rounded-lg hover:bg-blue-700 transition-colors text-sm">
|
|
Run Backup
|
|
</button>
|
|
</div>
|
|
</StatusCard>
|
|
</div>
|
|
|
|
{/* Container Metrics */}
|
|
<div className="mb-8">
|
|
<StatusCard title="Container Metrics" icon={ServerIcon}>
|
|
{systemInfo.containers.error ? (
|
|
<div className="text-center text-red-600 py-8">
|
|
<XCircleIcon className="mx-auto h-12 w-12 text-red-400 mb-2" />
|
|
<div className="font-medium mb-2">Container monitoring unavailable</div>
|
|
<div className="text-sm mb-4">{systemInfo.containers.lastError}</div>
|
|
|
|
{systemInfo.containers.troubleshooting && (
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 text-left text-sm">
|
|
<h5 className="font-medium text-blue-900 mb-2">💡 Troubleshooting Tips:</h5>
|
|
<ul className="space-y-1 text-blue-800">
|
|
<li>• {systemInfo.containers.troubleshooting.docker_access}</li>
|
|
<li>• {systemInfo.containers.troubleshooting.permissions}</li>
|
|
<li>• {systemInfo.containers.troubleshooting.environment}</li>
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{systemInfo.containers.suggestions && (
|
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 text-left text-sm mt-3">
|
|
<h5 className="font-medium text-yellow-900 mb-2">🔧 Quick Fixes:</h5>
|
|
<ul className="space-y-1 text-yellow-800">
|
|
{systemInfo.containers.suggestions.map((suggestion, index) => (
|
|
<li key={index}>• {suggestion}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : systemInfo.containers.info ? (
|
|
<div className="text-center text-gray-600 py-8">
|
|
<ServerIcon className="mx-auto h-12 w-12 text-gray-400 mb-2" />
|
|
<div className="font-medium mb-2">{systemInfo.containers.info}</div>
|
|
<div className="text-sm">{systemInfo.containers.message}</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-6">
|
|
{/* Group containers by type */}
|
|
{['application', 'database', 'cache', 'proxy', 'logging', 'monitoring', 'unknown'].map(type => {
|
|
const containersOfType = Object.entries(systemInfo.containers).filter(([name, metrics]) =>
|
|
metrics.type === type || (type === 'unknown' && !metrics.type)
|
|
);
|
|
|
|
if (containersOfType.length === 0) return null;
|
|
|
|
const typeLabels = {
|
|
application: '📱 Application Services',
|
|
database: '🗄️ Database Services',
|
|
cache: '⚡ Cache Services',
|
|
proxy: '🌐 Proxy & Load Balancers',
|
|
logging: '📋 Logging Services',
|
|
monitoring: '📊 Monitoring Services',
|
|
unknown: '📦 Other Services'
|
|
};
|
|
|
|
return (
|
|
<div key={type}>
|
|
<h4 className="text-sm font-medium text-gray-700 mb-3 border-b pb-2">
|
|
{typeLabels[type]} ({containersOfType.length})
|
|
</h4>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{containersOfType.map(([name, metrics]) => (
|
|
<ContainerCard key={name} name={name} metrics={metrics} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</StatusCard>
|
|
</div>
|
|
|
|
{/* SSL Certificates */}
|
|
<div className="mb-8">
|
|
<StatusCard
|
|
title="SSL Certificates"
|
|
icon={ShieldCheckIcon}
|
|
status={Object.values(systemInfo.ssl).some(ssl => ssl.status === 'critical') ? 'critical' :
|
|
Object.values(systemInfo.ssl).some(ssl => ssl.status === 'warning') ? 'warning' : 'success'}
|
|
>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{Object.entries(systemInfo.ssl).map(([domain, ssl]) => (
|
|
<SSLCard key={domain} domain={domain} ssl={ssl} />
|
|
))}
|
|
</div>
|
|
</StatusCard>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default System
|