-
@@ -98,13 +99,13 @@ const VolumeDetail: FC = () => {
{!activeTab && (
-
+
)}
{activeTab === "configuration" && (
- available soon
+
)}
@@ -114,4 +115,4 @@ const VolumeDetail: FC = () => {
);
};
-export default VolumeDetail;
+export default StorageVolumeDetail;
diff --git a/src/pages/storage/VolumeHeader.tsx b/src/pages/storage/StorageVolumeHeader.tsx
similarity index 93%
rename from src/pages/storage/VolumeHeader.tsx
rename to src/pages/storage/StorageVolumeHeader.tsx
index 9735674259..77ac38fb3b 100644
--- a/src/pages/storage/VolumeHeader.tsx
+++ b/src/pages/storage/StorageVolumeHeader.tsx
@@ -17,7 +17,7 @@ interface Props {
storagePool: string;
}
-const VolumeHeader: FC
= ({ volume, project, storagePool }) => {
+const StorageVolumeHeader: FC = ({ volume, project, storagePool }) => {
const navigate = useNavigate();
const notify = useNotify();
const controllerState = useState(null);
@@ -25,7 +25,9 @@ const VolumeHeader: FC = ({ volume, project, storagePool }) => {
const RenameSchema = Yup.object().shape({
name: Yup.string()
- .test(...testDuplicateName(project, volume, storagePool, controllerState))
+ .test(
+ ...testDuplicateName(project, volume.type, storagePool, controllerState)
+ )
.required("This field is required"),
});
@@ -100,4 +102,4 @@ const VolumeHeader: FC = ({ volume, project, storagePool }) => {
);
};
-export default VolumeHeader;
+export default StorageVolumeHeader;
diff --git a/src/pages/storage/VolumeOverview.tsx b/src/pages/storage/StorageVolumeOverview.tsx
similarity index 96%
rename from src/pages/storage/VolumeOverview.tsx
rename to src/pages/storage/StorageVolumeOverview.tsx
index f1afb63ea7..5c9a42569b 100644
--- a/src/pages/storage/VolumeOverview.tsx
+++ b/src/pages/storage/StorageVolumeOverview.tsx
@@ -11,7 +11,7 @@ interface Props {
volume: LxdStorageVolume;
}
-const VolumeOverview: FC = ({ project, volume }) => {
+const StorageVolumeOverview: FC = ({ project, volume }) => {
const updateContentHeight = () => {
updateMaxHeight("storage-overview-tab");
};
@@ -82,4 +82,4 @@ const VolumeOverview: FC = ({ project, volume }) => {
);
};
-export default VolumeOverview;
+export default StorageVolumeOverview;
diff --git a/src/pages/storage/forms/StorageVolumeCreate.tsx b/src/pages/storage/forms/StorageVolumeCreate.tsx
index 882b6ed5f8..0c2bd8d135 100644
--- a/src/pages/storage/forms/StorageVolumeCreate.tsx
+++ b/src/pages/storage/forms/StorageVolumeCreate.tsx
@@ -33,7 +33,7 @@ const StorageVolumeCreate: FC = () => {
const StorageSchema = Yup.object().shape({
name: Yup.string()
- .test(...testDuplicateName(project, pool, controllerState))
+ .test(...testDuplicateName(project, "custom", pool, controllerState))
.required("This field is required"),
});
diff --git a/src/pages/storage/forms/StorageVolumeEdit.tsx b/src/pages/storage/forms/StorageVolumeEdit.tsx
new file mode 100644
index 0000000000..e5a9a371f9
--- /dev/null
+++ b/src/pages/storage/forms/StorageVolumeEdit.tsx
@@ -0,0 +1,107 @@
+import React, { FC } from "react";
+import { Button, Col, Row, useNotify } from "@canonical/react-components";
+import { useFormik } from "formik";
+import * as Yup from "yup";
+import { useQueryClient } from "@tanstack/react-query";
+import { queryKeys } from "util/queryKeys";
+import SubmitButton from "components/SubmitButton";
+import { updateStorageVolume } from "api/storage-pools";
+import { useParams } from "react-router-dom";
+import BaseLayout from "components/BaseLayout";
+import StorageVolumeForm, {
+ StorageVolumeFormValues,
+ volumeFormToPayload,
+} from "pages/storage/forms/StorageVolumeForm";
+import { LxdStorageVolume } from "types/storage";
+import { getStorageVolumeEditValues } from "util/storageVolumeEdit";
+
+interface Props {
+ volume: LxdStorageVolume;
+ pool: string;
+}
+
+const StorageVolumeEdit: FC = ({ volume, pool }) => {
+ const notify = useNotify();
+ const queryClient = useQueryClient();
+ const { project } = useParams<{ project: string }>();
+
+ if (!project) {
+ return <>Missing project>;
+ }
+
+ const StorageSchema = Yup.object().shape({
+ name: Yup.string().required("This field is required"),
+ });
+
+ const formik = useFormik({
+ initialValues: getStorageVolumeEditValues(volume, pool),
+ validationSchema: StorageSchema,
+ onSubmit: (values) => {
+ const saveVolume = volumeFormToPayload(values, project);
+ updateStorageVolume(values.pool, project, {
+ ...saveVolume,
+ etag: volume.etag,
+ })
+ .then(() => {
+ formik.setSubmitting(false);
+ void formik.setValues(getStorageVolumeEditValues(saveVolume, pool));
+ void queryClient.invalidateQueries({
+ queryKey: [queryKeys.storage],
+ });
+ void queryClient.invalidateQueries({
+ queryKey: [
+ queryKeys.storage,
+ pool,
+ project,
+ saveVolume.type,
+ saveVolume.name,
+ ],
+ });
+ notify.success(`Storage volume updated.`);
+ })
+ .catch((e) => {
+ formik.setSubmitting(false);
+ notify.failure("Storage volume update failed", e);
+ });
+ },
+ });
+
+ return (
+
+
+
+
+
+
+ {formik.values.isReadOnly ? (
+
+ ) : (
+ <>
+
+ void formik.submitForm()}
+ />
+ >
+ )}
+
+
+
+
+ );
+};
+
+export default StorageVolumeEdit;
diff --git a/src/pages/storage/forms/StorageVolumeForm.tsx b/src/pages/storage/forms/StorageVolumeForm.tsx
index 3c1987feb3..51c69380f0 100644
--- a/src/pages/storage/forms/StorageVolumeForm.tsx
+++ b/src/pages/storage/forms/StorageVolumeForm.tsx
@@ -18,6 +18,7 @@ import StorageVolumeFormBlock from "pages/storage/forms/StorageVolumeFormBlock";
import StorageVolumeFormZFS from "pages/storage/forms/StorageVolumeFormZFS";
import { FormikProps } from "formik/dist/types";
import { getVolumeKey } from "util/storageVolume";
+import { LxdStorageVolume } from "types/storage";
export interface StorageVolumeFormValues {
name: string;
@@ -45,9 +46,8 @@ export interface StorageVolumeFormValues {
export const volumeFormToPayload = (
values: StorageVolumeFormValues,
project: string
-) => {
+): LxdStorageVolume => {
const hasValidSize = values.size?.match(/^\d/);
-
return {
name: values.name,
config: {
@@ -70,6 +70,9 @@ export const volumeFormToPayload = (
project: project,
type: "custom",
content_type: values.content_type,
+ description: "",
+ location: "",
+ created_at: "",
};
};
@@ -127,6 +130,7 @@ const StorageVolumeForm: FC = ({ formik }) => {
formik={formik}
poolDriver={poolDriver}
contentType={formik.values.content_type}
+ isCreating={formik.values.isCreating}
/>
diff --git a/src/pages/storage/forms/StorageVolumeFormMain.tsx b/src/pages/storage/forms/StorageVolumeFormMain.tsx
index fca1f00465..a3a99bf823 100644
--- a/src/pages/storage/forms/StorageVolumeFormMain.tsx
+++ b/src/pages/storage/forms/StorageVolumeFormMain.tsx
@@ -28,6 +28,7 @@ const StorageVolumeFormMain: FC = ({ formik }) => {
{...getFormProps(formik, "name")}
type="text"
label="Name"
+ disabled={formik.values.isReadOnly}
required
/>
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,
+ };
+};