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
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:
parent
a9a063f786
commit
64fc808908
@ -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' : ''}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">Format: storage:iso/filename.iso</p>
|
{isoError ? (
|
||||||
|
<p className="text-xs text-red-500">{isoError}</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>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user