Files
VirtWeb/virtweb_frontend/src/widgets/forms/VMDisksList.tsx
Pierre HUBERT 0de15af10e
All checks were successful
continuous-integration/drone/push Build is passing
Normalize disk size input
2025-06-09 18:11:49 +02:00

307 lines
8.2 KiB
TypeScript

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>
);
}