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:
253
src/components/Manifest/Create/index.jsx
Normal file
253
src/components/Manifest/Create/index.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
37
src/components/Manifest/ECUFilesList/index.jsx
Normal file
37
src/components/Manifest/ECUFilesList/index.jsx
Normal 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;
|
||||
@@ -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}`} />,
|
||||
},
|
||||
{
|
||||
|
||||
28
src/components/Manifest/ReleaseNotesList/index.jsx
Normal file
28
src/components/Manifest/ReleaseNotesList/index.jsx
Normal 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;
|
||||
Reference in New Issue
Block a user