CEC-377 Create multi-file updates (#71)

* Replace Deploy Package with Deploy Manifest page
Stub new controls for package files

* Add Release notes and ECU FIles to Create Manifest

* Add Release notes and ECU FIles to Create Manifest

* Oops

* Replace multi release notes with single url

* Implement multiple file uploads and progress

* Update snapshots

* Unused import

* Move file to end of form
Update progress layout
This commit is contained in:
John Wu
2021-08-09 08:54:48 -07:00
committed by GitHub
parent 5d82356991
commit 0545b54daf
19 changed files with 1533 additions and 1943 deletions

View File

@@ -0,0 +1,253 @@
import React, { useEffect, useRef, useState } from "react";
import { Redirect } from "react-router";
import {
Button,
Grid,
LinearProgress,
TextField,
Typography,
} from "@material-ui/core";
import { DropzoneArea } from "material-ui-dropzone";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import {
useManifestsContext,
ManifestsProvider,
} from "../../Contexts/ManifestsContext";
import useStyles from "../../useStyles";
import { logger } from "../../../services/monitoring";
import ECUFilesList from "../ECUFilesList";
const FileTemplate = {
name: "",
part_number: "",
update_version: "1.0.0",
};
const UploadProgress = (props) => {
const { uploadProgress, uploadStatus, uploadFileIndex, uploadedFiles } =
useManifestsContext();
const [progress, setProgress] = useState(0);
const [completed, setCompleted] = useState(0);
const [total, setTotal] = useState(0);
useEffect(() => {
const x = uploadedFiles.reduce(
(current, { file }) => current + file.size,
0
);
setTotal(x);
}, [uploadedFiles]);
useEffect(() => {
if (uploadFileIndex === 0 || uploadFileIndex >= uploadedFiles.length)
return;
let uploaded = 0;
uploadedFiles.forEach(({ file }, i) => {
if (i < uploadFileIndex) uploaded += file.size;
});
setCompleted(uploaded);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uploadFileIndex]);
useEffect(() => {
if (total === 0 || uploadFileIndex >= uploadedFiles.length) return;
const { file } = uploadedFiles[uploadFileIndex];
const uploaded = completed + file.size * uploadProgress;
const x = Math.min(99, Math.floor((uploaded / total) * 100));
setProgress(x);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uploadProgress, completed]);
return (
<>
<Grid container>
<Grid xs={12}>
<Typography align="center">
{`File ${uploadFileIndex + 1} of ${
uploadedFiles.length
}. ${uploadStatus}`}
</Typography>
</Grid>
</Grid>
<Grid container alignContent="center" spacing={0}>
<Grid xs={11}>
<LinearProgress
variant="determinate"
value={progress}
style={{ marginTop: 16 }}
/>
</Grid>
<Grid xs={1} alignContent="flex-end" align="right">
<Button onClick={props.onCancel}>Cancel</Button>
</Grid>
</Grid>
</>
);
};
const MainForm = () => {
const { createManifest, cancelUpload, busy } = useManifestsContext();
const { token } = useUserContext();
const { setMessage, setTitle } = useStatusContext();
const [redirect, setRedirect] = useState(null);
const [fileIndex, setFileIndex] = useState(0);
const [ecuFiles, setECUFiles] = useState([]);
const classes = useStyles();
const packagenameEl = useRef(null);
const versionEl = useRef(null);
const descEl = useRef(null);
const releasenotesEl = useRef(null);
const getNewFile = (file) => {
setFileIndex(fileIndex + 1);
return Object.assign(
{ data_id: fileIndex, filename: file.name, file },
FileTemplate
);
};
const addFile = (file) => {
setECUFiles(ecuFiles.concat(getNewFile(file)));
};
useEffect(() => {
setTitle("Create Package");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onSubmit = async (event) => {
try {
event.preventDefault();
const {
idToken: { jwtToken: authToken },
} = token;
const formData = {
name: packagenameEl.current.value,
version: versionEl.current.value,
description: descEl.current.value,
releasenotes: releasenotesEl.current.value,
files: ecuFiles,
};
const manifest = await createManifest(formData, authToken);
if (!manifest || manifest.error) return;
cancelUpload();
setMessage(`Package uploaded`);
setRedirect(`/package-deploy/${manifest.id}`);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
if (redirect && redirect.length > 0) {
return <Redirect to={redirect} />;
}
return (
<div className={classes.paper}>
<form className={classes.form} noValidate action="{onSubmit}">
<TextField
id="packagename"
name="packagename"
label="Package name"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "255",
}}
required
fullWidth
inputRef={packagenameEl}
/>
<TextField
id="version"
name="version"
label="Version"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "255",
}}
required
fullWidth
inputRef={versionEl}
/>
<TextField
id="description"
name="description"
label="Description"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "5120",
}}
required
fullWidth
multiline
rows={4}
placeholder="Package description"
inputRef={descEl}
/>
<TextField
id="releasenotes"
name="releasenotes"
label="Release Notes URL"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "1024",
}}
required
fullWidth
placeholder="Release Notes URL"
inputRef={releasenotesEl}
/>
<Typography variant="h6">ECU Files</Typography>
<ECUFilesList data={ecuFiles} onChange={setECUFiles} />
<DropzoneArea
id="dropzone"
dropzoneText="Add Files"
maxFileSize={1e9}
filesLimit={1000}
showAlerts={false}
showPreviewsInDropzone={false}
onDrop={(files) => {
files.forEach((file) => {
addFile(file);
});
}}
onDropRejected={(files) => {
setMessage(`Rejected ${files[0].name} too large`);
}}
/>
{busy ? (
<UploadProgress onCancel={cancelUpload} />
) : (
<Button
type="submit"
disabled={busy}
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={onSubmit}
>
"Submit"
</Button>
)}
</form>
</div>
);
};
export default function FileUploadForm() {
return (
<ManifestsProvider>
<MainForm />
</ManifestsProvider>
);
}

View File

@@ -71,7 +71,7 @@ const MainForm = () => {
setMessage(
`Deployed ${manifestName} ${version} to ${selected.length} cars`
);
setRedirect(`/manifest-status/${manifest_id}`);
setRedirect(`/package-status/${manifest_id}`);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);

View File

@@ -0,0 +1,37 @@
import React from "react";
import SubList from "../../Controls/SubList";
const ECUFilesList = ({ data, onChange }) => {
const options = [
{
label: "ID",
field: "data_id",
readonly: true,
},
{
label: "ECU",
field: "name",
},
{
label: "Part Number",
field: "part_number",
},
{
label: "Version",
field: "update_version",
},
{
label: "File",
field: "filename",
readonly: true,
},
{
label: "",
delete: true,
},
];
return <SubList data={data} options={options} onChange={onChange} />;
};
export default ECUFilesList;

View File

@@ -86,7 +86,7 @@ const MainForm = () => {
};
useEffect(() => {
setTitle("Deploy Manifest");
setTitle("Deploy Packages");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -137,7 +137,7 @@ const MainForm = () => {
if (hasRole([Roles.CREATE, Roles.READ], groups)) {
actions.push({
tip: `Status "${row.name} ${row.version}"`,
link: `/manifest-status/${row.id}`,
link: `/package-status/${row.id}`,
icon: (
<VisibilityIcon aria-label={`Status ${row.name} ${row.version}`} />
),
@@ -147,7 +147,7 @@ const MainForm = () => {
actions = actions.concat([
{
tip: `Deploy "${row.name} ${row.version}"`,
link: `/manifest-deploy/${row.id}`,
link: `/package-deploy/${row.id}`,
icon: <SendIcon aria-label={`Deploy ${row.name} ${row.version}`} />,
},
{

View File

@@ -0,0 +1,28 @@
import React from "react";
import SubList from "../../Controls/SubList";
const ReleaseNotesList = ({ data, onChange }) => {
const options = [
{
label: "ID",
field: "data_id",
readonly: true,
},
{
label: "Locale",
field: "locale",
},
{
label: "URL",
field: "url",
},
{
label: "",
delete: true,
},
];
return <SubList data={data} options={options} onChange={onChange} />;
};
export default ReleaseNotesList;