Compare commits
	
		
			1 Commits
		
	
	
		
			20250531
			...
			58c3257d1d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 58c3257d1d | 
							
								
								
									
										4
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -3413,9 +3413,9 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "tokio" | name = "tokio" | ||||||
| version = "1.45.0" | version = "1.45.1" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" | checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "backtrace", |  "backtrace", | ||||||
|  "bytes", |  "bytes", | ||||||
|   | |||||||
| @@ -36,7 +36,7 @@ lazy-regex = "3.4.1" | |||||||
| thiserror = "2.0.12" | thiserror = "2.0.12" | ||||||
| image = "0.25.6" | image = "0.25.6" | ||||||
| rand = "0.9.1" | 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" | futures = "0.3.31" | ||||||
| ipnetwork = { version = "0.21.1", features = ["serde"] } | ipnetwork = { version = "0.21.1", features = ["serde"] } | ||||||
| num = "0.4.3" | num = "0.4.3" | ||||||
|   | |||||||
| @@ -189,46 +189,6 @@ pub async fn handle_convert_request( | |||||||
|     Ok(HttpResponse::Accepted().json("Successfully converted disk file")) |     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 | /// Delete a disk image | ||||||
| pub async fn delete(p: web::Path<DiskFilePath>) -> HttpResult { | pub async fn delete(p: web::Path<DiskFilePath>) -> HttpResult { | ||||||
|     if !files_utils::check_file_name(&p.filename) { |     if !files_utils::check_file_name(&p.filename) { | ||||||
|   | |||||||
| @@ -22,13 +22,10 @@ pub struct DomainMetadataXML { | |||||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||||
| #[serde(rename = "os")] | #[serde(rename = "os")] | ||||||
| pub struct OSXML { | pub struct OSXML { | ||||||
|     #[serde(rename = "@firmware", default, skip_serializing_if = "Option::is_none")] |     #[serde(rename = "@firmware", default)] | ||||||
|     pub firmware: Option<String>, |     pub firmware: String, | ||||||
|     pub r#type: OSTypeXML, |     pub r#type: OSTypeXML, | ||||||
|     #[serde(skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub loader: Option<OSLoaderXML>, |     pub loader: Option<OSLoaderXML>, | ||||||
|     #[serde(skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub bootmenu: Option<OSBootMenuXML>, |  | ||||||
|     pub smbios: Option<OSSMBiosXML>, |     pub smbios: Option<OSSMBiosXML>, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -52,16 +49,6 @@ pub struct OSLoaderXML { | |||||||
|     pub secure: String, |     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 | /// SMBIOS System information | ||||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||||
| #[serde(rename = "smbios")] | #[serde(rename = "smbios")] | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ use crate::libvirt_rest_structures::LibVirtStructError; | |||||||
| use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction; | use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction; | ||||||
| use crate::utils::file_size_utils::FileSize; | use crate::utils::file_size_utils::FileSize; | ||||||
| use crate::utils::files_utils; | 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 lazy_regex::regex; | ||||||
| use num::Integer; | use num::Integer; | ||||||
|  |  | ||||||
| @@ -17,7 +17,6 @@ pub struct VMGroupId(pub String); | |||||||
|  |  | ||||||
| #[derive(serde::Serialize, serde::Deserialize)] | #[derive(serde::Serialize, serde::Deserialize)] | ||||||
| pub enum BootType { | pub enum BootType { | ||||||
|     Legacy, |  | ||||||
|     UEFI, |     UEFI, | ||||||
|     UEFISecureBoot, |     UEFISecureBoot, | ||||||
| } | } | ||||||
| @@ -314,11 +313,7 @@ impl VMInfo { | |||||||
|                         "vd{}", |                         "vd{}", | ||||||
|                         ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()] |                         ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()] | ||||||
|                     ), |                     ), | ||||||
|                     bus: match disk.bus { |                     bus: "virtio".to_string(), | ||||||
|                         VMDiskBus::Virtio => "virtio", |  | ||||||
|                         VMDiskBus::SATA => "sata", |  | ||||||
|                     } |  | ||||||
|                     .to_string(), |  | ||||||
|                 }, |                 }, | ||||||
|                 readonly: None, |                 readonly: None, | ||||||
|                 boot: DiskBootXML { |                 boot: DiskBootXML { | ||||||
| @@ -352,26 +347,13 @@ impl VMInfo { | |||||||
|                     machine: "q35".to_string(), |                     machine: "q35".to_string(), | ||||||
|                     body: "hvm".to_string(), |                     body: "hvm".to_string(), | ||||||
|                 }, |                 }, | ||||||
|                 firmware: match self.boot_type { |                 firmware: "efi".to_string(), | ||||||
|                     BootType::Legacy => None, |                 loader: Some(OSLoaderXML { | ||||||
|                     _ => Some("efi".to_string()), |                     secure: match self.boot_type { | ||||||
|                 }, |                         BootType::UEFI => "no".to_string(), | ||||||
|                 loader: match self.boot_type { |                         BootType::UEFISecureBoot => "yes".to_string(), | ||||||
|                     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, |  | ||||||
|                 }, |  | ||||||
|                 smbios: Some(OSSMBiosXML { |                 smbios: Some(OSSMBiosXML { | ||||||
|                     mode: "sysinfo".to_string(), |                     mode: "sysinfo".to_string(), | ||||||
|                 }), |                 }), | ||||||
| @@ -463,10 +445,9 @@ impl VMInfo { | |||||||
|                 .virtweb |                 .virtweb | ||||||
|                 .group |                 .group | ||||||
|                 .map(VMGroupId), |                 .map(VMGroupId), | ||||||
|             boot_type: match (domain.os.loader, domain.os.bootmenu) { |             boot_type: match domain.os.loader { | ||||||
|                 (_, Some(_)) => BootType::Legacy, |                 None => BootType::UEFI, | ||||||
|                 (None, _) => BootType::UEFI, |                 Some(l) => match l.secure.as_str() { | ||||||
|                 (Some(l), _) => match l.secure.as_str() { |  | ||||||
|                     "yes" => BootType::UEFISecureBoot, |                     "yes" => BootType::UEFISecureBoot, | ||||||
|                     _ => BootType::UEFI, |                     _ => BootType::UEFI, | ||||||
|                 }, |                 }, | ||||||
| @@ -498,7 +479,7 @@ impl VMInfo { | |||||||
|                 .iter() |                 .iter() | ||||||
|                 .filter(|d| d.device == "disk") |                 .filter(|d| d.device == "disk") | ||||||
|                 .map(|d| { |                 .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!") |                         .expect("Failed to load file disk information!") | ||||||
|                 }) |                 }) | ||||||
|                 .collect(), |                 .collect(), | ||||||
|   | |||||||
| @@ -352,10 +352,6 @@ async fn main() -> std::io::Result<()> { | |||||||
|                 "/api/disk_images/{filename}/convert", |                 "/api/disk_images/{filename}/convert", | ||||||
|                 web::post().to(disk_images_controller::convert), |                 web::post().to(disk_images_controller::convert), | ||||||
|             ) |             ) | ||||||
|             .route( |  | ||||||
|                 "/api/disk_images/{filename}/rename", |  | ||||||
|                 web::post().to(disk_images_controller::rename), |  | ||||||
|             ) |  | ||||||
|             .route( |             .route( | ||||||
|                 "/api/disk_images/{filename}", |                 "/api/disk_images/{filename}", | ||||||
|                 web::delete().to(disk_images_controller::delete), |                 web::delete().to(disk_images_controller::delete), | ||||||
|   | |||||||
| @@ -13,12 +13,6 @@ enum VMDisksError { | |||||||
|     Config(&'static str), |     Config(&'static str), | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(serde::Serialize, serde::Deserialize)] |  | ||||||
| pub enum VMDiskBus { |  | ||||||
|     Virtio, |  | ||||||
|     SATA, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Disk allocation type | /// Disk allocation type | ||||||
| #[derive(serde::Serialize, serde::Deserialize)] | #[derive(serde::Serialize, serde::Deserialize)] | ||||||
| #[serde(tag = "format")] | #[serde(tag = "format")] | ||||||
| @@ -36,8 +30,6 @@ pub struct VMFileDisk { | |||||||
|     pub name: String, |     pub name: String, | ||||||
|     /// Disk size, in bytes |     /// Disk size, in bytes | ||||||
|     pub size: FileSize, |     pub size: FileSize, | ||||||
|     /// Disk bus |  | ||||||
|     pub bus: VMDiskBus, |  | ||||||
|     /// Disk format |     /// Disk format | ||||||
|     #[serde(flatten)] |     #[serde(flatten)] | ||||||
|     pub format: VMDiskFormat, |     pub format: VMDiskFormat, | ||||||
| @@ -49,7 +41,7 @@ pub struct VMFileDisk { | |||||||
| } | } | ||||||
|  |  | ||||||
| impl 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 file = Path::new(path); | ||||||
|  |  | ||||||
|         let info = DiskFileInfo::load_file(file)?; |         let info = DiskFileInfo::load_file(file)?; | ||||||
| @@ -69,13 +61,6 @@ impl VMFileDisk { | |||||||
|                 DiskFileFormat::QCow2 { .. } => VMDiskFormat::QCow2, |                 DiskFileFormat::QCow2 { .. } => VMDiskFormat::QCow2, | ||||||
|                 _ => anyhow::bail!("Unsupported image format: {:?}", info.format), |                 _ => 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, |             delete: false, | ||||||
|             from_image: None, |             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 |    * Delete disk image file | ||||||
|    */ |    */ | ||||||
|   | |||||||
| @@ -19,13 +19,9 @@ export type VMState = | |||||||
|  |  | ||||||
| export type VMFileDisk = BaseFileVMDisk & (RawVMDisk | QCow2Disk); | export type VMFileDisk = BaseFileVMDisk & (RawVMDisk | QCow2Disk); | ||||||
|  |  | ||||||
| export type DiskBusType = "Virtio" | "SATA"; |  | ||||||
|  |  | ||||||
| export interface BaseFileVMDisk { | export interface BaseFileVMDisk { | ||||||
|   size: number; |   size: number; | ||||||
|   name: string; |   name: string; | ||||||
|   bus: DiskBusType; |  | ||||||
|  |  | ||||||
|   delete: boolean; |   delete: boolean; | ||||||
|  |  | ||||||
|   // For new disk only |   // For new disk only | ||||||
| @@ -82,8 +78,6 @@ export interface VMNetBridge { | |||||||
|   bridge: string; |   bridge: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| export type VMBootType = "UEFI" | "UEFISecureBoot" | "Legacy"; |  | ||||||
|  |  | ||||||
| interface VMInfoInterface { | interface VMInfoInterface { | ||||||
|   name: string; |   name: string; | ||||||
|   uuid?: string; |   uuid?: string; | ||||||
| @@ -91,7 +85,7 @@ interface VMInfoInterface { | |||||||
|   title?: string; |   title?: string; | ||||||
|   description?: string; |   description?: string; | ||||||
|   group?: string; |   group?: string; | ||||||
|   boot_type: VMBootType; |   boot_type: "UEFI" | "UEFISecureBoot"; | ||||||
|   architecture: "i686" | "x86_64"; |   architecture: "i686" | "x86_64"; | ||||||
|   memory: number; |   memory: number; | ||||||
|   number_vcpu: number; |   number_vcpu: number; | ||||||
| @@ -110,7 +104,7 @@ export class VMInfo implements VMInfoInterface { | |||||||
|   title?: string; |   title?: string; | ||||||
|   description?: string; |   description?: string; | ||||||
|   group?: string; |   group?: string; | ||||||
|   boot_type: VMBootType; |   boot_type: "UEFI" | "UEFISecureBoot"; | ||||||
|   architecture: "i686" | "x86_64"; |   architecture: "i686" | "x86_64"; | ||||||
|   number_vcpu: number; |   number_vcpu: number; | ||||||
|   memory: number; |   memory: number; | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| import DeleteIcon from "@mui/icons-material/Delete"; | import DeleteIcon from "@mui/icons-material/Delete"; | ||||||
| import DownloadIcon from "@mui/icons-material/Download"; | import DownloadIcon from "@mui/icons-material/Download"; | ||||||
| import LoopIcon from "@mui/icons-material/Loop"; | import LoopIcon from "@mui/icons-material/Loop"; | ||||||
| import MoreVertIcon from "@mui/icons-material/MoreVert"; |  | ||||||
| import RefreshIcon from "@mui/icons-material/Refresh"; | import RefreshIcon from "@mui/icons-material/Refresh"; | ||||||
| import { | import { | ||||||
|   Alert, |   Alert, | ||||||
| @@ -9,10 +8,6 @@ import { | |||||||
|   CircularProgress, |   CircularProgress, | ||||||
|   IconButton, |   IconButton, | ||||||
|   LinearProgress, |   LinearProgress, | ||||||
|   ListItemIcon, |  | ||||||
|   ListItemText, |  | ||||||
|   Menu, |  | ||||||
|   MenuItem, |  | ||||||
|   Tooltip, |   Tooltip, | ||||||
|   Typography, |   Typography, | ||||||
| } from "@mui/material"; | } from "@mui/material"; | ||||||
| @@ -169,11 +164,15 @@ function DiskImageList(p: { | |||||||
|   const confirm = useConfirm(); |   const confirm = useConfirm(); | ||||||
|   const loadingMessage = useLoadingMessage(); |   const loadingMessage = useLoadingMessage(); | ||||||
|  |  | ||||||
|   const [dlProgress, setDlProgress] = React.useState<undefined | number>(); |  | ||||||
|  |  | ||||||
|   const [currConversion, setCurrConversion] = React.useState< |   const [currConversion, setCurrConversion] = React.useState< | ||||||
|     DiskImage | undefined |     DiskImage | undefined | ||||||
|   >(); |   >(); | ||||||
|  |   const [dlProgress, setDlProgress] = React.useState<undefined | number>(); | ||||||
|  |  | ||||||
|  |   // Convert disk image file | ||||||
|  |   const convertDiskImage = (entry: DiskImage) => { | ||||||
|  |     setCurrConversion(entry); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   // Download disk image file |   // Download disk image file | ||||||
|   const downloadDiskImage = async (entry: DiskImage) => { |   const downloadDiskImage = async (entry: DiskImage) => { | ||||||
| @@ -191,11 +190,6 @@ function DiskImageList(p: { | |||||||
|     setDlProgress(undefined); |     setDlProgress(undefined); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   // Convert disk image file |  | ||||||
|   const convertDiskImage = (entry: DiskImage) => { |  | ||||||
|     setCurrConversion(entry); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   // Delete disk image |   // Delete disk image | ||||||
|   const deleteDiskImage = async (entry: DiskImage) => { |   const deleteDiskImage = async (entry: DiskImage) => { | ||||||
|     if ( |     if ( | ||||||
| @@ -227,7 +221,7 @@ function DiskImageList(p: { | |||||||
|     ); |     ); | ||||||
|  |  | ||||||
|   const columns: GridColDef<(typeof p.list)[number]>[] = [ |   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", |       field: "format", | ||||||
|       headerName: "Format", |       headerName: "Format", | ||||||
| @@ -266,21 +260,28 @@ function DiskImageList(p: { | |||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       field: "actions", |       field: "actions", | ||||||
|       type: "actions", |  | ||||||
|       headerName: "", |       headerName: "", | ||||||
|       width: 55, |       width: 140, | ||||||
|       cellClassName: "actions", |       renderCell(params) { | ||||||
|       editable: false, |         return ( | ||||||
|       getActions: (params) => { |           <> | ||||||
|         return [ |             <Tooltip title="Convert disk image"> | ||||||
|           <DiskImageActionMenu |               <IconButton onClick={() => { convertDiskImage(params.row); }}> | ||||||
|             key="menu" |                 <LoopIcon /> | ||||||
|             diskImage={params.row} |               </IconButton> | ||||||
|             onDownload={downloadDiskImage} |             </Tooltip> | ||||||
|             onConvert={convertDiskImage} |             <Tooltip title="Download disk image"> | ||||||
|             onDelete={deleteDiskImage} |               <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 */} |       {/* The table itself */} | ||||||
|       <DataGrid<DiskImage> |       <DataGrid getRowId={(c) => c.file_name} rows={p.list} columns={columns} /> | ||||||
|         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> |  | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 { | import { | ||||||
|   FormControl, |   FormControl, | ||||||
|   InputLabel, |   InputLabel, | ||||||
|   MenuItem, |  | ||||||
|   Select, |   Select, | ||||||
|  |   MenuItem, | ||||||
|   SelectChangeEvent, |   SelectChangeEvent, | ||||||
| } from "@mui/material"; | } from "@mui/material"; | ||||||
| import React from "react"; |  | ||||||
| import { DiskImage } from "../../api/DiskImageApi"; |  | ||||||
| import { FileDiskImageWidget } from "../FileDiskImageWidget"; | import { FileDiskImageWidget } from "../FileDiskImageWidget"; | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -30,7 +30,7 @@ export function DiskImageSelect(p: { | |||||||
|           <i>None</i> |           <i>None</i> | ||||||
|         </MenuItem> |         </MenuItem> | ||||||
|         {p.list.map((d) => ( |         {p.list.map((d) => ( | ||||||
|           <MenuItem key={d.file_name} value={d.file_name}> |           <MenuItem value={d.file_name}> | ||||||
|             <FileDiskImageWidget image={d} /> |             <FileDiskImageWidget image={d} /> | ||||||
|           </MenuItem> |           </MenuItem> | ||||||
|         ))} |         ))} | ||||||
|   | |||||||
| @@ -17,11 +17,8 @@ export function SelectInput(p: { | |||||||
|   value?: string; |   value?: string; | ||||||
|   editable: boolean; |   editable: boolean; | ||||||
|   label?: string; |   label?: string; | ||||||
|   size?: "medium" | "small"; |  | ||||||
|   options: SelectOption[]; |   options: SelectOption[]; | ||||||
|   onValueChange: (o?: string) => void; |   onValueChange: (o?: string) => void; | ||||||
|   disableUnderline?: boolean; |  | ||||||
|   disableBottomMargin?: boolean; |  | ||||||
| }): React.ReactElement { | }): React.ReactElement { | ||||||
|   if (!p.editable && !p.value) return <></>; |   if (!p.editable && !p.value) return <></>; | ||||||
|  |  | ||||||
| @@ -31,18 +28,12 @@ export function SelectInput(p: { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <FormControl |     <FormControl fullWidth variant="standard" style={{ marginBottom: "15px" }}> | ||||||
|       fullWidth |  | ||||||
|       variant="standard" |  | ||||||
|       style={{ marginBottom: p.disableBottomMargin ? "0px" : "15px" }} |  | ||||||
|     > |  | ||||||
|       {p.label && <InputLabel>{p.label}</InputLabel>} |       {p.label && <InputLabel>{p.label}</InputLabel>} | ||||||
|       <Select |       <Select | ||||||
|         {...p} |  | ||||||
|         value={p.value ?? ""} |         value={p.value ?? ""} | ||||||
|         onChange={(e) => { |         label={p.label} | ||||||
|           p.onValueChange(e.target.value); |         onChange={(e) => { p.onValueChange(e.target.value); }} | ||||||
|         }} |  | ||||||
|       > |       > | ||||||
|         {p.options.map((e) => ( |         {p.options.map((e) => ( | ||||||
|           <MenuItem |           <MenuItem | ||||||
|   | |||||||
| @@ -1,20 +1,28 @@ | |||||||
| import { mdiHarddiskPlus } from "@mdi/js"; | import { mdiHarddisk, mdiHarddiskPlus } from "@mdi/js"; | ||||||
| import Icon from "@mdi/react"; | import Icon from "@mdi/react"; | ||||||
| import CheckCircleIcon from "@mui/icons-material/CheckCircle"; | import CheckCircleIcon from "@mui/icons-material/CheckCircle"; | ||||||
| import DeleteIcon from "@mui/icons-material/Delete"; | 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 React from "react"; | ||||||
| import { DiskImage } from "../../api/DiskImageApi"; |  | ||||||
| import { ServerApi } from "../../api/ServerApi"; | import { ServerApi } from "../../api/ServerApi"; | ||||||
| import { VMFileDisk, VMInfo, VMState } from "../../api/VMApi"; | import { VMFileDisk, VMInfo, VMState } from "../../api/VMApi"; | ||||||
| import { ConvertDiskImageDialog } from "../../dialogs/ConvertDiskImageDialog"; | import { ConvertDiskImageDialog } from "../../dialogs/ConvertDiskImageDialog"; | ||||||
| import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; | import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; | ||||||
| import { VMDiskFileWidget } from "../vms/VMDiskFileWidget"; |  | ||||||
| import { CheckboxInput } from "./CheckboxInput"; | import { CheckboxInput } from "./CheckboxInput"; | ||||||
| import { DiskBusSelect } from "./DiskBusSelect"; |  | ||||||
| import { DiskImageSelect } from "./DiskImageSelect"; |  | ||||||
| import { SelectInput } from "./SelectInput"; | import { SelectInput } from "./SelectInput"; | ||||||
| import { TextInput } from "./TextInput"; | import { TextInput } from "./TextInput"; | ||||||
|  | import { DiskImageSelect } from "./DiskImageSelect"; | ||||||
|  | import { DiskImage } from "../../api/DiskImageApi"; | ||||||
|  |  | ||||||
| export function VMDisksList(p: { | export function VMDisksList(p: { | ||||||
|   vm: VMInfo; |   vm: VMInfo; | ||||||
| @@ -31,7 +39,6 @@ export function VMDisksList(p: { | |||||||
|     p.vm.file_disks.push({ |     p.vm.file_disks.push({ | ||||||
|       format: "QCow2", |       format: "QCow2", | ||||||
|       size: 10000 * 1000 * 1000, |       size: 10000 * 1000 * 1000, | ||||||
|       bus: "Virtio", |  | ||||||
|       delete: false, |       delete: false, | ||||||
|       name: `disk${p.vm.file_disks.length}`, |       name: `disk${p.vm.file_disks.length}`, | ||||||
|       new: true, |       new: true, | ||||||
| @@ -115,8 +122,7 @@ function DiskInfo(p: { | |||||||
|  |  | ||||||
|   if (!p.editable || !p.disk.new) |   if (!p.editable || !p.disk.new) | ||||||
|     return ( |     return ( | ||||||
|       <VMDiskFileWidget |       <ListItem | ||||||
|         {...p} |  | ||||||
|         secondaryAction={ |         secondaryAction={ | ||||||
|           <> |           <> | ||||||
|             {p.editable && ( |             {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 ( |   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" && ( |       {p.disk.format === "Raw" && ( | ||||||
|         <CheckboxInput |         <CheckboxInput | ||||||
|           editable |           editable | ||||||
|   | |||||||
| @@ -707,11 +707,6 @@ export function TokenRightsEditor(p: { | |||||||
|               right={{ verb: "POST", path: "/api/disk_images/*/convert" }} |               right={{ verb: "POST", path: "/api/disk_images/*/convert" }} | ||||||
|               label="Convert disk images" |               label="Convert disk images" | ||||||
|             /> |             /> | ||||||
|             <RouteRight |  | ||||||
|               {...p} |  | ||||||
|               right={{ verb: "POST", path: "/api/disk_images/*/rename" }} |  | ||||||
|               label="Rename disk images" |  | ||||||
|             /> |  | ||||||
|             <RouteRight |             <RouteRight | ||||||
|               {...p} |               {...p} | ||||||
|               right={{ verb: "DELETE", path: "/api/disk_images/*" }} |               right={{ verb: "DELETE", path: "/api/disk_images/*" }} | ||||||
|   | |||||||
| @@ -280,7 +280,6 @@ function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement { | |||||||
|           options={[ |           options={[ | ||||||
|             { label: "UEFI with Secure Boot", value: "UEFISecureBoot" }, |             { label: "UEFI with Secure Boot", value: "UEFISecureBoot" }, | ||||||
|             { label: "UEFI", value: "UEFI" }, |             { 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 { Avatar, ListItem, ListItemAvatar, ListItemText } from "@mui/material"; | ||||||
| import { filesize } from "filesize"; | import { filesize } from "filesize"; | ||||||
| import { VMFileDisk } from "../../api/VMApi"; | 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 ( |   return ( | ||||||
|     <ListItem secondaryAction={p.secondaryAction}> |     <ListItem> | ||||||
|       <ListItemAvatar> |       <ListItemAvatar> | ||||||
|         <Avatar> |         <Avatar> | ||||||
|           <Icon path={mdiHarddisk} /> |           <Icon path={mdiHarddisk} /> | ||||||
|         </Avatar> |         </Avatar> | ||||||
|       </ListItemAvatar> |       </ListItemAvatar> | ||||||
|       <ListItemText |       <ListItemText | ||||||
|         primary={ |         primary={p.disk.name} | ||||||
|           <> |         secondary={`${p.disk.format} - ${filesize(p.disk.size)}`} | ||||||
|             {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> |  | ||||||
|         } |  | ||||||
|       /> |       /> | ||||||
|     </ListItem> |     </ListItem> | ||||||
|   ); |   ); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user