+ {sortedNodes.map((node) => {
+ const running = groups[node].filter((l) => l.status === 'running');
+ const stopped = groups[node].filter((l) => l.status !== 'running');
+ const expanded = expandedNodes[node] ?? false;
- {stopped.length > 0 && (
-
-
Stopped ({stopped.length})
-
-
-
-
- | ID |
- Name |
- Node |
- Disk |
- Status |
- Actions |
-
-
-
- {stopped.map((item) => (
-
- | {item.vmid} |
- {item.name} |
- {item.node} |
- {formatBytes(item.disk?.used || 0)} |
- |
-
-
-
- |
-
- ))}
-
-
+ return (
+
+ {/* Node header */}
+
+
+ {/* Node body */}
+ {expanded && (
+ <>
+ {running.length > 0 && (
+
+
Running ({running.length})
+
+
+
+
+ | ID |
+ Name |
+ CPU |
+ Memory |
+ Disk |
+ Status |
+ Actions |
+
+
+
+ {running.map((item) => (
+
+ | {item.vmid} |
+ {item.name} |
+ {(item.cpu * 100).toFixed(1)}% |
+ {formatBytes(item.memory?.used || 0)} / {formatBytes(item.memory?.total || 0)} |
+ {formatBytes(item.disk?.used || 0)} |
+ |
+
+
+
+
+ |
+
+ ))}
+
+
+
+
+ )}
+
+ {stopped.length > 0 && (
+
+
Stopped ({stopped.length})
+
+
+
+
+ | ID |
+ Name |
+ Disk |
+ Status |
+ Actions |
+
+
+
+ {stopped.map((item) => (
+
+ | {item.vmid} |
+ {item.name} |
+ {formatBytes(item.disk?.used || 0)} |
+ |
+
+
+
+ |
+
+ ))}
+
+
+
+
+ )}
+ >
+ )}
-
- )}
+ );
+ })}
);
}
diff --git a/src/components/VMList.js b/src/components/VMList.js
index e902ad3..65a2f7f 100644
--- a/src/components/VMList.js
+++ b/src/components/VMList.js
@@ -1,119 +1,152 @@
+'use client';
+
+import { useState } from 'react';
import StatusBadge from './StatusBadge';
-/**
- * @param {{ vms: 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 {{ vms: Array<{ vmid: number; name: string; type: string; status: string; cpu: number; memory: { total: number; used: number }; disk: { total: number; used: number }; node: string; consoleUrl?: string }>, onPowerAction: (node: string, vmid: number, type: string, action: string) => void }} props
*/
export default function VMList({ vms, onPowerAction, onOpenConsole }) {
+ const [expandedNodes, setExpandedNodes] = useState({});
+
if (!vms?.length) {
return
No VMs found.
;
}
- const running = vms.filter((vm) => vm.status === 'running');
- const stopped = vms.filter((vm) => vm.status !== 'running');
+ const groups = Object.groupBy(vms, (vm) => vm.node);
+ const sortedNodes = Object.keys(groups).sort();
+
+ function toggleNode(node) {
+ setExpandedNodes((prev) => ({ ...prev, [node]: !prev[node] }));
+ }
+
+ function allExpanded() {
+ return sortedNodes.every((n) => expandedNodes[n]);
+ }
return (
-
- {running.length > 0 && (
-
-
Running ({running.length})
-
-
-
-
- | VMID |
- Name |
- Node |
- CPU |
- Memory |
- Status |
- Actions |
-
-
-
- {running.map((vm) => (
-
- | {vm.vmid} |
- {vm.name} |
- {vm.node} |
- {(vm.cpu * 100).toFixed(1)}% |
- {formatBytes(vm.memory?.used || 0)} / {formatBytes(vm.memory?.total || 0)} |
- |
-
-
-
-
- |
-
- ))}
-
-
-
-
- )}
+
+ {sortedNodes.map((node) => {
+ const running = groups[node].filter((vm) => vm.status === 'running');
+ const stopped = groups[node].filter((vm) => vm.status !== 'running');
+ const expanded = expandedNodes[node] ?? false;
- {stopped.length > 0 && (
-
-
Stopped ({stopped.length})
-
-
-
-
- | VMID |
- Name |
- Node |
- Disk |
- Status |
- Actions |
-
-
-
- {stopped.map((vm) => (
-
- | {vm.vmid} |
- {vm.name} |
- {vm.node} |
- {formatBytes(vm.disk?.used || 0)} |
- |
-
-
-
- |
-
- ))}
-
-
+ return (
+
+ {/* Node header */}
+
+
+ {/* Node body */}
+ {expanded && (
+ <>
+ {running.length > 0 && (
+
+
Running ({running.length})
+
+
+
+
+ | VMID |
+ Name |
+ CPU |
+ Memory |
+ Disk |
+ Status |
+ Actions |
+
+
+
+ {running.map((vm) => (
+
+ | {vm.vmid} |
+ {vm.name} |
+ {(vm.cpu * 100).toFixed(1)}% |
+ {formatBytes(vm.memory?.used || 0)} / {formatBytes(vm.memory?.total || 0)} |
+ {formatBytes(vm.disk?.used || 0)} |
+ |
+
+
+
+
+ |
+
+ ))}
+
+
+
+
+ )}
+
+ {stopped.length > 0 && (
+
+
Stopped ({stopped.length})
+
+
+
+
+ | VMID |
+ Name |
+ Disk |
+ Status |
+ Actions |
+
+
+
+ {stopped.map((vm) => (
+
+ | {vm.vmid} |
+ {vm.name} |
+ {formatBytes(vm.disk?.used || 0)} |
+ |
+
+
+
+ |
+
+ ))}
+
+
+
+
+ )}
+ >
+ )}
-
- )}
+ );
+ })}
);
}