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
|
||||
#[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</>;
|
||||
}
|
Loading…
Reference in New Issue
Block a user