- New POST /api/power route for Proxmox power operations - powerControl() method in proxmox.js library - Start/Stop/Restart action buttons in VMList and LXCList tables - Status feedback banner in Dashboard on power action Co-Authored-By: Claude <noreply@anthropic.com>
117 lines
6.4 KiB
JavaScript
117 lines
6.4 KiB
JavaScript
import StatusBadge from './StatusBadge';
|
|
|
|
/**
|
|
* @param {{ lxc: Array<{ vmid: number; name: string; type: string; status: string; cpu: number; memory: { total: number; used: number }; disk: { total: number; used: number }; node: string }> }} props
|
|
* @returns {import('react').ReactNode}
|
|
*/
|
|
/**
|
|
* @param {{ lxc: Array<{ vmid: number; name: string; type: string; status: string; cpu: number; memory: { total: number; used: number }; disk: { total: number; used: number }; node: string }>, onPowerAction: (node: string, vmid: number, type: string, action: string) => void }} props
|
|
*/
|
|
export default function LXCList({ lxc, onPowerAction }) {
|
|
if (!lxc?.length) {
|
|
return <p className="text-text-secondary text-sm">No LXC containers found.</p>;
|
|
}
|
|
|
|
const running = lxc.filter((item) => item.status === 'running');
|
|
const stopped = lxc.filter((item) => item.status !== 'running');
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{running.length > 0 && (
|
|
<div>
|
|
<h4 className="mb-2 text-xs font-medium uppercase tracking-wider text-status-green">Running ({running.length})</h4>
|
|
<div className="overflow-x-auto rounded-xl border border-border-card bg-bg-card">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-border-card">
|
|
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wider text-text-secondary">ID</th>
|
|
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wider text-text-secondary">Name</th>
|
|
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wider text-text-secondary">Node</th>
|
|
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wider text-text-secondary">CPU</th>
|
|
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wider text-text-secondary">Memory</th>
|
|
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wider text-text-secondary">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{running.map((item) => (
|
|
<tr key={item.vmid} className="border-b border-border-card last:border-0">
|
|
<td className="px-4 py-2 text-text-secondary">{item.vmid}</td>
|
|
<td className="px-4 py-2 font-medium text-text-primary">{item.name}</td>
|
|
<td className="px-4 py-2 text-text-secondary">{item.node}</td>
|
|
<td className="px-4 py-2 text-text-secondary">{(item.cpu * 100).toFixed(1)}%</td>
|
|
<td className="px-4 py-2 text-text-secondary">{formatBytes(item.memory?.used || 0)} / {formatBytes(item.memory?.total || 0)}</td>
|
|
<td className="px-4 py-2"><StatusBadge status={item.status} /></td>
|
|
<td className="px-4 py-2">
|
|
<button
|
|
onClick={() => onPowerAction(item.node, item.vmid, 'lxc', 'restart')}
|
|
className="mr-2 rounded-md border border-border-card bg-bg-card px-2 py-1 text-xs text-text-primary hover:border-accent hover:text-accent"
|
|
>
|
|
Restart
|
|
</button>
|
|
<button
|
|
onClick={() => onPowerAction(item.node, item.vmid, 'lxc', 'stop')}
|
|
className="rounded-md border border-status-red/30 bg-status-red/10 px-2 py-1 text-xs text-status-red hover:bg-status-red/20"
|
|
>
|
|
Stop
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{stopped.length > 0 && (
|
|
<div>
|
|
<h4 className="mb-2 text-xs font-medium uppercase tracking-wider text-status-red">Stopped ({stopped.length})</h4>
|
|
<div className="overflow-x-auto rounded-xl border border-border-card bg-bg-card">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-border-card">
|
|
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wider text-text-secondary">ID</th>
|
|
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wider text-text-secondary">Name</th>
|
|
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wider text-text-secondary">Node</th>
|
|
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wider text-text-secondary">Disk</th>
|
|
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wider text-text-secondary">Status</th>
|
|
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wider text-text-secondary">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{stopped.map((item) => (
|
|
<tr key={item.vmid} className="border-b border-border-card last:border-0">
|
|
<td className="px-4 py-2 text-text-secondary">{item.vmid}</td>
|
|
<td className="px-4 py-2 font-medium text-text-primary">{item.name}</td>
|
|
<td className="px-4 py-2 text-text-secondary">{item.node}</td>
|
|
<td className="px-4 py-2 text-text-secondary">{formatBytes(item.disk?.used || 0)}</td>
|
|
<td className="px-4 py-2"><StatusBadge status={item.status} /></td>
|
|
<td className="px-4 py-2">
|
|
<button
|
|
onClick={() => onPowerAction(item.node, item.vmid, 'lxc', 'start')}
|
|
className="rounded-md border border-status-green/30 bg-status-green/10 px-2 py-1 text-xs text-status-green hover:bg-status-green/20"
|
|
>
|
|
Start
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** @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]}`;
|
|
}
|