Can delete the VM from the WebUI

This commit is contained in:
Pierre HUBERT 2023-10-13 18:39:34 +02:00
parent 6a3cf2e5c8
commit 3c00c23205
7 changed files with 247 additions and 12 deletions

View File

@ -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,

View 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 },
});
}
}

View File

@ -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"}

View File

@ -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>
);
}

View File

@ -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} />}
/>

View File

@ -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>

View File

@ -0,0 +1,8 @@
import { VMInfo } from "../../api/VMApi";
export function VMStatusWidget(p: {
d: VMInfo;
onChange?: () => void;
}): React.ReactElement {
return <>TODO</>;
}