Some checks failed
Test / frontend-tests (pull_request) Successful in 1m37s
Test / frontend-typecheck (pull_request) Successful in 1m49s
PR Review Automation / review (pull_request) Successful in 10m13s
Test / rust-fmt-check (pull_request) Failing after 12m20s
Test / rust-clippy (pull_request) Successful in 13m53s
Test / rust-tests (pull_request) Has been cancelled
Issue 1 — VM actions silently doing nothing: The root cause was a missing <Toaster> mount in App.tsx. All toast.success/error calls were no-ops because the sonner Toaster component was never rendered. Added it at the App root. Also added dialog:allow-confirm capability (was missing, caused VM delete confirmation to throw silently). Issue 2 — Remove Disk column: PVE cluster/resources returns only static disk allocation, not actual usage, making the column misleading. Removed from VMList header, row, and the diskPercent calculation. Issue 3 — Add VM creation: - New list_proxmox_nodes Tauri command (GET /nodes) for real node list - New create_proxmox_vm Tauri command with server-side input validation: vmid range, numeric bounds, node/storage/bridge path-safety check, ISO volume-ID format check to prevent comma-property injection - CreateVmDialog component with node/storage discovery on open - "Add VM" button wired into VMsPage MigrationDialog now fetches real cluster nodes via list_proxmox_nodes instead of inferring them from the VMs already in the list. Added suspendProxmoxVm, resumeProxmoxVm, listProxmoxNodes, createProxmoxVm client wrappers to proxmoxClient.ts. Tests: 446 Rust + 405 frontend, all pass. 19 new VMList tests (TDD), 7 new Rust tests for security validation logic.
118 lines
3.9 KiB
TypeScript
118 lines
3.9 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { Button } from '@/components/ui/index';
|
|
import { RefreshCw, Plus } from 'lucide-react';
|
|
import { VMList, CreateVmDialog } from '@/components/Proxmox';
|
|
import { listProxmoxClusters, listProxmoxVms } from '@/lib/proxmoxClient';
|
|
import type { ClusterInfo } from '@/lib/domain';
|
|
import { toast } from 'sonner';
|
|
|
|
export function ProxmoxVMsPage() {
|
|
const [clusters, setClusters] = useState<ClusterInfo[]>([]);
|
|
const [selectedClusterId, setSelectedClusterId] = useState<string>('');
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const [vms, setVms] = useState<any[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [selectedVMs, setSelectedVMs] = useState<Set<string>>(new Set());
|
|
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
|
|
|
useEffect(() => {
|
|
listProxmoxClusters()
|
|
.then((cls) => {
|
|
setClusters(cls);
|
|
if (cls.length > 0) setSelectedClusterId(cls[0].id);
|
|
})
|
|
.catch((err) => {
|
|
console.error('Failed to load clusters:', err);
|
|
toast.error('Failed to load clusters');
|
|
});
|
|
}, []);
|
|
|
|
const loadVms = useCallback(async (clusterId: string) => {
|
|
if (!clusterId) return;
|
|
setIsLoading(true);
|
|
try {
|
|
const data = await listProxmoxVms(clusterId);
|
|
setVms(data);
|
|
} catch (err) {
|
|
console.error('Failed to load VMs:', err);
|
|
toast.error('Failed to load VMs');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (selectedClusterId) loadVms(selectedClusterId);
|
|
}, [selectedClusterId, loadVms]);
|
|
|
|
if (clusters.length === 0 && !isLoading) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Virtual Machines</h1>
|
|
<p className="text-muted-foreground">Manage QEMU/KVM virtual machines</p>
|
|
</div>
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<p>No Proxmox clusters configured.</p>
|
|
<p className="text-sm mt-1">Add a remote connection first.</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Virtual Machines</h1>
|
|
<p className="text-muted-foreground">Manage QEMU/KVM virtual machines</p>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
{clusters.length > 1 && (
|
|
<select
|
|
className="rounded-md border px-3 py-1.5 text-sm bg-background"
|
|
value={selectedClusterId}
|
|
onChange={(e) => setSelectedClusterId(e.target.value)}
|
|
>
|
|
{clusters.map((c) => (
|
|
<option key={c.id} value={c.id}>{c.name}</option>
|
|
))}
|
|
</select>
|
|
)}
|
|
<Button variant="outline" size="sm" onClick={() => loadVms(selectedClusterId)}>
|
|
<RefreshCw className="mr-2 h-4 w-4" />
|
|
Refresh
|
|
</Button>
|
|
<Button size="sm" onClick={() => setShowCreateDialog(true)} disabled={!selectedClusterId}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Add VM
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<VMList
|
|
vms={vms}
|
|
clusterId={selectedClusterId}
|
|
clusters={clusters}
|
|
onRefresh={() => loadVms(selectedClusterId)}
|
|
selectedVMs={selectedVMs}
|
|
onToggleSelect={(vm) => {
|
|
setSelectedVMs((prev) => {
|
|
const next = new Set(prev);
|
|
const id = String(vm.vmid);
|
|
if (next.has(id)) next.delete(id); else next.add(id);
|
|
return next;
|
|
});
|
|
}}
|
|
/>
|
|
|
|
<CreateVmDialog
|
|
isOpen={showCreateDialog}
|
|
clusterId={selectedClusterId}
|
|
onClose={() => setShowCreateDialog(false)}
|
|
onCreated={() => loadVms(selectedClusterId)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|