"use client" import type React from "react" import { useState, useMemo, useEffect } from "react" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" 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, } from "lucide-react" import useSWR from "swr" import { MetricsView } from "./metrics-dialog" import { formatStorage } from "@/lib/utils" // Import formatStorage utility interface VMData { vmid: number name: string status: string type: string cpu: number mem: number maxmem: number disk: number maxdisk: number uptime: number netin?: number netout?: number diskread?: number diskwrite?: number ip?: string } interface VMConfig { cores?: number memory?: number swap?: number rootfs?: string net0?: string net1?: string net2?: string nameserver?: string searchdomain?: string onboot?: number unprivileged?: number features?: string ostype?: string arch?: string hostname?: string // VM specific sockets?: number scsi0?: string ide0?: string boot?: string description?: string // Added for notes // Hardware specific numa?: boolean bios?: string machine?: string vga?: string agent?: boolean tablet?: boolean localtime?: boolean // Storage specific scsihw?: string efidisk0?: string tpmstate0?: string // Mount points for LXC mp0?: string mp1?: string mp2?: string mp3?: string mp4?: string mp5?: string // PCI Passthrough hostpci0?: string hostpci1?: string hostpci2?: string hostpci3?: string hostpci4?: string hostpci5?: string // USB Devices usb0?: string usb1?: string usb2?: string // Serial Devices serial0?: string serial1?: string // Advanced vmgenid?: string smbios1?: string meta?: string // CPU cpu?: string [key: string]: any } interface VMDetails extends VMData { config?: VMConfig node?: string vm_type?: string os_info?: { id?: string version_id?: string name?: string pretty_name?: string } hardware_info?: { privileged?: boolean | null gpu_passthrough?: string[] devices?: string[] } lxc_ip?: string lxc_ip_info?: { all_ips: string[] real_ips: string[] docker_ips: string[] primary_ip: string } } const fetcher = async (url: string) => { const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json", }, signal: AbortSignal.timeout(5000), }) if (!response.ok) { throw new Error(`Flask server responded with status: ${response.status}`) } const data = await response.json() return data } const formatBytes = (bytes: number | undefined): string => { if (!bytes || bytes === 0) return "0 B" const k = 1024 const sizes = ["B", "KB", "MB", "GB", "TB"] const i = Math.floor(Math.log(bytes) / Math.log(k)) return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}` } const formatUptime = (seconds: number) => { const days = Math.floor(seconds / 86400) const hours = Math.floor((seconds % 86400) / 3600) const minutes = Math.floor((seconds % 3600) / 60) return `${days}d ${hours}h ${minutes}m` } const extractIPFromConfig = (config?: VMConfig, lxcIPInfo?: VMDetails["lxc_ip_info"]): string => { // Use primary IP from lxc-info if available if (lxcIPInfo?.primary_ip) { return lxcIPInfo.primary_ip } if (!config) return "DHCP" // Check net0, net1, net2, etc. for (let i = 0; i < 10; i++) { const netKey = `net${i}` const netConfig = config[netKey] if (netConfig && typeof netConfig === "string") { // Look for ip=x.x.x.x/xx or ip=x.x.x.x pattern const ipMatch = netConfig.match(/ip=([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/) if (ipMatch) { return ipMatch[1] // Return just the IP without CIDR } // Check if it's explicitly DHCP if (netConfig.includes("ip=dhcp")) { return "DHCP" } } } return "DHCP" } // const formatStorage = (sizeInGB: number): string => { // if (sizeInGB < 1) { // // Less than 1 GB, show in MB // return `${(sizeInGB * 1024).toFixed(1)} MB` // } else if (sizeInGB < 1024) { // // Less than 1024 GB, show in GB // return `${sizeInGB.toFixed(1)} GB` // } else { // // 1024 GB or more, show in TB // return `${(sizeInGB / 1024).toFixed(1)} TB` // } // } const getUsageColor = (percent: number): string => { if (percent >= 95) return "text-red-500" if (percent >= 86) return "text-orange-500" if (percent >= 71) return "text-yellow-500" return "text-foreground" } const getIconColor = (percent: number): string => { if (percent >= 95) return "text-red-500" if (percent >= 86) return "text-orange-500" if (percent >= 71) return "text-yellow-500" return "text-green-500" } const getProgressColor = (percent: number): string => { if (percent >= 95) return "[&>div]:bg-red-500" if (percent >= 86) return "[&>div]:bg-orange-500" if (percent >= 71) return "[&>div]:bg-yellow-500" return "[&>div]:bg-blue-500" } const getModalProgressColor = (percent: number): string => { if (percent >= 95) return "[&>div]:bg-red-500" if (percent >= 86) return "[&>div]:bg-orange-500" if (percent >= 71) return "[&>div]:bg-yellow-500" return "[&>div]:bg-blue-500" } const getOSIcon = (osInfo: VMDetails["os_info"] | undefined, vmType: string): React.ReactNode => { if (vmType !== "lxc" || !osInfo?.id) { return null } const osId = osInfo.id.toLowerCase() switch (osId) { case "debian": return case "ubuntu": return case "alpine": return case "arch": return default: return null } } export function VirtualMachines() { const { data: vmData, error, isLoading, mutate, } = useSWR("/api/vms", fetcher, { refreshInterval: 30000, revalidateOnFocus: false, revalidateOnReconnect: true, }) const [selectedVM, setSelectedVM] = useState(null) const [vmDetails, setVMDetails] = useState(null) const [controlLoading, setControlLoading] = useState(false) const [detailsLoading, setDetailsLoading] = useState(false) const [vmConfigs, setVmConfigs] = useState>({}) const [currentView, setCurrentView] = useState<"main" | "metrics">("main") const [showAdditionalInfo, setShowAdditionalInfo] = useState(false) const [showNotes, setShowNotes] = useState(false) const [isEditingNotes, setIsEditingNotes] = useState(false) const [editedNotes, setEditedNotes] = useState("") const [savingNotes, setSavingNotes] = useState(false) const [selectedMetric, setSelectedMetric] = useState(null) useEffect(() => { const fetchLXCIPs = async () => { if (!vmData) return const lxcs = vmData.filter((vm) => vm.type === "lxc") const configs: Record = {} await Promise.all( lxcs.map(async (lxc) => { try { const response = await fetch(`/api/vms/${lxc.vmid}`) if (response.ok) { const details = await response.json() if (details.lxc_ip_info?.primary_ip) { configs[lxc.vmid] = details.lxc_ip_info.primary_ip } else if (details.config) { configs[lxc.vmid] = extractIPFromConfig(details.config, details.lxc_ip_info) } } } catch (error) { console.error(`Error fetching config for LXC ${lxc.vmid}:`, error) } }), ) setVmConfigs(configs) } fetchLXCIPs() }, [vmData]) const handleVMClick = async (vm: VMData) => { setSelectedVM(vm) setCurrentView("main") setShowAdditionalInfo(false) setShowNotes(false) setIsEditingNotes(false) setEditedNotes("") setDetailsLoading(true) try { const response = await fetch(`/api/vms/${vm.vmid}`) if (response.ok) { const details = await response.json() setVMDetails(details) } } catch (error) { console.error("Error fetching VM details:", error) } finally { setDetailsLoading(false) } } const handleMetricsClick = () => { setCurrentView("metrics") } const handleBackToMain = () => { setCurrentView("main") } const handleVMControl = async (vmid: number, action: string) => { setControlLoading(true) try { const response = await fetch(`/api/vms/${vmid}/control`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ action }), }) if (response.ok) { mutate() setSelectedVM(null) setVMDetails(null) } else { console.error("Failed to control VM") } } catch (error) { console.error("Error controlling VM:", error) } finally { setControlLoading(false) } } const handleDownloadLogs = async (vmid: number, vmName: string) => { try { const response = await fetch(`/api/vms/${vmid}/logs`) if (response.ok) { const data = await response.json() // Format logs as plain text let logText = `=== Logs for ${vmName} (VMID: ${vmid}) ===\n` logText += `Node: ${data.node}\n` logText += `Type: ${data.type}\n` logText += `Total lines: ${data.log_lines}\n` logText += `Generated: ${new Date().toISOString()}\n` logText += `\n${"=".repeat(80)}\n\n` if (data.logs && Array.isArray(data.logs)) { data.logs.forEach((log: any) => { if (typeof log === "object" && log.t) { logText += `${log.t}\n` } else if (typeof log === "string") { logText += `${log}\n` } }) } const blob = new Blob([logText], { type: "text/plain" }) const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = `${vmName}-${vmid}-logs.txt` a.click() URL.revokeObjectURL(url) } } catch (error) { console.error("Error downloading logs:", error) } } const getStatusColor = (status: string) => { switch (status) { case "running": return "bg-green-500/10 text-green-500 border-green-500/20" case "stopped": return "bg-red-500/10 text-red-500 border-red-500/20" default: return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20" } } const getStatusIcon = (status: string) => { switch (status) { case "running": return case "stopped": return default: return null } } const getTypeBadge = (type: string) => { if (type === "lxc") { return { color: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20", label: "LXC", icon: , } } return { color: "bg-purple-500/10 text-purple-500 border-purple-500/20", label: "VM", icon: , } } const safeVMData = vmData || [] const totalAllocatedMemoryGB = useMemo(() => { return (safeVMData.reduce((sum, vm) => sum + (vm.maxmem || 0), 0) / 1024 ** 3).toFixed(1) }, [safeVMData]) const { data: systemData } = useSWR<{ memory_total: number; memory_used: number; memory_usage: number }>( "/api/system", fetcher, { refreshInterval: 30000, revalidateOnFocus: false, }, ) const physicalMemoryGB = systemData?.memory_total ?? null const usedMemoryGB = systemData?.memory_used ?? null const memoryUsagePercent = systemData?.memory_usage ?? null const allocatedMemoryGB = Number.parseFloat(totalAllocatedMemoryGB) const isMemoryOvercommit = physicalMemoryGB !== null && allocatedMemoryGB > physicalMemoryGB const getMemoryUsageColor = (percent: number | null) => { if (percent === null) return "bg-blue-500" if (percent >= 95) return "bg-red-500" if (percent >= 86) return "bg-orange-500" if (percent >= 71) return "bg-yellow-500" return "bg-blue-500" } const getMemoryPercentTextColor = (percent: number | null) => { if (percent === null) return "text-muted-foreground" if (percent >= 95) return "text-red-500" if (percent >= 86) return "text-orange-500" if (percent >= 71) return "text-yellow-500" return "text-green-500" } if (isLoading) { return ( Loading virtual machines... ) } if (error) { return ( Error loading virtual machines: {error.message} ) } const isHTML = (str: string): boolean => { const htmlRegex = /<\/?[a-z][\s\S]*>/i return htmlRegex.test(str) } const decodeRecursively = (str: string, maxIterations = 5): string => { let decoded = str let iteration = 0 while (iteration < maxIterations) { try { const nextDecoded = decodeURIComponent(decoded.replace(/%0A/g, "\n")) // If decoding didn't change anything, we're done if (nextDecoded === decoded) { break } decoded = nextDecoded // If there are no more encoded characters, we're done if (!/(%[0-9A-F]{2})/i.test(decoded)) { break } iteration++ } catch (e) { // If decoding fails, try manual decoding of common sequences try { decoded = decoded .replace(/%0A/g, "\n") .replace(/%20/g, " ") .replace(/%3A/g, ":") .replace(/%2F/g, "/") .replace(/%3D/g, "=") .replace(/%3C/g, "<") .replace(/%3E/g, ">") .replace(/%22/g, '"') .replace(/%27/g, "'") .replace(/%26/g, "&") .replace(/%23/g, "#") .replace(/%25/g, "%") .replace(/%2B/g, "+") .replace(/%2C/g, ",") .replace(/%3B/g, ";") .replace(/%3F/g, "?") .replace(/%40/g, "@") .replace(/%5B/g, "[") .replace(/%5D/g, "]") .replace(/%7B/g, "{") .replace(/%7D/g, "}") .replace(/%7C/g, "|") .replace(/%5C/g, "\\") .replace(/%5E/g, "^") .replace(/%60/g, "`") break } catch (manualError) { // If manual decoding also fails, return what we have break } } } return decoded } const processDescription = (description: string): { html: string; isHtml: boolean; error: boolean } => { try { const decoded = decodeRecursively(description) // Check if it contains HTML if (isHTML(decoded)) { return { html: decoded, isHtml: true, error: false } } // If it's plain text, convert \n to return { html: decoded.replace(/\n/g, ""), isHtml: false, error: false } } catch (error) { // If all decoding fails, return error console.error("Error decoding description:", error) return { html: "", isHtml: false, error: true } } } const handleEditNotes = () => { if (vmDetails?.config?.description) { const decoded = decodeRecursively(vmDetails.config.description) setEditedNotes(decoded) } else { setEditedNotes("") // Ensure editedNotes is empty if no description exists } setIsEditingNotes(true) } const handleSaveNotes = async () => { if (!selectedVM || !vmDetails) return setSavingNotes(true) try { const response = await fetch(`/api/vms/${selectedVM.vmid}/config`, { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ description: editedNotes, // Send as-is, pvesh will handle encoding }), }) if (response.ok) { setVMDetails({ ...vmDetails, config: { ...vmDetails.config, description: editedNotes, // Store unencoded }, }) setIsEditingNotes(false) } else { console.error("Failed to save notes") alert("Failed to save notes. Please try again.") } } catch (error) { console.error("Error saving notes:", error) alert("Error saving notes. Please try again.") } finally { setSavingNotes(false) } } const handleCancelEditNotes = () => { setIsEditingNotes(false) setEditedNotes("") } return ( Total VMs & LXCs {safeVMData.length} {safeVMData.filter((vm) => vm.status === "running").length} Running {safeVMData.filter((vm) => vm.status === "stopped").length} Stopped Virtual machines configured Total CPU {(safeVMData.reduce((sum, vm) => sum + (vm.cpu || 0), 0) * 100).toFixed(0)}% Allocated CPU usage Total Memory {/* Memory Usage (current) */} {physicalMemoryGB !== null && usedMemoryGB !== null && memoryUsagePercent !== null ? ( {usedMemoryGB.toFixed(1)} GB {memoryUsagePercent.toFixed(1)}% {" "} of {physicalMemoryGB.toFixed(1)} GB ) : ( -- Loading memory usage... )} {/* Allocated RAM (configured) */} {/* Layout para desktop (sin cambios) */} {totalAllocatedMemoryGB} GB Allocated RAM {physicalMemoryGB !== null && ( {isMemoryOvercommit ? ( Exceeds Physical ) : ( Within Limits )} )} {/* Layout para móvil (44.0 GB solo, Allocated RAM en otra línea, badge en tercera línea) */} {totalAllocatedMemoryGB} GB Allocated RAM {physicalMemoryGB !== null && ( {isMemoryOvercommit ? ( Exceeds Physical ) : ( Within Limits )} )} Total Disk {formatStorage(safeVMData.reduce((sum, vm) => sum + (vm.maxdisk || 0), 0) / 1024 ** 3)} Allocated disk space Virtual Machines & Containers {safeVMData.length === 0 ? ( No virtual machines found ) : ( {safeVMData.map((vm) => { const cpuPercent = (vm.cpu * 100).toFixed(1) const memPercent = vm.maxmem > 0 ? ((vm.mem / vm.maxmem) * 100).toFixed(1) : "0" const memGB = (vm.mem / 1024 ** 3).toFixed(1) const maxMemGB = (vm.maxmem / 1024 ** 3).toFixed(1) const diskPercent = vm.maxdisk > 0 ? ((vm.disk / vm.maxdisk) * 100).toFixed(1) : "0" const diskGB = (vm.disk / 1024 ** 3).toFixed(1) const maxDiskGB = (vm.maxdisk / 1024 ** 3).toFixed(1) const typeBadge = getTypeBadge(vm.type) const lxcIP = vm.type === "lxc" ? vmConfigs[vm.vmid] : null return ( handleVMClick(vm)} > {getStatusIcon(vm.status)} {vm.status.toUpperCase()} {typeBadge.icon} {typeBadge.label} {vm.name} ID: {vm.vmid} ID: {vm.vmid} {lxcIP && ( IP: {lxcIP} )} Uptime: {formatUptime(vm.uptime)} CPU Usage { setSelectedMetric("cpu") // undeclared variable fix }} > {cpuPercent}% Memory { setSelectedMetric("memory") }} > {memGB} / {maxMemGB} GB Disk Usage { setSelectedMetric("disk") }} > {diskGB} / {maxDiskGB} GB Disk I/O ↓ {formatBytes(vm.diskread)} ↑ {formatBytes(vm.diskwrite)} Network I/O ↓ {formatBytes(vm.netin)} ↑ {formatBytes(vm.netout)} handleVMClick(vm)} > {vm.status === "running" ? ( ) : ( )} {getTypeBadge(vm.type).label} {/* Name and ID */} {vm.name} ID: {vm.vmid} {/* CPU icon with percentage */} {vm.status === "running" && ( {cpuPercent}% )} {/* Memory icon with percentage */} {vm.status === "running" && ( {memPercent}% )} {/* Disk icon with percentage */} {vm.status === "running" && ( {diskPercent}% )} ) })} )} { setSelectedVM(null) setVMDetails(null) setCurrentView("main") setSelectedMetric(null) setShowAdditionalInfo(false) setShowNotes(false) setIsEditingNotes(false) setEditedNotes("") }} > {currentView === "main" ? ( <> {selectedVM?.name} {selectedVM && ID: {selectedVM.vmid}} {selectedVM && ( <> {getTypeBadge(selectedVM.type).icon} {getTypeBadge(selectedVM.type).label} {selectedVM.status.toUpperCase()} {selectedVM.status === "running" && ( Uptime: {formatUptime(selectedVM.uptime)} )} > )} {selectedVM?.name} {selectedVM && ID: {selectedVM.vmid}} {selectedVM && ( {getTypeBadge(selectedVM.type).icon} {getTypeBadge(selectedVM.type).label} {selectedVM.status.toUpperCase()} {selectedVM.status === "running" && ( Uptime: {formatUptime(selectedVM.uptime)} )} )} Virtual machine details and controls for {selectedVM?.name} {selectedVM && ( <> {/* CPU Usage */} 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 {/* Network I/O */} Network I/O ↓ {((selectedVM.netin || 0) / 1024 ** 2).toFixed(2)} MB ↑ {((selectedVM.netout || 0) / 1024 ** 2).toFixed(2)} MB {getOSIcon(vmDetails?.os_info, selectedVM.type)} {detailsLoading ? ( Loading configuration... ) : vmDetails?.config ? ( <> Resources setShowNotes(!showNotes)} className="text-xs max-sm:bg-black/5 max-sm:dark:bg-white/5 sm:bg-transparent sm:hover:bg-black/5 sm:dark:hover:bg-white/5" > {showNotes ? ( <> Hide Notes > ) : ( <> Notes > )} setShowAdditionalInfo(!showAdditionalInfo)} className="text-xs max-sm:bg-black/5 max-sm:dark:bg-white/5 sm:bg-transparent sm:hover:bg-black/5 sm:dark:hover:bg-white/5" > {showAdditionalInfo ? ( <> Less Info > ) : ( <> + Info > )} {vmDetails.config.cores && ( CPU Cores {vmDetails.config.cores} )} {vmDetails.config.memory && ( Memory {vmDetails.config.memory} MB )} {vmDetails.config.swap && ( Swap {vmDetails.config.swap} MB )} {/* IP Addresses with proper keys */} {selectedVM?.type === "lxc" && vmDetails?.lxc_ip_info && ( IP Addresses {vmDetails.lxc_ip_info.real_ips.map((ip, index) => ( {ip} ))} {vmDetails.lxc_ip_info.docker_ips.map((ip, index) => ( {ip} (Bridge) ))} )} {showNotes && ( Notes {!isEditingNotes && ( Edit )} {isEditingNotes ? ( setEditedNotes(e.target.value)} className="w-full min-h-[200px] p-3 text-sm bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono" placeholder="Enter notes here..." /> Cancel {savingNotes ? "Saving..." : "Save"} ) : vmDetails.config.description ? ( <> {(() => { const processed = processDescription(vmDetails.config.description) if (processed.error) { return ( Error decoding notes. Please edit to fix. ) } return ( ) })()} > ) : ( No notes yet. Click Edit to add notes. )} )} {showAdditionalInfo && ( {selectedVM?.type === "lxc" && vmDetails?.hardware_info && ( Container Configuration {/* Privileged Status */} {vmDetails.hardware_info.privileged !== null && vmDetails.hardware_info.privileged !== undefined && ( Privilege Level {vmDetails.hardware_info.privileged ? "Privileged" : "Unprivileged"} )} {/* GPU Passthrough with proper keys */} {vmDetails.hardware_info.gpu_passthrough && vmDetails.hardware_info.gpu_passthrough.length > 0 && ( GPU Passthrough {vmDetails.hardware_info.gpu_passthrough.map((gpu, index) => ( {gpu} ))} )} {/* Hardware Devices with proper keys */} {vmDetails.hardware_info.devices && vmDetails.hardware_info.devices.length > 0 && ( Hardware Devices {vmDetails.hardware_info.devices.map((device, index) => ( {device} ))} )} )} {/* Hardware Section */} Hardware {vmDetails.config.sockets && ( CPU Sockets {vmDetails.config.sockets} )} {vmDetails.config.cpu && ( CPU Type {vmDetails.config.cpu} )} {vmDetails.config.numa !== undefined && ( NUMA {vmDetails.config.numa ? "Enabled" : "Disabled"} )} {vmDetails.config.bios && ( BIOS {vmDetails.config.bios} )} {vmDetails.config.machine && ( Machine Type {vmDetails.config.machine} )} {vmDetails.config.vga && ( VGA {vmDetails.config.vga} )} {vmDetails.config.agent !== undefined && ( QEMU Agent {vmDetails.config.agent ? "Enabled" : "Disabled"} )} {vmDetails.config.tablet !== undefined && ( Tablet Pointer {vmDetails.config.tablet ? "Enabled" : "Disabled"} )} {vmDetails.config.localtime !== undefined && ( Local Time {vmDetails.config.localtime ? "Enabled" : "Disabled"} )} {/* Storage Section */} Storage {vmDetails.config.rootfs && ( Root Filesystem {vmDetails.config.rootfs} )} {vmDetails.config.scsihw && ( SCSI Controller {vmDetails.config.scsihw} )} {/* Disk Storage with proper keys */} {Object.keys(vmDetails.config) .filter((key) => key.match(/^(scsi|sata|ide|virtio)\d+$/)) .map((diskKey) => ( {diskKey.toUpperCase().replace(/(\d+)/, " $1")} {vmDetails.config[diskKey]} ))} {vmDetails.config.efidisk0 && ( EFI Disk {vmDetails.config.efidisk0} )} {vmDetails.config.tpmstate0 && ( TPM State {vmDetails.config.tpmstate0} )} {/* Mount Points with proper keys */} {Object.keys(vmDetails.config) .filter((key) => key.match(/^mp\d+$/)) .map((mpKey) => ( Mount Point {mpKey.replace("mp", "")} {vmDetails.config[mpKey]} ))} {/* Network Section */} Network {/* Network Interfaces with proper keys */} {Object.keys(vmDetails.config) .filter((key) => key.match(/^net\d+$/)) .map((netKey) => ( Network Interface {netKey.replace("net", "")} {vmDetails.config[netKey]} ))} {vmDetails.config.nameserver && ( DNS Nameserver {vmDetails.config.nameserver} )} {vmDetails.config.searchdomain && ( Search Domain {vmDetails.config.searchdomain} )} {vmDetails.config.hostname && ( Hostname {vmDetails.config.hostname} )} {/* PCI Devices with proper keys */} {Object.keys(vmDetails.config).some((key) => key.match(/^hostpci\d+$/)) && ( PCI Passthrough {Object.keys(vmDetails.config) .filter((key) => key.match(/^hostpci\d+$/)) .map((pciKey) => ( {pciKey.toUpperCase().replace(/(\d+)/, " $1")} {vmDetails.config[pciKey]} ))} )} {/* USB Devices with proper keys */} {Object.keys(vmDetails.config).some((key) => key.match(/^usb\d+$/)) && ( USB Devices {Object.keys(vmDetails.config) .filter((key) => key.match(/^usb\d+$/)) .map((usbKey) => ( {usbKey.toUpperCase().replace(/(\d+)/, " $1")} {vmDetails.config[usbKey]} ))} )} {/* Serial Ports with proper keys */} {Object.keys(vmDetails.config).some((key) => key.match(/^serial\d+$/)) && ( Serial Ports {Object.keys(vmDetails.config) .filter((key) => key.match(/^serial\d+$/)) .map((serialKey) => ( {serialKey.toUpperCase().replace(/(\d+)/, " $1")} {vmDetails.config[serialKey]} ))} )} )} > ) : null} > )} selectedVM && handleVMControl(selectedVM.vmid, "start")} > Start selectedVM && handleVMControl(selectedVM.vmid, "shutdown")} > Shutdown selectedVM && handleVMControl(selectedVM.vmid, "reboot")} > Reboot selectedVM && handleVMControl(selectedVM.vmid, "stop")} > Force Stop > ) : ( selectedVM && ( ) )} ) }
Virtual machines configured
Allocated CPU usage
Allocated disk space
Virtual machine details and controls for {selectedVM?.name}