Show VM screenshot
This commit is contained in:
		@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user