- Now formats as "Xd Yh Zm", "Yh Zm", or "Zm" depending on value size - Backlog: mark Uptime Duration as done Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
85 lines
2.8 KiB
JavaScript
85 lines
2.8 KiB
JavaScript
import StatusBadge from './StatusBadge';
|
|
|
|
/**
|
|
* Formats bytes to human-readable string.
|
|
* @param {number} bytes
|
|
* @returns {string}
|
|
*/
|
|
function formatBytes(bytes) {
|
|
if (!bytes) return '0 B';
|
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
let i = 0;
|
|
let v = bytes;
|
|
while (v >= 1024 && i < units.length - 1) {
|
|
v /= 1024;
|
|
i++;
|
|
}
|
|
return `${v.toFixed(1)} ${units[i]}`;
|
|
}
|
|
|
|
/**
|
|
* Formats seconds to human-readable duration.
|
|
* @param {number} seconds
|
|
* @returns {string}
|
|
*/
|
|
function formatUptime(seconds) {
|
|
if (!seconds) return 'N/A';
|
|
const days = Math.floor(seconds / 86400);
|
|
const hours = Math.floor((seconds % 86400) / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
if (days) return `${days}d ${hours}h ${minutes}m`;
|
|
if (hours) return `${hours}h ${minutes}m`;
|
|
return `${minutes}m`;
|
|
}
|
|
|
|
/**
|
|
* @param {{ node: { name: string; type: string; cpu: number; memory: { total: number; used: number }; status: string; uptime: number; load: number } }} props
|
|
* @returns {import('react').ReactNode}
|
|
*/
|
|
export default function NodeCard({ node }) {
|
|
const cpuPercent = Math.round((node.cpu || 0) * 100);
|
|
const memPercent = node.memory?.total ? Math.round((node.memory.used / node.memory.total) * 100) : 0;
|
|
|
|
return (
|
|
<div className="rounded-xl border border-border-card bg-bg-card p-5 transition-colors hover:bg-bg-card-hover">
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<h3 className="text-lg font-semibold text-text-primary">{node.name}</h3>
|
|
<StatusBadge status={node.status} />
|
|
</div>
|
|
|
|
<div className="space-y-3 text-sm text-text-secondary">
|
|
<div className="flex justify-between">
|
|
<span>CPU</span>
|
|
<span className="text-text-primary">{cpuPercent}%</span>
|
|
</div>
|
|
<div className="h-2 w-full rounded-full bg-bg-primary">
|
|
<div
|
|
className="h-full rounded-full bg-accent transition-all"
|
|
style={{ width: `${Math.min(cpuPercent, 100)}%` }}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-between">
|
|
<span>Memory</span>
|
|
<span className="text-text-primary">{formatBytes(node.memory?.used || 0)} / {formatBytes(node.memory?.total || 0)}</span>
|
|
</div>
|
|
<div className="h-2 w-full rounded-full bg-bg-primary">
|
|
<div
|
|
className={`h-full rounded-full transition-all ${
|
|
memPercent > 90 ? 'bg-status-red' :
|
|
memPercent > 70 ? 'bg-status-yellow' :
|
|
'bg-accent'
|
|
}`}
|
|
style={{ width: `${Math.min(memPercent, 100)}%` }}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-between text-xs">
|
|
<span>Load: {node.load?.toFixed(2) || 'N/A'}</span>
|
|
<span>Uptime: {formatUptime(node.uptime)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|