Prepare UI for disks backups
This commit is contained in:
		@@ -55,7 +55,7 @@ impl FileSize {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(Self((value as f64).mul(fact) as usize))
 | 
					        Ok(Self((value as f64).mul(fact).ceil() as usize))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Get file size as bytes
 | 
					    /// Get file size as bytes
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { APIClient } from "./ApiClient";
 | 
					import { APIClient } from "./ApiClient";
 | 
				
			||||||
 | 
					import { DiskImageFormat } from "./DiskImageApi";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type VMState =
 | 
					export type VMState =
 | 
				
			||||||
  | "NoState"
 | 
					  | "NoState"
 | 
				
			||||||
@@ -24,7 +25,7 @@ export interface BaseFileVMDisk {
 | 
				
			|||||||
  name: string;
 | 
					  name: string;
 | 
				
			||||||
  delete: boolean;
 | 
					  delete: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // application attribute
 | 
					  // application attributes
 | 
				
			||||||
  new?: boolean;
 | 
					  new?: boolean;
 | 
				
			||||||
  deleteType?: "keepfile" | "deletefile";
 | 
					  deleteType?: "keepfile" | "deletefile";
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -384,4 +385,20 @@ export class VMApi {
 | 
				
			|||||||
      encodeURIComponent(token)
 | 
					      encodeURIComponent(token)
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Backup VM disk
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static async BackupDisk(
 | 
				
			||||||
 | 
					    vm: VMInfo,
 | 
				
			||||||
 | 
					    disk: VMFileDisk,
 | 
				
			||||||
 | 
					    file_name: string,
 | 
				
			||||||
 | 
					    format: DiskImageFormat
 | 
				
			||||||
 | 
					  ): Promise<void> {
 | 
				
			||||||
 | 
					    await APIClient.exec({
 | 
				
			||||||
 | 
					      uri: `/vm/${vm.uuid}/disk/${disk.name}/backup`,
 | 
				
			||||||
 | 
					      method: "POST",
 | 
				
			||||||
 | 
					      jsonData: { ...format, file_name },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ import {
 | 
				
			|||||||
import React from "react";
 | 
					import React from "react";
 | 
				
			||||||
import { DiskImage, DiskImageApi, DiskImageFormat } from "../api/DiskImageApi";
 | 
					import { DiskImage, DiskImageApi, DiskImageFormat } from "../api/DiskImageApi";
 | 
				
			||||||
import { ServerApi } from "../api/ServerApi";
 | 
					import { ServerApi } from "../api/ServerApi";
 | 
				
			||||||
 | 
					import { VMApi, VMFileDisk, VMInfo } from "../api/VMApi";
 | 
				
			||||||
import { useAlert } from "../hooks/providers/AlertDialogProvider";
 | 
					import { useAlert } from "../hooks/providers/AlertDialogProvider";
 | 
				
			||||||
import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider";
 | 
					import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider";
 | 
				
			||||||
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
 | 
					import { useSnackbar } from "../hooks/providers/SnackbarProvider";
 | 
				
			||||||
@@ -16,12 +17,17 @@ import { FileDiskImageWidget } from "../widgets/FileDiskImageWidget";
 | 
				
			|||||||
import { CheckboxInput } from "../widgets/forms/CheckboxInput";
 | 
					import { CheckboxInput } from "../widgets/forms/CheckboxInput";
 | 
				
			||||||
import { SelectInput } from "../widgets/forms/SelectInput";
 | 
					import { SelectInput } from "../widgets/forms/SelectInput";
 | 
				
			||||||
import { TextInput } from "../widgets/forms/TextInput";
 | 
					import { TextInput } from "../widgets/forms/TextInput";
 | 
				
			||||||
 | 
					import { VMDiskFileWidget } from "../widgets/vms/VMDiskFileWidget";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ConvertDiskImageDialog(p: {
 | 
					export function ConvertDiskImageDialog(
 | 
				
			||||||
  image: DiskImage;
 | 
					  p: {
 | 
				
			||||||
  onCancel: () => void;
 | 
					    onCancel: () => void;
 | 
				
			||||||
  onFinished: () => void;
 | 
					    onFinished: () => void;
 | 
				
			||||||
}): React.ReactElement {
 | 
					  } & (
 | 
				
			||||||
 | 
					    | { backup?: false; image: DiskImage }
 | 
				
			||||||
 | 
					    | { backup: true; disk: VMFileDisk; vm: VMInfo }
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					): React.ReactElement {
 | 
				
			||||||
  const alert = useAlert();
 | 
					  const alert = useAlert();
 | 
				
			||||||
  const snackbar = useSnackbar();
 | 
					  const snackbar = useSnackbar();
 | 
				
			||||||
  const loadingMessage = useLoadingMessage();
 | 
					  const loadingMessage = useLoadingMessage();
 | 
				
			||||||
@@ -30,35 +36,43 @@ export function ConvertDiskImageDialog(p: {
 | 
				
			|||||||
    format: "QCow2",
 | 
					    format: "QCow2",
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [filename, setFilename] = React.useState(p.image.file_name + ".qcow2");
 | 
					  const origFilename = p.backup ? p.disk.name : p.image.file_name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [filename, setFilename] = React.useState(origFilename + ".qcow2");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleFormatChange = (value?: string) => {
 | 
					  const handleFormatChange = (value?: string) => {
 | 
				
			||||||
    setFormat({ format: value ?? ("QCow2" as any) });
 | 
					    setFormat({ format: value ?? ("QCow2" as any) });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (value === "QCow2") setFilename(`${p.image.file_name}.qcow2`);
 | 
					    if (value === "QCow2") setFilename(`${origFilename}.qcow2`);
 | 
				
			||||||
    if (value === "CompressedQCow2")
 | 
					    if (value === "CompressedQCow2") setFilename(`${origFilename}.qcow2.gz`);
 | 
				
			||||||
      setFilename(`${p.image.file_name}.qcow2.gz`);
 | 
					 | 
				
			||||||
    if (value === "Raw") {
 | 
					    if (value === "Raw") {
 | 
				
			||||||
      setFilename(`${p.image.file_name}.raw`);
 | 
					      setFilename(`${origFilename}.raw`);
 | 
				
			||||||
      // Check sparse checkbox by default
 | 
					      // Check sparse checkbox by default
 | 
				
			||||||
      setFormat({ format: "Raw", is_sparse: true });
 | 
					      setFormat({ format: "Raw", is_sparse: true });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (value === "CompressedRaw") setFilename(`${p.image.file_name}.raw.gz`);
 | 
					    if (value === "CompressedRaw") setFilename(`${origFilename}.raw.gz`);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSubmit = async () => {
 | 
					  const handleSubmit = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      loadingMessage.show("Converting image...");
 | 
					      loadingMessage.show(
 | 
				
			||||||
 | 
					        p.backup ? "Performing backup..." : "Converting image..."
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Perform the conversion
 | 
					      // Perform the conversion / backup operation
 | 
				
			||||||
      await DiskImageApi.Convert(p.image, filename, format);
 | 
					      if (p.backup) await VMApi.BackupDisk(p.vm, p.disk, filename, format);
 | 
				
			||||||
 | 
					      else await DiskImageApi.Convert(p.image, filename, format);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      p.onFinished();
 | 
					      p.onFinished();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      snackbar("Conversion successful!");
 | 
					      snackbar(p.backup ? "Backup successful!" : "Conversion successful!");
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      console.error("Failed to convert image!", e);
 | 
					      console.error("Failed to perform backup/conversion!", e);
 | 
				
			||||||
      alert(`Failed to convert image! ${e}`);
 | 
					      alert(
 | 
				
			||||||
 | 
					        p.backup
 | 
				
			||||||
 | 
					          ? `Failed to perform backup! ${e}`
 | 
				
			||||||
 | 
					          : `Failed to convert image! ${e}`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      loadingMessage.hide();
 | 
					      loadingMessage.hide();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -66,13 +80,21 @@ export function ConvertDiskImageDialog(p: {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Dialog open onClose={p.onCancel}>
 | 
					    <Dialog open onClose={p.onCancel}>
 | 
				
			||||||
      <DialogTitle>Convert disk image</DialogTitle>
 | 
					      <DialogTitle>
 | 
				
			||||||
 | 
					        {p.backup ? `Backup disk ${p.disk.name}` : "Convert disk image"}
 | 
				
			||||||
 | 
					      </DialogTitle>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <DialogContent>
 | 
					      <DialogContent>
 | 
				
			||||||
        <DialogContentText>
 | 
					        <DialogContentText>
 | 
				
			||||||
          Select the destination format for this image:
 | 
					          Select the destination format for this image:
 | 
				
			||||||
        </DialogContentText>
 | 
					        </DialogContentText>
 | 
				
			||||||
        <FileDiskImageWidget image={p.image} />
 | 
					
 | 
				
			||||||
 | 
					        {/* Show details of of the image */}
 | 
				
			||||||
 | 
					        {p.backup ? (
 | 
				
			||||||
 | 
					          <VMDiskFileWidget {...p} />
 | 
				
			||||||
 | 
					        ) : (
 | 
				
			||||||
 | 
					          <FileDiskImageWidget {...p} />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {/* New image format */}
 | 
					        {/* New image format */}
 | 
				
			||||||
        <SelectInput
 | 
					        <SelectInput
 | 
				
			||||||
@@ -109,13 +131,13 @@ export function ConvertDiskImageDialog(p: {
 | 
				
			|||||||
            setFilename(s ?? "");
 | 
					            setFilename(s ?? "");
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
          size={ServerApi.Config.constraints.disk_image_name_size}
 | 
					          size={ServerApi.Config.constraints.disk_image_name_size}
 | 
				
			||||||
          helperText="The image name shall contain the proper file extension"
 | 
					          helperText="The image name shall contain the proper file extension for the selected target format"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </DialogContent>
 | 
					      </DialogContent>
 | 
				
			||||||
      <DialogActions>
 | 
					      <DialogActions>
 | 
				
			||||||
        <Button onClick={p.onCancel}>Cancel</Button>
 | 
					        <Button onClick={p.onCancel}>Cancel</Button>
 | 
				
			||||||
        <Button onClick={handleSubmit} autoFocus>
 | 
					        <Button onClick={handleSubmit} autoFocus>
 | 
				
			||||||
          Convert image
 | 
					          {p.backup ? "Perform backup" : "Convert image"}
 | 
				
			||||||
        </Button>
 | 
					        </Button>
 | 
				
			||||||
      </DialogActions>
 | 
					      </DialogActions>
 | 
				
			||||||
    </Dialog>
 | 
					    </Dialog>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,6 +59,7 @@ function VMRouteBody(p: { vm: VMInfo }): React.ReactElement {
 | 
				
			|||||||
      <VMDetails
 | 
					      <VMDetails
 | 
				
			||||||
        vm={p.vm}
 | 
					        vm={p.vm}
 | 
				
			||||||
        editable={false}
 | 
					        editable={false}
 | 
				
			||||||
 | 
					        state={state}
 | 
				
			||||||
        screenshot={p.vm.vnc_access && state === "Running"}
 | 
					        screenshot={p.vm.vnc_access && state === "Running"}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    </VirtWebRouteContainer>
 | 
					    </VirtWebRouteContainer>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import { mdiHarddisk } from "@mdi/js";
 | 
					import { mdiHarddisk, mdiHarddiskPlus } from "@mdi/js";
 | 
				
			||||||
import Icon from "@mdi/react";
 | 
					import Icon from "@mdi/react";
 | 
				
			||||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
 | 
					import CheckCircleIcon from "@mui/icons-material/CheckCircle";
 | 
				
			||||||
import DeleteIcon from "@mui/icons-material/Delete";
 | 
					import DeleteIcon from "@mui/icons-material/Delete";
 | 
				
			||||||
@@ -14,16 +14,23 @@ import {
 | 
				
			|||||||
} from "@mui/material";
 | 
					} from "@mui/material";
 | 
				
			||||||
import { filesize } from "filesize";
 | 
					import { filesize } from "filesize";
 | 
				
			||||||
import { ServerApi } from "../../api/ServerApi";
 | 
					import { ServerApi } from "../../api/ServerApi";
 | 
				
			||||||
import { VMFileDisk, VMInfo } from "../../api/VMApi";
 | 
					import { VMFileDisk, VMInfo, VMState } from "../../api/VMApi";
 | 
				
			||||||
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
 | 
					import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
 | 
				
			||||||
import { SelectInput } from "./SelectInput";
 | 
					import { SelectInput } from "./SelectInput";
 | 
				
			||||||
import { TextInput } from "./TextInput";
 | 
					import { TextInput } from "./TextInput";
 | 
				
			||||||
 | 
					import React from "react";
 | 
				
			||||||
 | 
					import { ConvertDiskImageDialog } from "../../dialogs/ConvertDiskImageDialog";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function VMDisksList(p: {
 | 
					export function VMDisksList(p: {
 | 
				
			||||||
  vm: VMInfo;
 | 
					  vm: VMInfo;
 | 
				
			||||||
 | 
					  state?: VMState;
 | 
				
			||||||
  onChange?: () => void;
 | 
					  onChange?: () => void;
 | 
				
			||||||
  editable: boolean;
 | 
					  editable: boolean;
 | 
				
			||||||
}): React.ReactElement {
 | 
					}): React.ReactElement {
 | 
				
			||||||
 | 
					  const [currBackupRequest, setCurrBackupRequest] = React.useState<
 | 
				
			||||||
 | 
					    VMFileDisk | undefined
 | 
				
			||||||
 | 
					  >();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const addNewDisk = () => {
 | 
					  const addNewDisk = () => {
 | 
				
			||||||
    p.vm.file_disks.push({
 | 
					    p.vm.file_disks.push({
 | 
				
			||||||
      format: "QCow2",
 | 
					      format: "QCow2",
 | 
				
			||||||
@@ -35,6 +42,14 @@ export function VMDisksList(p: {
 | 
				
			|||||||
    p.onChange?.();
 | 
					    p.onChange?.();
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleBackupRequest = (disk: VMFileDisk) => {
 | 
				
			||||||
 | 
					    setCurrBackupRequest(disk);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleFinishBackup = () => {
 | 
				
			||||||
 | 
					    setCurrBackupRequest(undefined);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      {/* disks list */}
 | 
					      {/* disks list */}
 | 
				
			||||||
@@ -43,25 +58,40 @@ export function VMDisksList(p: {
 | 
				
			|||||||
          // eslint-disable-next-line react-x/no-array-index-key
 | 
					          // eslint-disable-next-line react-x/no-array-index-key
 | 
				
			||||||
          key={num}
 | 
					          key={num}
 | 
				
			||||||
          editable={p.editable}
 | 
					          editable={p.editable}
 | 
				
			||||||
 | 
					          canBackup={!p.editable && !d.new && p.state !== "Running"}
 | 
				
			||||||
          disk={d}
 | 
					          disk={d}
 | 
				
			||||||
          onChange={p.onChange}
 | 
					          onChange={p.onChange}
 | 
				
			||||||
          removeFromList={() => {
 | 
					          removeFromList={() => {
 | 
				
			||||||
            p.vm.file_disks.splice(num, 1);
 | 
					            p.vm.file_disks.splice(num, 1);
 | 
				
			||||||
            p.onChange?.();
 | 
					            p.onChange?.();
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
 | 
					          onRequestBackup={handleBackupRequest}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      ))}
 | 
					      ))}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {p.editable && <Button onClick={addNewDisk}>Add new disk</Button>}
 | 
					      {p.editable && <Button onClick={addNewDisk}>Add new disk</Button>}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {/* Disk backup */}
 | 
				
			||||||
 | 
					      {currBackupRequest && (
 | 
				
			||||||
 | 
					        <ConvertDiskImageDialog
 | 
				
			||||||
 | 
					          backup
 | 
				
			||||||
 | 
					          onCancel={handleFinishBackup}
 | 
				
			||||||
 | 
					          onFinished={handleFinishBackup}
 | 
				
			||||||
 | 
					          vm={p.vm}
 | 
				
			||||||
 | 
					          disk={currBackupRequest}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function DiskInfo(p: {
 | 
					function DiskInfo(p: {
 | 
				
			||||||
  editable: boolean;
 | 
					  editable: boolean;
 | 
				
			||||||
 | 
					  canBackup: boolean;
 | 
				
			||||||
  disk: VMFileDisk;
 | 
					  disk: VMFileDisk;
 | 
				
			||||||
  onChange?: () => void;
 | 
					  onChange?: () => void;
 | 
				
			||||||
  removeFromList: () => void;
 | 
					  removeFromList: () => void;
 | 
				
			||||||
 | 
					  onRequestBackup: (disk: VMFileDisk) => void;
 | 
				
			||||||
}): React.ReactElement {
 | 
					}): React.ReactElement {
 | 
				
			||||||
  const confirm = useConfirm();
 | 
					  const confirm = useConfirm();
 | 
				
			||||||
  const deleteDisk = async () => {
 | 
					  const deleteDisk = async () => {
 | 
				
			||||||
@@ -88,23 +118,33 @@ function DiskInfo(p: {
 | 
				
			|||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <ListItem
 | 
					      <ListItem
 | 
				
			||||||
        secondaryAction={
 | 
					        secondaryAction={
 | 
				
			||||||
          p.editable && (
 | 
					          <>
 | 
				
			||||||
            <IconButton
 | 
					            {p.editable && (
 | 
				
			||||||
              edge="end"
 | 
					              <IconButton
 | 
				
			||||||
              aria-label="delete disk"
 | 
					                edge="end"
 | 
				
			||||||
              onClick={deleteDisk}
 | 
					                aria-label="delete disk"
 | 
				
			||||||
            >
 | 
					                onClick={deleteDisk}
 | 
				
			||||||
              {p.disk.deleteType ? (
 | 
					              >
 | 
				
			||||||
                <Tooltip title="Cancel disk removal">
 | 
					                {p.disk.deleteType ? (
 | 
				
			||||||
                  <CheckCircleIcon />
 | 
					                  <Tooltip title="Cancel disk removal">
 | 
				
			||||||
                </Tooltip>
 | 
					                    <CheckCircleIcon />
 | 
				
			||||||
              ) : (
 | 
					                  </Tooltip>
 | 
				
			||||||
                <Tooltip title="Remove disk">
 | 
					                ) : (
 | 
				
			||||||
                  <DeleteIcon />
 | 
					                  <Tooltip title="Remove disk">
 | 
				
			||||||
                </Tooltip>
 | 
					                    <DeleteIcon />
 | 
				
			||||||
              )}
 | 
					                  </Tooltip>
 | 
				
			||||||
            </IconButton>
 | 
					                )}
 | 
				
			||||||
          )
 | 
					              </IconButton>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {p.canBackup && (
 | 
				
			||||||
 | 
					              <Tooltip title="Backup this disk">
 | 
				
			||||||
 | 
					                <IconButton onClick={() => p.onRequestBackup(p.disk)}>
 | 
				
			||||||
 | 
					                  <Icon path={mdiHarddiskPlus} size={1} />
 | 
				
			||||||
 | 
					                </IconButton>
 | 
				
			||||||
 | 
					              </Tooltip>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </>
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <ListItemAvatar>
 | 
					        <ListItemAvatar>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ import { IsoFile, IsoFilesApi } from "../../api/IsoFilesApi";
 | 
				
			|||||||
import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";
 | 
					import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";
 | 
				
			||||||
import { NetworkApi, NetworkInfo } from "../../api/NetworksApi";
 | 
					import { NetworkApi, NetworkInfo } from "../../api/NetworksApi";
 | 
				
			||||||
import { ServerApi } from "../../api/ServerApi";
 | 
					import { ServerApi } from "../../api/ServerApi";
 | 
				
			||||||
import { VMApi, VMInfo } from "../../api/VMApi";
 | 
					import { VMApi, VMInfo, VMState } from "../../api/VMApi";
 | 
				
			||||||
import { useAlert } from "../../hooks/providers/AlertDialogProvider";
 | 
					import { useAlert } from "../../hooks/providers/AlertDialogProvider";
 | 
				
			||||||
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
 | 
					import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
 | 
				
			||||||
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
 | 
					import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
 | 
				
			||||||
@@ -33,6 +33,7 @@ interface DetailsProps {
 | 
				
			|||||||
  editable: boolean;
 | 
					  editable: boolean;
 | 
				
			||||||
  onChange?: () => void;
 | 
					  onChange?: () => void;
 | 
				
			||||||
  screenshot?: boolean;
 | 
					  screenshot?: boolean;
 | 
				
			||||||
 | 
					  state?: VMState | undefined;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function VMDetails(p: DetailsProps): React.ReactElement {
 | 
					export function VMDetails(p: DetailsProps): React.ReactElement {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										21
									
								
								virtweb_frontend/src/widgets/vms/VMDiskFileWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								virtweb_frontend/src/widgets/vms/VMDiskFileWidget.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					import { mdiHarddisk } from "@mdi/js";
 | 
				
			||||||
 | 
					import { Icon } from "@mdi/react";
 | 
				
			||||||
 | 
					import { Avatar, ListItem, ListItemAvatar, ListItemText } from "@mui/material";
 | 
				
			||||||
 | 
					import { filesize } from "filesize";
 | 
				
			||||||
 | 
					import { VMFileDisk } from "../../api/VMApi";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function VMDiskFileWidget(p: { disk: VMFileDisk }): React.ReactElement {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ListItem>
 | 
				
			||||||
 | 
					      <ListItemAvatar>
 | 
				
			||||||
 | 
					        <Avatar>
 | 
				
			||||||
 | 
					          <Icon path={mdiHarddisk} />
 | 
				
			||||||
 | 
					        </Avatar>
 | 
				
			||||||
 | 
					      </ListItemAvatar>
 | 
				
			||||||
 | 
					      <ListItemText
 | 
				
			||||||
 | 
					        primary={p.disk.name}
 | 
				
			||||||
 | 
					        secondary={`${p.disk.format} - ${filesize(p.disk.size)}`}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </ListItem>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user