-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
822 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.