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

32
package-lock.json generated
View File

@@ -1275,9 +1275,9 @@
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
},
"@datadog/browser-core": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/@datadog/browser-core/-/browser-core-2.17.0.tgz",
"integrity": "sha512-jGIiVIzxdfQ+FnuFY+tC/j4gBFmK74xW/NIVqHIdN/hbGqA0oXOYs3Qof39ILLhXM5+zxa44RYFY6r/9JaxMIg==",
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/@datadog/browser-core/-/browser-core-2.18.0.tgz",
"integrity": "sha512-1RvxLK8TiuAaDrwkrlOg7wM+7FilJtNbC30h5BxoGChWEBB7QsgeYGnliQ60byZUCzhbvARVzHHNZTxUiP+fPQ==",
"requires": {
"tslib": "^1.10.0"
},
@@ -1290,11 +1290,11 @@
}
},
"@datadog/browser-logs": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/@datadog/browser-logs/-/browser-logs-2.17.0.tgz",
"integrity": "sha512-2S6ryflc28EErDJ+SgWo2OdkvWQ5KA5uqzzvbcnEBeFQpAV5ukAIfElHLiQrwSF4J6NkfLFA3tLt6KPGZE5F2w==",
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/@datadog/browser-logs/-/browser-logs-2.18.0.tgz",
"integrity": "sha512-bDT5YkPNHGZmjADXtsVtwSyL+/J7MA4k2mBcHzutXK7/tKrIRiKH6ygHiBRryNHfD7/Q79tZqJzi32kSZy3AAA==",
"requires": {
"@datadog/browser-core": "2.17.0",
"@datadog/browser-core": "2.18.0",
"tslib": "^1.10.0"
},
"dependencies": {
@@ -1776,9 +1776,9 @@
}
},
"@material-ui/core": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.1.tgz",
"integrity": "sha512-C6hYsjkWCTfBx9FaqxhCZCITBagh7fyCKFtHyvO3tTOcBw6NJaktdhNZ2n82jQdQdgfFvg6OOxi7OOzsAdAcBQ==",
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz",
"integrity": "sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw==",
"requires": {
"@babel/runtime": "^7.4.4",
"@material-ui/styles": "^4.11.4",
@@ -2305,9 +2305,9 @@
"integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug=="
},
"@types/react": {
"version": "17.0.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.14.tgz",
"integrity": "sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==",
"version": "17.0.15",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.15.tgz",
"integrity": "sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw==",
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -12164,9 +12164,9 @@
"integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA=="
},
"react-leaflet": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-3.2.0.tgz",
"integrity": "sha512-eHVqoRGjW8T9GxLt7jyTKP3BDQ7XQ5AD+tc/zkbaABn1dbmREDy8GojNcYjZQa3QFLQoOLQMcUC1PTtzytZpUA==",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-3.2.1.tgz",
"integrity": "sha512-3iS1fpOO+uaRpbuq68Euw9kgaoM9oIGBiDfeFtVb/C9PWBQvXdrv1n946Z8GrbQEhrT+hM9ND6NLLF9fGxTGRw==",
"requires": {
"@react-leaflet/core": "^1.1.0"
}

View File

@@ -3,9 +3,9 @@
"version": "0.1.1",
"private": true,
"dependencies": {
"@datadog/browser-logs": "^2.17.0",
"@datadog/browser-logs": "^2.18.0",
"@datadog/browser-rum": "^2.17.0",
"@material-ui/core": "^4.12.1",
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
@@ -16,7 +16,7 @@
"material-ui-dropzone": "^3.5.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-leaflet": "^3.2.0",
"react-leaflet": "^3.2.1",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"web-vitals": "^0.2.4"

View File

@@ -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

View File

@@ -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}

View 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;

View 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;

View File

@@ -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() {

View File

@@ -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"

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;

View File

@@ -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>

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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") {