import { mdiHarddiskPlus } from "@mdi/js"; import Icon from "@mdi/react"; import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import DeleteIcon from "@mui/icons-material/Delete"; import ExpandIcon from "@mui/icons-material/Expand"; import { Button, IconButton, Paper, Tooltip, Typography } from "@mui/material"; import React from "react"; import { DiskImage } from "../../api/DiskImageApi"; import { ServerApi } from "../../api/ServerApi"; import { VMFileDisk, VMInfo, VMState } from "../../api/VMApi"; import { ConvertDiskImageDialog } from "../../dialogs/ConvertDiskImageDialog"; import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; import { VMDiskFileWidget } from "../vms/VMDiskFileWidget"; import { CheckboxInput } from "./CheckboxInput"; import { DiskBusSelect } from "./DiskBusSelect"; import { DiskImageSelect } from "./DiskImageSelect"; import { DiskSizeInput } from "./DiskSizeInput"; import { SelectInput } from "./SelectInput"; import { TextInput } from "./TextInput"; export function VMDisksList(p: { vm: VMInfo; state?: VMState; onChange?: () => void; editable: boolean; diskImagesList: DiskImage[]; }): React.ReactElement { const [currBackupRequest, setCurrBackupRequest] = React.useState< VMFileDisk | undefined >(); const addNewDisk = () => { p.vm.file_disks.push({ format: "QCow2", size: 10000 * 1000 * 1000, bus: "Virtio", delete: false, name: `disk${p.vm.file_disks.length}`, new: true, }); p.onChange?.(); }; const handleBackupRequest = (disk: VMFileDisk) => { setCurrBackupRequest(disk); }; const handleFinishBackup = () => { setCurrBackupRequest(undefined); }; return ( <> {/* disks list */} {p.vm.file_disks.map((d, num) => ( <DiskInfo // eslint-disable-next-line react-x/no-array-index-key key={num} editable={p.editable} canBackup={!p.editable && !d.new && p.state !== "Running"} disk={d} onChange={p.onChange} removeFromList={() => { p.vm.file_disks.splice(num, 1); p.onChange?.(); }} onRequestBackup={handleBackupRequest} diskImagesList={p.diskImagesList} /> ))} {p.vm.file_disks.length === 0 && ( <Typography style={{ textAlign: "center", paddingTop: "25px" }}> No disk file yet! </Typography> )} {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: { editable: boolean; canBackup: boolean; disk: VMFileDisk; onChange?: () => void; removeFromList: () => void; onRequestBackup: (disk: VMFileDisk) => void; diskImagesList: DiskImage[]; }): React.ReactElement { const confirm = useConfirm(); const expandDisk = () => { if (p.disk.resize === true) { p.disk.resize = false; p.disk.size = p.disk.originalSize!; } else { p.disk.resize = true; p.disk.originalSize = p.disk.size!; } p.onChange?.(); }; const deleteDisk = async () => { if (p.disk.deleteType) { p.disk.deleteType = undefined; p.onChange?.(); return; } const keepFile = await confirm( `You asked to delete the disk ${p.disk.name}. Do you want to keep the block file or not ? `, "Delete disk", "Keep the file", "Delete the file" ); if (!(await confirm("Do you really want to delete this disk?"))) return; p.disk.deleteType = keepFile ? "keepfile" : "deletefile"; p.onChange?.(); }; if (!p.editable || !p.disk.new) return ( <> <VMDiskFileWidget {...p} secondaryAction={ <> {p.editable && !p.disk.deleteType && ( <IconButton edge="end" aria-label="expand disk" onClick={expandDisk} > {p.disk.resize === true ? ( <Tooltip title="Cancel disk expansion"> <ExpandIcon color="error" /> </Tooltip> ) : ( <Tooltip title="Increase disk size"> <ExpandIcon /> </Tooltip> )} </IconButton> )} {p.editable && ( <IconButton edge="end" aria-label="delete disk" onClick={deleteDisk} > {p.disk.deleteType ? ( <Tooltip title="Cancel disk removal"> <CheckCircleIcon /> </Tooltip> ) : ( <Tooltip title="Remove disk"> <DeleteIcon /> </Tooltip> )} </IconButton> )} {p.canBackup && ( <Tooltip title="Backup this disk"> <IconButton onClick={() => { p.onRequestBackup(p.disk); }} > <Icon path={mdiHarddiskPlus} size={1} /> </IconButton> </Tooltip> )} </> } /> {/* New disk size*/} {p.disk.resize && !p.disk.deleteType && ( <DiskSizeInput editable label="New disk size (GB)" value={p.disk.size} onChange={(v) => { p.disk.size = v; p.onChange?.(); }} /> )} </> ); return ( <Paper elevation={3} style={{ margin: "10px", padding: "10px" }}> <div style={{ display: "flex", justifyContent: "space-between" }}> <TextInput editable={true} label="Disk name" size={ServerApi.Config.constraints.disk_name_size} checkValue={(v) => /^[a-zA-Z0-9]+$/.test(v)} value={p.disk.name} onValueChange={(v) => { p.disk.name = v ?? ""; p.onChange?.(); }} /> <IconButton onClick={p.removeFromList}> <DeleteIcon /> </IconButton> </div> <SelectInput editable={true} label="Disk format" options={[ { label: "Raw file", value: "Raw" }, { label: "QCow2", value: "QCow2" }, ]} value={p.disk.format} onValueChange={(v) => { p.disk.format = v as any; if (p.disk.format === "Raw") p.disk.is_sparse = true; p.onChange?.(); }} /> {/* Bus selection */} <DiskBusSelect editable value={p.disk.bus} onValueChange={(v) => { p.disk.bus = v; p.onChange?.(); }} /> {/* Raw disk: choose sparse mode */} {p.disk.format === "Raw" && ( <CheckboxInput editable label="Sparse file" checked={p.disk.is_sparse} onValueChange={(v) => { if (p.disk.format === "Raw") p.disk.is_sparse = v; p.onChange?.(); }} /> )} {/* Resize disk image */} {!!p.disk.from_image && ( <CheckboxInput editable checked={p.disk.resize} label="Resize disk file" onValueChange={(v) => { p.disk.resize = v; p.onChange?.(); }} /> )} {/* Disk size */} {(!p.disk.from_image || p.disk.resize === true) && ( <DiskSizeInput editable value={p.disk.size} onChange={(v) => { p.disk.size = v; p.onChange?.(); }} /> )} {/* Disk image selection */} <DiskImageSelect label="Use disk image as template" list={p.diskImagesList} value={p.disk.from_image} onValueChange={(v) => { p.disk.from_image = v; p.onChange?.(); }} /> </Paper> ); }