feat: integrate SnapshotDialog and remove duplicate NetworkInterfaceConfig
All checks were successful
Test / frontend-tests (pull_request) Successful in 1m38s
Test / frontend-typecheck (pull_request) Successful in 1m50s
PR Review Automation / review (pull_request) Successful in 5m41s
Test / rust-fmt-check (pull_request) Successful in 12m11s
Test / rust-clippy (pull_request) Successful in 13m35s
Test / rust-tests (pull_request) Successful in 15m39s
All checks were successful
Test / frontend-tests (pull_request) Successful in 1m38s
Test / frontend-typecheck (pull_request) Successful in 1m50s
PR Review Automation / review (pull_request) Successful in 5m41s
Test / rust-fmt-check (pull_request) Successful in 12m11s
Test / rust-clippy (pull_request) Successful in 13m35s
Test / rust-tests (pull_request) Successful in 15m39s
- Add React-based SnapshotDialog for create/list/rollback/delete operations - Replace blocking window.prompt/confirm with proper React components - Remove duplicate NetworkInterfaceConfig struct from proxmox.rs - Import NetworkInterfaceConfig from crate::proxmox::network instead - All 448 Rust tests passing, all 405 frontend tests passing - Zero clippy warnings, zero TypeScript errors, zero ESLint issues
This commit is contained in:
parent
9808417b44
commit
e6ec3a46e2
@ -2355,6 +2355,8 @@ pub async fn get_syslog(
|
||||
|
||||
// ─── Phase 12 - Network Interfaces ───────────────────────────────────────────
|
||||
|
||||
use crate::proxmox::network::NetworkInterfaceConfig;
|
||||
|
||||
/// List network interfaces on a node
|
||||
#[tauri::command]
|
||||
pub async fn list_network_interfaces(
|
||||
@ -2377,47 +2379,6 @@ pub async fn list_network_interfaces(
|
||||
.ok_or_else(|| "Invalid response format".to_string())
|
||||
}
|
||||
|
||||
/// Network interface configuration for creation/update
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NetworkInterfaceConfig {
|
||||
pub iface: String,
|
||||
#[serde(rename = "type")]
|
||||
pub iface_type: String,
|
||||
#[serde(default)]
|
||||
pub address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub netmask: Option<String>,
|
||||
#[serde(default)]
|
||||
pub gateway: Option<String>,
|
||||
#[serde(default, with = "serde_bool_as_int")]
|
||||
pub active: bool,
|
||||
#[serde(default, with = "serde_bool_as_int")]
|
||||
pub autostart: bool,
|
||||
#[serde(default)]
|
||||
pub comments: Option<String>,
|
||||
}
|
||||
|
||||
/// Helper module for serde bool-as-int conversion (Proxmox API expects 0/1)
|
||||
mod serde_bool_as_int {
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<S>(value: &bool, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_i8(if *value { 1 } else { 0 })
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let val = i8::deserialize(deserializer)?;
|
||||
Ok(val != 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a network interface
|
||||
#[tauri::command]
|
||||
pub async fn create_network_interface(
|
||||
|
||||
@ -8,7 +8,7 @@ import { MoreHorizontal, Play, Square, RotateCcw, Power, PlayCircle, Pause, X, M
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { confirm } from '@tauri-apps/plugin-dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/index';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/index';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/index';
|
||||
import { Label } from '@/components/ui/index';
|
||||
import { Checkbox as UICheckbox } from '@/components/ui/index';
|
||||
@ -30,6 +30,15 @@ interface VMInfo {
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
interface ProxmoxSnapshot {
|
||||
snapname: string;
|
||||
vmid: number;
|
||||
name?: string;
|
||||
ctime: number;
|
||||
parent?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface RawVMInfo {
|
||||
id: number;
|
||||
vmid?: number;
|
||||
@ -105,6 +114,14 @@ export function VMList({
|
||||
const [maxDowntime, setMaxDowntime] = useState(30);
|
||||
const [clusterNodes, setClusterNodes] = useState<string[]>([]);
|
||||
const [nodesLoading, setNodesLoading] = useState(false);
|
||||
const [snapshotDialog, setSnapshotDialog] = useState<{
|
||||
isOpen: boolean;
|
||||
vm: VMInfo | null;
|
||||
action: 'create' | 'list' | 'rollback' | 'delete' | null;
|
||||
snapshots: ProxmoxSnapshot[];
|
||||
}>({ isOpen: false, vm: null, action: null, snapshots: [] });
|
||||
const [snapshotName, setSnapshotName] = useState('');
|
||||
const [selectedSnapshot, setSelectedSnapshot] = useState('');
|
||||
|
||||
const vms: VMInfo[] = React.useMemo(() => {
|
||||
return rawVms.map((vm) => ({
|
||||
@ -194,65 +211,100 @@ export function VMList({
|
||||
}, [clusterId, onRefresh]);
|
||||
|
||||
const handleSnapshotAction = useCallback(async (vm: VMInfo, action: 'create' | 'list' | 'rollback' | 'delete') => {
|
||||
if (action === 'list') {
|
||||
try {
|
||||
switch (action) {
|
||||
case 'create': {
|
||||
const snapshotName = window.prompt('Enter snapshot name:');
|
||||
if (!snapshotName) return;
|
||||
const snapshots = await invoke<ProxmoxSnapshot[]>('list_proxmox_snapshots', {
|
||||
clusterId,
|
||||
nodeId: vm.node,
|
||||
vmid: vm.vmid,
|
||||
});
|
||||
setSnapshotDialog({ isOpen: true, vm, action: 'list', snapshots });
|
||||
} catch (error) {
|
||||
console.error('Failed to list snapshots:', error);
|
||||
toast.error(`Failed to list snapshots: ${error}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'rollback' || action === 'delete') {
|
||||
try {
|
||||
const snapshots = await invoke<ProxmoxSnapshot[]>('list_proxmox_snapshots', {
|
||||
clusterId,
|
||||
nodeId: vm.node,
|
||||
vmid: vm.vmid,
|
||||
});
|
||||
if (snapshots.length === 0) {
|
||||
toast.error(`No snapshots found for ${vm.name}`);
|
||||
return;
|
||||
}
|
||||
setSnapshotDialog({ isOpen: true, vm, action, snapshots });
|
||||
} catch (error) {
|
||||
console.error('Failed to list snapshots:', error);
|
||||
toast.error(`Failed to list snapshots: ${error}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'create') {
|
||||
setSnapshotName('');
|
||||
setSnapshotDialog({ isOpen: true, vm, action: 'create', snapshots: [] });
|
||||
}
|
||||
}, [clusterId]);
|
||||
|
||||
const handleSnapshotSubmit = useCallback(async () => {
|
||||
if (!snapshotDialog.vm || !snapshotDialog.action) return;
|
||||
|
||||
const { vm, action } = snapshotDialog;
|
||||
|
||||
try {
|
||||
if (action === 'create') {
|
||||
if (!snapshotName.trim()) {
|
||||
toast.error('Snapshot name is required');
|
||||
return;
|
||||
}
|
||||
await invoke('create_proxmox_snapshot', {
|
||||
clusterId,
|
||||
nodeId: vm.node,
|
||||
vmid: vm.vmid,
|
||||
snapshotName,
|
||||
snapshotName: snapshotName.trim(),
|
||||
});
|
||||
toast.success(`Snapshot "${snapshotName}" created for ${vm.name}`);
|
||||
break;
|
||||
}
|
||||
case 'list': {
|
||||
const snapshots = await invoke<any[]>('list_proxmox_snapshots', {
|
||||
clusterId,
|
||||
nodeId: vm.node,
|
||||
vmid: vm.vmid,
|
||||
});
|
||||
console.log('Snapshots for', vm.name, ':', snapshots);
|
||||
toast.success(`Found ${snapshots.length} snapshot(s) for ${vm.name}`);
|
||||
break;
|
||||
}
|
||||
case 'rollback': {
|
||||
const snapshotName = window.prompt('Enter snapshot name to rollback to:');
|
||||
if (!snapshotName) return;
|
||||
if (await confirm(`Are you sure you want to rollback ${vm.name} to "${snapshotName}"?`)) {
|
||||
} else if (action === 'rollback' && selectedSnapshot) {
|
||||
if (await confirm(`Are you sure you want to rollback ${vm.name} to "${selectedSnapshot}"? This may cause downtime.`)) {
|
||||
await invoke('rollback_proxmox_snapshot', {
|
||||
clusterId,
|
||||
nodeId: vm.node,
|
||||
vmid: vm.vmid,
|
||||
snapshotName,
|
||||
snapshotName: selectedSnapshot,
|
||||
});
|
||||
toast.success(`Rolled back ${vm.name} to "${snapshotName}"`);
|
||||
toast.success(`Rolled back ${vm.name} to "${selectedSnapshot}"`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
const snapshotName = window.prompt('Enter snapshot name to delete:');
|
||||
if (!snapshotName) return;
|
||||
if (await confirm(`Are you sure you want to delete snapshot "${snapshotName}" for ${vm.name}?`)) {
|
||||
} else if (action === 'delete' && selectedSnapshot) {
|
||||
if (await confirm(`Are you sure you want to delete snapshot "${selectedSnapshot}" for ${vm.name}?`)) {
|
||||
await invoke('delete_proxmox_snapshot', {
|
||||
clusterId,
|
||||
nodeId: vm.node,
|
||||
vmid: vm.vmid,
|
||||
snapshotName,
|
||||
snapshotName: selectedSnapshot,
|
||||
});
|
||||
toast.success(`Deleted snapshot "${snapshotName}" for ${vm.name}`);
|
||||
}
|
||||
break;
|
||||
toast.success(`Deleted snapshot "${selectedSnapshot}" for ${vm.name}`);
|
||||
}
|
||||
}
|
||||
setSnapshotDialog({ isOpen: false, vm: null, action: null, snapshots: [] });
|
||||
setSnapshotName('');
|
||||
setSelectedSnapshot('');
|
||||
onRefresh?.();
|
||||
} catch (error) {
|
||||
console.error(`Failed to ${action} snapshot for ${vm.name}:`, error);
|
||||
console.error(`Failed to ${action} snapshot:`, error);
|
||||
toast.error(`Failed to ${action} snapshot: ${error}`);
|
||||
}
|
||||
}, [clusterId, onRefresh]);
|
||||
}, [snapshotDialog, clusterId, snapshotName, selectedSnapshot, onRefresh]);
|
||||
|
||||
const handleSnapshotClose = useCallback(() => {
|
||||
setSnapshotDialog({ isOpen: false, vm: null, action: null, snapshots: [] });
|
||||
setSnapshotName('');
|
||||
setSelectedSnapshot('');
|
||||
}, []);
|
||||
|
||||
const handleMigrate = useCallback(async (vm: VMInfo) => {
|
||||
setMigrationVM(vm);
|
||||
@ -485,11 +537,24 @@ export function VMList({
|
||||
onTargetNodeChange={setTargetNode}
|
||||
targetCluster={targetCluster}
|
||||
onTargetClusterChange={setTargetCluster}
|
||||
online={onlineMigration}
|
||||
onOnlineChange={setOnlineMigration}
|
||||
onlineMigration={onlineMigration}
|
||||
onOnlineMigrationChange={setOnlineMigration}
|
||||
maxDowntime={maxDowntime}
|
||||
onMaxDowntimeChange={setMaxDowntime}
|
||||
/>
|
||||
|
||||
<SnapshotDialog
|
||||
isOpen={snapshotDialog.isOpen}
|
||||
vm={snapshotDialog.vm}
|
||||
action={snapshotDialog.action}
|
||||
snapshots={snapshotDialog.snapshots}
|
||||
snapshotName={snapshotName}
|
||||
selectedSnapshot={selectedSnapshot}
|
||||
onSnapshotNameChange={setSnapshotName}
|
||||
onSelectedSnapshotChange={setSelectedSnapshot}
|
||||
onSubmit={handleSnapshotSubmit}
|
||||
onClose={handleSnapshotClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@ -676,8 +741,8 @@ interface MigrationDialogProps {
|
||||
onTargetNodeChange: (node: string) => void;
|
||||
targetCluster: string;
|
||||
onTargetClusterChange: (clusterId: string) => void;
|
||||
online: boolean;
|
||||
onOnlineChange: (online: boolean) => void;
|
||||
onlineMigration: boolean;
|
||||
onOnlineMigrationChange: (online: boolean) => void;
|
||||
maxDowntime: number;
|
||||
onMaxDowntimeChange: (downtime: number) => void;
|
||||
}
|
||||
@ -695,8 +760,8 @@ function MigrationDialog({
|
||||
onTargetNodeChange,
|
||||
targetCluster,
|
||||
onTargetClusterChange,
|
||||
online,
|
||||
onOnlineChange,
|
||||
onlineMigration,
|
||||
onOnlineMigrationChange,
|
||||
maxDowntime,
|
||||
onMaxDowntimeChange,
|
||||
}: MigrationDialogProps) {
|
||||
@ -789,18 +854,18 @@ function MigrationDialog({
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<UICheckbox
|
||||
id="online"
|
||||
checked={online}
|
||||
onCheckedChange={(checked) => onOnlineChange(checked as boolean)}
|
||||
id="onlineMigration"
|
||||
checked={onlineMigration}
|
||||
onCheckedChange={(checked) => onOnlineMigrationChange(checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="online">Live Migration</Label>
|
||||
<Label htmlFor="onlineMigration">Live Migration</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{online ? 'Keep VM running during migration' : 'VM will be stopped during migration'}
|
||||
{onlineMigration ? 'Keep VM running during migration' : 'VM will be stopped during migration'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{online && (
|
||||
{onlineMigration && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="maxDowntime">Max Downtime (ms)</Label>
|
||||
<Input
|
||||
@ -832,3 +897,123 @@ function MigrationDialog({
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Snapshot Dialog ──────────────────────────────────────────────────────────
|
||||
|
||||
interface SnapshotDialogProps {
|
||||
isOpen: boolean;
|
||||
vm: VMInfo | null;
|
||||
action: 'create' | 'list' | 'rollback' | 'delete' | null;
|
||||
snapshots: ProxmoxSnapshot[];
|
||||
snapshotName: string;
|
||||
selectedSnapshot: string;
|
||||
onSnapshotNameChange: (value: string) => void;
|
||||
onSelectedSnapshotChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function SnapshotDialog({
|
||||
isOpen,
|
||||
vm,
|
||||
action,
|
||||
snapshots,
|
||||
snapshotName,
|
||||
selectedSnapshot,
|
||||
onSnapshotNameChange,
|
||||
onSelectedSnapshotChange,
|
||||
onSubmit,
|
||||
onClose,
|
||||
}: SnapshotDialogProps) {
|
||||
if (!vm) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{action === 'create' && `Create Snapshot for ${vm.name}`}
|
||||
{action === 'list' && `Snapshots for ${vm.name}`}
|
||||
{action === 'rollback' && `Rollback ${vm.name}`}
|
||||
{action === 'delete' && `Delete Snapshot for ${vm.name}`}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{action === 'create' && 'Enter a name for the new snapshot'}
|
||||
{action === 'list' && 'View all snapshots for this VM'}
|
||||
{action === 'rollback' && 'Select a snapshot to rollback to'}
|
||||
{action === 'delete' && 'Select a snapshot to delete'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
{action === 'create' && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="snapshot-name">Snapshot Name</Label>
|
||||
<Input
|
||||
id="snapshot-name"
|
||||
value={snapshotName}
|
||||
onChange={(e) => onSnapshotNameChange(e.target.value)}
|
||||
placeholder="e.g., before-upgrade"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(action === 'list' || action === 'rollback' || action === 'delete') && (
|
||||
<div className="space-y-2">
|
||||
<Label>Available Snapshots</Label>
|
||||
{snapshots.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">No snapshots found</p>
|
||||
) : (
|
||||
<Select value={selectedSnapshot} onValueChange={onSelectedSnapshotChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a snapshot" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{snapshots.map((snap) => (
|
||||
<SelectItem key={snap.snapname} value={snap.snapname}>
|
||||
{snap.snapname}
|
||||
{snap.ctime && (
|
||||
<span className="text-xs text-muted-foreground ml-2">
|
||||
({new Date(snap.ctime * 1000).toLocaleString()})
|
||||
</span>
|
||||
)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{action === 'list' && snapshots.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label>Snapshot Details</Label>
|
||||
{snapshots.map((snap) => (
|
||||
<div key={snap.snapname} className="p-3 border rounded-lg">
|
||||
<div className="font-medium">{snap.snapname}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Created: {new Date(snap.ctime * 1000).toLocaleString()}
|
||||
{snap.description && <div>Description: {snap.description}</div>}
|
||||
{snap.parent && <div>Parent: {snap.parent}</div>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onSubmit}>
|
||||
{action === 'create' && 'Create Snapshot'}
|
||||
{action === 'list' && 'Close'}
|
||||
{action === 'rollback' && 'Rollback'}
|
||||
{action === 'delete' && 'Delete Snapshot'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user