From e67bea9b92e139c5c6cb27930b0991cfe4fd02dd Mon Sep 17 00:00:00 2001 From: David Edler Date: Mon, 25 Sep 2023 15:16:09 +0200 Subject: [PATCH] add editing of storage volumes --- src/App.tsx | 12 +- src/api/storage-pools.tsx | 30 ++++- src/pages/projects/forms/DiskSizeSelector.tsx | 5 +- ...lumeDetail.tsx => StorageVolumeDetail.tsx} | 31 ++--- ...lumeHeader.tsx => StorageVolumeHeader.tsx} | 8 +- ...Overview.tsx => StorageVolumeOverview.tsx} | 4 +- .../storage/forms/StorageVolumeCreate.tsx | 2 +- src/pages/storage/forms/StorageVolumeEdit.tsx | 107 ++++++++++++++++++ src/pages/storage/forms/StorageVolumeForm.tsx | 8 +- .../storage/forms/StorageVolumeFormMain.tsx | 3 + .../storage/forms/StorageVolumeFormMenu.tsx | 4 +- src/types/storage.d.ts | 14 ++- src/util/helpers.tsx | 3 +- src/util/networkEdit.tsx | 2 +- src/util/storageVolume.tsx | 5 +- src/util/storageVolumeEdit.tsx | 30 +++++ 16 files changed, 229 insertions(+), 39 deletions(-) rename src/pages/storage/{VolumeDetail.tsx => StorageVolumeDetail.tsx} (73%) rename src/pages/storage/{VolumeHeader.tsx => StorageVolumeHeader.tsx} (93%) rename src/pages/storage/{VolumeOverview.tsx => StorageVolumeOverview.tsx} (96%) create mode 100644 src/pages/storage/forms/StorageVolumeEdit.tsx create mode 100644 src/util/storageVolumeEdit.tsx diff --git a/src/App.tsx b/src/App.tsx index 8a133094b8..caa23e78b2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,7 +30,9 @@ const CertificateGenerate = lazy( const Login = lazy(() => import("pages/login/Login")); const ProtectedRoute = lazy(() => import("components/ProtectedRoute")); const StorageDetail = lazy(() => import("pages/storage/StorageDetail")); -const VolumeDetail = lazy(() => import("pages/storage/VolumeDetail")); +const StorageVolumeDetail = lazy( + () => import("pages/storage/StorageVolumeDetail") +); const NetworkMap = lazy(() => import("pages/networks/NetworkMap")); const CreateInstanceForm = lazy( () => import("pages/instances/CreateInstanceForm") @@ -261,12 +263,12 @@ const App: FC = () => { element={} />} /> } />} + path="/ui/project/:project/storage/detail/:pool/:type/:volume" + element={} />} /> } />} + path="/ui/project/:project/storage/detail/:pool/:type/:volume/:activeTab" + element={} />} /> ) => resolve(data.metadata)) + .then(handleEtagResponse) + .then((data) => resolve(data as LxdStorageVolume)) .catch(reject); }); }; @@ -192,6 +192,30 @@ export const createStorageVolume = ( }); }; +export const updateStorageVolume = ( + pool: string, + project: string, + volume: Partial +) => { + return new Promise((resolve, reject) => { + fetch( + `/1.0/storage-pools/${pool}/volumes/${volume.type ?? ""}/${ + volume.name ?? "" + }?project=${project}`, + { + method: "PUT", + body: JSON.stringify(volume), + headers: { + "If-Match": volume.etag ?? "invalid-etag", + }, + } + ) + .then(handleResponse) + .then((data) => resolve(data)) + .catch(reject); + }); +}; + export const deleteStorageVolume = ( volume: string, pool: string, diff --git a/src/pages/projects/forms/DiskSizeSelector.tsx b/src/pages/projects/forms/DiskSizeSelector.tsx index ce7db5adba..6f4bfb3702 100644 --- a/src/pages/projects/forms/DiskSizeSelector.tsx +++ b/src/pages/projects/forms/DiskSizeSelector.tsx @@ -6,9 +6,10 @@ import { parseMemoryLimit } from "util/limits"; interface Props { value?: string; setMemoryLimit: (val?: string) => void; + disabled?: boolean; } -const DiskSizeSelector: FC = ({ value, setMemoryLimit }) => { +const DiskSizeSelector: FC = ({ value, setMemoryLimit, disabled }) => { const limit = parseMemoryLimit(value) ?? { value: 1, unit: BYTES_UNITS.GIB, @@ -33,6 +34,7 @@ const DiskSizeSelector: FC = ({ value, setMemoryLimit }) => { placeholder="Enter value" onChange={(e) => setMemoryLimit(e.target.value + limit.unit)} value={limit.value} + disabled={disabled} /> = ({ formik }) => { } formik.setFieldValue("content_type", e.target.value); }} + disabled={formik.values.isReadOnly} /> diff --git a/src/pages/storage/forms/StorageVolumeFormMenu.tsx b/src/pages/storage/forms/StorageVolumeFormMenu.tsx index 6e7d8b849b..0670a97ad8 100644 --- a/src/pages/storage/forms/StorageVolumeFormMenu.tsx +++ b/src/pages/storage/forms/StorageVolumeFormMenu.tsx @@ -17,6 +17,7 @@ interface Props { formik: FormikProps; poolDriver: string; contentType: "block" | "filesystem"; + isCreating: boolean; } const StorageVolumeFormMenu: FC = ({ @@ -25,9 +26,10 @@ const StorageVolumeFormMenu: FC = ({ formik, poolDriver, contentType, + isCreating, }) => { const notify = useNotify(); - const [isAdvancedOpen, setAdvancedOpen] = useState(false); + const [isAdvancedOpen, setAdvancedOpen] = useState(!isCreating); const menuItemProps = { active, setActive, diff --git a/src/types/storage.d.ts b/src/types/storage.d.ts index c4ae36ea21..03d0054f5d 100644 --- a/src/types/storage.d.ts +++ b/src/types/storage.d.ts @@ -18,9 +18,20 @@ export interface LxdStorageVolume { "block.filesystem"?: string; "block.mount_options"?: string; "volatile.rootfs.size"?: number; + "security.shifted"?: string; + "security.unmapped"?: string; + "snapshots.expiry"?: string; + "snapshots.pattern"?: string; + "snapshots.schedule"?: string; + "zfs.blocksize"?: string; + "zfs.block_mode"?: string; + "zfs.delegate"?: string; + "zfs.remove_snapshots"?: string; + "zfs.use_refquota"?: string; + "zfs.reserve_space"?: string; size?: string; }; - content_type: string; + content_type: "filesystem" | "block"; created_at: string; description: string; location: string; @@ -28,6 +39,7 @@ export interface LxdStorageVolume { project: string; type: string; used_by?: string[]; + etag?: string; } export interface LxdStoragePoolResources { diff --git a/src/util/helpers.tsx b/src/util/helpers.tsx index 5fcabec8b0..11db676dd8 100644 --- a/src/util/helpers.tsx +++ b/src/util/helpers.tsx @@ -5,6 +5,7 @@ import { LxdProject } from "types/project"; import { LxdProfile } from "types/profile"; import { LxdNetwork } from "types/network"; import { getCookie } from "./cookies"; +import { LxdStorageVolume } from "types/storage"; export const UNDEFINED_DATE = "0001-01-01T00:00:00Z"; @@ -68,7 +69,7 @@ export const handleResponse = async (response: Response) => { export const handleEtagResponse = async (response: Response) => { const data = (await handleResponse(response)) as LxdApiResponse< - LxdInstance | LxdProject | LxdProfile | LxdNetwork + LxdInstance | LxdProject | LxdProfile | LxdNetwork | LxdStorageVolume >; const result = data.metadata; result.etag = response.headers.get("etag")?.replace("W/", "") ?? undefined; diff --git a/src/util/networkEdit.tsx b/src/util/networkEdit.tsx index 2b61ae5fe5..862db9c27e 100644 --- a/src/util/networkEdit.tsx +++ b/src/util/networkEdit.tsx @@ -1,6 +1,6 @@ import { LxdNetwork } from "types/network"; -const toBool = (value: string | undefined): boolean | undefined => { +export const toBool = (value: string | undefined): boolean | undefined => { if (value === undefined) { return undefined; } diff --git a/src/util/storageVolume.tsx b/src/util/storageVolume.tsx index f1eefe967b..5e19e7d6ef 100644 --- a/src/util/storageVolume.tsx +++ b/src/util/storageVolume.tsx @@ -1,12 +1,11 @@ import { AbortControllerState, checkDuplicateName } from "util/helpers"; import { TestFunction } from "yup"; import { AnyObject } from "yup/lib/types"; -import { LxdStorageVolume } from "types/storage"; import { LxdStoragePool } from "types/storage"; export const testDuplicateName = ( project: string, - volume: LxdStorageVolume, + volumeType: string, storagePool: string, controllerState: AbortControllerState ): [string, string, TestFunction] => { @@ -18,7 +17,7 @@ export const testDuplicateName = ( value, project, controllerState, - `storage-pools/${storagePool}/volumes/${volume.type}` + `storage-pools/${storagePool}/volumes/${volumeType}` ); }, ]; diff --git a/src/util/storageVolumeEdit.tsx b/src/util/storageVolumeEdit.tsx new file mode 100644 index 0000000000..4d5dffbc96 --- /dev/null +++ b/src/util/storageVolumeEdit.tsx @@ -0,0 +1,30 @@ +import { LxdStorageVolume } from "types/storage"; +import { StorageVolumeFormValues } from "pages/storage/forms/StorageVolumeForm"; +import { toBool } from "util/networkEdit"; +export const getStorageVolumeEditValues = ( + volume: LxdStorageVolume, + pool: string +): StorageVolumeFormValues => { + return { + name: volume.name, + project: volume.project, + pool: pool, + size: volume.config.size ?? "GiB", + content_type: volume.content_type, + security_shifted: toBool(volume.config["security.shifted"]), + security_unmapped: toBool(volume.config["security.unmapped"]), + snapshots_expiry: volume.config["snapshots.expiry"], + snapshots_pattern: volume.config["snapshots.pattern"], + snapshots_schedule: volume.config["snapshots.schedule"], + block_filesystem: volume.config["block.filesystem"], + block_mount_options: volume.config["block.mount_options"], + zfs_blocksize: volume.config["zfs.blocksize"], + zfs_block_mode: toBool(volume.config["zfs.block_mode"]), + zfs_delegate: toBool(volume.config["zfs.delegate"]), + zfs_remove_snapshots: toBool(volume.config["zfs.remove_snapshots"]), + zfs_use_refquota: toBool(volume.config["zfs.use_refquota"]), + zfs_reserve_space: toBool(volume.config["zfs.reserve_space"]), + isReadOnly: true, + isCreating: false, + }; +};