Can delete the VM from the WebUI
This commit is contained in:
parent
6a3cf2e5c8
commit
3c00c23205
@ -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</>;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user