fix(proxmox): address PR review suggestions
Some checks failed
PR Review Automation / review (pull_request) Has been cancelled
Test / frontend-tests (pull_request) Has been cancelled
Test / rust-tests (pull_request) Has been cancelled
Test / rust-clippy (pull_request) Has been cancelled
Test / frontend-typecheck (pull_request) Has been cancelled
Test / rust-fmt-check (pull_request) Has been cancelled

- Cores max aligned to backend limit (128→512)
- MigrationDialog shows loading state while node list fetches
- CreateVmDialog validates ISO format client-side on change, blocks
  submit and highlights field on invalid input
This commit is contained in:
Shaun Arman 2026-06-21 18:16:13 -05:00
parent a9a063f786
commit 64fc808908
2 changed files with 34 additions and 5 deletions

View File

@ -40,6 +40,7 @@ export function CreateVmDialog({ isOpen, clusterId, onClose, onCreated }: Create
const [diskSize, setDiskSize] = useState(20); const [diskSize, setDiskSize] = useState(20);
const [netBridge, setNetBridge] = useState('vmbr0'); const [netBridge, setNetBridge] = useState('vmbr0');
const [iso, setIso] = useState(''); const [iso, setIso] = useState('');
const [isoError, setIsoError] = useState('');
useEffect(() => { useEffect(() => {
if (!isOpen || !clusterId) return; if (!isOpen || !clusterId) return;
@ -69,10 +70,23 @@ export function CreateVmDialog({ isOpen, clusterId, onClose, onCreated }: Create
}); });
}, [isOpen, clusterId]); }, [isOpen, clusterId]);
const ISO_RE = /^[a-zA-Z0-9_-]+:iso\/.+\.iso$/;
const validateIso = (value: string): string => {
if (!value) return '';
return ISO_RE.test(value) ? '' : "Must be in the format 'storage:iso/filename.iso'";
};
const handleIsoChange = (value: string) => {
setIso(value);
setIsoError(validateIso(value));
};
const handleSubmit = async () => { const handleSubmit = async () => {
if (!nodeId) { toast.error('Please select a target node'); return; } if (!nodeId) { toast.error('Please select a target node'); return; }
if (!name.trim()) { toast.error('VM name is required'); return; } if (!name.trim()) { toast.error('VM name is required'); return; }
if (vmid < 100 || vmid > 999999999) { toast.error('VMID must be between 100 and 999999999'); return; } if (vmid < 100 || vmid > 999999999) { toast.error('VMID must be between 100 and 999999999'); return; }
if (isoError) { toast.error(isoError); return; }
setIsSubmitting(true); setIsSubmitting(true);
try { try {
@ -109,6 +123,7 @@ export function CreateVmDialog({ isOpen, clusterId, onClose, onCreated }: Create
setDiskSize(20); setDiskSize(20);
setNetBridge('vmbr0'); setNetBridge('vmbr0');
setIso(''); setIso('');
setIsoError('');
onClose(); onClose();
}; };
@ -189,7 +204,7 @@ export function CreateVmDialog({ isOpen, clusterId, onClose, onCreated }: Create
id="vm-cores" id="vm-cores"
type="number" type="number"
min={1} min={1}
max={128} max={512}
value={cores} value={cores}
onChange={(e) => setCores(Number(e.target.value))} onChange={(e) => setCores(Number(e.target.value))}
/> />
@ -257,10 +272,15 @@ export function CreateVmDialog({ isOpen, clusterId, onClose, onCreated }: Create
<Input <Input
id="vm-iso" id="vm-iso"
value={iso} value={iso}
onChange={(e) => setIso(e.target.value)} onChange={(e) => handleIsoChange(e.target.value)}
placeholder="local:iso/ubuntu-24.04.iso" placeholder="local:iso/ubuntu-24.04.iso"
className={isoError ? 'border-red-500' : ''}
/> />
{isoError ? (
<p className="text-xs text-red-500">{isoError}</p>
) : (
<p className="text-xs text-muted-foreground">Format: storage:iso/filename.iso</p> <p className="text-xs text-muted-foreground">Format: storage:iso/filename.iso</p>
)}
</div> </div>
</div> </div>
@ -268,7 +288,7 @@ export function CreateVmDialog({ isOpen, clusterId, onClose, onCreated }: Create
<Button variant="outline" onClick={handleClose} disabled={isSubmitting}> <Button variant="outline" onClick={handleClose} disabled={isSubmitting}>
Cancel Cancel
</Button> </Button>
<Button onClick={handleSubmit} disabled={isSubmitting || !nodeId || !name.trim()}> <Button onClick={handleSubmit} disabled={isSubmitting || !nodeId || !name.trim() || !!isoError}>
{isSubmitting ? 'Creating...' : 'Create VM'} {isSubmitting ? 'Creating...' : 'Create VM'}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@ -104,6 +104,7 @@ export function VMList({
const [onlineMigration, setOnlineMigration] = useState(true); const [onlineMigration, setOnlineMigration] = useState(true);
const [maxDowntime, setMaxDowntime] = useState(30); const [maxDowntime, setMaxDowntime] = useState(30);
const [clusterNodes, setClusterNodes] = useState<string[]>([]); const [clusterNodes, setClusterNodes] = useState<string[]>([]);
const [nodesLoading, setNodesLoading] = useState(false);
const vms: VMInfo[] = React.useMemo(() => { const vms: VMInfo[] = React.useMemo(() => {
return rawVms.map((vm) => ({ return rawVms.map((vm) => ({
@ -199,6 +200,7 @@ export function VMList({
const handleMigrate = useCallback(async (vm: VMInfo) => { const handleMigrate = useCallback(async (vm: VMInfo) => {
setMigrationVM(vm); setMigrationVM(vm);
setTargetCluster(clusterId); setTargetCluster(clusterId);
setNodesLoading(true);
try { try {
const nodeData: { node?: string; status?: string }[] = await invoke('list_proxmox_nodes', { clusterId }); const nodeData: { node?: string; status?: string }[] = await invoke('list_proxmox_nodes', { clusterId });
const names = nodeData const names = nodeData
@ -212,6 +214,8 @@ export function VMList({
.filter((node, idx, self) => self.indexOf(node) === idx && node !== vm.node); .filter((node, idx, self) => self.indexOf(node) === idx && node !== vm.node);
setClusterNodes(fallback); setClusterNodes(fallback);
setTargetNode(fallback[0] || ''); setTargetNode(fallback[0] || '');
} finally {
setNodesLoading(false);
} }
}, [clusterId, vms]); }, [clusterId, vms]);
@ -417,6 +421,7 @@ export function VMList({
onClose={() => { setMigrationVM(null); setTargetNode(''); setTargetCluster(''); setClusterNodes([]); }} onClose={() => { setMigrationVM(null); setTargetNode(''); setTargetCluster(''); setClusterNodes([]); }}
onSubmit={submitMigration} onSubmit={submitMigration}
availableNodeNames={clusterNodes} availableNodeNames={clusterNodes}
nodesLoading={nodesLoading}
clusters={clusters} clusters={clusters}
currentClusterId={clusterId} currentClusterId={clusterId}
targetNode={targetNode} targetNode={targetNode}
@ -607,6 +612,7 @@ interface MigrationDialogProps {
onClose: () => void; onClose: () => void;
onSubmit: () => void; onSubmit: () => void;
availableNodeNames: string[]; availableNodeNames: string[];
nodesLoading: boolean;
clusters: ClusterInfo[]; clusters: ClusterInfo[];
currentClusterId: string; currentClusterId: string;
targetNode: string; targetNode: string;
@ -625,6 +631,7 @@ function MigrationDialog({
onClose, onClose,
onSubmit, onSubmit,
availableNodeNames, availableNodeNames,
nodesLoading,
clusters, clusters,
currentClusterId, currentClusterId,
targetNode, targetNode,
@ -685,7 +692,9 @@ function MigrationDialog({
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="targetNode">Target Node</Label> <Label htmlFor="targetNode">Target Node</Label>
{isCrossCluster ? ( {nodesLoading ? (
<p className="text-sm text-muted-foreground animate-pulse">Loading nodes</p>
) : isCrossCluster ? (
<> <>
<Input <Input
id="targetNode" id="targetNode"