diff --git a/remote_frontend/src/api/VMApi.ts b/remote_frontend/src/api/VMApi.ts index fa58303..a0b0649 100644 --- a/remote_frontend/src/api/VMApi.ts +++ b/remote_frontend/src/api/VMApi.ts @@ -86,4 +86,16 @@ export class VMApi { static async ResetVM(vm: VMInfo): Promise { await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/reset` }); } + + /** + * Get a screenshot of a VM + */ + static async Screenshot(vm: VMInfo): Promise { + return ( + await APIClient.exec({ + uri: `/vm/${vm.uiid}/screenshot`, + method: "GET", + }) + ).data; + } } diff --git a/remote_frontend/src/widgets/VMLiveScreenshot.tsx b/remote_frontend/src/widgets/VMLiveScreenshot.tsx new file mode 100644 index 0000000..115b958 --- /dev/null +++ b/remote_frontend/src/widgets/VMLiveScreenshot.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { VMApi, VMInfo } from "../api/VMApi"; +import { useToast } from "../hooks/providers/ToastProvider"; + +export function VMLiveScreenshot(p: { vm: VMInfo }): React.ReactElement { + const toast = useToast(); + + const [screenshotURL, setScreenshotURL] = React.useState< + string | undefined + >(); + + const int = React.useRef(); + + React.useEffect(() => { + const refresh = async () => { + try { + const screenshot = await VMApi.Screenshot(p.vm); + const u = URL.createObjectURL(screenshot); + setScreenshotURL(u); + } catch (e) { + console.error(e); + toast(p.vm.name, "Failed to get a screenshot of the VM!", "error"); + } + }; + + if (int.current === undefined) { + refresh(); + int.current = setInterval(() => refresh(), 5000); + } + + return () => { + if (int.current !== undefined) { + clearInterval(int.current); + int.current = undefined; + } + }; + }, [p.vm, toast]); + + return ( + VM screenshot + ); +} diff --git a/remote_frontend/src/widgets/VirtualMachinesWidget.tsx b/remote_frontend/src/widgets/VirtualMachinesWidget.tsx index a4c0de7..6f01f6c 100644 --- a/remote_frontend/src/widgets/VirtualMachinesWidget.tsx +++ b/remote_frontend/src/widgets/VirtualMachinesWidget.tsx @@ -8,6 +8,8 @@ import { CardPreview, Spinner, Tooltip, + makeStyles, + typographyStyles, } from "@fluentui/react-components"; import { ArrowResetRegular, @@ -22,6 +24,11 @@ import { VMApi, VMInfo, VMState } from "../api/VMApi"; import { useToast } from "../hooks/providers/ToastProvider"; import { AsyncWidget } from "./AsyncWidget"; import { SectionContainer } from "./SectionContainer"; +import { VMLiveScreenshot } from "./VMLiveScreenshot"; + +const useStyles = makeStyles({ + body1Stronger: typographyStyles.body1Stronger, +}); export function VirtualMachinesWidget(): React.ReactElement { const [list, setList] = React.useState(); @@ -86,13 +93,15 @@ function VMWidget(p: { vm: VMInfo }): React.ReactElement { style={{ width: "400px", maxWidth: "49%", - height: "250px", + height: "350px", margin: "10px", display: "flex", flexDirection: "column", }} > - TODO preview + + + } @@ -183,6 +192,26 @@ function VMWidget(p: { vm: VMInfo }): React.ReactElement { ); } +function VMPreview(p: { vm: VMInfo; state?: VMState }): React.ReactElement { + const styles = useStyles(); + if (!p.vm.can_screenshot || p.state !== "Running") { + return ( +
+ {p.vm.name} +
+ ); + } + + return ; +} + function VMAction(p: { vm: VMInfo; label: string; @@ -194,6 +223,7 @@ function VMAction(p: { onClick: (vm: VMInfo) => Promise; }): React.ReactElement { const toast = useToast(); + const [loading, setLoading] = React.useState(false); const onClick = async () => {