diff --git a/remote_frontend/src/api/VMApi.ts b/remote_frontend/src/api/VMApi.ts index 912922f..f84c471 100644 --- a/remote_frontend/src/api/VMApi.ts +++ b/remote_frontend/src/api/VMApi.ts @@ -44,4 +44,25 @@ export class VMApi { await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/state` }) ).data.state; } + + /** + * Request to start VM + */ + static async StartVM(vm: VMInfo): Promise { + await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/start` }); + } + + /** + * Request to shutdown VM + */ + static async ShutdownVM(vm: VMInfo): Promise { + await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/shutdown` }); + } + + /** + * Request to kill VM + */ + static async KillVM(vm: VMInfo): Promise { + await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/kill` }); + } } diff --git a/remote_frontend/src/widgets/VirtualMachinesWidget.tsx b/remote_frontend/src/widgets/VirtualMachinesWidget.tsx index d0a8aec..0884ed8 100644 --- a/remote_frontend/src/widgets/VirtualMachinesWidget.tsx +++ b/remote_frontend/src/widgets/VirtualMachinesWidget.tsx @@ -6,11 +6,15 @@ import { CardFooter, CardHeader, CardPreview, + Spinner, + Tooltip, } from "@fluentui/react-components"; import { DesktopRegular, + FluentIcon, Play16Regular, PowerRegular, + StopRegular, } from "@fluentui/react-icons"; import React from "react"; import { VMApi, VMInfo, VMState } from "../api/VMApi"; @@ -60,7 +64,8 @@ function VMWidget(p: { vm: VMInfo }): React.ReactElement { const [state, setState] = React.useState(); const load = async () => { - setState(await VMApi.State(p.vm)); + const newState = await VMApi.State(p.vm); + if (state !== newState) setState(newState); }; React.useEffect(() => { @@ -105,11 +110,96 @@ function VMWidget(p: { vm: VMInfo }): React.ReactElement {

{p.vm.description}

- - + } + enabled={p.vm.can_start} + currState={state} + possibleStates={["Shutdown", "Shutoff", "Crashed"]} + onClick={VMApi.StartVM} + /> + } + enabled={p.vm.can_shutdown} + currState={state} + possibleStates={["Running"]} + onClick={VMApi.ShutdownVM} + /> + } + enabled={p.vm.can_kill} + currState={state} + possibleStates={[ + "Running", + "Paused", + "PowerManagementSuspended", + "Blocked", + ]} + onClick={VMApi.KillVM} + /> ); } + +function VMAction(p: { + vm: VMInfo; + label: string; + primary?: boolean; + icon: React.ReactElement; + enabled: boolean; + currState?: VMState; + possibleStates: VMState[]; + onClick: (vm: VMInfo) => Promise; +}): React.ReactElement { + const toast = useToast(); + const [loading, setLoading] = React.useState(false); + + const onClick = async () => { + try { + setLoading(true); + + await p.onClick(p.vm); + + toast(p.label, `Action successfully executed!`, "success"); + } catch (e) { + console.error(e); + toast(p.label, `Failed to perform action: ${e}`, "error"); + } finally { + setLoading(false); + } + }; + + if (!p.currState || !p.possibleStates.includes(p.currState)) { + return <>; + } + + if (!p.enabled) + return ( + + + + ); + + return ( + + ); +}