Can delete the VM from the WebUI
This commit is contained in:
		@@ -44,14 +44,14 @@ pub struct OSLoaderXML {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Hypervisor features
 | 
					/// Hypervisor features
 | 
				
			||||||
#[derive(serde::Serialize, serde::Deserialize)]
 | 
					#[derive(serde::Serialize, serde::Deserialize, Default)]
 | 
				
			||||||
#[serde(rename = "features")]
 | 
					#[serde(rename = "features")]
 | 
				
			||||||
pub struct FeaturesXML {
 | 
					pub struct FeaturesXML {
 | 
				
			||||||
    pub acpi: ACPIXML,
 | 
					    pub acpi: ACPIXML,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// ACPI feature
 | 
					/// ACPI feature
 | 
				
			||||||
#[derive(serde::Serialize, serde::Deserialize)]
 | 
					#[derive(serde::Serialize, serde::Deserialize, Default)]
 | 
				
			||||||
#[serde(rename = "acpi")]
 | 
					#[serde(rename = "acpi")]
 | 
				
			||||||
pub struct ACPIXML {}
 | 
					pub struct ACPIXML {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -98,6 +98,7 @@ pub struct DomainXML {
 | 
				
			|||||||
    pub title: Option<String>,
 | 
					    pub title: Option<String>,
 | 
				
			||||||
    pub description: Option<String>,
 | 
					    pub description: Option<String>,
 | 
				
			||||||
    pub os: OSXML,
 | 
					    pub os: OSXML,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
    pub features: FeaturesXML,
 | 
					    pub features: FeaturesXML,
 | 
				
			||||||
    pub devices: DevicesXML,
 | 
					    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 = (
 | 
					type ConfirmContext = (
 | 
				
			||||||
  message: string,
 | 
					  message: string,
 | 
				
			||||||
  title?: string,
 | 
					  title?: string,
 | 
				
			||||||
  confirmButton?: string
 | 
					  confirmButton?: string,
 | 
				
			||||||
 | 
					  cancelButton?: string
 | 
				
			||||||
) => Promise<boolean>;
 | 
					) => Promise<boolean>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ConfirmContextK = React.createContext<ConfirmContext | null>(null);
 | 
					const ConfirmContextK = React.createContext<ConfirmContext | null>(null);
 | 
				
			||||||
@@ -26,6 +27,9 @@ export function ConfirmDialogProvider(
 | 
				
			|||||||
  const [confirmButton, setConfirmButton] = React.useState<string | undefined>(
 | 
					  const [confirmButton, setConfirmButton] = React.useState<string | undefined>(
 | 
				
			||||||
    undefined
 | 
					    undefined
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					  const [cancelButton, setCancelButton] = React.useState<string | undefined>(
 | 
				
			||||||
 | 
					    undefined
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const cb = React.useRef<null | ((a: boolean) => void)>(null);
 | 
					  const cb = React.useRef<null | ((a: boolean) => void)>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -36,10 +40,16 @@ export function ConfirmDialogProvider(
 | 
				
			|||||||
    cb.current = null;
 | 
					    cb.current = null;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const hook: ConfirmContext = (message, title, confirmButton) => {
 | 
					  const hook: ConfirmContext = (
 | 
				
			||||||
 | 
					    message,
 | 
				
			||||||
 | 
					    title,
 | 
				
			||||||
 | 
					    confirmButton,
 | 
				
			||||||
 | 
					    cancelButton
 | 
				
			||||||
 | 
					  ) => {
 | 
				
			||||||
    setTitle(title);
 | 
					    setTitle(title);
 | 
				
			||||||
    setMessage(message);
 | 
					    setMessage(message);
 | 
				
			||||||
    setConfirmButton(confirmButton);
 | 
					    setConfirmButton(confirmButton);
 | 
				
			||||||
 | 
					    setCancelButton(cancelButton);
 | 
				
			||||||
    setOpen(true);
 | 
					    setOpen(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return new Promise((res) => {
 | 
					    return new Promise((res) => {
 | 
				
			||||||
@@ -67,7 +77,7 @@ export function ConfirmDialogProvider(
 | 
				
			|||||||
        </DialogContent>
 | 
					        </DialogContent>
 | 
				
			||||||
        <DialogActions>
 | 
					        <DialogActions>
 | 
				
			||||||
          <Button onClick={() => handleClose(false)} autoFocus>
 | 
					          <Button onClick={() => handleClose(false)} autoFocus>
 | 
				
			||||||
            Cancel
 | 
					            {cancelButton ?? "Cancel"}
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
          <Button onClick={() => handleClose(true)} color="error">
 | 
					          <Button onClick={() => handleClose(true)} color="error">
 | 
				
			||||||
            {confirmButton ?? "Confirm"}
 | 
					            {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 {
 | 
					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
 | 
					          dense
 | 
				
			||||||
          component="nav"
 | 
					          component="nav"
 | 
				
			||||||
          sx={{
 | 
					          sx={{
 | 
				
			||||||
            minWidth: "180px",
 | 
					            minWidth: "200px",
 | 
				
			||||||
            backgroundColor: "background.paper",
 | 
					            backgroundColor: "background.paper",
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
@@ -45,7 +45,7 @@ export function BaseAuthenticatedPage(): React.ReactElement {
 | 
				
			|||||||
            icon={<Icon path={mdiHome} size={1} />}
 | 
					            icon={<Icon path={mdiHome} size={1} />}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
          <NavLink
 | 
					          <NavLink
 | 
				
			||||||
            label="Virtual machines"
 | 
					            label="Virtual Machines"
 | 
				
			||||||
            uri="/vms"
 | 
					            uri="/vms"
 | 
				
			||||||
            icon={<Icon path={mdiBoxShadow} size={1} />}
 | 
					            icon={<Icon path={mdiBoxShadow} size={1} />}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,16 +1,25 @@
 | 
				
			|||||||
import { Typography } from "@mui/material";
 | 
					import { Typography } from "@mui/material";
 | 
				
			||||||
import { PropsWithChildren } from "react";
 | 
					import React, { PropsWithChildren } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function VirtWebRouteContainer(
 | 
					export function VirtWebRouteContainer(
 | 
				
			||||||
  p: {
 | 
					  p: {
 | 
				
			||||||
    label: string;
 | 
					    label: string;
 | 
				
			||||||
 | 
					    actions?: React.ReactElement;
 | 
				
			||||||
  } & PropsWithChildren
 | 
					  } & PropsWithChildren
 | 
				
			||||||
): React.ReactElement {
 | 
					): React.ReactElement {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div style={{ margin: "50px" }}>
 | 
					    <div style={{ margin: "50px" }}>
 | 
				
			||||||
      <Typography variant="h4" style={{ marginBottom: "20px" }}>
 | 
					      <div
 | 
				
			||||||
        {p.label}
 | 
					        style={{
 | 
				
			||||||
      </Typography>
 | 
					          display: "flex",
 | 
				
			||||||
 | 
					          justifyContent: "space-between",
 | 
				
			||||||
 | 
					          alignItems: "center",
 | 
				
			||||||
 | 
					          marginBottom: "20px",
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Typography variant="h4">{p.label}</Typography>
 | 
				
			||||||
 | 
					        {p.actions ?? <></>}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {p.children}
 | 
					      {p.children}
 | 
				
			||||||
    </div>
 | 
					    </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