Show VM screenshot
This commit is contained in:
		@@ -103,6 +103,7 @@ export class APIClient {
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Process response
 | 
			
		||||
      // JSON response
 | 
			
		||||
      if (res.headers.get("content-type") === "application/json")
 | 
			
		||||
        data = await res.json();
 | 
			
		||||
      // Binary file
 | 
			
		||||
@@ -146,7 +147,7 @@ export class APIClient {
 | 
			
		||||
        data = await resInt.blob();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Do not track progress
 | 
			
		||||
      // Do not track progress (binary file)
 | 
			
		||||
      else data = await res.blob();
 | 
			
		||||
 | 
			
		||||
      status = res.status;
 | 
			
		||||
 
 | 
			
		||||
@@ -136,6 +136,18 @@ export class VMApi {
 | 
			
		||||
    ).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
 | 
			
		||||
   */
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
 | 
			
		||||
import { VMStatusWidget } from "../widgets/vms/VMStatusWidget";
 | 
			
		||||
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
 | 
			
		||||
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
 | 
			
		||||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
 | 
			
		||||
export function VMListRoute(): React.ReactElement {
 | 
			
		||||
  const [list, setList] = React.useState<VMInfo[] | undefined>();
 | 
			
		||||
@@ -66,6 +67,7 @@ function VMListWidget(p: {
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  const confirm = useConfirm();
 | 
			
		||||
  const snackbar = useSnackbar();
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  const deleteVM = async (v: VMInfo) => {
 | 
			
		||||
    try {
 | 
			
		||||
@@ -110,8 +112,10 @@ function VMListWidget(p: {
 | 
			
		||||
        <TableBody>
 | 
			
		||||
          {p.list.map((row) => (
 | 
			
		||||
            <TableRow
 | 
			
		||||
              hover
 | 
			
		||||
              key={row.name}
 | 
			
		||||
              sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
 | 
			
		||||
              onDoubleClick={() => navigate(row.ViewURL)}
 | 
			
		||||
            >
 | 
			
		||||
              <TableCell component="th" scope="row">
 | 
			
		||||
                {row.name}
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,11 @@ function VMRouteBody(p: { vm: VMInfo }): React.ReactElement {
 | 
			
		||||
        </span>
 | 
			
		||||
      }
 | 
			
		||||
    >
 | 
			
		||||
      <VMDetails vm={p.vm} editable={false} />
 | 
			
		||||
      <VMDetails
 | 
			
		||||
        vm={p.vm}
 | 
			
		||||
        editable={false}
 | 
			
		||||
        screenshot={p.vm.vnc_access && state === "Running"}
 | 
			
		||||
      />
 | 
			
		||||
    </VirtWebRouteContainer>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,14 +6,24 @@ import { VMInfo } from "../../api/VMApi";
 | 
			
		||||
import { CheckboxInput } from "../forms/CheckboxInput";
 | 
			
		||||
import { SelectInput } from "../forms/SelectInput";
 | 
			
		||||
import { TextInput } from "../forms/TextInput";
 | 
			
		||||
import { VMScreenshot } from "./VMScreenshot";
 | 
			
		||||
 | 
			
		||||
export function VMDetails(p: {
 | 
			
		||||
  vm: VMInfo;
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
  onChange?: () => void;
 | 
			
		||||
  screenshot?: boolean;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <Grid container spacing={2}>
 | 
			
		||||
      {
 | 
			
		||||
        /* Screenshot section */ p.screenshot && (
 | 
			
		||||
          <EditSection title="Screenshot">
 | 
			
		||||
            <VMScreenshot vm={p.vm} />
 | 
			
		||||
          </EditSection>
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      {/* Metadata section */}
 | 
			
		||||
      <EditSection title="Metadata">
 | 
			
		||||
        <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 ReplayIcon from "@mui/icons-material/Replay";
 | 
			
		||||
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 { VMApi, VMInfo, VMState } from "../../api/VMApi";
 | 
			
		||||
import { useAlert } from "../../hooks/providers/AlertDialogProvider";
 | 
			
		||||
@@ -47,7 +52,7 @@ export function VMStatusWidget(p: {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div style={{ display: "inline-flex" }}>
 | 
			
		||||
      {state}
 | 
			
		||||
      <Typography>{state}</Typography>
 | 
			
		||||
 | 
			
		||||
      {/* Start VM */}
 | 
			
		||||
      <ActionButton
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user