Can delete the VM from the WebUI
This commit is contained in:
		| @@ -44,14 +44,14 @@ pub struct OSLoaderXML { | ||||
| } | ||||
|  | ||||
| /// Hypervisor features | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[derive(serde::Serialize, serde::Deserialize, Default)] | ||||
| #[serde(rename = "features")] | ||||
| pub struct FeaturesXML { | ||||
|     pub acpi: ACPIXML, | ||||
| } | ||||
|  | ||||
| /// ACPI feature | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[derive(serde::Serialize, serde::Deserialize, Default)] | ||||
| #[serde(rename = "acpi")] | ||||
| pub struct ACPIXML {} | ||||
|  | ||||
| @@ -98,6 +98,7 @@ pub struct DomainXML { | ||||
|     pub title: Option<String>, | ||||
|     pub description: Option<String>, | ||||
|     pub os: OSXML, | ||||
|     #[serde(default)] | ||||
|     pub features: FeaturesXML, | ||||
|     pub devices: DevicesXML, | ||||
|  | ||||
|   | ||||
							
								
								
									
										68
									
								
								virtweb_frontend/src/api/VMApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								virtweb_frontend/src/api/VMApi.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| /** | ||||
|  * Virtual Machines API | ||||
|  * | ||||
|  * @author Pierre HUBERT | ||||
|  */ | ||||
|  | ||||
| import { APIClient } from "./ApiClient"; | ||||
|  | ||||
| interface VMInfoInterface { | ||||
|   name: string; | ||||
|   uuid?: string; | ||||
|   genid?: string; | ||||
|   title?: string; | ||||
|   description?: string; | ||||
|   boot_type: "UEFI" | "UEFISecureBoot"; | ||||
|   architecture: "i686" | "x86_64"; | ||||
|   memory: number; | ||||
|   vnc_access: boolean; | ||||
| } | ||||
|  | ||||
| export class VMInfo implements VMInfoInterface { | ||||
|   name: string; | ||||
|   uuid?: string | undefined; | ||||
|   genid?: string | undefined; | ||||
|   title?: string | undefined; | ||||
|   description?: string | undefined; | ||||
|   boot_type: "UEFI" | "UEFISecureBoot"; | ||||
|   architecture: "i686" | "x86_64"; | ||||
|   memory: number; | ||||
|   vnc_access: boolean; | ||||
|  | ||||
|   constructor(int: VMInfoInterface) { | ||||
|     this.name = int.name; | ||||
|     this.uuid = int.uuid; | ||||
|     this.genid = int.genid; | ||||
|     this.title = int.title; | ||||
|     this.description = int.description; | ||||
|     this.boot_type = int.boot_type; | ||||
|     this.architecture = int.architecture; | ||||
|     this.memory = int.memory; | ||||
|     this.vnc_access = int.vnc_access; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export class VMApi { | ||||
|   /** | ||||
|    * Get the list of defined virtual machines | ||||
|    */ | ||||
|   static async GetList(): Promise<VMInfo[]> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         uri: "/vm/list", | ||||
|         method: "GET", | ||||
|       }) | ||||
|     ).data.map((i: VMInfoInterface) => new VMInfo(i)); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Delete a virtual machine | ||||
|    */ | ||||
|   static async Delete(vm: VMInfo, keep_files: boolean): Promise<void> { | ||||
|     await APIClient.exec({ | ||||
|       uri: `/vm/${vm.uuid}`, | ||||
|       method: "DELETE", | ||||
|       jsonData: { keep_files }, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| @@ -11,7 +11,8 @@ import React, { PropsWithChildren } from "react"; | ||||
| type ConfirmContext = ( | ||||
|   message: string, | ||||
|   title?: string, | ||||
|   confirmButton?: string | ||||
|   confirmButton?: string, | ||||
|   cancelButton?: string | ||||
| ) => Promise<boolean>; | ||||
|  | ||||
| const ConfirmContextK = React.createContext<ConfirmContext | null>(null); | ||||
| @@ -26,6 +27,9 @@ export function ConfirmDialogProvider( | ||||
|   const [confirmButton, setConfirmButton] = React.useState<string | undefined>( | ||||
|     undefined | ||||
|   ); | ||||
|   const [cancelButton, setCancelButton] = React.useState<string | undefined>( | ||||
|     undefined | ||||
|   ); | ||||
|  | ||||
|   const cb = React.useRef<null | ((a: boolean) => void)>(null); | ||||
|  | ||||
| @@ -36,10 +40,16 @@ export function ConfirmDialogProvider( | ||||
|     cb.current = null; | ||||
|   }; | ||||
|  | ||||
|   const hook: ConfirmContext = (message, title, confirmButton) => { | ||||
|   const hook: ConfirmContext = ( | ||||
|     message, | ||||
|     title, | ||||
|     confirmButton, | ||||
|     cancelButton | ||||
|   ) => { | ||||
|     setTitle(title); | ||||
|     setMessage(message); | ||||
|     setConfirmButton(confirmButton); | ||||
|     setCancelButton(cancelButton); | ||||
|     setOpen(true); | ||||
|  | ||||
|     return new Promise((res) => { | ||||
| @@ -67,7 +77,7 @@ export function ConfirmDialogProvider( | ||||
|         </DialogContent> | ||||
|         <DialogActions> | ||||
|           <Button onClick={() => handleClose(false)} autoFocus> | ||||
|             Cancel | ||||
|             {cancelButton ?? "Cancel"} | ||||
|           </Button> | ||||
|           <Button onClick={() => handleClose(true)} color="error"> | ||||
|             {confirmButton ?? "Confirm"} | ||||
|   | ||||
| @@ -1,3 +1,142 @@ | ||||
| import DeleteIcon from "@mui/icons-material/Delete"; | ||||
| import VisibilityIcon from "@mui/icons-material/Visibility"; | ||||
| import { | ||||
|   Button, | ||||
|   IconButton, | ||||
|   Paper, | ||||
|   Table, | ||||
|   TableBody, | ||||
|   TableCell, | ||||
|   TableContainer, | ||||
|   TableHead, | ||||
|   TableRow, | ||||
|   Tooltip, | ||||
| } from "@mui/material"; | ||||
| import { filesize } from "filesize"; | ||||
| import React from "react"; | ||||
| import { VMApi, VMInfo } from "../api/VMApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { VMStatusWidget } from "../widgets/vms/VMStatusWidget"; | ||||
| import { useSnackbar } from "../hooks/providers/SnackbarProvider"; | ||||
| import { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; | ||||
|  | ||||
| export function VirtualMachinesRoute(): React.ReactElement { | ||||
|   return <></>; | ||||
|   const [list, setList] = React.useState<VMInfo[] | undefined>(); | ||||
|  | ||||
|   const loadKey = React.useRef(1); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setList(await VMApi.GetList()); | ||||
|   }; | ||||
|  | ||||
|   const reload = () => { | ||||
|     loadKey.current += 1; | ||||
|     setList(undefined); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={loadKey.current} | ||||
|       errMsg="Failed to load Virtual Machines list!" | ||||
|       load={load} | ||||
|       ready={list !== undefined} | ||||
|       build={() => ( | ||||
|         <VirtWebRouteContainer | ||||
|           label="Virtual Machines" | ||||
|           actions={ | ||||
|             <> | ||||
|               <RouterLink to="/vms/new"> | ||||
|                 <Button>New</Button> | ||||
|               </RouterLink> | ||||
|             </> | ||||
|           } | ||||
|         > | ||||
|           <VMListWidget list={list!} onReload={reload} /> | ||||
|         </VirtWebRouteContainer> | ||||
|       )} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function VMListWidget(p: { | ||||
|   list: VMInfo[]; | ||||
|   onReload: () => void; | ||||
| }): React.ReactElement { | ||||
|   const confirm = useConfirm(); | ||||
|   const snackbar = useSnackbar(); | ||||
|  | ||||
|   const deleteVM = async (v: VMInfo) => { | ||||
|     try { | ||||
|       if ( | ||||
|         !(await confirm( | ||||
|           `Do you really want to delete the vm ${v.name}? The operation CANNOT be undone!`, | ||||
|           "Delete a VM", | ||||
|           "DELETE" | ||||
|         )) | ||||
|       ) | ||||
|         return; | ||||
|  | ||||
|       const keepData = !(await confirm( | ||||
|         "Do you want to delete the files of the VM?", | ||||
|         "Delete a VM", | ||||
|         "Delete the data", | ||||
|         "keep the data" | ||||
|       )); | ||||
|  | ||||
|       await VMApi.Delete(v, keepData); | ||||
|       snackbar("The VM was successfully deleted!"); | ||||
|  | ||||
|       p.onReload(); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       snackbar("Failed to delete VM!"); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <TableContainer component={Paper}> | ||||
|       <Table> | ||||
|         <TableHead> | ||||
|           <TableRow> | ||||
|             <TableCell>Name</TableCell> | ||||
|             <TableCell>Description</TableCell> | ||||
|             <TableCell>Memory</TableCell> | ||||
|             <TableCell>Status</TableCell> | ||||
|             <TableCell>Actions</TableCell> | ||||
|           </TableRow> | ||||
|         </TableHead> | ||||
|         <TableBody> | ||||
|           {p.list.map((row) => ( | ||||
|             <TableRow | ||||
|               key={row.name} | ||||
|               sx={{ "&:last-child td, &:last-child th": { border: 0 } }} | ||||
|             > | ||||
|               <TableCell component="th" scope="row"> | ||||
|                 {row.name} | ||||
|               </TableCell> | ||||
|               <TableCell>{row.description ?? ""}</TableCell> | ||||
|               <TableCell>{filesize(row.memory * 1000 * 1000)}</TableCell> | ||||
|               <TableCell> | ||||
|                 <VMStatusWidget d={row} /> | ||||
|               </TableCell> | ||||
|               <TableCell> | ||||
|                 <Tooltip title="View this VM"> | ||||
|                   <IconButton> | ||||
|                     <VisibilityIcon /> | ||||
|                   </IconButton> | ||||
|                 </Tooltip> | ||||
|                 <Tooltip title="Delete this VM"> | ||||
|                   <IconButton onClick={() => deleteVM(row)}> | ||||
|                     <DeleteIcon /> | ||||
|                   </IconButton> | ||||
|                 </Tooltip> | ||||
|               </TableCell> | ||||
|             </TableRow> | ||||
|           ))} | ||||
|         </TableBody> | ||||
|       </Table> | ||||
|     </TableContainer> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -35,7 +35,7 @@ export function BaseAuthenticatedPage(): React.ReactElement { | ||||
|           dense | ||||
|           component="nav" | ||||
|           sx={{ | ||||
|             minWidth: "180px", | ||||
|             minWidth: "200px", | ||||
|             backgroundColor: "background.paper", | ||||
|           }} | ||||
|         > | ||||
| @@ -45,7 +45,7 @@ export function BaseAuthenticatedPage(): React.ReactElement { | ||||
|             icon={<Icon path={mdiHome} size={1} />} | ||||
|           /> | ||||
|           <NavLink | ||||
|             label="Virtual machines" | ||||
|             label="Virtual Machines" | ||||
|             uri="/vms" | ||||
|             icon={<Icon path={mdiBoxShadow} size={1} />} | ||||
|           /> | ||||
|   | ||||
| @@ -1,16 +1,25 @@ | ||||
| import { Typography } from "@mui/material"; | ||||
| import { PropsWithChildren } from "react"; | ||||
| import React, { PropsWithChildren } from "react"; | ||||
|  | ||||
| export function VirtWebRouteContainer( | ||||
|   p: { | ||||
|     label: string; | ||||
|     actions?: React.ReactElement; | ||||
|   } & PropsWithChildren | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <div style={{ margin: "50px" }}> | ||||
|       <Typography variant="h4" style={{ marginBottom: "20px" }}> | ||||
|         {p.label} | ||||
|       </Typography> | ||||
|       <div | ||||
|         style={{ | ||||
|           display: "flex", | ||||
|           justifyContent: "space-between", | ||||
|           alignItems: "center", | ||||
|           marginBottom: "20px", | ||||
|         }} | ||||
|       > | ||||
|         <Typography variant="h4">{p.label}</Typography> | ||||
|         {p.actions ?? <></>} | ||||
|       </div> | ||||
|  | ||||
|       {p.children} | ||||
|     </div> | ||||
|   | ||||
							
								
								
									
										8
									
								
								virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| import { VMInfo } from "../../api/VMApi"; | ||||
|  | ||||
| export function VMStatusWidget(p: { | ||||
|   d: VMInfo; | ||||
|   onChange?: () => void; | ||||
| }): React.ReactElement { | ||||
|   return <>TODO</>; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user