From 774da61da11e39422d5ce1ca7ae883524eae791e Mon Sep 17 00:00:00 2001 From: MacRimi Date: Tue, 3 Feb 2026 18:07:55 +0100 Subject: [PATCH] Update virtual-machines.tsx --- AppImage/components/virtual-machines.tsx | 590 +++++++++++------------ 1 file changed, 290 insertions(+), 300 deletions(-) diff --git a/AppImage/components/virtual-machines.tsx b/AppImage/components/virtual-machines.tsx index c2c90df9..dc54f65a 100644 --- a/AppImage/components/virtual-machines.tsx +++ b/AppImage/components/virtual-machines.tsx @@ -8,8 +8,9 @@ import { Badge } from "./ui/badge" import { Progress } from "./ui/progress" import { Button } from "./ui/button" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog" -import { Server, Play, Square, Cpu, MemoryStick, HardDrive, Network, Power, RotateCcw, StopCircle, Container, ChevronDown, ChevronUp, Terminal, Archive, Loader2, ChevronLeft, ChevronRight, RefreshCw, Save } from 'lucide-react' +import { Server, Play, Square, Cpu, MemoryStick, HardDrive, Network, Power, RotateCcw, StopCircle, Container, ChevronDown, ChevronUp, Terminal, Archive, Plus, Loader2, Clock, Database } from 'lucide-react' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" +import { ScrollArea } from "./ui/scroll-area" import useSWR from "swr" import { MetricsView } from "./metrics-dialog" import { LxcTerminalModal } from "./lxc-terminal-modal" @@ -122,16 +123,6 @@ interface VMDetails extends VMData { } } -interface BackupInfo { - volid: string - storage: string - type: string - size: number - size_human: string - timestamp: number - date: string -} - interface BackupStorage { storage: string type: string @@ -139,9 +130,20 @@ interface BackupStorage { total: number used: number avail: number - total_human: string - used_human: string - avail_human: string + total_human?: string + used_human?: string + avail_human?: string +} + +interface VMBackup { + volid: string + storage: string + type: string + size: number + size_human: string + timestamp: number + date: string + notes?: string } const fetcher = async (url: string) => { @@ -296,12 +298,12 @@ export function VirtualMachines() { const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">("Bytes") // Backup states - const [modalPage, setModalPage] = useState(0) // 0 = main, 1 = backups - const [vmBackups, setVmBackups] = useState([]) + const [vmBackups, setVmBackups] = useState([]) const [backupStorages, setBackupStorages] = useState([]) + const [selectedBackupStorage, setSelectedBackupStorage] = useState("") const [loadingBackups, setLoadingBackups] = useState(false) const [creatingBackup, setCreatingBackup] = useState(false) - const [selectedBackupStorage, setSelectedBackupStorage] = useState("") + const [modalPage, setModalPage] = useState(0) useEffect(() => { const fetchLXCIPs = async () => { @@ -379,79 +381,19 @@ export function VirtualMachines() { setIsEditingNotes(false) setEditedNotes("") setModalPage(0) - setVmBackups([]) setDetailsLoading(true) - setLoadingBackups(true) + + // Load backups and storages immediately (independent of config) + fetchBackupStorages() + fetchVmBackups(vm.vmid) + try { - const [details, storagesData, backupsData] = await Promise.all([ - fetchApi(`/api/vms/${vm.vmid}`), - fetchApi('/api/backup-storages'), - fetchApi(`/api/vms/${vm.vmid}/backups`) - ]) + const details = await fetchApi(`/api/vms/${vm.vmid}`) setVMDetails(details) - setBackupStorages(storagesData.storages || []) - setVmBackups(backupsData.backups || []) - if (storagesData.storages?.length > 0) { - setSelectedBackupStorage(storagesData.storages[0].storage) - } } catch (error) { console.error("Error fetching VM details:", error) } finally { setDetailsLoading(false) - setLoadingBackups(false) - } - } - - // Fetch backups for current VM - const fetchVmBackups = async (vmid: number) => { - setLoadingBackups(true) - try { - const [backupsData, storagesData] = await Promise.all([ - fetchApi(`/api/vms/${vmid}/backups`), - fetchApi('/api/backup-storages') - ]) - setVmBackups(backupsData.backups || []) - setBackupStorages(storagesData.storages || []) - if (storagesData.storages?.length > 0 && !selectedBackupStorage) { - setSelectedBackupStorage(storagesData.storages[0].storage) - } - } catch (error) { - console.error("Error fetching backups:", error) - } finally { - setLoadingBackups(false) - } - } - - // Create backup - const handleCreateBackup = async () => { - if (!selectedVM || !selectedBackupStorage) return - - setCreatingBackup(true) - try { - await fetchApi(`/api/vms/${selectedVM.vmid}/backup`, { - method: "POST", - body: JSON.stringify({ - storage: selectedBackupStorage, - mode: "snapshot", - compress: "zstd" - }), - }) - // Refresh backups list after a short delay - setTimeout(() => { - fetchVmBackups(selectedVM.vmid) - }, 2000) - } catch (error) { - console.error("Error creating backup:", error) - } finally { - setCreatingBackup(false) - } - } - - // Switch to backups page - const handleShowBackups = () => { - if (selectedVM) { - setModalPage(1) - fetchVmBackups(selectedVM.vmid) } } @@ -463,6 +405,54 @@ export function VirtualMachines() { setCurrentView("main") } + // Backup functions + const fetchBackupStorages = async () => { + try { + const response = await fetchApi("/api/backup-storages") + if (response.storages) { + setBackupStorages(response.storages) + if (response.storages.length > 0 && !selectedBackupStorage) { + setSelectedBackupStorage(response.storages[0].storage) + } + } + } catch (error) { + console.error("Error fetching backup storages:", error) + } + } + + const fetchVmBackups = async (vmid: number) => { + setLoadingBackups(true) + try { + const response = await fetchApi(`/api/vms/${vmid}/backups`) + if (response.backups) { + setVmBackups(response.backups) + } + } catch (error) { + console.error("Error fetching VM backups:", error) + setVmBackups([]) + } finally { + setLoadingBackups(false) + } + } + + const handleCreateBackup = async () => { + if (!selectedVM || !selectedBackupStorage) return + + setCreatingBackup(true) + try { + await fetchApi(`/api/vms/${selectedVM.vmid}/backup`, { + method: "POST", + body: JSON.stringify({ storage: selectedBackupStorage }), + }) + // Refresh backups list after creation + setTimeout(() => fetchVmBackups(selectedVM.vmid), 2000) + } catch (error) { + console.error("Error creating backup:", error) + } finally { + setCreatingBackup(false) + } + } + const handleVMControl = async (vmid: number, action: string) => { setControlLoading(true) try { @@ -623,7 +613,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { } const isHTML = (str: string): boolean => { - const htmlRegex = new RegExp('<\\/?[a-z][\\s\\S]*>', 'i') + const htmlRegex = /<\/?[a-z][\s\S]*>/i return htmlRegex.test(str) } @@ -1241,71 +1231,69 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { -
- {/* Mobile carousel container */} -
+
+ {/* Mobile carousel */} +
{/* Page 0: Main content */}
-
+
{selectedVM && ( <> -
+
+ {/* CPU */}
CPU Usage
{(selectedVM.cpu * 100).toFixed(1)}%
- +
+ {/* Memory */}
Memory
{(selectedVM.mem / 1024 ** 3).toFixed(1)} / {(selectedVM.maxmem / 1024 ** 3).toFixed(1)} GB
- +
+ {/* Disk */}
Disk
{(selectedVM.disk / 1024 ** 3).toFixed(1)} / {(selectedVM.maxdisk / 1024 ** 3).toFixed(1)} GB
- +
+ {/* Disk I/O */}
Disk I/O
-
- - {((selectedVM.diskread || 0) / 1024 ** 2).toFixed(2)} MB -
-
- - {((selectedVM.diskwrite || 0) / 1024 ** 2).toFixed(2)} MB -
+
↓ {((selectedVM.diskread || 0) / 1024 ** 2).toFixed(2)} MB
+
↑ {((selectedVM.diskwrite || 0) / 1024 ** 2).toFixed(2)} MB
+ {/* Network I/O */}
Network I/O
-
- - {formatNetworkTraffic(selectedVM.netin || 0, networkUnit)} -
-
- - {formatNetworkTraffic(selectedVM.netout || 0, networkUnit)} -
+
↓ {formatNetworkTraffic(selectedVM.netin || 0, networkUnit)}
+
↑ {formatNetworkTraffic(selectedVM.netout || 0, networkUnit)}
+ {/* OS Icon */}
{getOSIcon(vmDetails?.os_info, selectedVM.type)}
@@ -1314,21 +1302,15 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
- {detailsLoading ? ( -
Loading configuration...
- ) : vmDetails?.config ? ( + {/* Resources Section Mobile */} + {vmDetails?.config && ( -
-

Resources

-
- - +
+
+
+

Resources

{vmDetails.config.cores && ( @@ -1343,134 +1325,153 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
{vmDetails.config.memory} MB
)} - {vmDetails.config.swap && ( + {vmDetails.config.swap !== undefined && (
Swap
{vmDetails.config.swap} MB
)}
- {selectedVM?.type === "lxc" && vmDetails?.lxc_ip_info && ( + {selectedVM?.type === "lxc" && vmDetails?.lxc_ip_info && vmDetails.lxc_ip_info.real_ips.length > 0 && (
-

IP Addresses

+

IP Addresses

{vmDetails.lxc_ip_info.real_ips.map((ip, index) => ( - {ip} - ))} - {vmDetails.lxc_ip_info.docker_ips.map((ip, index) => ( - {ip} (Bridge) + + {ip} + ))}
)} - ) : null} + )} )} -
+
- + {/* Page 1: Backups */}
- - -

Create Backup

-
-
- - -
- + + + {/* Header */} +
+
+
- - - - - -

- Backups ({vmBackups.length}) -

- {loadingBackups ? ( -
- - Loading backups... -
- ) : vmBackups.length === 0 ? ( -
- No backups found -
- ) : ( -
- {vmBackups.map((backup, index) => ( -
-
-
-
{backup.date}
-
{backup.storage}
-
- {backup.size_human} -
-
+

Backups

+
+ + {/* Create Backup */} +
+ + +
+ + {/* Divider */} +
+ + {/* Backup List */} +
+ Available backups + {vmBackups.length} +
+ + {loadingBackups ? ( +
+ + Loading... +
+ ) : vmBackups.length === 0 ? ( +
+ + No backups found +
+ ) : ( +
+ {vmBackups.map((backup, index) => ( +
+
+
+ + {backup.date} +
+ + {backup.size_human} + +
+ ))} +
+ )} + +
- {/* Mobile pagination dots */} -
+ {/* Pagination dots */} +
- -{/* Desktop layout */} -
-
+ + {/* Desktop layout */} +
+
{selectedVM && ( <>
- @@ -1557,103 +1558,90 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { {/* Backups Section - Always visible, loads independently */} - -
+ + {/* Header */} +
-
+
-

- Backups -

+

Backups

-
- {/* Create Backup - Top section */} -
- -
- - -
-
- {/* Divider */} -
+
- {/* Backup List - Bottom section */} -
-
- - - {vmBackups.length} total - -
- {loadingBackups ? ( -
- - Loading backups... -
- ) : vmBackups.length === 0 ? ( -
- No backups found for this VM -
- ) : ( -
- {vmBackups.map((backup, index) => ( -
-
-
- {backup.date} -
- - {backup.size_human} - -
- ))} -
- )} + {/* Backup List */} +
+ Available backups + {vmBackups.length}
+ + {loadingBackups ? ( +
+ + Loading backups... +
+ ) : vmBackups.length === 0 ? ( +
+ + No backups found +
+ ) : ( +
+ {vmBackups.map((backup, index) => ( +
+
+
+ + {backup.date} +
+ + {backup.size_human} + +
+ ))} +
+ )} @@ -1664,9 +1652,12 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
-

- Resources -

+
+
+ +
+

Resources

+
+ + )}