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:
@@ -64,18 +64,10 @@ describe("App", () => {
|
||||
await check("/home", "span.MuiButton-label", "Sign In");
|
||||
});
|
||||
|
||||
it("Route /package-upload unauthenticated", async () => {
|
||||
await check("/package-upload", "span.MuiButton-label", "Sign In");
|
||||
});
|
||||
|
||||
it("Route /vehicle-add unauthenticated", async () => {
|
||||
await check("/vehicle-add", "span.MuiButton-label", "Sign In");
|
||||
});
|
||||
|
||||
it("Route /updates unauthenticated", async () => {
|
||||
await check("/updates", "span.MuiButton-label", "Sign In");
|
||||
});
|
||||
|
||||
it("Route /carupdate-deploy unauthenticated", async () => {
|
||||
await check("/carupdate-deploy/1", "span.MuiButton-label", "Sign In");
|
||||
});
|
||||
@@ -100,16 +92,20 @@ describe("App", () => {
|
||||
await check("/dashboard", "span.MuiButton-label", "Sign In");
|
||||
});
|
||||
|
||||
it("Route /manifests unauthenticated", async () => {
|
||||
await check("/manifests", "span.MuiButton-label", "Sign In");
|
||||
it("Route /packages unauthenticated", async () => {
|
||||
await check("/packages", "span.MuiButton-label", "Sign In");
|
||||
});
|
||||
|
||||
it("Route /manifest-status unauthenticated", async () => {
|
||||
await check("/manifest-status/1", "span.MuiButton-label", "Sign In");
|
||||
it("Route /package-status unauthenticated", async () => {
|
||||
await check("/package-status/1", "span.MuiButton-label", "Sign In");
|
||||
});
|
||||
|
||||
it("Route /manifest-deploy unauthenticated", async () => {
|
||||
await check("/manifest-deploy/1", "span.MuiButton-label", "Sign In");
|
||||
it("Route /package-deploy unauthenticated", async () => {
|
||||
await check("/package-deploy/1", "span.MuiButton-label", "Sign In");
|
||||
});
|
||||
|
||||
it("Route /package-create unauthenticated", async () => {
|
||||
await check("/package-create", "span.MuiButton-label", "Sign In");
|
||||
});
|
||||
|
||||
it("Route / authenticated", async () => {
|
||||
@@ -122,21 +118,11 @@ describe("App", () => {
|
||||
await sleepAndCheck("/home", "h1", "Welcome John!");
|
||||
});
|
||||
|
||||
it("Route /package-upload authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/package-upload", "h6", "Create Update Package");
|
||||
});
|
||||
|
||||
it("Route /vehicle-add authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/vehicle-add", "h6", "Add Vehicle");
|
||||
});
|
||||
|
||||
it("Route /updates authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/updates", "h6", "Deploy Packages");
|
||||
});
|
||||
|
||||
it("Route /carupdate-status authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/carupdate-status/1", "h6", "Package Package 1.0");
|
||||
@@ -176,18 +162,24 @@ describe("App", () => {
|
||||
await check("/dashboard", "h6", "Dashboard");
|
||||
});
|
||||
|
||||
it("Route /manifests authenticated", async () => {
|
||||
it("Route /packages authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/manifests", "h6", "Deploy Manifest");
|
||||
await check("/packages", "h6", "Deploy Packages");
|
||||
});
|
||||
|
||||
it("Route /manifest-status authenticated", async () => {
|
||||
it("Route /package-status authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/manifest-status/1", "h6", "Manifest Test Manifest 1.0");
|
||||
await check("/package-status/1", "h6", "Manifest Test Manifest 1.0");
|
||||
});
|
||||
|
||||
it("Route /manifest-deploy authenticated", async () => {
|
||||
it("Route /package-deploy authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/manifest-deploy/1", "h6", "Deploy Test Manifest 1.0");
|
||||
await check("/package-deploy/1", "h6", "Deploy Test Manifest 1.0");
|
||||
});
|
||||
|
||||
it("Route /package-create authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/package-create", "h6", "Create Package");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,17 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
|
||||
import api from "../../services/manifests";
|
||||
import { uploadFile, getCancelToken } from "../../services/uploadFile";
|
||||
|
||||
const ManifestsContext = React.createContext();
|
||||
|
||||
export const ManifestsProvider = ({ children }) => {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [uploadStatus, setUploadStatus] = useState(null);
|
||||
const [cancelUploadToken, setCancelUploadToken] = useState(null);
|
||||
const [uploadFileIndex, setUploadFileIndex] = useState(0);
|
||||
const [uploadedFiles, setUploadedFiles] = useState([]);
|
||||
const [manifests, setManifests] = useState([]);
|
||||
const [totalManifests, setTotalManifests] = useState(0);
|
||||
|
||||
@@ -48,14 +54,153 @@ export const ManifestsProvider = ({ children }) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const validateManifest = (data, accessToken) => {
|
||||
const fileKeys = {};
|
||||
|
||||
if (!accessToken || accessToken.length === 0) {
|
||||
throw new Error("Access token required");
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
throw new Error("Missing manifest data");
|
||||
}
|
||||
|
||||
if (!data.name || data.name.length === 0) {
|
||||
throw new Error("Package name required");
|
||||
}
|
||||
|
||||
if (!data.version || data.version.length === 0) {
|
||||
throw new Error("Package version required");
|
||||
}
|
||||
|
||||
if (!data.description || data.description.length === 0) {
|
||||
throw new Error("Package description required");
|
||||
}
|
||||
|
||||
if (!data.releasenotes || data.releasenotes.length === 0) {
|
||||
throw new Error("Package release notes link required");
|
||||
}
|
||||
|
||||
if (!data.files || data.files.length === 0) {
|
||||
throw new Error("Package files required");
|
||||
}
|
||||
|
||||
data.files.forEach((file) => validateFile(file, fileKeys));
|
||||
};
|
||||
|
||||
const validateFile = (file, keys) => {
|
||||
if (!file) {
|
||||
throw new Error("File data required");
|
||||
}
|
||||
|
||||
if (!file.filename || file.filename.length === 0) {
|
||||
throw new Error("Filename required");
|
||||
}
|
||||
|
||||
if (!file.name || file.name.length === 0) {
|
||||
throw new Error(`${file.filename} ECU name required`);
|
||||
}
|
||||
|
||||
if (!file.update_version || file.update_version.length === 0) {
|
||||
throw new Error(`${file.filename} version required`);
|
||||
}
|
||||
|
||||
if (!file.part_number || file.part_number.length === 0) {
|
||||
throw new Error(`${file.filename} part number required`);
|
||||
}
|
||||
|
||||
const key = `${file.name}, ${file.update_version}, ${file.filename}`;
|
||||
if (!keys[key]) {
|
||||
keys[key] = true;
|
||||
} else {
|
||||
throw new Error(`${key} already exists`);
|
||||
}
|
||||
};
|
||||
|
||||
const createManifest = async (data, token) => {
|
||||
let result;
|
||||
|
||||
try {
|
||||
setBusy(true);
|
||||
validateManifest(data, token);
|
||||
setUploadedFiles(data.files);
|
||||
|
||||
result = await api.createManifest(data, token);
|
||||
if (result.error)
|
||||
throw new Error(`Create manifest error. ${result.message}`);
|
||||
|
||||
for (let i = 0, len = data.files.length; i < len; i++) {
|
||||
setUploadFileIndex(i);
|
||||
const resp = await uploadECUFile(result.id, data.files[i], token);
|
||||
if (resp.error)
|
||||
throw new Error(`Upload manifest file error. ${resp.error}`);
|
||||
}
|
||||
} finally {
|
||||
setBusy(false);
|
||||
setUploadFileIndex(0);
|
||||
setUploadedFiles([]);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const cancelUpload = () => {
|
||||
if (cancelUploadToken) cancelUploadToken.cancel();
|
||||
setBusy(false);
|
||||
setUploadStatus("Upload cancelled");
|
||||
setCancelUploadToken(null);
|
||||
setUploadProgress(0);
|
||||
};
|
||||
|
||||
const uploadECUFile = async (manifest_id, data, accessToken) => {
|
||||
try {
|
||||
Object.assign(data, { manifest_id });
|
||||
|
||||
const filename = data.file.name;
|
||||
const cancel = getCancelToken();
|
||||
|
||||
setBusy(true);
|
||||
setUploadProgress(0);
|
||||
setUploadStatus(`Uploading ${filename}`);
|
||||
setCancelUploadToken(cancel);
|
||||
|
||||
const result = await uploadFile(
|
||||
data,
|
||||
accessToken,
|
||||
setUploadProgress,
|
||||
cancel.token
|
||||
);
|
||||
if (result.message) {
|
||||
throw new Error(`${result.error}. ${result.message}`);
|
||||
}
|
||||
setUploadStatus(`Uploaded ${filename}`);
|
||||
setCancelUploadToken(null);
|
||||
setUploadProgress(100);
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
setBusy(false);
|
||||
setUploadStatus(`Error occured: ${e.message}`);
|
||||
setUploadProgress(-1);
|
||||
|
||||
return { error: e.message };
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ManifestsContext.Provider
|
||||
value={{
|
||||
busy,
|
||||
uploadProgress,
|
||||
uploadStatus,
|
||||
uploadFileIndex,
|
||||
uploadedFiles,
|
||||
manifests,
|
||||
totalManifests,
|
||||
getManifests,
|
||||
deleteManifest,
|
||||
createManifest,
|
||||
cancelUpload,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
45
src/components/Controls/SubList/index.jsx
Normal file
45
src/components/Controls/SubList/index.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@material-ui/core";
|
||||
import React from "react";
|
||||
import SubListItem from "../SubListItem";
|
||||
|
||||
const SubList = ({ data, options, onChange }) => {
|
||||
const onDelete = (id) => {
|
||||
if (!onChange) return;
|
||||
data.some((item, index) => {
|
||||
if (item.data_id !== id) return false;
|
||||
data.splice(index, 1);
|
||||
onChange(data);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{options.map((option) => (
|
||||
<TableCell key={option.label || "none"}>{option.label}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((item) => (
|
||||
<SubListItem
|
||||
key={item.data_id}
|
||||
data={item}
|
||||
options={options}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubList;
|
||||
58
src/components/Controls/SubListItem/index.jsx
Normal file
58
src/components/Controls/SubListItem/index.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
import { TableCell, TableRow, TextField } from "@material-ui/core";
|
||||
import { Link } from "react-router-dom";
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import { useState } from "react";
|
||||
|
||||
const DataDisplay = ({ data, option, onDelete }) => {
|
||||
const [text, setText] = useState(data[option.field]);
|
||||
const onChange = (e) => {
|
||||
data[e.target.id] = e.target.value;
|
||||
setText(e.target.value);
|
||||
};
|
||||
const deleteHandler = (id) => {
|
||||
if (onDelete) onDelete(id);
|
||||
};
|
||||
|
||||
if (option.readonly) {
|
||||
return `${data[option.field]}`;
|
||||
} else if (option.delete) {
|
||||
return (
|
||||
<Link
|
||||
to="#"
|
||||
onClick={() => {
|
||||
deleteHandler(data.data_id);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextField
|
||||
id={option.field}
|
||||
name={option.field}
|
||||
placeholder={option.label}
|
||||
inputProps={option.inputProps}
|
||||
requried={option.required}
|
||||
fullWidth
|
||||
onChange={onChange}
|
||||
value={text}
|
||||
></TextField>
|
||||
);
|
||||
};
|
||||
|
||||
const SubListItem = ({ data, options, onDelete }) => {
|
||||
return (
|
||||
<TableRow>
|
||||
{options.map((item) => (
|
||||
<TableCell key={item.label} width={item.delete ? 25 : null}>
|
||||
<DataDisplay data={data} option={item} onDelete={onDelete} />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubListItem;
|
||||
@@ -18,19 +18,14 @@ const menuData = [
|
||||
},
|
||||
{
|
||||
label: "Deploy Packages",
|
||||
to: "/updates",
|
||||
to: "/packages",
|
||||
roles: [Roles.CREATE, Roles.READ],
|
||||
},
|
||||
{
|
||||
label: "Create Package",
|
||||
to: "/package-upload",
|
||||
to: "/package-create",
|
||||
roles: [Roles.CREATE],
|
||||
},
|
||||
{
|
||||
label: "Deploy Manifest",
|
||||
to: "/manifests",
|
||||
roles: [Roles.CREATE, Roles.READ],
|
||||
},
|
||||
{
|
||||
label: "View Vehicles",
|
||||
to: "/vehicles",
|
||||
@@ -45,7 +40,7 @@ const menuData = [
|
||||
label: "Send Command",
|
||||
to: "/vehicles-command",
|
||||
roles: [Roles.CREATE],
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
export default function SideMenu() {
|
||||
|
||||
@@ -56,7 +56,7 @@ exports[`SideMenu Authenticated 1`] = `
|
||||
<a
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
||||
href="/updates"
|
||||
href="/packages"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
@@ -78,7 +78,7 @@ exports[`SideMenu Authenticated 1`] = `
|
||||
<a
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
||||
href="/package-upload"
|
||||
href="/package-create"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
@@ -96,28 +96,6 @@ exports[`SideMenu Authenticated 1`] = `
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
||||
href="/manifests"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
Deploy Manifest
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
aria-disabled="false"
|
||||
|
||||
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;
|
||||
@@ -8,10 +8,8 @@ import { Roles } from "../../utils/roles";
|
||||
|
||||
const SSOForm = React.lazy(() => import("../SSOForm"));
|
||||
const Home = React.lazy(() => import("../Home"));
|
||||
const FileUploadForm = React.lazy(() => import("../UpdatePackages/Create"));
|
||||
const VehicleAddForm = React.lazy(() => import("../Cars/Add"));
|
||||
const PageNotFound = React.lazy(() => import("../404"));
|
||||
const UpdatePackagesForm = React.lazy(() => import("../UpdatePackages/List"));
|
||||
const UpdatePackageEdit = React.lazy(() => import("../UpdatePackages/Edit"));
|
||||
const CarUpdatesDeploy = React.lazy(() => import("../CarUpdates/Deploy"));
|
||||
const CarUpdatesStatus = React.lazy(() => import("../CarUpdates/Status"));
|
||||
@@ -22,6 +20,7 @@ const Dashboard = React.lazy(() => import("../Dashboard"));
|
||||
const Manifests = React.lazy(() => import("../Manifest/List"));
|
||||
const ManifestDeploy = React.lazy(() => import("../Manifest/Deploy"));
|
||||
const ManifestStatus = React.lazy(() => import("../Manifest/Status"));
|
||||
const ManifestCreate = React.lazy(() => import("../Manifest/Create"));
|
||||
|
||||
const SiteRoutes = () => {
|
||||
const { token, groups } = useUserContext();
|
||||
@@ -42,22 +41,6 @@ const SiteRoutes = () => {
|
||||
type={TYPES.PROTECTED}
|
||||
token={token}
|
||||
/>
|
||||
<AuthRoute
|
||||
path="/package-upload"
|
||||
render={() => <FileUploadForm />}
|
||||
type={TYPES.PROTECTED}
|
||||
token={token}
|
||||
groups={groups}
|
||||
roles={[Roles.CREATE]}
|
||||
/>
|
||||
<AuthRoute
|
||||
path="/updates"
|
||||
render={() => <UpdatePackagesForm />}
|
||||
type={TYPES.PROTECTED}
|
||||
token={token}
|
||||
groups={groups}
|
||||
roles={[Roles.CREATE]}
|
||||
/>
|
||||
<AuthRoute
|
||||
path="/update/:id"
|
||||
render={() => <UpdatePackageEdit />}
|
||||
@@ -123,7 +106,7 @@ const SiteRoutes = () => {
|
||||
roles={[Roles.READ, Roles.CREATE]}
|
||||
/>
|
||||
<AuthRoute
|
||||
path="/manifests"
|
||||
path="/packages"
|
||||
render={() => <Manifests />}
|
||||
type={TYPES.PROTECTED}
|
||||
token={token}
|
||||
@@ -131,7 +114,7 @@ const SiteRoutes = () => {
|
||||
roles={[Roles.READ, Roles.CREATE]}
|
||||
/>
|
||||
<AuthRoute
|
||||
path="/manifest-deploy/:manifest_id"
|
||||
path="/package-deploy/:manifest_id"
|
||||
render={() => <ManifestDeploy />}
|
||||
type={TYPES.PROTECTED}
|
||||
token={token}
|
||||
@@ -139,13 +122,21 @@ const SiteRoutes = () => {
|
||||
roles={[Roles.CREATE]}
|
||||
/>
|
||||
<AuthRoute
|
||||
path="/manifest-status/:manifest_id"
|
||||
path="/package-status/:manifest_id"
|
||||
render={() => <ManifestStatus />}
|
||||
type={TYPES.PROTECTED}
|
||||
token={token}
|
||||
groups={groups}
|
||||
roles={[Roles.READ, Roles.CREATE]}
|
||||
/>
|
||||
<AuthRoute
|
||||
path="/package-create"
|
||||
render={() => <ManifestCreate />}
|
||||
type={TYPES.PROTECTED}
|
||||
token={token}
|
||||
groups={groups}
|
||||
roles={[Roles.CREATE]}
|
||||
/>
|
||||
<PageNotFound />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
|
||||
@@ -46,10 +46,10 @@ exports[`File Upload Form Should render 1`] = `
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-39 MuiOutlinedInput-notchedOutline"
|
||||
class="PrivateNotchedOutline-root-40 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-41"
|
||||
class="PrivateNotchedOutline-legendLabelled-42"
|
||||
>
|
||||
<span>
|
||||
Package name
|
||||
@@ -92,10 +92,10 @@ exports[`File Upload Form Should render 1`] = `
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-39 MuiOutlinedInput-notchedOutline"
|
||||
class="PrivateNotchedOutline-root-40 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-41"
|
||||
class="PrivateNotchedOutline-legendLabelled-42"
|
||||
>
|
||||
<span>
|
||||
Version
|
||||
@@ -138,10 +138,10 @@ exports[`File Upload Form Should render 1`] = `
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-39 MuiOutlinedInput-notchedOutline"
|
||||
class="PrivateNotchedOutline-root-40 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-41"
|
||||
class="PrivateNotchedOutline-legendLabelled-42"
|
||||
>
|
||||
<span>
|
||||
Description
|
||||
@@ -185,10 +185,10 @@ exports[`File Upload Form Should render 1`] = `
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-39 MuiOutlinedInput-notchedOutline"
|
||||
class="PrivateNotchedOutline-root-40 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-41"
|
||||
class="PrivateNotchedOutline-legendLabelled-42"
|
||||
>
|
||||
<span>
|
||||
Release Notes URL
|
||||
|
||||
@@ -174,7 +174,7 @@ const useStyles = makeStyles((theme) => ({
|
||||
paddingTop: "56.25%",
|
||||
},
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
position: "absolute",
|
||||
right: theme.spacing(1),
|
||||
top: theme.spacing(1),
|
||||
color: theme.palette.grey[500],
|
||||
@@ -198,6 +198,10 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
paddingBottom: "2vh",
|
||||
},
|
||||
toolbarFooter: {
|
||||
width: "100%",
|
||||
textAlign: "right",
|
||||
},
|
||||
}));
|
||||
|
||||
export default useStyles;
|
||||
|
||||
@@ -18,6 +18,13 @@ const manifestsAPI = {
|
||||
})
|
||||
.then(fetchRespHandler);
|
||||
},
|
||||
|
||||
createManifest: async (data, token) => fetch(`${API_ENDPOINT}/manifest`, {
|
||||
method: "POST",
|
||||
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(fetchRespHandler),
|
||||
};
|
||||
|
||||
export default manifestsAPI;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const UPLOAD_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL || "https://gw-dev.fiskerdps.com/ota_update";
|
||||
const fileField = "file";
|
||||
|
||||
export const getCancelToken = () => {
|
||||
const token = axios.CancelToken;
|
||||
return token.source();
|
||||
}
|
||||
|
||||
export const uploadFile = (file, data, token, onProgress, cancelToken) => {
|
||||
export const uploadFile = (data, token, onProgress, cancelToken) => {
|
||||
const form = new FormData();
|
||||
let options = {
|
||||
method: "POST",
|
||||
@@ -17,19 +18,23 @@ export const uploadFile = (file, data, token, onProgress, cancelToken) => {
|
||||
},
|
||||
cancelToken,
|
||||
};
|
||||
|
||||
if (onProgress) {
|
||||
options = {
|
||||
...options,
|
||||
onUploadProgress: (event) => {
|
||||
onProgress(Math.min(99, Math.floor((event.loaded / event.total) * 100)));
|
||||
onProgress(event.loaded / event.total);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let key in data) {
|
||||
form.append(key, data[key]);
|
||||
if (key !== fileField) form.append(key, data[key]);
|
||||
}
|
||||
form.append("file", file);
|
||||
return axios.post(`${UPLOAD_ENDPOINT}/update`, form, options)
|
||||
|
||||
form.append(fileField, data[fileField]);
|
||||
|
||||
return axios.post(`${UPLOAD_ENDPOINT}/manifestfile`, form, options)
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
if (typeof error.response.data === "string") {
|
||||
|
||||
Reference in New Issue
Block a user