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:
32
package-lock.json
generated
32
package-lock.json
generated
@@ -1275,9 +1275,9 @@
|
|||||||
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
|
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
|
||||||
},
|
},
|
||||||
"@datadog/browser-core": {
|
"@datadog/browser-core": {
|
||||||
"version": "2.17.0",
|
"version": "2.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@datadog/browser-core/-/browser-core-2.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@datadog/browser-core/-/browser-core-2.18.0.tgz",
|
||||||
"integrity": "sha512-jGIiVIzxdfQ+FnuFY+tC/j4gBFmK74xW/NIVqHIdN/hbGqA0oXOYs3Qof39ILLhXM5+zxa44RYFY6r/9JaxMIg==",
|
"integrity": "sha512-1RvxLK8TiuAaDrwkrlOg7wM+7FilJtNbC30h5BxoGChWEBB7QsgeYGnliQ60byZUCzhbvARVzHHNZTxUiP+fPQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tslib": "^1.10.0"
|
"tslib": "^1.10.0"
|
||||||
},
|
},
|
||||||
@@ -1290,11 +1290,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@datadog/browser-logs": {
|
"@datadog/browser-logs": {
|
||||||
"version": "2.17.0",
|
"version": "2.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@datadog/browser-logs/-/browser-logs-2.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@datadog/browser-logs/-/browser-logs-2.18.0.tgz",
|
||||||
"integrity": "sha512-2S6ryflc28EErDJ+SgWo2OdkvWQ5KA5uqzzvbcnEBeFQpAV5ukAIfElHLiQrwSF4J6NkfLFA3tLt6KPGZE5F2w==",
|
"integrity": "sha512-bDT5YkPNHGZmjADXtsVtwSyL+/J7MA4k2mBcHzutXK7/tKrIRiKH6ygHiBRryNHfD7/Q79tZqJzi32kSZy3AAA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@datadog/browser-core": "2.17.0",
|
"@datadog/browser-core": "2.18.0",
|
||||||
"tslib": "^1.10.0"
|
"tslib": "^1.10.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1776,9 +1776,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@material-ui/core": {
|
"@material-ui/core": {
|
||||||
"version": "4.12.1",
|
"version": "4.12.3",
|
||||||
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz",
|
||||||
"integrity": "sha512-C6hYsjkWCTfBx9FaqxhCZCITBagh7fyCKFtHyvO3tTOcBw6NJaktdhNZ2n82jQdQdgfFvg6OOxi7OOzsAdAcBQ==",
|
"integrity": "sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.4.4",
|
"@babel/runtime": "^7.4.4",
|
||||||
"@material-ui/styles": "^4.11.4",
|
"@material-ui/styles": "^4.11.4",
|
||||||
@@ -2305,9 +2305,9 @@
|
|||||||
"integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug=="
|
"integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug=="
|
||||||
},
|
},
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
"version": "17.0.14",
|
"version": "17.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.15.tgz",
|
||||||
"integrity": "sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==",
|
"integrity": "sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"@types/scheduler": "*",
|
"@types/scheduler": "*",
|
||||||
@@ -12164,9 +12164,9 @@
|
|||||||
"integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA=="
|
"integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA=="
|
||||||
},
|
},
|
||||||
"react-leaflet": {
|
"react-leaflet": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-3.2.1.tgz",
|
||||||
"integrity": "sha512-eHVqoRGjW8T9GxLt7jyTKP3BDQ7XQ5AD+tc/zkbaABn1dbmREDy8GojNcYjZQa3QFLQoOLQMcUC1PTtzytZpUA==",
|
"integrity": "sha512-3iS1fpOO+uaRpbuq68Euw9kgaoM9oIGBiDfeFtVb/C9PWBQvXdrv1n946Z8GrbQEhrT+hM9ND6NLLF9fGxTGRw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@react-leaflet/core": "^1.1.0"
|
"@react-leaflet/core": "^1.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@datadog/browser-logs": "^2.17.0",
|
"@datadog/browser-logs": "^2.18.0",
|
||||||
"@datadog/browser-rum": "^2.17.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",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^11.2.7",
|
"@testing-library/react": "^11.2.7",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"material-ui-dropzone": "^3.5.0",
|
"material-ui-dropzone": "^3.5.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^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-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"web-vitals": "^0.2.4"
|
"web-vitals": "^0.2.4"
|
||||||
|
|||||||
@@ -64,18 +64,10 @@ describe("App", () => {
|
|||||||
await check("/home", "span.MuiButton-label", "Sign In");
|
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 () => {
|
it("Route /vehicle-add unauthenticated", async () => {
|
||||||
await check("/vehicle-add", "span.MuiButton-label", "Sign In");
|
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 () => {
|
it("Route /carupdate-deploy unauthenticated", async () => {
|
||||||
await check("/carupdate-deploy/1", "span.MuiButton-label", "Sign In");
|
await check("/carupdate-deploy/1", "span.MuiButton-label", "Sign In");
|
||||||
});
|
});
|
||||||
@@ -100,16 +92,20 @@ describe("App", () => {
|
|||||||
await check("/dashboard", "span.MuiButton-label", "Sign In");
|
await check("/dashboard", "span.MuiButton-label", "Sign In");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Route /manifests unauthenticated", async () => {
|
it("Route /packages unauthenticated", async () => {
|
||||||
await check("/manifests", "span.MuiButton-label", "Sign In");
|
await check("/packages", "span.MuiButton-label", "Sign In");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Route /manifest-status unauthenticated", async () => {
|
it("Route /package-status unauthenticated", async () => {
|
||||||
await check("/manifest-status/1", "span.MuiButton-label", "Sign In");
|
await check("/package-status/1", "span.MuiButton-label", "Sign In");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Route /manifest-deploy unauthenticated", async () => {
|
it("Route /package-deploy unauthenticated", async () => {
|
||||||
await check("/manifest-deploy/1", "span.MuiButton-label", "Sign In");
|
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 () => {
|
it("Route / authenticated", async () => {
|
||||||
@@ -122,21 +118,11 @@ describe("App", () => {
|
|||||||
await sleepAndCheck("/home", "h1", "Welcome John!");
|
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 () => {
|
it("Route /vehicle-add authenticated", async () => {
|
||||||
setToken(TEST_AUTH_OBJECT);
|
setToken(TEST_AUTH_OBJECT);
|
||||||
await check("/vehicle-add", "h6", "Add Vehicle");
|
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 () => {
|
it("Route /carupdate-status authenticated", async () => {
|
||||||
setToken(TEST_AUTH_OBJECT);
|
setToken(TEST_AUTH_OBJECT);
|
||||||
await check("/carupdate-status/1", "h6", "Package Package 1.0");
|
await check("/carupdate-status/1", "h6", "Package Package 1.0");
|
||||||
@@ -176,18 +162,24 @@ describe("App", () => {
|
|||||||
await check("/dashboard", "h6", "Dashboard");
|
await check("/dashboard", "h6", "Dashboard");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Route /manifests authenticated", async () => {
|
it("Route /packages authenticated", async () => {
|
||||||
setToken(TEST_AUTH_OBJECT);
|
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);
|
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);
|
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 React, { useContext, useState } from "react";
|
||||||
|
|
||||||
import api from "../../services/manifests";
|
import api from "../../services/manifests";
|
||||||
|
import { uploadFile, getCancelToken } from "../../services/uploadFile";
|
||||||
|
|
||||||
const ManifestsContext = React.createContext();
|
const ManifestsContext = React.createContext();
|
||||||
|
|
||||||
export const ManifestsProvider = ({ children }) => {
|
export const ManifestsProvider = ({ children }) => {
|
||||||
const [busy, setBusy] = useState(false);
|
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 [manifests, setManifests] = useState([]);
|
||||||
const [totalManifests, setTotalManifests] = useState(0);
|
const [totalManifests, setTotalManifests] = useState(0);
|
||||||
|
|
||||||
@@ -48,14 +54,153 @@ export const ManifestsProvider = ({ children }) => {
|
|||||||
return result;
|
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 (
|
return (
|
||||||
<ManifestsContext.Provider
|
<ManifestsContext.Provider
|
||||||
value={{
|
value={{
|
||||||
busy,
|
busy,
|
||||||
|
uploadProgress,
|
||||||
|
uploadStatus,
|
||||||
|
uploadFileIndex,
|
||||||
|
uploadedFiles,
|
||||||
manifests,
|
manifests,
|
||||||
totalManifests,
|
totalManifests,
|
||||||
getManifests,
|
getManifests,
|
||||||
deleteManifest,
|
deleteManifest,
|
||||||
|
createManifest,
|
||||||
|
cancelUpload,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{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",
|
label: "Deploy Packages",
|
||||||
to: "/updates",
|
to: "/packages",
|
||||||
roles: [Roles.CREATE, Roles.READ],
|
roles: [Roles.CREATE, Roles.READ],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Create Package",
|
label: "Create Package",
|
||||||
to: "/package-upload",
|
to: "/package-create",
|
||||||
roles: [Roles.CREATE],
|
roles: [Roles.CREATE],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "Deploy Manifest",
|
|
||||||
to: "/manifests",
|
|
||||||
roles: [Roles.CREATE, Roles.READ],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "View Vehicles",
|
label: "View Vehicles",
|
||||||
to: "/vehicles",
|
to: "/vehicles",
|
||||||
@@ -45,7 +40,7 @@ const menuData = [
|
|||||||
label: "Send Command",
|
label: "Send Command",
|
||||||
to: "/vehicles-command",
|
to: "/vehicles-command",
|
||||||
roles: [Roles.CREATE],
|
roles: [Roles.CREATE],
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function SideMenu() {
|
export default function SideMenu() {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ exports[`SideMenu Authenticated 1`] = `
|
|||||||
<a
|
<a
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
||||||
href="/updates"
|
href="/packages"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
@@ -78,7 +78,7 @@ exports[`SideMenu Authenticated 1`] = `
|
|||||||
<a
|
<a
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
||||||
href="/package-upload"
|
href="/package-create"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
@@ -96,28 +96,6 @@ exports[`SideMenu Authenticated 1`] = `
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
<li>
|
||||||
<a
|
<a
|
||||||
aria-disabled="false"
|
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(
|
setMessage(
|
||||||
`Deployed ${manifestName} ${version} to ${selected.length} cars`
|
`Deployed ${manifestName} ${version} to ${selected.length} cars`
|
||||||
);
|
);
|
||||||
setRedirect(`/manifest-status/${manifest_id}`);
|
setRedirect(`/package-status/${manifest_id}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setMessage(e.message);
|
setMessage(e.message);
|
||||||
logger.warn(e.stack);
|
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(() => {
|
useEffect(() => {
|
||||||
setTitle("Deploy Manifest");
|
setTitle("Deploy Packages");
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ const MainForm = () => {
|
|||||||
if (hasRole([Roles.CREATE, Roles.READ], groups)) {
|
if (hasRole([Roles.CREATE, Roles.READ], groups)) {
|
||||||
actions.push({
|
actions.push({
|
||||||
tip: `Status "${row.name} ${row.version}"`,
|
tip: `Status "${row.name} ${row.version}"`,
|
||||||
link: `/manifest-status/${row.id}`,
|
link: `/package-status/${row.id}`,
|
||||||
icon: (
|
icon: (
|
||||||
<VisibilityIcon aria-label={`Status ${row.name} ${row.version}`} />
|
<VisibilityIcon aria-label={`Status ${row.name} ${row.version}`} />
|
||||||
),
|
),
|
||||||
@@ -147,7 +147,7 @@ const MainForm = () => {
|
|||||||
actions = actions.concat([
|
actions = actions.concat([
|
||||||
{
|
{
|
||||||
tip: `Deploy "${row.name} ${row.version}"`,
|
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}`} />,
|
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 SSOForm = React.lazy(() => import("../SSOForm"));
|
||||||
const Home = React.lazy(() => import("../Home"));
|
const Home = React.lazy(() => import("../Home"));
|
||||||
const FileUploadForm = React.lazy(() => import("../UpdatePackages/Create"));
|
|
||||||
const VehicleAddForm = React.lazy(() => import("../Cars/Add"));
|
const VehicleAddForm = React.lazy(() => import("../Cars/Add"));
|
||||||
const PageNotFound = React.lazy(() => import("../404"));
|
const PageNotFound = React.lazy(() => import("../404"));
|
||||||
const UpdatePackagesForm = React.lazy(() => import("../UpdatePackages/List"));
|
|
||||||
const UpdatePackageEdit = React.lazy(() => import("../UpdatePackages/Edit"));
|
const UpdatePackageEdit = React.lazy(() => import("../UpdatePackages/Edit"));
|
||||||
const CarUpdatesDeploy = React.lazy(() => import("../CarUpdates/Deploy"));
|
const CarUpdatesDeploy = React.lazy(() => import("../CarUpdates/Deploy"));
|
||||||
const CarUpdatesStatus = React.lazy(() => import("../CarUpdates/Status"));
|
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 Manifests = React.lazy(() => import("../Manifest/List"));
|
||||||
const ManifestDeploy = React.lazy(() => import("../Manifest/Deploy"));
|
const ManifestDeploy = React.lazy(() => import("../Manifest/Deploy"));
|
||||||
const ManifestStatus = React.lazy(() => import("../Manifest/Status"));
|
const ManifestStatus = React.lazy(() => import("../Manifest/Status"));
|
||||||
|
const ManifestCreate = React.lazy(() => import("../Manifest/Create"));
|
||||||
|
|
||||||
const SiteRoutes = () => {
|
const SiteRoutes = () => {
|
||||||
const { token, groups } = useUserContext();
|
const { token, groups } = useUserContext();
|
||||||
@@ -42,22 +41,6 @@ const SiteRoutes = () => {
|
|||||||
type={TYPES.PROTECTED}
|
type={TYPES.PROTECTED}
|
||||||
token={token}
|
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
|
<AuthRoute
|
||||||
path="/update/:id"
|
path="/update/:id"
|
||||||
render={() => <UpdatePackageEdit />}
|
render={() => <UpdatePackageEdit />}
|
||||||
@@ -123,7 +106,7 @@ const SiteRoutes = () => {
|
|||||||
roles={[Roles.READ, Roles.CREATE]}
|
roles={[Roles.READ, Roles.CREATE]}
|
||||||
/>
|
/>
|
||||||
<AuthRoute
|
<AuthRoute
|
||||||
path="/manifests"
|
path="/packages"
|
||||||
render={() => <Manifests />}
|
render={() => <Manifests />}
|
||||||
type={TYPES.PROTECTED}
|
type={TYPES.PROTECTED}
|
||||||
token={token}
|
token={token}
|
||||||
@@ -131,7 +114,7 @@ const SiteRoutes = () => {
|
|||||||
roles={[Roles.READ, Roles.CREATE]}
|
roles={[Roles.READ, Roles.CREATE]}
|
||||||
/>
|
/>
|
||||||
<AuthRoute
|
<AuthRoute
|
||||||
path="/manifest-deploy/:manifest_id"
|
path="/package-deploy/:manifest_id"
|
||||||
render={() => <ManifestDeploy />}
|
render={() => <ManifestDeploy />}
|
||||||
type={TYPES.PROTECTED}
|
type={TYPES.PROTECTED}
|
||||||
token={token}
|
token={token}
|
||||||
@@ -139,13 +122,21 @@ const SiteRoutes = () => {
|
|||||||
roles={[Roles.CREATE]}
|
roles={[Roles.CREATE]}
|
||||||
/>
|
/>
|
||||||
<AuthRoute
|
<AuthRoute
|
||||||
path="/manifest-status/:manifest_id"
|
path="/package-status/:manifest_id"
|
||||||
render={() => <ManifestStatus />}
|
render={() => <ManifestStatus />}
|
||||||
type={TYPES.PROTECTED}
|
type={TYPES.PROTECTED}
|
||||||
token={token}
|
token={token}
|
||||||
groups={groups}
|
groups={groups}
|
||||||
roles={[Roles.READ, Roles.CREATE]}
|
roles={[Roles.READ, Roles.CREATE]}
|
||||||
/>
|
/>
|
||||||
|
<AuthRoute
|
||||||
|
path="/package-create"
|
||||||
|
render={() => <ManifestCreate />}
|
||||||
|
type={TYPES.PROTECTED}
|
||||||
|
token={token}
|
||||||
|
groups={groups}
|
||||||
|
roles={[Roles.CREATE]}
|
||||||
|
/>
|
||||||
<PageNotFound />
|
<PageNotFound />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -46,10 +46,10 @@ exports[`File Upload Form Should render 1`] = `
|
|||||||
/>
|
/>
|
||||||
<fieldset
|
<fieldset
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="PrivateNotchedOutline-root-39 MuiOutlinedInput-notchedOutline"
|
class="PrivateNotchedOutline-root-40 MuiOutlinedInput-notchedOutline"
|
||||||
>
|
>
|
||||||
<legend
|
<legend
|
||||||
class="PrivateNotchedOutline-legendLabelled-41"
|
class="PrivateNotchedOutline-legendLabelled-42"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Package name
|
Package name
|
||||||
@@ -92,10 +92,10 @@ exports[`File Upload Form Should render 1`] = `
|
|||||||
/>
|
/>
|
||||||
<fieldset
|
<fieldset
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="PrivateNotchedOutline-root-39 MuiOutlinedInput-notchedOutline"
|
class="PrivateNotchedOutline-root-40 MuiOutlinedInput-notchedOutline"
|
||||||
>
|
>
|
||||||
<legend
|
<legend
|
||||||
class="PrivateNotchedOutline-legendLabelled-41"
|
class="PrivateNotchedOutline-legendLabelled-42"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Version
|
Version
|
||||||
@@ -138,10 +138,10 @@ exports[`File Upload Form Should render 1`] = `
|
|||||||
/>
|
/>
|
||||||
<fieldset
|
<fieldset
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="PrivateNotchedOutline-root-39 MuiOutlinedInput-notchedOutline"
|
class="PrivateNotchedOutline-root-40 MuiOutlinedInput-notchedOutline"
|
||||||
>
|
>
|
||||||
<legend
|
<legend
|
||||||
class="PrivateNotchedOutline-legendLabelled-41"
|
class="PrivateNotchedOutline-legendLabelled-42"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Description
|
Description
|
||||||
@@ -185,10 +185,10 @@ exports[`File Upload Form Should render 1`] = `
|
|||||||
/>
|
/>
|
||||||
<fieldset
|
<fieldset
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="PrivateNotchedOutline-root-39 MuiOutlinedInput-notchedOutline"
|
class="PrivateNotchedOutline-root-40 MuiOutlinedInput-notchedOutline"
|
||||||
>
|
>
|
||||||
<legend
|
<legend
|
||||||
class="PrivateNotchedOutline-legendLabelled-41"
|
class="PrivateNotchedOutline-legendLabelled-42"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Release Notes URL
|
Release Notes URL
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
paddingTop: "56.25%",
|
paddingTop: "56.25%",
|
||||||
},
|
},
|
||||||
closeButton: {
|
closeButton: {
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
right: theme.spacing(1),
|
right: theme.spacing(1),
|
||||||
top: theme.spacing(1),
|
top: theme.spacing(1),
|
||||||
color: theme.palette.grey[500],
|
color: theme.palette.grey[500],
|
||||||
@@ -198,6 +198,10 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
},
|
},
|
||||||
paddingBottom: "2vh",
|
paddingBottom: "2vh",
|
||||||
},
|
},
|
||||||
|
toolbarFooter: {
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "right",
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default useStyles;
|
export default useStyles;
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ const manifestsAPI = {
|
|||||||
})
|
})
|
||||||
.then(fetchRespHandler);
|
.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;
|
export default manifestsAPI;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const UPLOAD_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL || "https://gw-dev.fiskerdps.com/ota_update";
|
const UPLOAD_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL || "https://gw-dev.fiskerdps.com/ota_update";
|
||||||
|
const fileField = "file";
|
||||||
|
|
||||||
export const getCancelToken = () => {
|
export const getCancelToken = () => {
|
||||||
const token = axios.CancelToken;
|
const token = axios.CancelToken;
|
||||||
return token.source();
|
return token.source();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const uploadFile = (file, data, token, onProgress, cancelToken) => {
|
export const uploadFile = (data, token, onProgress, cancelToken) => {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
let options = {
|
let options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -17,19 +18,23 @@ export const uploadFile = (file, data, token, onProgress, cancelToken) => {
|
|||||||
},
|
},
|
||||||
cancelToken,
|
cancelToken,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (onProgress) {
|
if (onProgress) {
|
||||||
options = {
|
options = {
|
||||||
...options,
|
...options,
|
||||||
onUploadProgress: (event) => {
|
onUploadProgress: (event) => {
|
||||||
onProgress(Math.min(99, Math.floor((event.loaded / event.total) * 100)));
|
onProgress(event.loaded / event.total);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let key in data) {
|
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)
|
.then((response) => response.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (typeof error.response.data === "string") {
|
if (typeof error.response.data === "string") {
|
||||||
|
|||||||
Reference in New Issue
Block a user