From 7ef5afb9789c779dc5f0fdfdf2ec3b7038eb1411 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Mon, 16 Oct 2023 13:46:46 +0200 Subject: [PATCH] Show VM status --- virtweb_frontend/src/api/VMApi.ts | 99 ++++++++++++ .../src/routes/VirtualMachinesRoute.tsx | 8 +- .../src/widgets/vms/VMStatusWidget.tsx | 152 +++++++++++++++++- 3 files changed, 253 insertions(+), 6 deletions(-) diff --git a/virtweb_frontend/src/api/VMApi.ts b/virtweb_frontend/src/api/VMApi.ts index 969f559..88e9bc0 100644 --- a/virtweb_frontend/src/api/VMApi.ts +++ b/virtweb_frontend/src/api/VMApi.ts @@ -6,6 +6,17 @@ import { APIClient } from "./ApiClient"; +export type VMState = + | "NoState" + | "Running" + | "Blocked" + | "Paused" + | "Shutdown" + | "Shutoff" + | "Crashed" + | "PowerManagementSuspended" + | "Other"; + interface VMInfoInterface { name: string; uuid?: string; @@ -40,6 +51,10 @@ export class VMInfo implements VMInfoInterface { this.memory = int.memory; this.vnc_access = int.vnc_access; } + + get ViewURL(): string { + return `/api/vm/${this.uuid}`; + } } export class VMApi { @@ -55,6 +70,90 @@ export class VMApi { ).data.map((i: VMInfoInterface) => new VMInfo(i)); } + /** + * Get the state of a VM + */ + static async GetState(vm: VMInfo): Promise { + return ( + await APIClient.exec({ + uri: `/vm/${vm.uuid}/state`, + method: "GET", + }) + ).data.state; + } + + /** + * Start the VM + */ + static async StartVM(vm: VMInfo): Promise { + return ( + await APIClient.exec({ + uri: `/vm/${vm.uuid}/start`, + method: "GET", + }) + ).data.state; + } + + /** + * Shutdown the VM + */ + static async ShutdownVM(vm: VMInfo): Promise { + return ( + await APIClient.exec({ + uri: `/vm/${vm.uuid}/shutdown`, + method: "GET", + }) + ).data.state; + } + + /** + * Restt the VM + */ + static async ResetVM(vm: VMInfo): Promise { + return ( + await APIClient.exec({ + uri: `/vm/${vm.uuid}/reset`, + method: "GET", + }) + ).data.state; + } + + /** + * Kill the VM + */ + static async KillVM(vm: VMInfo): Promise { + return ( + await APIClient.exec({ + uri: `/vm/${vm.uuid}/kill`, + method: "GET", + }) + ).data.state; + } + + /** + * Suspend the VM + */ + static async SuspendVM(vm: VMInfo): Promise { + return ( + await APIClient.exec({ + uri: `/vm/${vm.uuid}/suspend`, + method: "GET", + }) + ).data.state; + } + + /** + * Resume the VM + */ + static async ResumeVM(vm: VMInfo): Promise { + return ( + await APIClient.exec({ + uri: `/vm/${vm.uuid}/resume`, + method: "GET", + }) + ).data.state; + } + /** * Delete a virtual machine */ diff --git a/virtweb_frontend/src/routes/VirtualMachinesRoute.tsx b/virtweb_frontend/src/routes/VirtualMachinesRoute.tsx index c3e8418..842c45d 100644 --- a/virtweb_frontend/src/routes/VirtualMachinesRoute.tsx +++ b/virtweb_frontend/src/routes/VirtualMachinesRoute.tsx @@ -123,9 +123,11 @@ function VMListWidget(p: { - - - + + + + + deleteVM(row)}> diff --git a/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx b/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx index 1170899..66c0305 100644 --- a/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx +++ b/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx @@ -1,8 +1,154 @@ -import { VMInfo } from "../../api/VMApi"; +import PauseIcon from "@mui/icons-material/Pause"; +import PlayArrowIcon from "@mui/icons-material/PlayArrow"; +import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew"; +import ReplayIcon from "@mui/icons-material/Replay"; +import StopIcon from "@mui/icons-material/Stop"; +import { CircularProgress, IconButton, Tooltip } from "@mui/material"; +import React from "react"; +import { VMApi, VMInfo, VMState } from "../../api/VMApi"; +import { useAlert } from "../../hooks/providers/AlertDialogProvider"; +import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; +import { useSnackbar } from "../../hooks/providers/SnackbarProvider"; export function VMStatusWidget(p: { d: VMInfo; - onChange?: () => void; + onChange?: (s: VMState) => void; }): React.ReactElement { - return <>TODO; + const snackbar = useSnackbar(); + + const [state, setState] = React.useState(undefined); + + const refresh = async () => { + try { + const s = await VMApi.GetState(p.d); + if (s !== state) p.onChange?.(s); + setState(s); + } catch (e) { + console.error(e); + snackbar("Failed to refresh VM status!"); + } + }; + + const changedAction = () => setState(undefined); + + React.useEffect(() => { + const i = setInterval(() => refresh(), 3000); + + return () => clearInterval(i); + }); + + if (state === undefined) + return ( + <> + + + ); + + return ( +
+ {state} + + {/* Start VM */} + } + tooltip="Start the Virtual Machine" + performAction={() => VMApi.StartVM(p.d)} + onExecuted={changedAction} + /> + + {/* Resume VM */} + } + tooltip="Resume the Virtual Machine" + performAction={() => VMApi.ResumeVM(p.d)} + onExecuted={changedAction} + /> + + {/* Suspend VM */} + } + tooltip="Suspend the Virtual Machine" + confirmMessage="Do you really want to supsend this VM?" + performAction={() => VMApi.SuspendVM(p.d)} + onExecuted={changedAction} + /> + + {/* Shutdown VM */} + } + tooltip="Shutdown the Virtual Machine" + confirmMessage="Do you really want to shutdown this VM?" + performAction={() => VMApi.ShutdownVM(p.d)} + onExecuted={changedAction} + /> + + {/* Kill VM */} + } + tooltip="Kill the Virtual Machine" + confirmMessage="Do you really want to kill this VM? This could lead to data loss / corruption!" + performAction={() => VMApi.KillVM(p.d)} + onExecuted={changedAction} + /> + + {/* Reset VM */} + } + tooltip="Reset the Virtual Machine" + confirmMessage="Do you really want to reset this VM?" + performAction={() => VMApi.ResetVM(p.d)} + onExecuted={changedAction} + /> +
+ ); +} + +function ActionButton(p: { + currState: VMState; + cond: VMState[]; + icon: React.ReactElement; + tooltip: string; + confirmMessage?: string; + performAction: () => Promise; + onExecuted: () => void; +}): React.ReactElement { + const confirm = useConfirm(); + const alert = useAlert(); + + if (!p.cond.includes(p.currState)) return <>; + + const performAction = async () => { + try { + if (p.confirmMessage && !(await confirm(p.confirmMessage))) return; + await p.performAction(); + p.onExecuted(); + } catch (e) { + console.error(e); + alert("Failed to perform action! " + e); + } + }; + + return ( + + + {p.icon} + + + ); }