Skip to content

Commit 8facc4c

Browse files
edlerdGitHub Action
authored andcommitted
wip
1 parent fed2b7c commit 8facc4c

11 files changed

Lines changed: 834 additions & 0 deletions

src/api/storages.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { handleResponse } from "util/helpers";
2+
import { LxdStorage, LxdStorageResources } from "types/storage";
3+
import { LxdApiResponse } from "types/apiResponse";
4+
5+
export const fetchStorage = (
6+
storage: string,
7+
project: string
8+
): Promise<LxdStorage> => {
9+
return new Promise((resolve, reject) => {
10+
fetch(`/1.0/storage-pools/${storage}?project=${project}&recursion=1`)
11+
.then(handleResponse)
12+
.then((data: LxdApiResponse<LxdStorage>) => resolve(data.metadata))
13+
.catch(reject);
14+
});
15+
};
16+
17+
export const fetchStorages = (project: string): Promise<LxdStorage[]> => {
18+
return new Promise((resolve, reject) => {
19+
fetch(`/1.0/storage-pools?project=${project}&recursion=1`)
20+
.then(handleResponse)
21+
.then((data: LxdApiResponse<LxdStorage[]>) => resolve(data.metadata))
22+
.catch(reject);
23+
});
24+
};
25+
26+
export const fetchStorageResources = (
27+
storage: string
28+
): Promise<LxdStorageResources> => {
29+
return new Promise((resolve, reject) => {
30+
fetch(`/1.0/storage-pools/${storage}/resources`)
31+
.then(handleResponse)
32+
.then((data: LxdApiResponse<LxdStorageResources>) =>
33+
resolve(data.metadata)
34+
)
35+
.catch(reject);
36+
});
37+
};
38+
39+
export const createStoragePool = (storage: LxdStorage, project: string) => {
40+
return new Promise((resolve, reject) => {
41+
fetch(`/1.0/storage-pools?project=${project}`, {
42+
method: "POST",
43+
body: JSON.stringify(storage),
44+
})
45+
.then(handleResponse)
46+
.then((data) => resolve(data))
47+
.catch(reject);
48+
});
49+
};
50+
51+
export const deleteStoragePool = (name: string, project: string) => {
52+
return new Promise((resolve, reject) => {
53+
fetch(`/1.0/storage-pools/${name}?project=${project}`, {
54+
method: "DELETE",
55+
})
56+
.then(handleResponse)
57+
.then((data) => resolve(data))
58+
.catch(reject);
59+
});
60+
};
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React, { FC } from "react";
2+
import { useParams } from "react-router-dom";
3+
import { useQuery } from "@tanstack/react-query";
4+
import BaseLayout from "components/BaseLayout";
5+
import NotificationRow from "components/NotificationRow";
6+
import { queryKeys } from "util/queryKeys";
7+
import { useNotify } from "context/notify";
8+
import { Row } from "@canonical/react-components";
9+
import Loader from "components/Loader";
10+
import { fetchStorage } from "api/storages";
11+
import StorageSize from "pages/storages/StorageSize";
12+
import StorageUsedBy from "pages/storages/StorageUsedBy";
13+
14+
const StorageDetail: FC = () => {
15+
const notify = useNotify();
16+
const { name, project } = useParams<{
17+
name: string;
18+
project: string;
19+
}>();
20+
21+
if (!name) {
22+
return <>Missing name</>;
23+
}
24+
if (!project) {
25+
return <>Missing project</>;
26+
}
27+
28+
const {
29+
data: storage,
30+
error,
31+
isLoading,
32+
} = useQuery({
33+
queryKey: [queryKeys.storage, project, name],
34+
queryFn: () => fetchStorage(name, project),
35+
});
36+
37+
if (error) {
38+
notify.failure("Loading storage details failed", error);
39+
}
40+
41+
if (isLoading) {
42+
return <Loader text="Loading storage details..." />;
43+
} else if (!storage) {
44+
return <>Loading storage details failed</>;
45+
}
46+
47+
return (
48+
<BaseLayout title={`Storage details for ${name}`}>
49+
<NotificationRow />
50+
<Row>
51+
<table className="storage-detail-table">
52+
<tbody>
53+
<tr>
54+
<th className="u-text--muted">Name</th>
55+
<td>{storage.name}</td>
56+
</tr>
57+
<tr>
58+
<th className="u-text--muted">Status</th>
59+
<td>{storage.status}</td>
60+
</tr>
61+
<tr>
62+
<th className="u-text--muted">Size</th>
63+
<td>
64+
<StorageSize storage={storage} />
65+
</td>
66+
</tr>
67+
<tr>
68+
<th className="u-text--muted">Source</th>
69+
<td>{storage.config?.source ?? "-"}</td>
70+
</tr>
71+
<tr>
72+
<th className="u-text--muted">Description</th>
73+
<td>{storage.description ? storage.description : "-"}</td>
74+
</tr>
75+
<tr>
76+
<th className="u-text--muted">Driver</th>
77+
<td>{storage.driver}</td>
78+
</tr>
79+
</tbody>
80+
</table>
81+
<h2 className="p-heading--5">Used by</h2>
82+
<StorageUsedBy storage={storage} project={project} />
83+
</Row>
84+
</BaseLayout>
85+
);
86+
};
87+
88+
export default StorageDetail;

src/pages/storages/StorageForm.tsx

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import React, { FC, useState } from "react";
2+
import {
3+
Button,
4+
Col,
5+
Form,
6+
Input,
7+
Row,
8+
Select,
9+
} from "@canonical/react-components";
10+
import { useFormik } from "formik";
11+
import * as Yup from "yup";
12+
import Aside from "components/Aside";
13+
import NotificationRow from "components/NotificationRow";
14+
import PanelHeader from "components/PanelHeader";
15+
import { useNotify } from "context/notify";
16+
import { useQueryClient } from "@tanstack/react-query";
17+
import { queryKeys } from "util/queryKeys";
18+
import SubmitButton from "components/SubmitButton";
19+
import { checkDuplicateName } from "util/helpers";
20+
import usePanelParams from "util/usePanelParams";
21+
import { LxdStorage } from "types/storage";
22+
import { createStoragePool } from "api/storages";
23+
import { getSourceHelpForDriver, storageDrivers } from "util/storageOptions";
24+
import ItemName from "components/ItemName";
25+
26+
const StorageForm: FC = () => {
27+
const panelParams = usePanelParams();
28+
const notify = useNotify();
29+
const queryClient = useQueryClient();
30+
const controllerState = useState<AbortController | null>(null);
31+
32+
const StorageSchema = Yup.object().shape({
33+
name: Yup.string()
34+
.test(
35+
"deduplicate",
36+
"A storage pool with this name already exists",
37+
(value) =>
38+
checkDuplicateName(
39+
value,
40+
panelParams.project,
41+
controllerState,
42+
"storage-pools"
43+
)
44+
)
45+
.required("This field is required"),
46+
});
47+
48+
const formik = useFormik({
49+
initialValues: {
50+
name: "",
51+
description: "",
52+
driver: "zfs",
53+
source: "",
54+
size: "",
55+
},
56+
validationSchema: StorageSchema,
57+
onSubmit: ({ name, description, driver, source, size }) => {
58+
const storagePool: LxdStorage = {
59+
name,
60+
description,
61+
driver,
62+
source: driver !== "btrfs" ? source : undefined,
63+
config: {
64+
size: size ? `${size}GiB` : undefined,
65+
},
66+
};
67+
68+
createStoragePool(storagePool, panelParams.project)
69+
.then(() => {
70+
void queryClient.invalidateQueries({
71+
queryKey: [queryKeys.storage],
72+
});
73+
notify.success(
74+
<>
75+
Storage <ItemName item={storagePool} bold /> created.
76+
</>
77+
);
78+
panelParams.clear();
79+
})
80+
.catch((e) => {
81+
formik.setSubmitting(false);
82+
notify.failure("Storage pool creation failed", e);
83+
});
84+
},
85+
});
86+
87+
const submitForm = () => {
88+
void formik.submitForm();
89+
};
90+
91+
return (
92+
<Aside>
93+
<div className="p-panel l-site">
94+
<PanelHeader
95+
title={<h2 className="p-heading--4">Create storage pool</h2>}
96+
/>
97+
<div className="p-panel__content">
98+
<NotificationRow />
99+
<Row>
100+
<Form onSubmit={formik.handleSubmit} stacked>
101+
<Input
102+
id="name"
103+
name="name"
104+
type="text"
105+
label="Name"
106+
onBlur={formik.handleBlur}
107+
onChange={formik.handleChange}
108+
value={formik.values.name}
109+
error={formik.touched.name ? formik.errors.name : null}
110+
required
111+
stacked
112+
/>
113+
<Input
114+
id="description"
115+
name="description"
116+
type="text"
117+
label="Description"
118+
onBlur={formik.handleBlur}
119+
onChange={formik.handleChange}
120+
value={formik.values.description}
121+
error={
122+
formik.touched.description ? formik.errors.description : null
123+
}
124+
stacked
125+
/>
126+
<Select
127+
id="driver"
128+
name="driver"
129+
help={
130+
formik.values.driver === "zfs"
131+
? "ZFS gives best performance and reliability"
132+
: undefined
133+
}
134+
label="Driver"
135+
options={storageDrivers}
136+
onChange={formik.handleChange}
137+
value={formik.values.driver}
138+
required
139+
stacked
140+
/>
141+
<Input
142+
id="size"
143+
name="size"
144+
type="number"
145+
help="When left blank, defaults to 20% of free disk space. Default will be between 5GiB and 30GiB"
146+
label="Size in GiB"
147+
onBlur={formik.handleBlur}
148+
onChange={formik.handleChange}
149+
value={formik.values.size}
150+
error={formik.touched.size ? formik.errors.size : null}
151+
stacked
152+
/>
153+
<Input
154+
id="source"
155+
name="source"
156+
type="text"
157+
disabled={formik.values.driver === "btrfs"}
158+
help={getSourceHelpForDriver(formik.values.driver)}
159+
label="Source"
160+
onBlur={formik.handleBlur}
161+
onChange={formik.handleChange}
162+
value={formik.values.source}
163+
error={formik.touched.source ? formik.errors.source : null}
164+
stacked
165+
/>
166+
</Form>
167+
</Row>
168+
</div>
169+
<div className="l-footer--sticky p-bottom-controls">
170+
<hr />
171+
<Row className="u-align--right">
172+
<Col size={12}>
173+
<Button appearance="base" onClick={panelParams.clear}>
174+
Cancel
175+
</Button>
176+
<SubmitButton
177+
isSubmitting={formik.isSubmitting}
178+
isDisabled={!formik.isValid}
179+
onClick={submitForm}
180+
buttonLabel="Create"
181+
/>
182+
</Col>
183+
</Row>
184+
</div>
185+
</div>
186+
</Aside>
187+
);
188+
};
189+
190+
export default StorageForm;

0 commit comments

Comments
 (0)