Compare commits
	
		
			1 Commits
		
	
	
		
			20250531v2
			...
			58c3257d1d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 58c3257d1d | 
							
								
								
									
										4
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -3413,9 +3413,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "tokio" | ||||
| version = "1.45.0" | ||||
| version = "1.45.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" | ||||
| checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" | ||||
| dependencies = [ | ||||
|  "backtrace", | ||||
|  "bytes", | ||||
|   | ||||
| @@ -36,7 +36,7 @@ lazy-regex = "3.4.1" | ||||
| thiserror = "2.0.12" | ||||
| image = "0.25.6" | ||||
| rand = "0.9.1" | ||||
| tokio = { version = "1.45.0", features = ["rt", "time", "macros"] } | ||||
| tokio = { version = "1.45.1", features = ["rt", "time", "macros"] } | ||||
| futures = "0.3.31" | ||||
| ipnetwork = { version = "0.21.1", features = ["serde"] } | ||||
| num = "0.4.3" | ||||
|   | ||||
| @@ -189,46 +189,6 @@ pub async fn handle_convert_request( | ||||
|     Ok(HttpResponse::Accepted().json("Successfully converted disk file")) | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct RenameDiskImageRequest { | ||||
|     name: String, | ||||
| } | ||||
|  | ||||
| /// Rename disk image | ||||
| pub async fn rename( | ||||
|     p: web::Path<DiskFilePath>, | ||||
|     req: web::Json<RenameDiskImageRequest>, | ||||
| ) -> HttpResult { | ||||
|     // Check source | ||||
|     if !files_utils::check_file_name(&p.filename) { | ||||
|         return Ok(HttpResponse::BadRequest().json("Invalid src file name!")); | ||||
|     } | ||||
|     let src_path = AppConfig::get().disk_images_file_path(&p.filename); | ||||
|     if !src_path.exists() { | ||||
|         return Ok(HttpResponse::NotFound().json("Disk image does not exists!")); | ||||
|     } | ||||
|  | ||||
|     // Check destination | ||||
|     if !files_utils::check_file_name(&req.name) { | ||||
|         return Ok(HttpResponse::BadRequest().json("Invalid dst file name!")); | ||||
|     } | ||||
|     let dst_path = AppConfig::get().disk_images_file_path(&req.name); | ||||
|     if dst_path.exists() { | ||||
|         return Ok(HttpResponse::Conflict().json("Destination name already exists!")); | ||||
|     } | ||||
|  | ||||
|     // Check extension | ||||
|     let disk = DiskFileInfo::load_file(&src_path)?; | ||||
|     if !disk.format.ext().iter().any(|e| req.name.ends_with(e)) { | ||||
|         return Ok(HttpResponse::BadRequest().json("Invalid destination file extension!")); | ||||
|     } | ||||
|  | ||||
|     // Perform rename | ||||
|     std::fs::rename(&src_path, &dst_path)?; | ||||
|  | ||||
|     Ok(HttpResponse::Accepted().finish()) | ||||
| } | ||||
|  | ||||
| /// Delete a disk image | ||||
| pub async fn delete(p: web::Path<DiskFilePath>) -> HttpResult { | ||||
|     if !files_utils::check_file_name(&p.filename) { | ||||
|   | ||||
| @@ -22,13 +22,10 @@ pub struct DomainMetadataXML { | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "os")] | ||||
| pub struct OSXML { | ||||
|     #[serde(rename = "@firmware", default, skip_serializing_if = "Option::is_none")] | ||||
|     pub firmware: Option<String>, | ||||
|     #[serde(rename = "@firmware", default)] | ||||
|     pub firmware: String, | ||||
|     pub r#type: OSTypeXML, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub loader: Option<OSLoaderXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub bootmenu: Option<OSBootMenuXML>, | ||||
|     pub smbios: Option<OSSMBiosXML>, | ||||
| } | ||||
|  | ||||
| @@ -52,16 +49,6 @@ pub struct OSLoaderXML { | ||||
|     pub secure: String, | ||||
| } | ||||
|  | ||||
| /// Legacy boot menu information | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "bootmenu")] | ||||
| pub struct OSBootMenuXML { | ||||
|     #[serde(rename = "@enable")] | ||||
|     pub enable: String, | ||||
|     #[serde(rename = "@timeout")] | ||||
|     pub timeout: usize, | ||||
| } | ||||
|  | ||||
| /// SMBIOS System information | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "smbios")] | ||||
|   | ||||
| @@ -6,7 +6,7 @@ use crate::libvirt_rest_structures::LibVirtStructError; | ||||
| use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction; | ||||
| use crate::utils::file_size_utils::FileSize; | ||||
| use crate::utils::files_utils; | ||||
| use crate::utils::vm_file_disks_utils::{VMDiskBus, VMDiskFormat, VMFileDisk}; | ||||
| use crate::utils::vm_file_disks_utils::{VMDiskFormat, VMFileDisk}; | ||||
| use lazy_regex::regex; | ||||
| use num::Integer; | ||||
|  | ||||
| @@ -17,7 +17,6 @@ pub struct VMGroupId(pub String); | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| pub enum BootType { | ||||
|     Legacy, | ||||
|     UEFI, | ||||
|     UEFISecureBoot, | ||||
| } | ||||
| @@ -314,11 +313,7 @@ impl VMInfo { | ||||
|                         "vd{}", | ||||
|                         ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()] | ||||
|                     ), | ||||
|                     bus: match disk.bus { | ||||
|                         VMDiskBus::Virtio => "virtio", | ||||
|                         VMDiskBus::SATA => "sata", | ||||
|                     } | ||||
|                     .to_string(), | ||||
|                     bus: "virtio".to_string(), | ||||
|                 }, | ||||
|                 readonly: None, | ||||
|                 boot: DiskBootXML { | ||||
| @@ -352,26 +347,13 @@ impl VMInfo { | ||||
|                     machine: "q35".to_string(), | ||||
|                     body: "hvm".to_string(), | ||||
|                 }, | ||||
|                 firmware: match self.boot_type { | ||||
|                     BootType::Legacy => None, | ||||
|                     _ => Some("efi".to_string()), | ||||
|                 }, | ||||
|                 loader: match self.boot_type { | ||||
|                     BootType::Legacy => None, | ||||
|                     _ => Some(OSLoaderXML { | ||||
|                         secure: match self.boot_type { | ||||
|                             BootType::UEFISecureBoot => "yes".to_string(), | ||||
|                             _ => "no".to_string(), | ||||
|                         }, | ||||
|                     }), | ||||
|                 }, | ||||
|                 bootmenu: match self.boot_type { | ||||
|                     BootType::Legacy => Some(OSBootMenuXML { | ||||
|                         enable: "yes".to_string(), | ||||
|                         timeout: 3000, | ||||
|                     }), | ||||
|                     _ => None, | ||||
|                 }, | ||||
|                 firmware: "efi".to_string(), | ||||
|                 loader: Some(OSLoaderXML { | ||||
|                     secure: match self.boot_type { | ||||
|                         BootType::UEFI => "no".to_string(), | ||||
|                         BootType::UEFISecureBoot => "yes".to_string(), | ||||
|                     }, | ||||
|                 }), | ||||
|                 smbios: Some(OSSMBiosXML { | ||||
|                     mode: "sysinfo".to_string(), | ||||
|                 }), | ||||
| @@ -463,10 +445,9 @@ impl VMInfo { | ||||
|                 .virtweb | ||||
|                 .group | ||||
|                 .map(VMGroupId), | ||||
|             boot_type: match (domain.os.loader, domain.os.bootmenu) { | ||||
|                 (_, Some(_)) => BootType::Legacy, | ||||
|                 (None, _) => BootType::UEFI, | ||||
|                 (Some(l), _) => match l.secure.as_str() { | ||||
|             boot_type: match domain.os.loader { | ||||
|                 None => BootType::UEFI, | ||||
|                 Some(l) => match l.secure.as_str() { | ||||
|                     "yes" => BootType::UEFISecureBoot, | ||||
|                     _ => BootType::UEFI, | ||||
|                 }, | ||||
| @@ -498,7 +479,7 @@ impl VMInfo { | ||||
|                 .iter() | ||||
|                 .filter(|d| d.device == "disk") | ||||
|                 .map(|d| { | ||||
|                     VMFileDisk::load_from_file(&d.source.file, &d.target.bus) | ||||
|                     VMFileDisk::load_from_file(&d.source.file) | ||||
|                         .expect("Failed to load file disk information!") | ||||
|                 }) | ||||
|                 .collect(), | ||||
|   | ||||
| @@ -352,10 +352,6 @@ async fn main() -> std::io::Result<()> { | ||||
|                 "/api/disk_images/{filename}/convert", | ||||
|                 web::post().to(disk_images_controller::convert), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/disk_images/{filename}/rename", | ||||
|                 web::post().to(disk_images_controller::rename), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/disk_images/{filename}", | ||||
|                 web::delete().to(disk_images_controller::delete), | ||||
|   | ||||
| @@ -183,13 +183,7 @@ impl DiskFileInfo { | ||||
|             // Convert QCow2 to Raw file | ||||
|             (DiskFileFormat::QCow2 { .. }, DiskFileFormat::Raw { is_sparse }) => { | ||||
|                 let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM); | ||||
|                 cmd.arg("convert") | ||||
|                     .arg("-f") | ||||
|                     .arg("qcow2") | ||||
|                     .arg("-O") | ||||
|                     .arg("raw") | ||||
|                     .arg(&self.file_path) | ||||
|                     .arg(&temp_file); | ||||
|                 cmd.arg("convert").arg(&self.file_path).arg(&temp_file); | ||||
|  | ||||
|                 if !is_sparse { | ||||
|                     cmd.args(["-S", "0"]); | ||||
| @@ -203,8 +197,6 @@ impl DiskFileInfo { | ||||
|             (DiskFileFormat::QCow2 { .. }, DiskFileFormat::QCow2 { .. }) => { | ||||
|                 let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM); | ||||
|                 cmd.arg("convert") | ||||
|                     .arg("-f") | ||||
|                     .arg("qcow2") | ||||
|                     .arg("-O") | ||||
|                     .arg("qcow2") | ||||
|                     .arg(&self.file_path) | ||||
| @@ -215,13 +207,7 @@ impl DiskFileInfo { | ||||
|             // Convert Raw to QCow2 file | ||||
|             (DiskFileFormat::Raw { .. }, DiskFileFormat::QCow2 { .. }) => { | ||||
|                 let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM); | ||||
|                 cmd.arg("convert") | ||||
|                     .arg("-f") | ||||
|                     .arg("raw") | ||||
|                     .arg("-O") | ||||
|                     .arg("qcow2") | ||||
|                     .arg(&self.file_path) | ||||
|                     .arg(&temp_file); | ||||
|                 cmd.arg("convert").arg(&self.file_path).arg(&temp_file); | ||||
|  | ||||
|                 cmd | ||||
|             } | ||||
|   | ||||
| @@ -13,12 +13,6 @@ enum VMDisksError { | ||||
|     Config(&'static str), | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| pub enum VMDiskBus { | ||||
|     Virtio, | ||||
|     SATA, | ||||
| } | ||||
|  | ||||
| /// Disk allocation type | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(tag = "format")] | ||||
| @@ -36,8 +30,6 @@ pub struct VMFileDisk { | ||||
|     pub name: String, | ||||
|     /// Disk size, in bytes | ||||
|     pub size: FileSize, | ||||
|     /// Disk bus | ||||
|     pub bus: VMDiskBus, | ||||
|     /// Disk format | ||||
|     #[serde(flatten)] | ||||
|     pub format: VMDiskFormat, | ||||
| @@ -49,7 +41,7 @@ pub struct VMFileDisk { | ||||
| } | ||||
|  | ||||
| impl VMFileDisk { | ||||
|     pub fn load_from_file(path: &str, bus: &str) -> anyhow::Result<Self> { | ||||
|     pub fn load_from_file(path: &str) -> anyhow::Result<Self> { | ||||
|         let file = Path::new(path); | ||||
|  | ||||
|         let info = DiskFileInfo::load_file(file)?; | ||||
| @@ -69,13 +61,6 @@ impl VMFileDisk { | ||||
|                 DiskFileFormat::QCow2 { .. } => VMDiskFormat::QCow2, | ||||
|                 _ => anyhow::bail!("Unsupported image format: {:?}", info.format), | ||||
|             }, | ||||
|  | ||||
|             bus: match bus { | ||||
|                 "virtio" => VMDiskBus::Virtio, | ||||
|                 "sata" => VMDiskBus::SATA, | ||||
|                 _ => anyhow::bail!("Unsupported disk bus type: {bus}"), | ||||
|             }, | ||||
|  | ||||
|             delete: false, | ||||
|             from_image: None, | ||||
|         }) | ||||
|   | ||||
| @@ -94,17 +94,6 @@ export class DiskImageApi { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Rename disk image file | ||||
|    */ | ||||
|   static async Rename(file: DiskImage, name: string): Promise<void> { | ||||
|     await APIClient.exec({ | ||||
|       method: "POST", | ||||
|       uri: `/disk_images/${file.file_name}/rename`, | ||||
|       jsonData: { name }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Delete disk image file | ||||
|    */ | ||||
|   | ||||
| @@ -19,13 +19,9 @@ export type VMState = | ||||
|  | ||||
| export type VMFileDisk = BaseFileVMDisk & (RawVMDisk | QCow2Disk); | ||||
|  | ||||
| export type DiskBusType = "Virtio" | "SATA"; | ||||
|  | ||||
| export interface BaseFileVMDisk { | ||||
|   size: number; | ||||
|   name: string; | ||||
|   bus: DiskBusType; | ||||
|  | ||||
|   delete: boolean; | ||||
|  | ||||
|   // For new disk only | ||||
| @@ -82,8 +78,6 @@ export interface VMNetBridge { | ||||
|   bridge: string; | ||||
| } | ||||
|  | ||||
| export type VMBootType = "UEFI" | "UEFISecureBoot" | "Legacy"; | ||||
|  | ||||
| interface VMInfoInterface { | ||||
|   name: string; | ||||
|   uuid?: string; | ||||
| @@ -91,7 +85,7 @@ interface VMInfoInterface { | ||||
|   title?: string; | ||||
|   description?: string; | ||||
|   group?: string; | ||||
|   boot_type: VMBootType; | ||||
|   boot_type: "UEFI" | "UEFISecureBoot"; | ||||
|   architecture: "i686" | "x86_64"; | ||||
|   memory: number; | ||||
|   number_vcpu: number; | ||||
| @@ -110,7 +104,7 @@ export class VMInfo implements VMInfoInterface { | ||||
|   title?: string; | ||||
|   description?: string; | ||||
|   group?: string; | ||||
|   boot_type: VMBootType; | ||||
|   boot_type: "UEFI" | "UEFISecureBoot"; | ||||
|   architecture: "i686" | "x86_64"; | ||||
|   number_vcpu: number; | ||||
|   memory: number; | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import DeleteIcon from "@mui/icons-material/Delete"; | ||||
| import DownloadIcon from "@mui/icons-material/Download"; | ||||
| import LoopIcon from "@mui/icons-material/Loop"; | ||||
| import MoreVertIcon from "@mui/icons-material/MoreVert"; | ||||
| import RefreshIcon from "@mui/icons-material/Refresh"; | ||||
| import { | ||||
|   Alert, | ||||
| @@ -9,10 +8,6 @@ import { | ||||
|   CircularProgress, | ||||
|   IconButton, | ||||
|   LinearProgress, | ||||
|   ListItemIcon, | ||||
|   ListItemText, | ||||
|   Menu, | ||||
|   MenuItem, | ||||
|   Tooltip, | ||||
|   Typography, | ||||
| } from "@mui/material"; | ||||
| @@ -169,11 +164,15 @@ function DiskImageList(p: { | ||||
|   const confirm = useConfirm(); | ||||
|   const loadingMessage = useLoadingMessage(); | ||||
|  | ||||
|   const [dlProgress, setDlProgress] = React.useState<undefined | number>(); | ||||
|  | ||||
|   const [currConversion, setCurrConversion] = React.useState< | ||||
|     DiskImage | undefined | ||||
|   >(); | ||||
|   const [dlProgress, setDlProgress] = React.useState<undefined | number>(); | ||||
|  | ||||
|   // Convert disk image file | ||||
|   const convertDiskImage = (entry: DiskImage) => { | ||||
|     setCurrConversion(entry); | ||||
|   }; | ||||
|  | ||||
|   // Download disk image file | ||||
|   const downloadDiskImage = async (entry: DiskImage) => { | ||||
| @@ -191,11 +190,6 @@ function DiskImageList(p: { | ||||
|     setDlProgress(undefined); | ||||
|   }; | ||||
|  | ||||
|   // Convert disk image file | ||||
|   const convertDiskImage = (entry: DiskImage) => { | ||||
|     setCurrConversion(entry); | ||||
|   }; | ||||
|  | ||||
|   // Delete disk image | ||||
|   const deleteDiskImage = async (entry: DiskImage) => { | ||||
|     if ( | ||||
| @@ -227,7 +221,7 @@ function DiskImageList(p: { | ||||
|     ); | ||||
|  | ||||
|   const columns: GridColDef<(typeof p.list)[number]>[] = [ | ||||
|     { field: "file_name", headerName: "File name", flex: 3, editable: true }, | ||||
|     { field: "file_name", headerName: "File name", flex: 3 }, | ||||
|     { | ||||
|       field: "format", | ||||
|       headerName: "Format", | ||||
| @@ -266,21 +260,28 @@ function DiskImageList(p: { | ||||
|     }, | ||||
|     { | ||||
|       field: "actions", | ||||
|       type: "actions", | ||||
|       headerName: "", | ||||
|       width: 55, | ||||
|       cellClassName: "actions", | ||||
|       editable: false, | ||||
|       getActions: (params) => { | ||||
|         return [ | ||||
|           <DiskImageActionMenu | ||||
|             key="menu" | ||||
|             diskImage={params.row} | ||||
|             onDownload={downloadDiskImage} | ||||
|             onConvert={convertDiskImage} | ||||
|             onDelete={deleteDiskImage} | ||||
|           />, | ||||
|         ]; | ||||
|       width: 140, | ||||
|       renderCell(params) { | ||||
|         return ( | ||||
|           <> | ||||
|             <Tooltip title="Convert disk image"> | ||||
|               <IconButton onClick={() => { convertDiskImage(params.row); }}> | ||||
|                 <LoopIcon /> | ||||
|               </IconButton> | ||||
|             </Tooltip> | ||||
|             <Tooltip title="Download disk image"> | ||||
|               <IconButton onClick={() => downloadDiskImage(params.row)}> | ||||
|                 <DownloadIcon /> | ||||
|               </IconButton> | ||||
|             </Tooltip> | ||||
|             <Tooltip title="Delete disk image"> | ||||
|               <IconButton onClick={() => deleteDiskImage(params.row)}> | ||||
|                 <DeleteIcon /> | ||||
|               </IconButton> | ||||
|             </Tooltip> | ||||
|           </> | ||||
|         ); | ||||
|       }, | ||||
|     }, | ||||
|   ]; | ||||
| @@ -326,92 +327,7 @@ function DiskImageList(p: { | ||||
|       )} | ||||
|  | ||||
|       {/* The table itself */} | ||||
|       <DataGrid<DiskImage> | ||||
|         getRowId={(c) => c.file_name} | ||||
|         rows={p.list} | ||||
|         columns={columns} | ||||
|         processRowUpdate={async (n, o) => { | ||||
|           try { | ||||
|             await DiskImageApi.Rename(o, n.file_name); | ||||
|             return n; | ||||
|           } catch (e) { | ||||
|             console.error("Failed to rename disk image!", e); | ||||
|             alert(`Failed to rename disk image! ${e}`); | ||||
|             throw e; | ||||
|           } finally { | ||||
|             p.onReload(); | ||||
|           } | ||||
|         }} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function DiskImageActionMenu(p: { | ||||
|   diskImage: DiskImage; | ||||
|   onDownload: (d: DiskImage) => void; | ||||
|   onConvert: (d: DiskImage) => void; | ||||
|   onDelete: (d: DiskImage) => void; | ||||
| }): React.ReactElement { | ||||
|   const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); | ||||
|   const open = Boolean(anchorEl); | ||||
|   const handleClick = (event: React.MouseEvent<HTMLElement>) => { | ||||
|     setAnchorEl(event.currentTarget); | ||||
|   }; | ||||
|   const handleClose = () => { | ||||
|     setAnchorEl(null); | ||||
|   }; | ||||
|   return ( | ||||
|     <> | ||||
|       <IconButton | ||||
|         aria-label="Actions" | ||||
|         aria-haspopup="true" | ||||
|         onClick={handleClick} | ||||
|       > | ||||
|         <MoreVertIcon /> | ||||
|       </IconButton> | ||||
|       <Menu anchorEl={anchorEl} open={open} onClose={handleClose}> | ||||
|         {/* Download disk image */} | ||||
|         <MenuItem | ||||
|           onClick={() => { | ||||
|             handleClose(); | ||||
|             p.onDownload(p.diskImage); | ||||
|           }} | ||||
|         > | ||||
|           <ListItemIcon> | ||||
|             <DownloadIcon /> | ||||
|           </ListItemIcon> | ||||
|           <ListItemText secondary={"Download disk image"}> | ||||
|             Download | ||||
|           </ListItemText> | ||||
|         </MenuItem> | ||||
|  | ||||
|         {/* Convert disk image */} | ||||
|         <MenuItem | ||||
|           onClick={() => { | ||||
|             handleClose(); | ||||
|             p.onConvert(p.diskImage); | ||||
|           }} | ||||
|         > | ||||
|           <ListItemIcon> | ||||
|             <LoopIcon /> | ||||
|           </ListItemIcon> | ||||
|           <ListItemText secondary={"Convert disk image"}>Convert</ListItemText> | ||||
|         </MenuItem> | ||||
|  | ||||
|         {/* Delete disk image */} | ||||
|         <MenuItem | ||||
|           onClick={() => { | ||||
|             handleClose(); | ||||
|             p.onDelete(p.diskImage); | ||||
|           }} | ||||
|         > | ||||
|           <ListItemIcon> | ||||
|             <DeleteIcon color="error" /> | ||||
|           </ListItemIcon> | ||||
|           <ListItemText secondary={"Delete disk image"}>Delete</ListItemText> | ||||
|         </MenuItem> | ||||
|       </Menu> | ||||
|       <DataGrid getRowId={(c) => c.file_name} rows={p.list} columns={columns} /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -1,24 +0,0 @@ | ||||
| import { DiskBusType } from "../../api/VMApi"; | ||||
| import { SelectInput } from "./SelectInput"; | ||||
|  | ||||
| export function DiskBusSelect(p: { | ||||
|   editable: boolean; | ||||
|   value: DiskBusType; | ||||
|   label?: string; | ||||
|   onValueChange: (value: DiskBusType) => void; | ||||
|   size?: "medium" | "small"; | ||||
|   disableUnderline?: boolean; | ||||
|   disableBottomMargin?: boolean; | ||||
| }): React.ReactElement { | ||||
|   return ( | ||||
|     <SelectInput | ||||
|       {...p} | ||||
|       label={p.label ?? "Disk bus type"} | ||||
|       options={[ | ||||
|         { label: "virtio", value: "Virtio" }, | ||||
|         { label: "sata", value: "SATA" }, | ||||
|       ]} | ||||
|       onValueChange={(v) => { p.onValueChange(v as any); }} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @@ -1,12 +1,12 @@ | ||||
| import React from "react"; | ||||
| import { DiskImage } from "../../api/DiskImageApi"; | ||||
| import { | ||||
|   FormControl, | ||||
|   InputLabel, | ||||
|   MenuItem, | ||||
|   Select, | ||||
|   MenuItem, | ||||
|   SelectChangeEvent, | ||||
| } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { DiskImage } from "../../api/DiskImageApi"; | ||||
| import { FileDiskImageWidget } from "../FileDiskImageWidget"; | ||||
|  | ||||
| /** | ||||
| @@ -30,7 +30,7 @@ export function DiskImageSelect(p: { | ||||
|           <i>None</i> | ||||
|         </MenuItem> | ||||
|         {p.list.map((d) => ( | ||||
|           <MenuItem key={d.file_name} value={d.file_name}> | ||||
|           <MenuItem value={d.file_name}> | ||||
|             <FileDiskImageWidget image={d} /> | ||||
|           </MenuItem> | ||||
|         ))} | ||||
|   | ||||
| @@ -17,11 +17,8 @@ export function SelectInput(p: { | ||||
|   value?: string; | ||||
|   editable: boolean; | ||||
|   label?: string; | ||||
|   size?: "medium" | "small"; | ||||
|   options: SelectOption[]; | ||||
|   onValueChange: (o?: string) => void; | ||||
|   disableUnderline?: boolean; | ||||
|   disableBottomMargin?: boolean; | ||||
| }): React.ReactElement { | ||||
|   if (!p.editable && !p.value) return <></>; | ||||
|  | ||||
| @@ -31,18 +28,12 @@ export function SelectInput(p: { | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <FormControl | ||||
|       fullWidth | ||||
|       variant="standard" | ||||
|       style={{ marginBottom: p.disableBottomMargin ? "0px" : "15px" }} | ||||
|     > | ||||
|     <FormControl fullWidth variant="standard" style={{ marginBottom: "15px" }}> | ||||
|       {p.label && <InputLabel>{p.label}</InputLabel>} | ||||
|       <Select | ||||
|         {...p} | ||||
|         value={p.value ?? ""} | ||||
|         onChange={(e) => { | ||||
|           p.onValueChange(e.target.value); | ||||
|         }} | ||||
|         label={p.label} | ||||
|         onChange={(e) => { p.onValueChange(e.target.value); }} | ||||
|       > | ||||
|         {p.options.map((e) => ( | ||||
|           <MenuItem | ||||
|   | ||||
| @@ -1,20 +1,28 @@ | ||||
| import { mdiHarddiskPlus } from "@mdi/js"; | ||||
| import { mdiHarddisk, mdiHarddiskPlus } from "@mdi/js"; | ||||
| import Icon from "@mdi/react"; | ||||
| import CheckCircleIcon from "@mui/icons-material/CheckCircle"; | ||||
| import DeleteIcon from "@mui/icons-material/Delete"; | ||||
| import { Button, IconButton, Paper, Tooltip } from "@mui/material"; | ||||
| import { | ||||
|   Avatar, | ||||
|   Button, | ||||
|   IconButton, | ||||
|   ListItem, | ||||
|   ListItemAvatar, | ||||
|   ListItemText, | ||||
|   Paper, | ||||
|   Tooltip, | ||||
| } from "@mui/material"; | ||||
| import { filesize } from "filesize"; | ||||
| import React from "react"; | ||||
| import { DiskImage } from "../../api/DiskImageApi"; | ||||
| import { ServerApi } from "../../api/ServerApi"; | ||||
| import { VMFileDisk, VMInfo, VMState } from "../../api/VMApi"; | ||||
| import { ConvertDiskImageDialog } from "../../dialogs/ConvertDiskImageDialog"; | ||||
| import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; | ||||
| import { VMDiskFileWidget } from "../vms/VMDiskFileWidget"; | ||||
| import { CheckboxInput } from "./CheckboxInput"; | ||||
| import { DiskBusSelect } from "./DiskBusSelect"; | ||||
| import { DiskImageSelect } from "./DiskImageSelect"; | ||||
| import { SelectInput } from "./SelectInput"; | ||||
| import { TextInput } from "./TextInput"; | ||||
| import { DiskImageSelect } from "./DiskImageSelect"; | ||||
| import { DiskImage } from "../../api/DiskImageApi"; | ||||
|  | ||||
| export function VMDisksList(p: { | ||||
|   vm: VMInfo; | ||||
| @@ -31,7 +39,6 @@ export function VMDisksList(p: { | ||||
|     p.vm.file_disks.push({ | ||||
|       format: "QCow2", | ||||
|       size: 10000 * 1000 * 1000, | ||||
|       bus: "Virtio", | ||||
|       delete: false, | ||||
|       name: `disk${p.vm.file_disks.length}`, | ||||
|       new: true, | ||||
| @@ -115,8 +122,7 @@ function DiskInfo(p: { | ||||
|  | ||||
|   if (!p.editable || !p.disk.new) | ||||
|     return ( | ||||
|       <VMDiskFileWidget | ||||
|         {...p} | ||||
|       <ListItem | ||||
|         secondaryAction={ | ||||
|           <> | ||||
|             {p.editable && ( | ||||
| @@ -150,7 +156,32 @@ function DiskInfo(p: { | ||||
|             )} | ||||
|           </> | ||||
|         } | ||||
|       /> | ||||
|       > | ||||
|         <ListItemAvatar> | ||||
|           <Avatar> | ||||
|             <Icon path={mdiHarddisk} /> | ||||
|           </Avatar> | ||||
|         </ListItemAvatar> | ||||
|         <ListItemText | ||||
|           primary={ | ||||
|             <> | ||||
|               {p.disk.name}{" "} | ||||
|               {p.disk.deleteType && ( | ||||
|                 <span style={{ color: "red" }}> | ||||
|                   {p.disk.deleteType === "deletefile" | ||||
|                     ? "Remove, DELETING block file" | ||||
|                     : "Remove, keeping block file"} | ||||
|                 </span> | ||||
|               )} | ||||
|             </> | ||||
|           } | ||||
|           secondary={`${filesize(p.disk.size)} - ${p.disk.format}${ | ||||
|             p.disk.format == "Raw" | ||||
|               ? " - " + (p.disk.is_sparse ? "Sparse" : "Fixed") | ||||
|               : "" | ||||
|           }`} | ||||
|         /> | ||||
|       </ListItem> | ||||
|     ); | ||||
|  | ||||
|   return ( | ||||
| @@ -189,17 +220,6 @@ function DiskInfo(p: { | ||||
|         }} | ||||
|       /> | ||||
|  | ||||
|       {/* Bus selection */} | ||||
|       <DiskBusSelect | ||||
|         editable | ||||
|         value={p.disk.bus} | ||||
|         onValueChange={(v) => { | ||||
|           p.disk.bus = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|  | ||||
|       {/* Raw disk: choose sparse mode */} | ||||
|       {p.disk.format === "Raw" && ( | ||||
|         <CheckboxInput | ||||
|           editable | ||||
|   | ||||
| @@ -707,11 +707,6 @@ export function TokenRightsEditor(p: { | ||||
|               right={{ verb: "POST", path: "/api/disk_images/*/convert" }} | ||||
|               label="Convert disk images" | ||||
|             /> | ||||
|             <RouteRight | ||||
|               {...p} | ||||
|               right={{ verb: "POST", path: "/api/disk_images/*/rename" }} | ||||
|               label="Rename disk images" | ||||
|             /> | ||||
|             <RouteRight | ||||
|               {...p} | ||||
|               right={{ verb: "DELETE", path: "/api/disk_images/*" }} | ||||
|   | ||||
| @@ -280,7 +280,6 @@ function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement { | ||||
|           options={[ | ||||
|             { label: "UEFI with Secure Boot", value: "UEFISecureBoot" }, | ||||
|             { label: "UEFI", value: "UEFI" }, | ||||
|             { label: "Legacy", value: "Legacy" }, | ||||
|           ]} | ||||
|         /> | ||||
|  | ||||
|   | ||||
| @@ -3,69 +3,18 @@ import { Icon } from "@mdi/react"; | ||||
| import { Avatar, ListItem, ListItemAvatar, ListItemText } from "@mui/material"; | ||||
| import { filesize } from "filesize"; | ||||
| import { VMFileDisk } from "../../api/VMApi"; | ||||
| import { DiskBusSelect } from "../forms/DiskBusSelect"; | ||||
|  | ||||
| export function VMDiskFileWidget(p: { | ||||
|   editable?: boolean; | ||||
|   disk: VMFileDisk; | ||||
|   secondaryAction?: React.ReactElement; | ||||
|   onChange?: () => void; | ||||
| }): React.ReactElement { | ||||
|   const info = [filesize(p.disk.size), p.disk.format]; | ||||
|  | ||||
|   if (p.disk.format === "Raw") info.push(p.disk.is_sparse ? "Sparse" : "Fixed"); | ||||
|  | ||||
|   if (!p.editable) info.push(p.disk.bus); | ||||
|  | ||||
| export function VMDiskFileWidget(p: { disk: VMFileDisk }): React.ReactElement { | ||||
|   return ( | ||||
|     <ListItem secondaryAction={p.secondaryAction}> | ||||
|     <ListItem> | ||||
|       <ListItemAvatar> | ||||
|         <Avatar> | ||||
|           <Icon path={mdiHarddisk} /> | ||||
|         </Avatar> | ||||
|       </ListItemAvatar> | ||||
|       <ListItemText | ||||
|         primary={ | ||||
|           <> | ||||
|             {p.disk.name}{" "} | ||||
|             {p.disk.deleteType && ( | ||||
|               <span style={{ color: "red" }}> | ||||
|                 {p.disk.deleteType === "deletefile" | ||||
|                   ? "Remove, DELETING block file" | ||||
|                   : "Remove, keeping block file"} | ||||
|               </span> | ||||
|             )} | ||||
|           </> | ||||
|         } | ||||
|         secondary={ | ||||
|           <div style={{ display: "flex", alignItems: "center" }}> | ||||
|             {p.editable ? ( | ||||
|               <div | ||||
|                 style={{ | ||||
|                   maxWidth: "80px", | ||||
|                   display: "inline-block", | ||||
|                   marginRight: "10px", | ||||
|                 }} | ||||
|               > | ||||
|                 <DiskBusSelect | ||||
|                   onValueChange={(v) => { | ||||
|                     p.disk.bus = v; | ||||
|                     p.onChange?.(); | ||||
|                   }} | ||||
|                   label="" | ||||
|                   editable | ||||
|                   value={p.disk.bus} | ||||
|                   size="small" | ||||
|                   disableUnderline | ||||
|                   disableBottomMargin | ||||
|                 /> | ||||
|               </div> | ||||
|             ) : ( | ||||
|               "" | ||||
|             )} | ||||
|             <div style={{ height: "100%" }}>{info.join(" - ")}</div> | ||||
|           </div> | ||||
|         } | ||||
|         primary={p.disk.name} | ||||
|         secondary={`${p.disk.format} - ${filesize(p.disk.size)}`} | ||||
|       /> | ||||
|     </ListItem> | ||||
|   ); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user