Add the logic which will call disk image conversion from disk image route
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is failing
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	continuous-integration/drone/push Build is failing
				
			This commit is contained in:
		@@ -1,16 +1,17 @@
 | 
			
		||||
import { APIClient } from "./ApiClient";
 | 
			
		||||
 | 
			
		||||
export type DiskImageFormat =
 | 
			
		||||
  | { format: "Raw"; is_sparse: boolean }
 | 
			
		||||
  | { format: "QCow2"; virtual_size?: number }
 | 
			
		||||
  | { format: "CompressedQCow2" }
 | 
			
		||||
  | { format: "CompressedRaw" };
 | 
			
		||||
 | 
			
		||||
export type DiskImage = {
 | 
			
		||||
  file_size: number;
 | 
			
		||||
  file_name: string;
 | 
			
		||||
  name: string;
 | 
			
		||||
  created: number;
 | 
			
		||||
} & (
 | 
			
		||||
  | { format: "Raw"; is_sparse: boolean }
 | 
			
		||||
  | { format: "QCow2"; virtual_size: number }
 | 
			
		||||
  | { format: "CompressedQCow2" }
 | 
			
		||||
  | { format: "CompressedRaw" }
 | 
			
		||||
);
 | 
			
		||||
} & DiskImageFormat;
 | 
			
		||||
 | 
			
		||||
export class DiskImageApi {
 | 
			
		||||
  /**
 | 
			
		||||
@@ -61,6 +62,21 @@ export class DiskImageApi {
 | 
			
		||||
    ).data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Convert disk image file
 | 
			
		||||
   */
 | 
			
		||||
  static async Convert(
 | 
			
		||||
    file: DiskImage,
 | 
			
		||||
    dest_file_name: string,
 | 
			
		||||
    dest_format: DiskImageFormat
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    await APIClient.exec({
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      uri: `/disk_images/${file.file_name}/convert`,
 | 
			
		||||
      jsonData: { ...dest_format, dest_file_name },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Delete disk image file
 | 
			
		||||
   */
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ export interface ServerConstraints {
 | 
			
		||||
  memory_size: LenConstraint;
 | 
			
		||||
  disk_name_size: LenConstraint;
 | 
			
		||||
  disk_size: LenConstraint;
 | 
			
		||||
  disk_image_name_size: LenConstraint;
 | 
			
		||||
  net_name_size: LenConstraint;
 | 
			
		||||
  net_title_size: LenConstraint;
 | 
			
		||||
  net_nat_comment_size: LenConstraint;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										106
									
								
								virtweb_frontend/src/dialogs/ConvertDiskImageDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								virtweb_frontend/src/dialogs/ConvertDiskImageDialog.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogActions,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogContentText,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
} from "@mui/material";
 | 
			
		||||
import { DiskImage, DiskImageApi, DiskImageFormat } from "../api/DiskImageApi";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { FileDiskImageWidget } from "../widgets/FileDiskImageWidget";
 | 
			
		||||
import { FileInput } from "../widgets/forms/FileInput";
 | 
			
		||||
import { TextInput } from "../widgets/forms/TextInput";
 | 
			
		||||
import { ServerApi } from "../api/ServerApi";
 | 
			
		||||
import { SelectInput } from "../widgets/forms/SelectInput";
 | 
			
		||||
import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider";
 | 
			
		||||
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
 | 
			
		||||
import { useAlert } from "../hooks/providers/AlertDialogProvider";
 | 
			
		||||
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
 | 
			
		||||
 | 
			
		||||
export function ConvertDiskImageDialog(p: {
 | 
			
		||||
  image: DiskImage;
 | 
			
		||||
  onCancel: () => void;
 | 
			
		||||
  onFinished: () => void;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  const alert = useAlert();
 | 
			
		||||
  const snackbar = useSnackbar();
 | 
			
		||||
  const loadingMessage = useLoadingMessage();
 | 
			
		||||
 | 
			
		||||
  const [format, setFormat] = React.useState<DiskImageFormat>({
 | 
			
		||||
    format: "QCow2",
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const [filename, setFilename] = React.useState(p.image.file_name + ".qcow2");
 | 
			
		||||
 | 
			
		||||
  const handleFormatChange = async (value?: string) => {
 | 
			
		||||
    setFormat({ format: value ?? ("QCow2" as any) });
 | 
			
		||||
 | 
			
		||||
    if (value === "QCow2") setFilename(`${p.image.file_name}.qcow2`);
 | 
			
		||||
    if (value === "CompressedQCow2")
 | 
			
		||||
      setFilename(`${p.image.file_name}.qcow2.gz`);
 | 
			
		||||
    if (value === "Raw") setFilename(`${p.image.file_name}.raw`);
 | 
			
		||||
    if (value === "CompressedRaw") setFilename(`${p.image.file_name}.raw.gz`);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      loadingMessage.show("Converting image...");
 | 
			
		||||
 | 
			
		||||
      // Perform the conversion
 | 
			
		||||
      await DiskImageApi.Convert(p.image, filename, format);
 | 
			
		||||
 | 
			
		||||
      p.onFinished();
 | 
			
		||||
 | 
			
		||||
      snackbar("Conversion successful!");
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.error("Failed to convert image!", e);
 | 
			
		||||
      alert(`Failed to convert image! ${e}`);
 | 
			
		||||
    } finally {
 | 
			
		||||
      loadingMessage.hide();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog open onClose={p.onCancel}>
 | 
			
		||||
      <DialogTitle>Convert disk image</DialogTitle>
 | 
			
		||||
 | 
			
		||||
      <DialogContent>
 | 
			
		||||
        <DialogContentText>
 | 
			
		||||
          Select the destination format for this image:
 | 
			
		||||
        </DialogContentText>
 | 
			
		||||
        <FileDiskImageWidget image={p.image} />
 | 
			
		||||
 | 
			
		||||
        {/* New image format */}
 | 
			
		||||
        <SelectInput
 | 
			
		||||
          editable
 | 
			
		||||
          label="Target format"
 | 
			
		||||
          value={format.format}
 | 
			
		||||
          onValueChange={handleFormatChange}
 | 
			
		||||
          options={[
 | 
			
		||||
            { value: "QCow2" },
 | 
			
		||||
            { value: "Raw" },
 | 
			
		||||
            { value: "CompressedRaw" },
 | 
			
		||||
            { value: "CompressedQCow2" },
 | 
			
		||||
          ]}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        {/* New image name */}
 | 
			
		||||
        <TextInput
 | 
			
		||||
          editable
 | 
			
		||||
          label="New image name"
 | 
			
		||||
          value={filename}
 | 
			
		||||
          onValueChange={(s) => setFilename(s ?? "")}
 | 
			
		||||
          size={ServerApi.Config.constraints.disk_image_name_size}
 | 
			
		||||
          helperText="The image name shall contain the proper file extension"
 | 
			
		||||
        />
 | 
			
		||||
      </DialogContent>
 | 
			
		||||
      <DialogActions>
 | 
			
		||||
        <Button onClick={p.onCancel}>Cancel</Button>
 | 
			
		||||
        <Button onClick={handleSubmit} autoFocus>
 | 
			
		||||
          Convert image
 | 
			
		||||
        </Button>
 | 
			
		||||
      </DialogActions>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import DeleteIcon from "@mui/icons-material/Delete";
 | 
			
		||||
import DownloadIcon from "@mui/icons-material/Download";
 | 
			
		||||
import RefreshIcon from "@mui/icons-material/Refresh";
 | 
			
		||||
import LoopIcon from "@mui/icons-material/Loop";
 | 
			
		||||
import {
 | 
			
		||||
  Alert,
 | 
			
		||||
  Button,
 | 
			
		||||
@@ -25,6 +26,7 @@ import { FileInput } from "../widgets/forms/FileInput";
 | 
			
		||||
import { VirtWebPaper } from "../widgets/VirtWebPaper";
 | 
			
		||||
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
 | 
			
		||||
import { downloadBlob } from "../utils/FilesUtils";
 | 
			
		||||
import { ConvertDiskImageDialog } from "../dialogs/ConvertDiskImageDialog";
 | 
			
		||||
 | 
			
		||||
export function DiskImagesRoute(): React.ReactElement {
 | 
			
		||||
  const [list, setList] = React.useState<DiskImage[] | undefined>();
 | 
			
		||||
@@ -162,8 +164,16 @@ function DiskImageList(p: {
 | 
			
		||||
  const confirm = useConfirm();
 | 
			
		||||
  const loadingMessage = useLoadingMessage();
 | 
			
		||||
 | 
			
		||||
  const [currConversion, setCurrConversion] = React.useState<
 | 
			
		||||
    DiskImage | undefined
 | 
			
		||||
  >();
 | 
			
		||||
  const [dlProgress, setDlProgress] = React.useState<undefined | number>();
 | 
			
		||||
 | 
			
		||||
  // Convert disk image file
 | 
			
		||||
  const convertDiskImage = async (entry: DiskImage) => {
 | 
			
		||||
    setCurrConversion(entry);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Download disk image file
 | 
			
		||||
  const downloadDiskImage = async (entry: DiskImage) => {
 | 
			
		||||
    setDlProgress(0);
 | 
			
		||||
@@ -236,11 +246,16 @@ function DiskImageList(p: {
 | 
			
		||||
    {
 | 
			
		||||
      field: "actions",
 | 
			
		||||
      headerName: "",
 | 
			
		||||
      width: 120,
 | 
			
		||||
      width: 140,
 | 
			
		||||
      renderCell(params) {
 | 
			
		||||
        return (
 | 
			
		||||
          <>
 | 
			
		||||
            <Tooltip title="Download image">
 | 
			
		||||
            <Tooltip title="Convert disk image">
 | 
			
		||||
              <IconButton onClick={() => convertDiskImage(params.row)}>
 | 
			
		||||
                <LoopIcon />
 | 
			
		||||
              </IconButton>
 | 
			
		||||
            </Tooltip>
 | 
			
		||||
            <Tooltip title="Download disk image">
 | 
			
		||||
              <IconButton onClick={() => downloadDiskImage(params.row)}>
 | 
			
		||||
                <DownloadIcon />
 | 
			
		||||
              </IconButton>
 | 
			
		||||
@@ -281,6 +296,20 @@ function DiskImageList(p: {
 | 
			
		||||
          </div>
 | 
			
		||||
        </Alert>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {/* Disk image conversion dialog */}
 | 
			
		||||
      {currConversion && (
 | 
			
		||||
        <ConvertDiskImageDialog
 | 
			
		||||
          image={currConversion}
 | 
			
		||||
          onCancel={() => setCurrConversion(undefined)}
 | 
			
		||||
          onFinished={() => {
 | 
			
		||||
            setCurrConversion(undefined);
 | 
			
		||||
            p.onReload();
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {/* The table itself */}
 | 
			
		||||
      <DataGrid getRowId={(c) => c.file_name} rows={p.list} columns={columns} />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								virtweb_frontend/src/widgets/FileDiskImageWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								virtweb_frontend/src/widgets/FileDiskImageWidget.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
import { Avatar, ListItem, ListItemAvatar, ListItemText } from "@mui/material";
 | 
			
		||||
import { DiskImage } from "../api/DiskImageApi";
 | 
			
		||||
import { mdiHarddisk } from "@mdi/js";
 | 
			
		||||
import { filesize } from "filesize";
 | 
			
		||||
import Icon from "@mdi/react";
 | 
			
		||||
 | 
			
		||||
export function FileDiskImageWidget(p: {
 | 
			
		||||
  image: DiskImage;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <ListItem>
 | 
			
		||||
      <ListItemAvatar>
 | 
			
		||||
        <Avatar>
 | 
			
		||||
          <Icon path={mdiHarddisk} />
 | 
			
		||||
        </Avatar>
 | 
			
		||||
      </ListItemAvatar>
 | 
			
		||||
      <ListItemText
 | 
			
		||||
        primary={p.image.file_name}
 | 
			
		||||
        secondary={`${p.image.format} - ${filesize(p.image.file_size)}`}
 | 
			
		||||
      />
 | 
			
		||||
    </ListItem>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user