diff --git a/src/components/Proxmox/VMList.tsx b/src/components/Proxmox/VMList.tsx index f7ce6242..d1b9cbf9 100644 --- a/src/components/Proxmox/VMList.tsx +++ b/src/components/Proxmox/VMList.tsx @@ -114,6 +114,10 @@ export function VMList({ }>({ isOpen: false, vm: null, action: null, snapshots: [] }); const [snapshotName, setSnapshotName] = useState(''); const [selectedSnapshot, setSelectedSnapshot] = useState(''); + const [cloneDialog, setCloneDialog] = useState<{ isOpen: boolean; vm: VMInfo | null }>({ isOpen: false, vm: null }); + const [cloneVmid, setCloneVmid] = useState(''); + const [cloneName, setCloneName] = useState(''); + const [cloneSubmitting, setCloneSubmitting] = useState(false); const vms: VMInfo[] = React.useMemo(() => { return rawVms.map((vm) => ({ @@ -349,44 +353,42 @@ export function VMList({ } }, [migrationVM, targetNode, targetCluster, clusterId, onRefresh]); - const handleClone = useCallback(async (vm: VMInfo) => { - if (!clusterId) { - toast.error('No cluster selected'); - return; - } - try { - const nextVmid = Math.max(...vms.map((v) => v.vmid), 100) + 1; - const newVmidStr = window.prompt(`Enter new VM ID for ${vm.name}:`, `${nextVmid}`); - if (!newVmidStr) { - toast.info('Clone cancelled'); - return; - } - const newVmid = parseInt(newVmidStr); - if (isNaN(newVmid) || newVmid < 100) { - toast.error('Invalid VM ID. Must be >= 100'); - return; - } - const newName = window.prompt(`Enter name for cloned VM:`, `${vm.name}-clone`); - if (!newName) { - toast.info('Clone cancelled'); - return; - } + const handleClone = useCallback((vm: VMInfo) => { + if (!clusterId) { toast.error('No cluster selected'); return; } + const nextVmid = Math.max(...vms.map((v) => v.vmid), 100) + 1; + setCloneVmid(String(nextVmid)); + setCloneName(`${vm.name}-clone`); + setCloneDialog({ isOpen: true, vm }); + }, [clusterId, vms]); + const handleCloneSubmit = useCallback(async () => { + if (!cloneDialog.vm || !clusterId) return; + const vm = cloneDialog.vm; + const newVmid = parseInt(cloneVmid); + if (isNaN(newVmid) || newVmid < 100) { toast.error('VM ID must be ≥ 100'); return; } + if (!cloneName.trim()) { toast.error('Clone name is required'); return; } + setCloneSubmitting(true); + try { await invoke('clone_vm', { clusterId, nodeId: vm.node, vmId: vm.vmid, newVmid, - name: newName, + name: cloneName.trim(), }); - - toast.success(`VM ${vm.name} cloned successfully to VM ${newVmid}`); + toast.success(`VM ${vm.name} cloned to VM ${newVmid}`); + setCloneDialog({ isOpen: false, vm: null }); onRefresh?.(); } catch (error) { - console.error('Failed to clone VM:', error); toast.error(`Failed to clone VM ${vm.name}: ${error}`); + } finally { + setCloneSubmitting(false); } - }, [clusterId, vms, onRefresh]); + }, [cloneDialog, clusterId, cloneVmid, cloneName, onRefresh]); + + const handleCloneClose = useCallback(() => { + setCloneDialog({ isOpen: false, vm: null }); + }, []); const handleDelete = useCallback(async (vm: VMInfo) => { if (!clusterId) { @@ -547,6 +549,18 @@ export function VMList({ onSubmit={handleSnapshotSubmit} onClose={handleSnapshotClose} /> + + void handleCloneSubmit()} + onClose={handleCloneClose} + /> ); } @@ -1009,3 +1023,61 @@ function SnapshotDialog({ ); } + +// ─── Clone Dialog ───────────────────────────────────────────────────────────── + +interface CloneDialogProps { + isOpen: boolean; + vm: VMInfo | null; + vmid: string; + name: string; + submitting: boolean; + onVmidChange: (v: string) => void; + onNameChange: (v: string) => void; + onSubmit: () => void; + onClose: () => void; +} + +function CloneDialog({ isOpen, vm, vmid, name, submitting, onVmidChange, onNameChange, onSubmit, onClose }: CloneDialogProps) { + if (!vm) return null; + return ( + !open && onClose()}> + + + Clone {vm.name} (VM {vm.vmid}) + Enter details for the cloned VM. + +
+
+ + onVmidChange(e.target.value)} + disabled={submitting} + /> +
+
+ + onNameChange(e.target.value)} + placeholder={`${vm.name}-clone`} + disabled={submitting} + /> +
+
+ + + + +
+
+ ); +}