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