sawa-control-panel/frontend/src/pages/SystemMonitor.jsx

225 lines
10 KiB
JavaScript

import React, { useState, useEffect } from 'react';
const SystemMonitor = () => {
const [metrics, setMetrics] = useState({
cpu: 0,
memory: { total: 0, used: 0, free: 0 },
disk: [],
uptime: { seconds: 0, human: 'Loading...' },
load: { one: 0, five: 0, fifteen: 0 }
});
const [error, setError] = useState(null);
const fetchCPU = async () => {
try {
const res = await fetch('/api/v1/system/cpu');
const json = await res.json();
if (json.success) setMetrics(prev => ({ ...prev, cpu: json.usage }));
} catch (err) {
console.error('CPU fetch failed', err);
}
};
const fetchOtherMetrics = async () => {
try {
const [memRes, diskRes, uptimeRes, loadRes] = await Promise.all([
fetch('/api/v1/system/memory'),
fetch('/api/v1/system/disk'),
fetch('/api/v1/system/uptime'),
fetch('/api/v1/system/load')
]);
const [memJson, diskJson, uptimeJson, loadJson] = await Promise.all([
memRes.json(),
diskRes.json(),
uptimeRes.json(),
loadRes.json()
]);
setMetrics(prev => ({
...prev,
memory: memJson.success ? { total: memJson.total, used: memJson.used, free: memJson.free } : prev.memory,
disk: diskJson.success ? diskJson.partitions : prev.disk,
uptime: uptimeJson.success ? { seconds: uptimeJson.seconds, human: uptimeJson.human } : prev.uptime,
load: loadJson.success ? { one: loadJson.one, five: loadJson.five, fifteen: loadJson.fifteen } : prev.load
}));
setError(null);
} catch (err) {
setError('Failed to fetch system metrics');
console.error('Metrics fetch failed', err);
}
};
useEffect(() => {
fetchCPU();
fetchOtherMetrics();
const cpuInterval = setInterval(fetchCPU, 5000);
const otherInterval = setInterval(fetchOtherMetrics, 10000);
return () => {
clearInterval(cpuInterval);
clearInterval(otherInterval);
};
}, []);
const getStatusColor = (percent, thresholds) => {
if (percent < thresholds.yellow) return 'text-green-500 bg-green-500';
if (percent < thresholds.red) return 'text-yellow-500 bg-yellow-500';
return 'text-red-500 bg-red-500';
};
const ProgressBar = ({ label, used, total, unit = 'MB', colorClass }) => {
const percent = total > 0 ? (used / total) * 100 : 0;
const barColor = colorClass.split(' ')[1];
const textColor = colorClass.split(' ')[0];
return (
<div className="mb-6">
<div className="flex justify-between items-center mb-2">
<span className="text-gray-400 font-medium">{label}</span>
<span className={`font-bold ${textColor}`}>
{used} / {total} {unit} ({percent.toFixed(1)}%)
</span>
</div>
<div className="w-full bg-gray-800 rounded-full h-3 overflow-hidden">
<div
className={`h-full ${barColor} transition-all duration-1000 ease-out`}
style={{ width: `${percent}%` }}
/>
</div>
</div>
);
};
const Gauge = ({ value, label }) => {
const radius = 40;
const circumference = 2 * Math.PI * radius;
const offset = circumference - (value / 100) * circumference;
const colorClass = getStatusColor(value, { yellow: 50, red: 80 }).split(' ')[0];
return (
<div className="flex flex-col items-center">
<div className="relative w-32 h-32 flex items-center justify-center">
<svg className="w-full h-full transform -rotate-90">
<circle
cx="64" cy="64" r={radius}
className="stroke-gray-800 fill-none"
strokeWidth="10"
/>
<circle
cx="64" cy="64" r={radius}
className={`${colorClass} stroke-current fill-none transition-all duration-1000 ease-out`}
strokeWidth="10"
strokeDasharray={circumference}
strokeDashoffset={offset}
strokeLinecap="round"
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className={`text-2xl font-bold ${colorClass}`}>{value}%</span>
</div>
</div>
<span className="text-gray-400 mt-2 font-medium">{label}</span>
</div>
);
};
const getLoadColor = (load) => {
if (load < 1.0) return 'text-green-500';
if (load < 2.0) return 'text-yellow-500';
return 'text-red-500';
};
return (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-300">
{error && (
<div className="bg-red-900/30 border border-red-500 text-red-500 px-4 py-3 rounded-lg flex justify-between items-center">
<span>{error}</span>
<button onClick={() => fetchOtherMetrics()} className="underline text-sm hover:no-underline font-bold">Retry</button>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* CPU Gauge Card */}
<div className="bg-gray-900 border border-gray-800 rounded-xl p-6 flex items-center justify-center shadow-lg">
<Gauge value={metrics.cpu} label="CPU Usage" />
</div>
{/* Load Average Card */}
<div className="bg-gray-900 border border-gray-800 rounded-xl p-6 shadow-lg">
<h3 className="text-gray-500 font-bold mb-6 text-center uppercase tracking-widest text-[10px]">Load Average</h3>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<div className={`text-xl font-bold ${getLoadColor(metrics.load.one)}`}>
{metrics.load.one.toFixed(2)}
</div>
<div className="text-[10px] text-gray-500 mt-1 font-bold uppercase">1 Min</div>
</div>
<div>
<div className="text-xl font-bold text-gray-200">{metrics.load.five.toFixed(2)}</div>
<div className="text-[10px] text-gray-500 mt-1 font-bold uppercase">5 Min</div>
</div>
<div>
<div className="text-xl font-bold text-gray-200">{metrics.load.fifteen.toFixed(2)}</div>
<div className="text-[10px] text-gray-500 mt-1 font-bold uppercase">15 Min</div>
</div>
</div>
</div>
{/* Uptime Card */}
<div className="bg-gray-900 border border-gray-800 rounded-xl p-6 flex flex-col items-center justify-center shadow-lg text-center">
<h3 className="text-gray-500 font-bold mb-2 uppercase tracking-widest text-[10px]">System Uptime</h3>
<div className="text-xl font-bold text-blue-400">{metrics.uptime.human}</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Memory Usage */}
<div className="bg-gray-900 border border-gray-800 rounded-xl p-6 shadow-lg">
<h3 className="text-gray-500 font-bold mb-6 flex items-center uppercase tracking-widest text-[10px]">
<span className="w-1.5 h-1.5 bg-blue-500 rounded-full mr-2"></span>
Memory Breakdown
</h3>
<ProgressBar
label="RAM"
used={metrics.memory.used}
total={metrics.memory.total}
colorClass={getStatusColor((metrics.memory.used / metrics.memory.total) * 100, { yellow: 70, red: 85 })}
/>
</div>
{/* Disk Usage */}
<div className="bg-gray-900 border border-gray-800 rounded-xl p-6 shadow-lg">
<h3 className="text-gray-500 font-bold mb-6 flex items-center uppercase tracking-widest text-[10px]">
<span className="w-1.5 h-1.5 bg-purple-500 rounded-full mr-2"></span>
Storage Health
</h3>
{metrics.disk.length > 0 ? (
metrics.disk.map((p, idx) => {
const usedVal = parseFloat(p.used);
const totalVal = parseFloat(p.total);
const unit = p.unit || 'GB';
const percentNum = parseInt(p.percent);
return (
<ProgressBar
key={idx}
label={p.mount}
used={usedVal}
total={totalVal}
unit={unit}
colorClass={getStatusColor(percentNum, { yellow: 70, red: 85 })}
/>
);
})
) : (
<div className="text-gray-600 italic text-center py-4 text-xs font-bold uppercase">Scanning disks...</div>
)}
</div>
</div>
</div>
);
};
export default SystemMonitor;