diff --git a/virtweb_backend/src/libvirt_lib_structures.rs b/virtweb_backend/src/libvirt_lib_structures.rs index a4ba609..39ecaa3 100644 --- a/virtweb_backend/src/libvirt_lib_structures.rs +++ b/virtweb_backend/src/libvirt_lib_structures.rs @@ -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, pub description: Option, pub os: OSXML, + #[serde(default)] pub features: FeaturesXML, pub devices: DevicesXML, diff --git a/virtweb_frontend/src/api/VMApi.ts b/virtweb_frontend/src/api/VMApi.ts new file mode 100644 index 0000000..969f559 --- /dev/null +++ b/virtweb_frontend/src/api/VMApi.ts @@ -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 { + 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 { + await APIClient.exec({ + uri: `/vm/${vm.uuid}`, + method: "DELETE", + jsonData: { keep_files }, + }); + } +} diff --git a/virtweb_frontend/src/hooks/providers/ConfirmDialogProvider.tsx b/virtweb_frontend/src/hooks/providers/ConfirmDialogProvider.tsx index edd2fc9..15fac2c 100644 --- a/virtweb_frontend/src/hooks/providers/ConfirmDialogProvider.tsx +++ b/virtweb_frontend/src/hooks/providers/ConfirmDialogProvider.tsx @@ -11,7 +11,8 @@ import React, { PropsWithChildren } from "react"; type ConfirmContext = ( message: string, title?: string, - confirmButton?: string + confirmButton?: string, + cancelButton?: string ) => Promise; const ConfirmContextK = React.createContext(null); @@ -26,6 +27,9 @@ export function ConfirmDialogProvider( const [confirmButton, setConfirmButton] = React.useState( undefined ); + const [cancelButton, setCancelButton] = React.useState( + undefined + ); const cb = React.useRef 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( + + + } + > + + + )} + /> + ); +} + +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 ( + + + + + Name + Description + Memory + Status + Actions + + + + {p.list.map((row) => ( + + + {row.name} + + {row.description ?? ""} + {filesize(row.memory * 1000 * 1000)} + + + + + + + + + + + deleteVM(row)}> + + + + + + ))} + +
+
+ ); } diff --git a/virtweb_frontend/src/widgets/BaseAuthenticatedPage.tsx b/virtweb_frontend/src/widgets/BaseAuthenticatedPage.tsx index b60adda..9197967 100644 --- a/virtweb_frontend/src/widgets/BaseAuthenticatedPage.tsx +++ b/virtweb_frontend/src/widgets/BaseAuthenticatedPage.tsx @@ -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={} /> } /> diff --git a/virtweb_frontend/src/widgets/VirtWebRouteContainer.tsx b/virtweb_frontend/src/widgets/VirtWebRouteContainer.tsx index 4467516..be69b45 100644 --- a/virtweb_frontend/src/widgets/VirtWebRouteContainer.tsx +++ b/virtweb_frontend/src/widgets/VirtWebRouteContainer.tsx @@ -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 (
- - {p.label} - +
+ {p.label} + {p.actions ?? <>} +
{p.children}
diff --git a/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx b/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx new file mode 100644 index 0000000..1170899 --- /dev/null +++ b/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx @@ -0,0 +1,8 @@ +import { VMInfo } from "../../api/VMApi"; + +export function VMStatusWidget(p: { + d: VMInfo; + onChange?: () => void; +}): React.ReactElement { + return <>TODO; +}