Skip to content

Commit

Permalink
add editing of storage volumes
Browse files Browse the repository at this point in the history
  • Loading branch information
edlerd committed Sep 25, 2023
1 parent db1d2fb commit e67bea9
Show file tree
Hide file tree
Showing 16 changed files with 229 additions and 39 deletions.
12 changes: 7 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -261,12 +263,12 @@ const App: FC = () => {
element={<ProtectedRoute outlet={<StorageDetail />} />}
/>
<Route
path="/ui/project/:project/storage/detail/:name/:type/:volume"
element={<ProtectedRoute outlet={<VolumeDetail />} />}
path="/ui/project/:project/storage/detail/:pool/:type/:volume"
element={<ProtectedRoute outlet={<StorageVolumeDetail />} />}
/>
<Route
path="/ui/project/:project/storage/detail/:name/:type/:volume/:activeTab"
element={<ProtectedRoute outlet={<VolumeDetail />} />}
path="/ui/project/:project/storage/detail/:pool/:type/:volume/:activeTab"
element={<ProtectedRoute outlet={<StorageVolumeDetail />} />}
/>
<Route
path="/ui/cluster"
Expand Down
30 changes: 27 additions & 3 deletions src/api/storage-pools.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { handleResponse } from "util/helpers";
import { handleEtagResponse, handleResponse } from "util/helpers";
import {
LxdStoragePool,
LxdStoragePoolResources,
Expand Down Expand Up @@ -111,8 +111,8 @@ export const fetchStorageVolume = (
fetch(
`/1.0/storage-pools/${pool}/volumes/${type}/${volume}?project=${project}&recursion=1`
)
.then(handleResponse)
.then((data: LxdApiResponse<LxdStorageVolume>) => resolve(data.metadata))
.then(handleEtagResponse)
.then((data) => resolve(data as LxdStorageVolume))
.catch(reject);
});
};
Expand Down Expand Up @@ -192,6 +192,30 @@ export const createStorageVolume = (
});
};

export const updateStorageVolume = (
pool: string,
project: string,
volume: Partial<LxdStorageVolume>
) => {
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,
Expand Down
5 changes: 4 additions & 1 deletion src/pages/projects/forms/DiskSizeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { parseMemoryLimit } from "util/limits";
interface Props {
value?: string;
setMemoryLimit: (val?: string) => void;
disabled?: boolean;
}

const DiskSizeSelector: FC<Props> = ({ value, setMemoryLimit }) => {
const DiskSizeSelector: FC<Props> = ({ value, setMemoryLimit, disabled }) => {
const limit = parseMemoryLimit(value) ?? {
value: 1,
unit: BYTES_UNITS.GIB,
Expand All @@ -33,6 +34,7 @@ const DiskSizeSelector: FC<Props> = ({ value, setMemoryLimit }) => {
placeholder="Enter value"
onChange={(e) => setMemoryLimit(e.target.value + limit.unit)}
value={limit.value}
disabled={disabled}
/>
<Select
id="memUnitSelect"
Expand All @@ -42,6 +44,7 @@ const DiskSizeSelector: FC<Props> = ({ value, setMemoryLimit }) => {
setMemoryLimit(`${limit.value ?? 0}${e.target.value}`)
}
value={limit.unit}
disabled={disabled}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,30 @@ import Loader from "components/Loader";
import { fetchStorageVolume } from "api/storage-pools";
import NotificationRow from "components/NotificationRow";
import { slugify } from "util/slugify";
import VolumeHeader from "pages/storage/VolumeHeader";
import VolumeOverview from "pages/storage/VolumeOverview";
import StorageVolumeHeader from "pages/storage/StorageVolumeHeader";
import StorageVolumeOverview from "pages/storage/StorageVolumeOverview";
import StorageVolumeEdit from "pages/storage/forms/StorageVolumeEdit";

const TABS: string[] = ["Overview", "Configuration"];

const VolumeDetail: FC = () => {
const StorageVolumeDetail: FC = () => {
const navigate = useNavigate();
const notify = useNotify();
const {
name: storagePool,
pool,
project,
activeTab,
type,
volume: volumeName,
} = useParams<{
name: string;
pool: string;
project: string;
activeTab?: string;
type: string;
volume: string;
}>();

if (!storagePool) {
if (!pool) {
return <>Missing storage pool</>;
}
if (!project) {
Expand All @@ -47,8 +48,8 @@ const VolumeDetail: FC = () => {
error,
isLoading,
} = useQuery({
queryKey: [queryKeys.storage, storagePool, project, type, volumeName],
queryFn: () => fetchStorageVolume(storagePool, project, type, volumeName),
queryKey: [queryKeys.storage, pool, project, type, volumeName],
queryFn: () => fetchStorageVolume(pool, project, type, volumeName),
});

if (error) {
Expand All @@ -65,20 +66,20 @@ const VolumeDetail: FC = () => {
notify.clear();
if (newTab === "overview") {
navigate(
`/ui/project/${project}/storage/detail/${storagePool}/${type}/${volume.name}`
`/ui/project/${project}/storage/detail/${pool}/${type}/${volume.name}`
);
} else {
navigate(
`/ui/project/${project}/storage/detail/${storagePool}/${type}/${volume.name}/${newTab}`
`/ui/project/${project}/storage/detail/${pool}/${type}/${volume.name}/${newTab}`
);
}
};

return (
<main className="l-main">
<div className="p-panel instance-detail-page">
<VolumeHeader
storagePool={storagePool}
<StorageVolumeHeader
storagePool={pool}
volume={volume}
project={project}
/>
Expand All @@ -98,13 +99,13 @@ const VolumeDetail: FC = () => {

{!activeTab && (
<div role="tabpanel" aria-labelledby="overview">
<VolumeOverview volume={volume} project={project} />
<StorageVolumeOverview volume={volume} project={project} />
</div>
)}

{activeTab === "configuration" && (
<div role="tabpanel" aria-labelledby="volumes">
available soon
<StorageVolumeEdit volume={volume} pool={pool} />
</div>
)}
</Row>
Expand All @@ -114,4 +115,4 @@ const VolumeDetail: FC = () => {
);
};

export default VolumeDetail;
export default StorageVolumeDetail;
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ interface Props {
storagePool: string;
}

const VolumeHeader: FC<Props> = ({ volume, project, storagePool }) => {
const StorageVolumeHeader: FC<Props> = ({ volume, project, storagePool }) => {
const navigate = useNavigate();
const notify = useNotify();
const controllerState = useState<AbortController | null>(null);
const queryClient = useQueryClient();

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

Expand Down Expand Up @@ -100,4 +102,4 @@ const VolumeHeader: FC<Props> = ({ volume, project, storagePool }) => {
);
};

export default VolumeHeader;
export default StorageVolumeHeader;
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface Props {
volume: LxdStorageVolume;
}

const VolumeOverview: FC<Props> = ({ project, volume }) => {
const StorageVolumeOverview: FC<Props> = ({ project, volume }) => {
const updateContentHeight = () => {
updateMaxHeight("storage-overview-tab");
};
Expand Down Expand Up @@ -82,4 +82,4 @@ const VolumeOverview: FC<Props> = ({ project, volume }) => {
);
};

export default VolumeOverview;
export default StorageVolumeOverview;
2 changes: 1 addition & 1 deletion src/pages/storage/forms/StorageVolumeCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
});

Expand Down
107 changes: 107 additions & 0 deletions src/pages/storage/forms/StorageVolumeEdit.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ 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<StorageVolumeFormValues>({
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 (
<BaseLayout title="Create volume" contentClassName="storage-volume-form">
<StorageVolumeForm formik={formik} />
<div className="l-footer--sticky p-bottom-controls">
<hr />
<Row className="u-align--right">
<Col size={12}>
{formik.values.isReadOnly ? (
<Button
appearance="positive"
onClick={() => formik.setFieldValue("isReadOnly", false)}
>
Edit volume
</Button>
) : (
<>
<Button
onClick={() =>
formik.setValues(getStorageVolumeEditValues(volume, pool))
}
>
Cancel
</Button>
<SubmitButton
isSubmitting={formik.isSubmitting}
isDisabled={!formik.isValid || !formik.values.name}
buttonLabel="Save changes"
onClick={() => void formik.submitForm()}
/>
</>
)}
</Col>
</Row>
</div>
</BaseLayout>
);
};

export default StorageVolumeEdit;
8 changes: 6 additions & 2 deletions src/pages/storage/forms/StorageVolumeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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: {
Expand All @@ -70,6 +70,9 @@ export const volumeFormToPayload = (
project: project,
type: "custom",
content_type: values.content_type,
description: "",
location: "",
created_at: "",
};
};

Expand Down Expand Up @@ -127,6 +130,7 @@ const StorageVolumeForm: FC<Props> = ({ formik }) => {
formik={formik}
poolDriver={poolDriver}
contentType={formik.values.content_type}
isCreating={formik.values.isCreating}
/>
<Row className="form-contents">
<Col size={12}>
Expand Down
Loading

0 comments on commit e67bea9

Please sign in to comment.