Show VM screenshot

This commit is contained in:
Pierre HUBERT 2023-10-18 10:23:40 +02:00
parent 62364594c9
commit 3042bbdac6
7 changed files with 82 additions and 4 deletions

View File

@ -103,6 +103,7 @@ export class APIClient {
}); });
// Process response // Process response
// JSON response
if (res.headers.get("content-type") === "application/json") if (res.headers.get("content-type") === "application/json")
data = await res.json(); data = await res.json();
// Binary file // Binary file
@ -146,7 +147,7 @@ export class APIClient {
data = await resInt.blob(); data = await resInt.blob();
} }
// Do not track progress // Do not track progress (binary file)
else data = await res.blob(); else data = await res.blob();
status = res.status; status = res.status;

View File

@ -136,6 +136,18 @@ export class VMApi {
).data.state; ).data.state;
} }
/**
* Get a screenshot of a VM
*/
static async Screenshot(vm: VMInfo): Promise<Blob> {
return (
await APIClient.exec({
uri: `/vm/${vm.uuid}/screenshot`,
method: "GET",
})
).data;
}
/** /**
* Start the VM * Start the VM
*/ */

View File

@ -21,6 +21,7 @@ import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
import { VMStatusWidget } from "../widgets/vms/VMStatusWidget"; import { VMStatusWidget } from "../widgets/vms/VMStatusWidget";
import { useSnackbar } from "../hooks/providers/SnackbarProvider"; import { useSnackbar } from "../hooks/providers/SnackbarProvider";
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
import { useNavigate } from "react-router-dom";
export function VMListRoute(): React.ReactElement { export function VMListRoute(): React.ReactElement {
const [list, setList] = React.useState<VMInfo[] | undefined>(); const [list, setList] = React.useState<VMInfo[] | undefined>();
@ -66,6 +67,7 @@ function VMListWidget(p: {
}): React.ReactElement { }): React.ReactElement {
const confirm = useConfirm(); const confirm = useConfirm();
const snackbar = useSnackbar(); const snackbar = useSnackbar();
const navigate = useNavigate();
const deleteVM = async (v: VMInfo) => { const deleteVM = async (v: VMInfo) => {
try { try {
@ -110,8 +112,10 @@ function VMListWidget(p: {
<TableBody> <TableBody>
{p.list.map((row) => ( {p.list.map((row) => (
<TableRow <TableRow
hover
key={row.name} key={row.name}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }} sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
onDoubleClick={() => navigate(row.ViewURL)}
> >
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
{row.name} {row.name}

View File

@ -50,7 +50,11 @@ function VMRouteBody(p: { vm: VMInfo }): React.ReactElement {
</span> </span>
} }
> >
<VMDetails vm={p.vm} editable={false} /> <VMDetails
vm={p.vm}
editable={false}
screenshot={p.vm.vnc_access && state === "Running"}
/>
</VirtWebRouteContainer> </VirtWebRouteContainer>
); );
} }

View File

@ -6,14 +6,24 @@ import { VMInfo } from "../../api/VMApi";
import { CheckboxInput } from "../forms/CheckboxInput"; import { CheckboxInput } from "../forms/CheckboxInput";
import { SelectInput } from "../forms/SelectInput"; import { SelectInput } from "../forms/SelectInput";
import { TextInput } from "../forms/TextInput"; import { TextInput } from "../forms/TextInput";
import { VMScreenshot } from "./VMScreenshot";
export function VMDetails(p: { export function VMDetails(p: {
vm: VMInfo; vm: VMInfo;
editable: boolean; editable: boolean;
onChange?: () => void; onChange?: () => void;
screenshot?: boolean;
}): React.ReactElement { }): React.ReactElement {
return ( return (
<Grid container spacing={2}> <Grid container spacing={2}>
{
/* Screenshot section */ p.screenshot && (
<EditSection title="Screenshot">
<VMScreenshot vm={p.vm} />
</EditSection>
)
}
{/* Metadata section */} {/* Metadata section */}
<EditSection title="Metadata"> <EditSection title="Metadata">
<TextInput <TextInput

View File

@ -0,0 +1,42 @@
import React from "react";
import { VMApi, VMInfo } from "../../api/VMApi";
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
export function VMScreenshot(p: { vm: VMInfo }): React.ReactElement {
const snackbar = useSnackbar();
const [screenshotURL, setScreenshotURL] = React.useState<
string | undefined
>();
const int = React.useRef<NodeJS.Timer | undefined>();
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);
snackbar("Failed to get a screenshot of the VM!");
}
};
if (int.current === undefined) {
refresh();
int.current = setInterval(() => refresh(), 5000000);
}
return () => {
if (int.current !== undefined) {
clearInterval(int.current);
int.current = undefined;
}
};
}, [p.vm, snackbar]);
return (
<img src={screenshotURL} style={{ width: "100%" }} alt="VM screenshot" />
);
}

View File

@ -3,7 +3,12 @@ import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew"; import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew";
import ReplayIcon from "@mui/icons-material/Replay"; import ReplayIcon from "@mui/icons-material/Replay";
import StopIcon from "@mui/icons-material/Stop"; import StopIcon from "@mui/icons-material/Stop";
import { CircularProgress, IconButton, Tooltip } from "@mui/material"; import {
CircularProgress,
IconButton,
Tooltip,
Typography,
} from "@mui/material";
import React from "react"; import React from "react";
import { VMApi, VMInfo, VMState } from "../../api/VMApi"; import { VMApi, VMInfo, VMState } from "../../api/VMApi";
import { useAlert } from "../../hooks/providers/AlertDialogProvider"; import { useAlert } from "../../hooks/providers/AlertDialogProvider";
@ -47,7 +52,7 @@ export function VMStatusWidget(p: {
return ( return (
<div style={{ display: "inline-flex" }}> <div style={{ display: "inline-flex" }}>
{state} <Typography>{state}</Typography>
{/* Start VM */} {/* Start VM */}
<ActionButton <ActionButton