-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: [WD-13704] Create image from backup file (#848)
## Done - Added import tarball button as well as associated modal. - Minor scss changes - Additional API calls for importing the file and amending image properties. ## QA 1. Run the LXD-UI: - On the demo server via the link posted by @webteam-app below. This is only available for PRs created by collaborators of the repo. Ask @mas-who or @edlerd for access. - With a local copy of this branch, [build and run as described in the docs](../CONTRIBUTING.md#setting-up-for-development). 2. Perform the following QA steps: - Upload a unified tarball image or a split tarball image to LXD-UI by selecting the "Import from Tarball" button on the Image list page. ## Screenshots   
- Loading branch information
Showing
7 changed files
with
266 additions
and
9 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
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
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
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,27 @@ | ||
import { FC } from "react"; | ||
import { ActionButton } from "@canonical/react-components"; | ||
import usePortal from "react-useportal"; | ||
import UploadImageForm from "./forms/UploadImageForm"; | ||
|
||
interface Props { | ||
project: string; | ||
} | ||
|
||
const UploadImageBtn: FC<Props> = ({ project }) => { | ||
const { openPortal, closePortal, isOpen, Portal } = usePortal(); | ||
|
||
return ( | ||
<> | ||
{isOpen && ( | ||
<Portal> | ||
<UploadImageForm close={closePortal} project={project} /> | ||
</Portal> | ||
)} | ||
<ActionButton className="u-no-margin--bottom" onClick={openPortal}> | ||
<span>Upload image</span> | ||
</ActionButton> | ||
</> | ||
); | ||
}; | ||
|
||
export default UploadImageBtn; |
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,198 @@ | ||
import { ChangeEvent, FC, useState } from "react"; | ||
import { useEventQueue } from "context/eventQueue"; | ||
import { useFormik } from "formik"; | ||
import { useToastNotification } from "context/toastNotificationProvider"; | ||
import { createImageAlias, uploadImage } from "api/images"; | ||
import { | ||
ActionButton, | ||
Button, | ||
Form, | ||
Input, | ||
Modal, | ||
} from "@canonical/react-components"; | ||
import * as Yup from "yup"; | ||
import { Link } from "react-router-dom"; | ||
import { humanFileSize } from "util/helpers"; | ||
import ProgressBar from "components/ProgressBar"; | ||
import { UploadState } from "types/storage"; | ||
import Loader from "components/Loader"; | ||
import { useQueryClient } from "@tanstack/react-query"; | ||
import { queryKeys } from "util/queryKeys"; | ||
import { LxdSyncResponse } from "types/apiResponse"; | ||
import { AxiosError } from "axios"; | ||
|
||
interface Props { | ||
close: () => void; | ||
project: string; | ||
} | ||
|
||
const UploadImageForm: FC<Props> = ({ close, project }) => { | ||
const eventQueue = useEventQueue(); | ||
const toastNotify = useToastNotification(); | ||
const [uploadState, setUploadState] = useState<UploadState | null>(null); | ||
const queryClient = useQueryClient(); | ||
|
||
const notifySuccess = () => { | ||
const uploaded = <Link to={`/ui/project/${project}/images`}>uploaded</Link>; | ||
toastNotify.success(<>Image {uploaded}.</>); | ||
}; | ||
|
||
const changeFile = (e: ChangeEvent<HTMLInputElement>) => { | ||
if (e.target.files) { | ||
void formik.setFieldValue("fileList", e.target.files); | ||
} | ||
}; | ||
|
||
const getImageUploadBody = (fileList: FileList): File | FormData => { | ||
if (fileList.length === 1) { | ||
return fileList[0]; | ||
} else { | ||
// Sorting by Size. The metadata file is very likely to be smaller than the image itself. | ||
const formData = new FormData(); | ||
const sortedFiles = Array.from(fileList).sort((a, b) => a.size - b.size); | ||
|
||
formData.append("metadata", sortedFiles[0]); | ||
formData.append("rootfs.img", sortedFiles[1]); | ||
|
||
return formData; | ||
} | ||
}; | ||
|
||
const formik = useFormik<{ | ||
alias: string; | ||
isPublic: boolean; | ||
fileList: FileList | null; | ||
}>({ | ||
initialValues: { | ||
alias: "", | ||
isPublic: false, | ||
fileList: null, | ||
}, | ||
validationSchema: Yup.object().shape({ | ||
alias: Yup.string(), | ||
}), | ||
onSubmit: (values) => { | ||
if (values.fileList) { | ||
if (values.fileList.length > 2) { | ||
close(); | ||
toastNotify.failure( | ||
`Image upload failed.`, | ||
new Error("Too many files selected"), | ||
); | ||
return; | ||
} | ||
uploadImage( | ||
getImageUploadBody(values.fileList), | ||
values.isPublic, | ||
setUploadState, | ||
) | ||
.then((operation) => { | ||
toastNotify.info(<>Creation of image from file started.</>); | ||
|
||
eventQueue.set( | ||
operation.metadata.id, | ||
(event) => { | ||
const fingerprint = event.metadata.metadata?.fingerprint ?? ""; | ||
if (values.alias) { | ||
void createImageAlias(fingerprint, values.alias); | ||
} | ||
void queryClient.invalidateQueries({ | ||
queryKey: [queryKeys.images, project], | ||
}); | ||
notifySuccess(); | ||
}, | ||
(msg) => { | ||
toastNotify.failure(`Image upload failed.`, new Error(msg)); | ||
}, | ||
); | ||
}) | ||
.catch((e: AxiosError<LxdSyncResponse<null>>) => { | ||
const error = new Error(e.response?.data.error); | ||
toastNotify.failure("Image upload failed", error); | ||
}) | ||
.finally(() => { | ||
close(); | ||
}); | ||
} else { | ||
close(); | ||
toastNotify.failure(`Image upload failed`, new Error("Missing files")); | ||
} | ||
}, | ||
}); | ||
|
||
return ( | ||
<Modal | ||
close={close} | ||
title="Import image from file" | ||
className="upload-image-modal" | ||
buttonRow={ | ||
<> | ||
{uploadState && ( | ||
<> | ||
<ProgressBar percentage={Math.floor(uploadState.percentage)} /> | ||
<p> | ||
{humanFileSize(uploadState.loaded)} loaded of{" "} | ||
{humanFileSize(uploadState.total ?? 0)} | ||
</p> | ||
{uploadState.loaded === uploadState.total && ( | ||
<Loader text="Uploading file" /> | ||
)} | ||
</> | ||
)} | ||
<Button | ||
appearance="base" | ||
className="u-no-margin--bottom" | ||
type="button" | ||
onClick={close} | ||
> | ||
Cancel | ||
</Button> | ||
<ActionButton | ||
appearance="positive" | ||
className="u-no-margin--bottom" | ||
loading={formik.isSubmitting} | ||
disabled={!formik.isValid || !formik.values.fileList} | ||
onClick={() => void formik.submitForm()} | ||
> | ||
Upload image | ||
</ActionButton> | ||
</> | ||
} | ||
> | ||
<Form | ||
className={uploadState ? "u-hide" : ""} | ||
onSubmit={formik.handleSubmit} | ||
> | ||
<Input | ||
type="file" | ||
name="fileList" | ||
label="Image backup file" | ||
onChange={changeFile} | ||
multiple | ||
/> | ||
<Input | ||
{...formik.getFieldProps("alias")} | ||
type="text" | ||
label="Alias" | ||
placeholder="Enter alias" | ||
error={formik.touched.alias ? formik.errors.alias : null} | ||
/> | ||
<Input | ||
{...formik.getFieldProps("isPublic")} | ||
type="checkbox" | ||
label="Make the image publicly available" | ||
error={formik.touched.isPublic ? formik.errors.isPublic : null} | ||
/> | ||
{/* hidden submit to enable enter key in inputs */} | ||
<Input | ||
type="submit" | ||
hidden | ||
value="Hidden input" | ||
disabled={!formik.isValid || !formik.values.fileList} | ||
/> | ||
</Form> | ||
</Modal> | ||
); | ||
}; | ||
|
||
export default UploadImageForm; |
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 |
---|---|---|
|
@@ -29,3 +29,11 @@ | |
margin-top: -7px; | ||
} | ||
} | ||
|
||
.upload-image-modal { | ||
.p-modal__dialog { | ||
@include large { | ||
width: 35rem; | ||
} | ||
} | ||
} |
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