225 lines
10 KiB
JavaScript
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;
|