Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
edlerd authored and GitHub Action committed May 15, 2023
1 parent 5d8af71 commit 105d490
Show file tree
Hide file tree
Showing 10 changed files with 822 additions and 0 deletions.
60 changes: 60 additions & 0 deletions src/api/storages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { handleResponse } from "util/helpers";
import { LxdStorage, LxdStorageResources } from "types/storage";
import { LxdApiResponse } from "types/apiResponse";

export const fetchStorage = (
storage: string,
project: string
): Promise<LxdStorage> => {
return new Promise((resolve, reject) => {
fetch(`/1.0/storage-pools/${storage}?project=${project}&recursion=1`)
.then(handleResponse)
.then((data: LxdApiResponse<LxdStorage>) => resolve(data.metadata))
.catch(reject);
});
};

export const fetchStorages = (project: string): Promise<LxdStorage[]> => {
return new Promise((resolve, reject) => {
fetch(`/1.0/storage-pools?project=${project}&recursion=1`)
.then(handleResponse)
.then((data: LxdApiResponse<LxdStorage[]>) => resolve(data.metadata))
.catch(reject);
});
};

export const fetchStorageResources = (
storage: string
): Promise<LxdStorageResources> => {
return new Promise((resolve, reject) => {
fetch(`/1.0/storage-pools/${storage}/resources`)
.then(handleResponse)
.then((data: LxdApiResponse<LxdStorageResources>) =>
resolve(data.metadata)
)
.catch(reject);
});
};

export const createStoragePool = (storage: LxdStorage, project: string) => {
return new Promise((resolve, reject) => {
fetch(`/1.0/storage-pools?project=${project}`, {
method: "POST",
body: JSON.stringify(storage),
})
.then(handleResponse)
.then((data) => resolve(data))
.catch(reject);
});
};

export const deleteStoragePool = (name: string, project: string) => {
return new Promise((resolve, reject) => {
fetch(`/1.0/storage-pools/${name}?project=${project}`, {
method: "DELETE",
})
.then(handleResponse)
.then((data) => resolve(data))
.catch(reject);
});
};
88 changes: 88 additions & 0 deletions src/pages/storages/StorageDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React, { FC } from "react";
import { useParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import BaseLayout from "components/BaseLayout";
import NotificationRow from "components/NotificationRow";
import { queryKeys } from "util/queryKeys";
import { useNotify } from "context/notify";
import { Row } from "@canonical/react-components";
import Loader from "components/Loader";
import { fetchStorage } from "api/storages";
import StorageSize from "pages/storages/StorageSize";
import StorageUsedBy from "pages/storages/StorageUsedBy";

const StorageDetail: FC = () => {
const notify = useNotify();
const { name, project } = useParams<{
name: string;
project: string;
}>();

if (!name) {
return <>Missing name</>;
}
if (!project) {
return <>Missing project</>;
}

const {
data: storage,
error,
isLoading,
} = useQuery({
queryKey: [queryKeys.storage, project, name],
queryFn: () => fetchStorage(name, project),
});

if (error) {
notify.failure("Loading storage details failed", error);
}

if (isLoading) {
return <Loader text="Loading storage details..." />;
} else if (!storage) {
return <>Loading storage details failed</>;
}

return (
<BaseLayout title={`Storage details for ${name}`}>
<NotificationRow />
<Row>
<table className="storage-detail-table">
<tbody>
<tr>
<th className="u-text--muted">Name</th>
<td>{storage.name}</td>
</tr>
<tr>
<th className="u-text--muted">Status</th>
<td>{storage.status}</td>
</tr>
<tr>
<th className="u-text--muted">Size</th>
<td>
<StorageSize storage={storage} />
</td>
</tr>
<tr>
<th className="u-text--muted">Source</th>
<td>{storage.config?.source ?? "-"}</td>
</tr>
<tr>
<th className="u-text--muted">Description</th>
<td>{storage.description ? storage.description : "-"}</td>
</tr>
<tr>
<th className="u-text--muted">Driver</th>
<td>{storage.driver}</td>
</tr>
</tbody>
</table>
<h2 className="p-heading--5">Used by</h2>
<StorageUsedBy storage={storage} project={project} />
</Row>
</BaseLayout>
);
};

export default StorageDetail;
190 changes: 190 additions & 0 deletions src/pages/storages/StorageForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import React, { FC, useState } from "react";
import {
Button,
Col,
Form,
Input,
Row,
Select,
} from "@canonical/react-components";
import { useFormik } from "formik";
import * as Yup from "yup";
import Aside from "components/Aside";
import NotificationRow from "components/NotificationRow";
import PanelHeader from "components/PanelHeader";
import { useNotify } from "context/notify";
import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import SubmitButton from "components/SubmitButton";
import { checkDuplicateName } from "util/helpers";
import usePanelParams from "util/usePanelParams";
import { LxdStorage } from "types/storage";
import { createStoragePool } from "api/storages";
import { getSourceHelpForDriver, storageDrivers } from "util/storageOptions";
import ItemName from "components/ItemName";

const StorageForm: FC = () => {
const panelParams = usePanelParams();
const notify = useNotify();
const queryClient = useQueryClient();
const controllerState = useState<AbortController | null>(null);

const StorageSchema = Yup.object().shape({
name: Yup.string()
.test(
"deduplicate",
"A storage pool with this name already exists",
(value) =>
checkDuplicateName(
value,
panelParams.project,
controllerState,
"storage-pools"
)
)
.required("This field is required"),
});

const formik = useFormik({
initialValues: {
name: "",
description: "",
driver: "zfs",
source: "",
size: "",
},
validationSchema: StorageSchema,
onSubmit: ({ name, description, driver, source, size }) => {
const storagePool: LxdStorage = {
name,
description,
driver,
source: driver !== "btrfs" ? source : undefined,
config: {
size: size ? `${size}GiB` : undefined,
},
};

createStoragePool(storagePool, panelParams.project)
.then(() => {
void queryClient.invalidateQueries({
queryKey: [queryKeys.storage],
});
notify.success(
<>
Storage <ItemName item={storagePool} bold /> created.
</>
);
panelParams.clear();
})
.catch((e) => {
formik.setSubmitting(false);
notify.failure("Storage pool creation failed", e);
});
},
});

const submitForm = () => {
void formik.submitForm();
};

return (
<Aside>
<div className="p-panel l-site">
<PanelHeader
title={<h2 className="p-heading--4">Create storage pool</h2>}
/>
<div className="p-panel__content">
<NotificationRow />
<Row>
<Form onSubmit={formik.handleSubmit} stacked>
<Input
id="name"
name="name"
type="text"
label="Name"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
value={formik.values.name}
error={formik.touched.name ? formik.errors.name : null}
required
stacked
/>
<Input
id="description"
name="description"
type="text"
label="Description"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
value={formik.values.description}
error={
formik.touched.description ? formik.errors.description : null
}
stacked
/>
<Select
id="driver"
name="driver"
help={
formik.values.driver === "zfs"
? "ZFS gives best performance and reliability"
: undefined
}
label="Driver"
options={storageDrivers}
onChange={formik.handleChange}
value={formik.values.driver}
required
stacked
/>
<Input
id="size"
name="size"
type="number"
help="When left blank, defaults to 20% of free disk space. Default will be between 5GiB and 30GiB"
label="Size in GiB"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
value={formik.values.size}
error={formik.touched.size ? formik.errors.size : null}
stacked
/>
<Input
id="source"
name="source"
type="text"
disabled={formik.values.driver === "btrfs"}
help={getSourceHelpForDriver(formik.values.driver)}
label="Source"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
value={formik.values.source}
error={formik.touched.source ? formik.errors.source : null}
stacked
/>
</Form>
</Row>
</div>
<div className="l-footer--sticky p-bottom-controls">
<hr />
<Row className="u-align--right">
<Col size={12}>
<Button appearance="base" onClick={panelParams.clear}>
Cancel
</Button>
<SubmitButton
isSubmitting={formik.isSubmitting}
isDisabled={!formik.isValid}
onClick={submitForm}
buttonLabel="Create"
/>
</Col>
</Row>
</div>
</div>
</Aside>
);
};

export default StorageForm;
Loading

0 comments on commit 105d490

Please sign in to comment.