Show VM screenshot
This commit is contained in:
parent
62364594c9
commit
3042bbdac6
@ -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;
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
42
virtweb_frontend/src/widgets/vms/VMScreenshot.tsx
Normal file
42
virtweb_frontend/src/widgets/vms/VMScreenshot.tsx
Normal 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" />
|
||||||
|
);
|
||||||
|
}
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user