Development (#94)
* CEC-371 Car ECU display (#79) * Merge Development (#53) * Use responsive iframe control for charts (#49) * Use responsive iframe control to charts * Move external Grafana link to Dashboard page * Remove unused embedded style class * Add button label * added delete button to deploy packages * Fix unit test warning Remove unused route from test * Fix styling of button * minor fixes per pr review Co-authored-by: jcw-fisker <jwatson@fiskerinc.com> Co-authored-by: John Cotten Watson <83605808+jcw-fisker@users.noreply.github.com> * Development Merge (#57) * CEC-287 Car connection status (#59) (#60) * Car connection status * Formatting * Merge Development (#64) * Add connection status to vehicles page * ConnectedIcon control * Handle Style * Development (#67) * preliminary map for vehicles * weird zoom bug * passing react tests * fixing warnings and updating snapshots * update node environment to 14 * addressing comments by changing variable types and adding styles to home page title * adding CODEOWNERS file * fixing token error * CEC-371 Update car ECUs display (#78) * Clean up className styles Update car status page to show update and ECUs * Add update ecu version button Show all ECUs on car status page Only show car ecus for search Co-authored-by: jcw-fisker <jwatson@fiskerinc.com> Co-authored-by: John Cotten Watson <83605808+jcw-fisker@users.noreply.github.com> Co-authored-by: Drew Taylor <69828061+drew-fisker@users.noreply.github.com> * CEC-394 Car update log (#81) * CEC-394 Car update status control * Remove Datadog RUM Remove package update components Move control components into Controls folder Add Car update status page * Display update status log Clean up unused update package code * Remove console.logs * no vars * adding timestamp to vehicle popup * modifying vehicle data query * removing extraneous code * removing console log * Clean up SonarCloud warnings (#83) * Clean up SonarCloud warnings * Bogus security warning * Fix another warning * Fix unauthorized locations request * Fix update progress control * CEC-563 New manifest format (#88) * Add ManifestCreateContext Update create manifest page * Finish UI changes and API integration * Fixes * Fix test * Remove manifest ECU file version and type * Fixes * Add manifest ecu file type control * Fix Sonar warnings * Fix test * Update codeowners * Formatting * CEC-553 Change file type to string (#90) * CEC-553 File type uses string enum * Fix test timeout * Fix * Merge development * Increase timeout * Clean up (#95) * Clean up Mock missing methods * Smell Co-authored-by: jcw-fisker <jwatson@fiskerinc.com> Co-authored-by: John Cotten Watson <83605808+jcw-fisker@users.noreply.github.com> Co-authored-by: Drew Taylor <69828061+drew-fisker@users.noreply.github.com> Co-authored-by: Drew Taylor <dtaylor@fiskerinc.com>
This commit is contained in:
10
.github/CODEOWNERS
vendored
10
.github/CODEOWNERS
vendored
@@ -1,8 +1,8 @@
|
|||||||
# default codeowners
|
# default codeowners
|
||||||
* jwu@fiskerinc.com dtaylor@fiskerinc.com ggetsin@fiskerinc.com
|
* jwu@fiskerinc.com dtaylor@fiskerinc.com ggetsin@fiskerinc.com bbaker@fiskerinc.com
|
||||||
|
|
||||||
# devops
|
# devops
|
||||||
.github rgreenberg@fiskerinc.com jwu@fiskerinc.com dtaylor@fiskerinc.com ggetsin@fiskerinc.com
|
.github rgreenberg@fiskerinc.com jwu@fiskerinc.com dtaylor@fiskerinc.com ggetsin@fiskerinc.com bbaker@fiskerinc.com
|
||||||
Jenkinsfile rgreenberg@fiskerinc.com jwu@fiskerinc.com dtaylor@fiskerinc.com ggetsin@fiskerinc.com
|
Jenkinsfile rgreenberg@fiskerinc.com jwu@fiskerinc.com dtaylor@fiskerinc.com ggetsin@fiskerinc.com bbaker@fiskerinc.com
|
||||||
k8s rgreenberg@fiskerinc.com jwu@fiskerinc.com dtaylor@fiskerinc.com ggetsin@fiskerinc.com
|
k8s rgreenberg@fiskerinc.com jwu@fiskerinc.com dtaylor@fiskerinc.com ggetsin@fiskerinc.com bbaker@fiskerinc.com
|
||||||
Dockerfile rgreenberg@fiskerinc.com jwu@fiskerinc.com dtaylor@fiskerinc.com ggetsin@fiskerinc.com
|
Dockerfile rgreenberg@fiskerinc.com jwu@fiskerinc.com dtaylor@fiskerinc.com ggetsin@fiskerinc.com bbaker@fiskerinc.com
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
|
jest.mock("../Contexts/CarUpdatesContext");
|
||||||
jest.mock("../Contexts/FileUploadContext");
|
jest.mock("../Contexts/FileUploadContext");
|
||||||
jest.mock("../Contexts/VehicleContext");
|
jest.mock("../Contexts/VehicleContext");
|
||||||
jest.mock("../Contexts/UserContext");
|
jest.mock("../Contexts/ManifestCreateContext");
|
||||||
jest.mock("../Contexts/ManifestsContext");
|
jest.mock("../Contexts/ManifestsContext");
|
||||||
jest.mock("../Contexts/CarUpdatesContext");
|
jest.mock("../Contexts/UserContext");
|
||||||
jest.mock("../../services/monitoring");
|
|
||||||
jest.mock("../../services/grafanaAPI");
|
jest.mock("../../services/grafanaAPI");
|
||||||
|
jest.mock("../../services/monitoring");
|
||||||
|
jest.mock("../../services/vehiclesAPI");
|
||||||
|
|
||||||
import { render, screen, cleanup, waitFor, waitForElementToBeRemoved } from "@testing-library/react";
|
import {
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
cleanup,
|
||||||
|
waitFor,
|
||||||
|
waitForElementToBeRemoved,
|
||||||
|
} from "@testing-library/react";
|
||||||
import { setToken } from "../Contexts/UserContext";
|
import { setToken } from "../Contexts/UserContext";
|
||||||
import { TEST_AUTH_OBJECT } from "../../utils/testing"
|
import { TEST_AUTH_OBJECT } from "../../utils/testing";
|
||||||
import App from ".";
|
import App from ".";
|
||||||
|
|
||||||
const LOADING_STATUS = "Loading...";
|
const LOADING_STATUS = "Loading...";
|
||||||
@@ -30,7 +38,7 @@ const check = async (path, selector, compare) => {
|
|||||||
|
|
||||||
const sleepAndCheck = async (path, selector, compare) => {
|
const sleepAndCheck = async (path, selector, compare) => {
|
||||||
const container = await renderRoute(path);
|
const container = await renderRoute(path);
|
||||||
await waitFor(() => { });
|
await waitFor(() => {});
|
||||||
expect(container.querySelector(selector).innerHTML).toEqual(compare);
|
expect(container.querySelector(selector).innerHTML).toEqual(compare);
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
};
|
};
|
||||||
@@ -44,12 +52,12 @@ describe("App", () => {
|
|||||||
},
|
},
|
||||||
print: function (val) {
|
print: function (val) {
|
||||||
let str = val;
|
let str = val;
|
||||||
str = str.replace(/mui-[0-9]*/g, "mui-00000");
|
str = str.replace(/mui-\d*/g, "mui-00000");
|
||||||
|
|
||||||
return `"${str}"`;
|
return `"${str}"`;
|
||||||
}
|
},
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
setToken(null);
|
setToken(null);
|
||||||
@@ -101,7 +109,11 @@ describe("App", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("Route /vehicle-status/vin/carupdateid unauthenticated", async () => {
|
it("Route /vehicle-status/vin/carupdateid unauthenticated", async () => {
|
||||||
await check("/vehicle-status/1G1FP87S3GN100062/283", "span.MuiButton-label", "Sign In");
|
await check(
|
||||||
|
"/vehicle-status/1G1FP87S3GN100062/283",
|
||||||
|
"span.MuiButton-label",
|
||||||
|
"Sign In"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Route /page-not-found unauthenticated", async () => {
|
it("Route /page-not-found unauthenticated", async () => {
|
||||||
@@ -169,6 +181,10 @@ describe("App", () => {
|
|||||||
|
|
||||||
it("Route /vehicle-status/vin/carupdateid authenticated", async () => {
|
it("Route /vehicle-status/vin/carupdateid authenticated", async () => {
|
||||||
setToken(TEST_AUTH_OBJECT);
|
setToken(TEST_AUTH_OBJECT);
|
||||||
await sleepAndCheck("/vehicle-status/1G1FP87S3GN100062/283", "h6", "Vehicle 1G1FP87S3GN100062, Update TEST UPDATE");
|
await sleepAndCheck(
|
||||||
|
"/vehicle-status/1G1FP87S3GN100062/283",
|
||||||
|
"h6",
|
||||||
|
"Vehicle 1G1FP87S3GN100062, Update TEST UPDATE"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
250
src/components/Contexts/ManifestCreateContext.jsx
Normal file
250
src/components/Contexts/ManifestCreateContext.jsx
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import React, { useContext, useState } from "react";
|
||||||
|
|
||||||
|
import api from "../../services/manifestsAPI";
|
||||||
|
import { uploadFile, getCancelToken } from "../../services/uploadFile";
|
||||||
|
import { useStatusContext } from "./StatusContext";
|
||||||
|
import {
|
||||||
|
validateManifest,
|
||||||
|
validateManifestECUs,
|
||||||
|
} from "../../utils/manifestValidation";
|
||||||
|
|
||||||
|
const ManifestCreateContext = React.createContext();
|
||||||
|
|
||||||
|
const checkExistingManifest = async (data, token) => {
|
||||||
|
const check = {
|
||||||
|
name: data.name,
|
||||||
|
version: data.version,
|
||||||
|
};
|
||||||
|
const { data: result } = await api.getManifests(check, token);
|
||||||
|
if (result.length > 0)
|
||||||
|
throw new Error(`Update ${data.name} ${data.version} already exists`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ECUTemplate = {
|
||||||
|
name: "AGS",
|
||||||
|
part_number: "",
|
||||||
|
version: "",
|
||||||
|
serial_number: "",
|
||||||
|
hw_version: "",
|
||||||
|
vendor: "",
|
||||||
|
configuration: "",
|
||||||
|
fingerprint: "",
|
||||||
|
files: [],
|
||||||
|
manifest_id: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FileTemplate = {
|
||||||
|
offset: "0",
|
||||||
|
checksum: "",
|
||||||
|
type: "none",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ManifestCreateProvider = ({ children }) => {
|
||||||
|
const { setMessage } = useStatusContext();
|
||||||
|
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 [ecus, setECUs] = useState([]);
|
||||||
|
const [ecuIndex, setECUIndex] = useState(0);
|
||||||
|
|
||||||
|
const addECU = () => {
|
||||||
|
try {
|
||||||
|
const result = ecus.concat(
|
||||||
|
Object.assign({ data_id: ecuIndex }, ECUTemplate)
|
||||||
|
);
|
||||||
|
setECUIndex(ecuIndex + 1);
|
||||||
|
setECUs(result);
|
||||||
|
} catch (err) {
|
||||||
|
setMessage(err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteECU = (data_id) => {
|
||||||
|
try {
|
||||||
|
const result = ecus.filter((item) => item.data_id !== data_id);
|
||||||
|
setECUs(result);
|
||||||
|
} catch (err) {
|
||||||
|
setMessage(err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getECU = (ecu_data_id) =>
|
||||||
|
ecus.find((item) => item.data_id === ecu_data_id);
|
||||||
|
|
||||||
|
const getECUFile = (ecu, filename) =>
|
||||||
|
ecu.files.find((file) => file.filename === filename);
|
||||||
|
|
||||||
|
const validateECUFiles = (ecu, files) => {
|
||||||
|
files.forEach((file) => {
|
||||||
|
if (file.size === 0) throw new Error(`${file.name} is 0 size`);
|
||||||
|
const result = getECUFile(ecu, file.name);
|
||||||
|
if (result) throw new Error(`${file.name} already exists`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAllECUFiles = () => {
|
||||||
|
let result = [];
|
||||||
|
|
||||||
|
for (let i = 0, len = ecus.length; i < len; i++) {
|
||||||
|
const ecu = ecus[i];
|
||||||
|
result = result.concat(ecu.files);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addECUFile = (ecu_data_id, items) => {
|
||||||
|
try {
|
||||||
|
const ecu = getECU(ecu_data_id);
|
||||||
|
if (ecu === undefined) return;
|
||||||
|
|
||||||
|
const files = Array.from(items);
|
||||||
|
validateECUFiles(ecu, files);
|
||||||
|
|
||||||
|
const total = ecu.files.length;
|
||||||
|
const result = files.map((file, index) =>
|
||||||
|
Object.assign(
|
||||||
|
{ order: total + index, file: file, filename: file.name },
|
||||||
|
FileTemplate
|
||||||
|
)
|
||||||
|
);
|
||||||
|
ecu.files = ecu.files.concat(result);
|
||||||
|
updateECUs();
|
||||||
|
} catch (err) {
|
||||||
|
setMessage(err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortECUFiles = (ecu) => {
|
||||||
|
ecu.files.sort((x, y) => {
|
||||||
|
if (x.order === y.order) return 0;
|
||||||
|
return x.order < y.order ? -1 : 1;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteECUFile = (ecu_data_id, filename) => {
|
||||||
|
try {
|
||||||
|
const ecu = getECU(ecu_data_id);
|
||||||
|
if (ecu === undefined) return;
|
||||||
|
ecu.files = ecu.files.filter((file) => file.filename !== filename);
|
||||||
|
sortECUFiles(ecu);
|
||||||
|
updateECUs();
|
||||||
|
} catch (err) {
|
||||||
|
setMessage(err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateECUs = () => {
|
||||||
|
setECUs(ecus.concat([]));
|
||||||
|
};
|
||||||
|
|
||||||
|
const createManifest = async (data, token) => {
|
||||||
|
let result;
|
||||||
|
let currentFileIndex = 0;
|
||||||
|
const incremFileIndex = () => {
|
||||||
|
setUploadFileIndex(currentFileIndex);
|
||||||
|
currentFileIndex++;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
setBusy(true);
|
||||||
|
validateManifest(data, token);
|
||||||
|
validateManifestECUs(ecus);
|
||||||
|
await checkExistingManifest(data, token);
|
||||||
|
|
||||||
|
setUploadedFiles(getAllECUFiles());
|
||||||
|
if (result !== null) result = await api.createManifest(data, token);
|
||||||
|
if (result.error)
|
||||||
|
throw new Error(`Create manifest error. ${result.message}`);
|
||||||
|
|
||||||
|
for (let i = 0, len = ecus.length; i < len; i++) {
|
||||||
|
const ecu = ecus[i];
|
||||||
|
ecu.manifest_id = result.id;
|
||||||
|
await createManifestECU(ecu, token, incremFileIndex);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
setUploadFileIndex(0);
|
||||||
|
setUploadedFiles([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createManifestECU = async (ecu, token, incremFileIndexFn) => {
|
||||||
|
const { files, ...data } = ecu;
|
||||||
|
|
||||||
|
const result = await api.createManifestECU(data, token);
|
||||||
|
if (result.error)
|
||||||
|
throw new Error(`Create manifest error. ${result.message}`);
|
||||||
|
|
||||||
|
for (let i = 0, len = ecu.files.length; i < len; i++) {
|
||||||
|
const file = ecu.files[i];
|
||||||
|
file.manifest_ecu_id = result.id;
|
||||||
|
incremFileIndexFn();
|
||||||
|
const resp = await uploadECUFile(file, token);
|
||||||
|
if (resp.error)
|
||||||
|
throw new Error(`Upload manifest file error. ${resp.error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelUpload = () => {
|
||||||
|
if (cancelUploadToken) cancelUploadToken.cancel();
|
||||||
|
setBusy(false);
|
||||||
|
setUploadStatus("Upload cancelled");
|
||||||
|
setCancelUploadToken(null);
|
||||||
|
setUploadProgress(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadECUFile = async (file, accessToken) => {
|
||||||
|
const cancel = getCancelToken();
|
||||||
|
|
||||||
|
setUploadProgress(0);
|
||||||
|
setUploadStatus(`Uploading ${file.filename}`);
|
||||||
|
setCancelUploadToken(cancel);
|
||||||
|
|
||||||
|
const result = await uploadFile(
|
||||||
|
file,
|
||||||
|
accessToken,
|
||||||
|
setUploadProgress,
|
||||||
|
cancel.token
|
||||||
|
);
|
||||||
|
if (result.message) {
|
||||||
|
throw new Error(`${result.error}. ${result.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadStatus(`Uploaded ${file.filename}`);
|
||||||
|
setCancelUploadToken(null);
|
||||||
|
setUploadProgress(100);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ManifestCreateContext.Provider
|
||||||
|
value={{
|
||||||
|
busy,
|
||||||
|
ecus,
|
||||||
|
uploadedFiles,
|
||||||
|
uploadFileIndex,
|
||||||
|
uploadProgress,
|
||||||
|
uploadStatus,
|
||||||
|
addECU,
|
||||||
|
addECUFile,
|
||||||
|
cancelUpload,
|
||||||
|
createManifest,
|
||||||
|
createManifestECU,
|
||||||
|
deleteECU,
|
||||||
|
deleteECUFile,
|
||||||
|
updateECUs,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ManifestCreateContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useManifestCreateContext = () => useContext(ManifestCreateContext);
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
import React, { useContext, useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
|
|
||||||
import api from "../../services/manifestsAPI";
|
import api from "../../services/manifestsAPI";
|
||||||
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);
|
||||||
|
|
||||||
@@ -54,164 +48,14 @@ 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 checkExistingManifest = async (data, token) => {
|
|
||||||
const check = {
|
|
||||||
name: data.name,
|
|
||||||
version: data.version,
|
|
||||||
};
|
|
||||||
const { data: result } = await api.getManifests(check, token);
|
|
||||||
if (result.length > 0)
|
|
||||||
throw new Error(`Update ${data.name} ${data.version} already exists`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createManifest = async (data, token) => {
|
|
||||||
let result;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setBusy(true);
|
|
||||||
validateManifest(data, token);
|
|
||||||
setUploadedFiles(data.files);
|
|
||||||
|
|
||||||
await checkExistingManifest(data, token);
|
|
||||||
if (result !== null) 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}
|
||||||
|
|||||||
@@ -150,16 +150,16 @@ export const UserProvider = ({ children }) => {
|
|||||||
return (
|
return (
|
||||||
<UserContext.Provider
|
<UserContext.Provider
|
||||||
value={{
|
value={{
|
||||||
fetching,
|
|
||||||
token,
|
|
||||||
groups,
|
|
||||||
error,
|
error,
|
||||||
|
fetching,
|
||||||
|
groups,
|
||||||
|
token,
|
||||||
|
getAuthorizeURL,
|
||||||
|
getLogoutURL,
|
||||||
setError,
|
setError,
|
||||||
signIn,
|
signIn,
|
||||||
signOut,
|
signOut,
|
||||||
refresh,
|
refresh,
|
||||||
getAuthorizeURL,
|
|
||||||
getLogoutURL,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useContext, useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { logger } from "../../services/monitoring";
|
import { logger } from "../../services/monitoring";
|
||||||
import api from "../../services/vehicles";
|
import api from "../../services/vehiclesAPI";
|
||||||
|
|
||||||
const VehicleContext = React.createContext();
|
const VehicleContext = React.createContext();
|
||||||
|
|
||||||
@@ -163,10 +163,10 @@ export const VehicleProvider = ({ children }) => {
|
|||||||
<VehicleContext.Provider
|
<VehicleContext.Provider
|
||||||
value={{
|
value={{
|
||||||
busy,
|
busy,
|
||||||
vehicles,
|
|
||||||
models,
|
models,
|
||||||
years,
|
|
||||||
totalVehicles,
|
totalVehicles,
|
||||||
|
vehicles,
|
||||||
|
years,
|
||||||
addVehicle,
|
addVehicle,
|
||||||
getConnections,
|
getConnections,
|
||||||
getECUs,
|
getECUs,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
jest.mock("../../services/vehicles");
|
jest.mock("../../services/vehiclesAPI");
|
||||||
|
|
||||||
import {
|
import {
|
||||||
render,
|
render,
|
||||||
|
|||||||
53
src/components/Contexts/__mocks__/ManifestCreateContext.jsx
Normal file
53
src/components/Contexts/__mocks__/ManifestCreateContext.jsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const ManifestsContext = React.createContext();
|
||||||
|
|
||||||
|
let busy = false;
|
||||||
|
let ecus = [
|
||||||
|
{
|
||||||
|
data_id: 0,
|
||||||
|
name: "AGS",
|
||||||
|
part_number: "",
|
||||||
|
version: "",
|
||||||
|
serial_number: "",
|
||||||
|
hw_version: "",
|
||||||
|
vendor: "",
|
||||||
|
configuration: "",
|
||||||
|
fingerprint: "",
|
||||||
|
manifest_id: 0,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
filename: "test.bin",
|
||||||
|
order: 0,
|
||||||
|
offset: "0",
|
||||||
|
checksum: "",
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let uploadedFiles = [];
|
||||||
|
let uploadFileIndex = 0;
|
||||||
|
let uploadProgress = 0;
|
||||||
|
let uploadStatus = null;
|
||||||
|
|
||||||
|
export const ManifestCreateProvider = ({ children }) => {
|
||||||
|
return <div data-testid="mocked-manifestcreateprovider">{children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useManifestCreateContext = () => ({
|
||||||
|
busy,
|
||||||
|
ecus,
|
||||||
|
uploadedFiles,
|
||||||
|
uploadFileIndex,
|
||||||
|
uploadProgress,
|
||||||
|
uploadStatus,
|
||||||
|
addECU: jest.fn(),
|
||||||
|
addECUFile: jest.fn(),
|
||||||
|
cancelUpload: jest.fn(),
|
||||||
|
createManifest: jest.fn(),
|
||||||
|
createManifestECU: jest.fn(),
|
||||||
|
deleteECU: jest.fn(),
|
||||||
|
deleteECUFile: jest.fn(),
|
||||||
|
updateECUs: jest.fn(),
|
||||||
|
});
|
||||||
@@ -17,17 +17,18 @@ export const UserProvider = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useUserContext = () => ({
|
export const useUserContext = () => ({
|
||||||
token,
|
|
||||||
fetching,
|
|
||||||
error,
|
error,
|
||||||
|
fetching,
|
||||||
groups,
|
groups,
|
||||||
signIn: jest.fn(() => signInResp),
|
token,
|
||||||
signOut: jest.fn(),
|
|
||||||
getAuthorizeURL: jest.fn(() => authorizeURL),
|
getAuthorizeURL: jest.fn(() => authorizeURL),
|
||||||
getLogoutURL: jest.fn(() => logoutURL),
|
getLogoutURL: jest.fn(() => logoutURL),
|
||||||
setError: jest.fn((value) => {
|
setError: jest.fn((value) => {
|
||||||
error = value;
|
error = value;
|
||||||
}),
|
}),
|
||||||
|
signIn: jest.fn(() => signInResp),
|
||||||
|
signOut: jest.fn(),
|
||||||
|
refresh: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setToken = (val) => {
|
export const setToken = (val) => {
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ export const VehicleProvider = ({ children }) => {
|
|||||||
|
|
||||||
export const useVehicleContext = () => ({
|
export const useVehicleContext = () => ({
|
||||||
busy,
|
busy,
|
||||||
vehicles,
|
|
||||||
totalVehicles,
|
|
||||||
models,
|
models,
|
||||||
|
totalVehicles,
|
||||||
|
vehicles,
|
||||||
years,
|
years,
|
||||||
addVehicle: jest.fn(),
|
addVehicle: jest.fn(),
|
||||||
getConnections: jest.fn((vins, token) => {
|
getConnections: jest.fn((vins, token) => {
|
||||||
@@ -58,18 +58,19 @@ export const useVehicleContext = () => ({
|
|||||||
total: 2,
|
total: 2,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
getModels: jest.fn(() => {
|
|
||||||
models = ["Ocean", "PEAR"];
|
|
||||||
}),
|
|
||||||
getLocations: jest
|
getLocations: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValue([
|
.mockResolvedValue([
|
||||||
{ altitude: 5, longitude: 10, latitude: 15, vin: "TESTVIN123" },
|
{ altitude: 5, longitude: 10, latitude: 15, vin: "TESTVIN123" },
|
||||||
]),
|
]),
|
||||||
getVehicles: jest.fn(() => vehicles),
|
getModels: jest.fn(() => {
|
||||||
|
models = ["Ocean", "PEAR"];
|
||||||
|
}),
|
||||||
|
getState: jest.fn(),
|
||||||
getYears: jest.fn(() => {
|
getYears: jest.fn(() => {
|
||||||
years = [2023, 2024];
|
years = [2023, 2024];
|
||||||
}),
|
}),
|
||||||
|
getVehicles: jest.fn(() => vehicles),
|
||||||
sendCommand: jest.fn((vins, command, parameters, token) => ({
|
sendCommand: jest.fn((vins, command, parameters, token) => ({
|
||||||
vins,
|
vins,
|
||||||
command,
|
command,
|
||||||
|
|||||||
87
src/components/Controls/FileDragArea/index.jsx
Normal file
87
src/components/Controls/FileDragArea/index.jsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import useStyles from "../../useStyles";
|
||||||
|
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||||
|
|
||||||
|
const FileDragArea = ({
|
||||||
|
onFileSelect,
|
||||||
|
onDragEnter,
|
||||||
|
onDragOver,
|
||||||
|
onDragLeave,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const { setMessage } = useStatusContext();
|
||||||
|
const [over, setOver] = useState(false);
|
||||||
|
const classes = useStyles();
|
||||||
|
const inputFile = useRef();
|
||||||
|
|
||||||
|
const dragEnterHandler = (e) => {
|
||||||
|
setOver(true);
|
||||||
|
if (onDragEnter) onDragEnter(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dragOverHandler = (e) => {
|
||||||
|
setOver(true);
|
||||||
|
if (onDragEnter) onDragOver(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dragLeaveHandler = (e) => {
|
||||||
|
setOver(false);
|
||||||
|
if (onDragLeave) onDragLeave(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropHandler = (e) => {
|
||||||
|
try {
|
||||||
|
const { files } = e.dataTransfer;
|
||||||
|
if (onFileSelect) onFileSelect(files);
|
||||||
|
setOver(false);
|
||||||
|
} catch (err) {
|
||||||
|
setMessage(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectHandler = (e) => {
|
||||||
|
try {
|
||||||
|
const { files } = e.target;
|
||||||
|
if (onFileSelect) onFileSelect(files);
|
||||||
|
} catch (err) {
|
||||||
|
setMessage(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClick = (e) => {
|
||||||
|
try {
|
||||||
|
inputFile.current.click();
|
||||||
|
} catch (err) {
|
||||||
|
setMessage(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
onDragEnter={dragEnterHandler}
|
||||||
|
onDragOver={dragOverHandler}
|
||||||
|
onDragLeave={dragLeaveHandler}
|
||||||
|
onDrop={dropHandler}
|
||||||
|
onClick={onClick}
|
||||||
|
className={clsx(
|
||||||
|
classes.fileDropArea,
|
||||||
|
classes.clickable,
|
||||||
|
over ? classes.overHighlight : null
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
onChange={selectHandler}
|
||||||
|
ref={inputFile}
|
||||||
|
className={classes.hidden}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileDragArea;
|
||||||
14
src/components/Controls/ListHead/index.jsx
Normal file
14
src/components/Controls/ListHead/index.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { TableCell, TableHead, TableRow } from "@material-ui/core";
|
||||||
|
|
||||||
|
const ListHead = ({ options }) => (
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
{options.map((option) => (
|
||||||
|
<TableCell key={option.label || "none"}>{option.label}</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ListHead;
|
||||||
79
src/components/Controls/ManifestECUFileList/index.jsx
Normal file
79
src/components/Controls/ManifestECUFileList/index.jsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableFooter,
|
||||||
|
TableRow,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
|
||||||
|
import FileDragArea from "../FileDragArea";
|
||||||
|
import ListHead from "../ListHead";
|
||||||
|
import SubListItem from "../SubListItem";
|
||||||
|
import { useManifestCreateContext } from "../../Contexts/ManifestCreateContext";
|
||||||
|
import ManifestECUFileTypes from "../ManifestECUFileTypes";
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
label: "Filename",
|
||||||
|
field: "filename",
|
||||||
|
readonly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Offset",
|
||||||
|
field: "offset",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Checksum",
|
||||||
|
field: "checksum",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Type",
|
||||||
|
field: "type",
|
||||||
|
control: ManifestECUFileTypes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "",
|
||||||
|
field: "filename",
|
||||||
|
delete: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ManifestECUFileList = ({ data }) => {
|
||||||
|
const { addECUFile, deleteECUFile } = useManifestCreateContext();
|
||||||
|
|
||||||
|
const onAddFile = (files) => {
|
||||||
|
addECUFile(data.data_id, files);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeletFile = (filename) => {
|
||||||
|
deleteECUFile(data.data_id, filename);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
{data && data.files && data.files.length > 0 && (
|
||||||
|
<ListHead options={options} />
|
||||||
|
)}
|
||||||
|
<TableBody>
|
||||||
|
{data.files.map((file) => (
|
||||||
|
<SubListItem
|
||||||
|
key={file.filename}
|
||||||
|
data={file}
|
||||||
|
options={options}
|
||||||
|
onDelete={onDeletFile}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
<TableFooter>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={options.length}>
|
||||||
|
<FileDragArea onFileSelect={onAddFile}>ADD FILES</FileDragArea>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManifestECUFileList;
|
||||||
36
src/components/Controls/ManifestECUFileTypes/index.jsx
Normal file
36
src/components/Controls/ManifestECUFileTypes/index.jsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Select } from "@material-ui/core";
|
||||||
|
|
||||||
|
const ManifestECUFileTypes = (props) => {
|
||||||
|
const changeHandler = (e) => {
|
||||||
|
if (!props.changeHandler) return;
|
||||||
|
props.changeHandler(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
id={props.id}
|
||||||
|
native
|
||||||
|
variant="outlined"
|
||||||
|
value={props.value}
|
||||||
|
onChange={changeHandler}
|
||||||
|
>
|
||||||
|
{FileTypes.map((item, index) => (
|
||||||
|
<option key={index} value={item[0]}>
|
||||||
|
{item[1]}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManifestECUFileTypes;
|
||||||
|
|
||||||
|
const FileTypes = [
|
||||||
|
["bootloader", "Bootloader"],
|
||||||
|
["software", "Software"],
|
||||||
|
["calibration", "Calibration"],
|
||||||
|
["configuration", "Configuration"],
|
||||||
|
["none", "None"],
|
||||||
|
];
|
||||||
87
src/components/Controls/ManifestECUList/index.jsx
Normal file
87
src/components/Controls/ManifestECUList/index.jsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableFooter,
|
||||||
|
TableRow,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
|
||||||
|
import ECUDropDrop from "../ECUDropDown";
|
||||||
|
import ListHead from "../ListHead";
|
||||||
|
import ManifestECURow from "../ManifestECURow";
|
||||||
|
import { useManifestCreateContext } from "../../Contexts/ManifestCreateContext";
|
||||||
|
import PageDragPreventDefault from "../PageDragPreventDefault";
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
label: "ID",
|
||||||
|
field: "data_id",
|
||||||
|
readonly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "ECU",
|
||||||
|
field: "name",
|
||||||
|
control: ECUDropDrop,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Version",
|
||||||
|
field: "version",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Part Number",
|
||||||
|
field: "part_number",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Serial",
|
||||||
|
field: "serial_number",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Hardware",
|
||||||
|
field: "hw_version",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Vendor",
|
||||||
|
field: "vendor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "",
|
||||||
|
delete: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ManifestECUList = () => {
|
||||||
|
const { ecus, addECU } = useManifestCreateContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageDragPreventDefault />
|
||||||
|
<Table>
|
||||||
|
<ListHead options={options} />
|
||||||
|
<TableBody>
|
||||||
|
{ecus.map((item) => {
|
||||||
|
return (
|
||||||
|
<ManifestECURow
|
||||||
|
key={item.data_id}
|
||||||
|
data={item}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
<TableFooter>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} align="center">
|
||||||
|
<Button onClick={addECU}>Add ECU</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</Table>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManifestECUList;
|
||||||
33
src/components/Controls/ManifestECURow/index.jsx
Normal file
33
src/components/Controls/ManifestECURow/index.jsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { TableCell, TableRow } from "@material-ui/core";
|
||||||
|
|
||||||
|
import ManifestECUFileList from "../ManifestECUFileList";
|
||||||
|
import SubListItem from "../SubListItem";
|
||||||
|
import { useManifestCreateContext } from "../../Contexts/ManifestCreateContext";
|
||||||
|
|
||||||
|
const ManifestECURow = ({ data, options }) => {
|
||||||
|
const { deleteECU } = useManifestCreateContext();
|
||||||
|
|
||||||
|
const onDeleteECU = () => {
|
||||||
|
deleteECU(data.data_id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SubListItem
|
||||||
|
key={data.data_id}
|
||||||
|
data={data}
|
||||||
|
options={options}
|
||||||
|
onDelete={onDeleteECU}
|
||||||
|
/>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={options.length}>
|
||||||
|
<ManifestECUFileList data={data} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManifestECURow;
|
||||||
68
src/components/Controls/ManifestUploadProgress/index.jsx
Normal file
68
src/components/Controls/ManifestUploadProgress/index.jsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Button, Grid, LinearProgress, Typography } from "@material-ui/core";
|
||||||
|
|
||||||
|
import { useManifestCreateContext } from "../../Contexts/ManifestCreateContext";
|
||||||
|
|
||||||
|
const ManifestUploadProgress = (props) => {
|
||||||
|
const { uploadProgress, uploadStatus, uploadFileIndex, uploadedFiles } =
|
||||||
|
useManifestCreateContext();
|
||||||
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManifestUploadProgress;
|
||||||
24
src/components/Controls/PageDragPreventDefault/index.jsx
Normal file
24
src/components/Controls/PageDragPreventDefault/index.jsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
|
const PageDragPreventDefault = () => {
|
||||||
|
const preventDefaults = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const dragEvents = ["dragenter", "dragover", "dragleave", "drop"];
|
||||||
|
dragEvents.forEach((eventName) => {
|
||||||
|
document.body.addEventListener(eventName, preventDefaults, false);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
dragEvents.forEach((eventName) => {
|
||||||
|
document.body.removeEventListener(eventName, preventDefaults, false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageDragPreventDefault;
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -16,11 +16,13 @@ const DataDisplay = ({ data, option, onDelete }) => {
|
|||||||
if (option.readonly) {
|
if (option.readonly) {
|
||||||
return `${data[option.field]}`;
|
return `${data[option.field]}`;
|
||||||
} else if (option.delete) {
|
} else if (option.delete) {
|
||||||
|
const idField = option.field || "data_id";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to="#"
|
to="#"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
deleteHandler(data.data_id);
|
deleteHandler(data[idField]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
@@ -43,7 +45,7 @@ const DataDisplay = ({ data, option, onDelete }) => {
|
|||||||
name={option.field}
|
name={option.field}
|
||||||
placeholder={option.label}
|
placeholder={option.label}
|
||||||
inputProps={option.inputProps}
|
inputProps={option.inputProps}
|
||||||
requried={option.required}
|
requried={option.required ? "true" : "false"}
|
||||||
fullWidth
|
fullWidth
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={text}
|
value={text}
|
||||||
|
|||||||
@@ -1,117 +1,29 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { Redirect } from "react-router";
|
import { Redirect } from "react-router";
|
||||||
import {
|
import { Button, TextField, Typography } from "@material-ui/core";
|
||||||
Button,
|
|
||||||
Grid,
|
|
||||||
LinearProgress,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import { DropzoneArea } from "material-ui-dropzone";
|
|
||||||
|
|
||||||
import { useUserContext } from "../../Contexts/UserContext";
|
import { useUserContext } from "../../Contexts/UserContext";
|
||||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||||
import {
|
import {
|
||||||
useManifestsContext,
|
ManifestCreateProvider,
|
||||||
ManifestsProvider,
|
useManifestCreateContext,
|
||||||
} from "../../Contexts/ManifestsContext";
|
} from "../../Contexts/ManifestCreateContext";
|
||||||
import useStyles from "../../useStyles";
|
import useStyles from "../../useStyles";
|
||||||
import { logger } from "../../../services/monitoring";
|
import { logger } from "../../../services/monitoring";
|
||||||
import ECUFilesList from "../ECUFilesList";
|
import ManifestECUList from "../../Controls/ManifestECUList";
|
||||||
|
import ManifestUploadProgress from "../../Controls/ManifestUploadProgress";
|
||||||
const FileTemplate = {
|
|
||||||
name: "AGS",
|
|
||||||
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 MainForm = () => {
|
||||||
const { createManifest, cancelUpload, busy } = useManifestsContext();
|
const { createManifest, cancelUpload, busy } = useManifestCreateContext();
|
||||||
const { token } = useUserContext();
|
const { token } = useUserContext();
|
||||||
const { setMessage, setTitle, setSitePath } = useStatusContext();
|
const { setMessage, setTitle, setSitePath } = useStatusContext();
|
||||||
const [redirect, setRedirect] = useState(null);
|
const [redirect, setRedirect] = useState(null);
|
||||||
const [fileIndex, setFileIndex] = useState(0);
|
|
||||||
const [ecuFiles, setECUFiles] = useState([]);
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const packagenameEl = useRef(null);
|
const packagenameEl = useRef(null);
|
||||||
const versionEl = useRef(null);
|
const versionEl = useRef(null);
|
||||||
const descEl = useRef(null);
|
const descEl = useRef(null);
|
||||||
const releasenotesEl = 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(() => {
|
useEffect(() => {
|
||||||
setTitle("Create Deployments");
|
setTitle("Create Deployments");
|
||||||
setSitePath([
|
setSitePath([
|
||||||
@@ -137,7 +49,6 @@ const MainForm = () => {
|
|||||||
version: versionEl.current.value,
|
version: versionEl.current.value,
|
||||||
description: descEl.current.value,
|
description: descEl.current.value,
|
||||||
releasenotes: releasenotesEl.current.value,
|
releasenotes: releasenotesEl.current.value,
|
||||||
files: ecuFiles,
|
|
||||||
};
|
};
|
||||||
const manifest = await createManifest(formData, authToken);
|
const manifest = await createManifest(formData, authToken);
|
||||||
|
|
||||||
@@ -216,25 +127,9 @@ const MainForm = () => {
|
|||||||
inputRef={releasenotesEl}
|
inputRef={releasenotesEl}
|
||||||
/>
|
/>
|
||||||
<Typography variant="h6">ECU Files</Typography>
|
<Typography variant="h6">ECU Files</Typography>
|
||||||
<ECUFilesList data={ecuFiles} onChange={setECUFiles} />
|
<ManifestECUList />
|
||||||
<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 ? (
|
{busy ? (
|
||||||
<UploadProgress onCancel={cancelUpload} />
|
<ManifestUploadProgress onCancel={cancelUpload} />
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -255,8 +150,8 @@ const MainForm = () => {
|
|||||||
|
|
||||||
export default function FileUploadForm() {
|
export default function FileUploadForm() {
|
||||||
return (
|
return (
|
||||||
<ManifestsProvider>
|
<ManifestCreateProvider>
|
||||||
<MainForm />
|
<MainForm />
|
||||||
</ManifestsProvider>
|
</ManifestCreateProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import SubList from "../../Controls/SubList";
|
|
||||||
import ECUDropDrop from "../../Controls/ECUDropDown";
|
|
||||||
|
|
||||||
const ECUFilesList = ({ data, onChange }) => {
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
label: "ID",
|
|
||||||
field: "data_id",
|
|
||||||
readonly: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "ECU",
|
|
||||||
field: "name",
|
|
||||||
control: ECUDropDrop,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
@@ -20,14 +20,20 @@ const HeaderSortable = (props) => {
|
|||||||
selectCount,
|
selectCount,
|
||||||
rowCount,
|
rowCount,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const sortHandler = (property) => (event) => {
|
const sortHandler = (property) => (event) => {
|
||||||
if (!onSortRequest) return;
|
if (!onSortRequest) return;
|
||||||
onSortRequest(event, property);
|
onSortRequest(event, property);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectAllHandler = (event) => {
|
const selectAllHandler = (event) => {
|
||||||
if (!onSelectAll) return;
|
if (!onSelectAll) return;
|
||||||
onSelectAll(event);
|
onSelectAll(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getOrder = (value) =>
|
||||||
|
value === "desc" ? "sorted descending" : "sorted ascending";
|
||||||
|
|
||||||
const ColumnLabel = (column) => {
|
const ColumnLabel = (column) => {
|
||||||
if (column.id) {
|
if (column.id) {
|
||||||
return (
|
return (
|
||||||
@@ -37,11 +43,9 @@ const HeaderSortable = (props) => {
|
|||||||
onClick={sortHandler(column.id)}
|
onClick={sortHandler(column.id)}
|
||||||
>
|
>
|
||||||
{column.label}
|
{column.label}
|
||||||
{orderBy === column.id ? (
|
{orderBy === column.id && (
|
||||||
<span className={classes.hiddenSortSpan}>
|
<span className={classes.hiddenSortSpan}>{getOrder(order)}</span>
|
||||||
{order === "desc" ? "sorted descending" : "sorted ascending"}
|
)}
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</TableSortLabel>
|
</TableSortLabel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const Component = () => {
|
|||||||
.catch((error) => logger.warn(error.stack));
|
.catch((error) => logger.warn(error.stack));
|
||||||
};
|
};
|
||||||
|
|
||||||
const centerAroundMarkers = (markers) => {
|
const centerAroundMarkers = (points) => {
|
||||||
// if (markers == null) {
|
// if (markers == null) {
|
||||||
// markers = []
|
// markers = []
|
||||||
// }
|
// }
|
||||||
@@ -77,8 +77,8 @@ const Component = () => {
|
|||||||
if (!token) return;
|
if (!token) return;
|
||||||
if (markers.length > 0) {
|
if (markers.length > 0) {
|
||||||
const vins = markers.map((marker) => marker[2]);
|
const vins = markers.map((marker) => marker[2]);
|
||||||
getConnections(vins, token).then((connections) => {
|
getConnections(vins, token).then((conns) => {
|
||||||
setConnections(connections);
|
setConnections(conns);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
|
|||||||
@@ -268,6 +268,15 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
progressIcon: { width: 40, height: 40 },
|
progressIcon: { width: 40, height: 40 },
|
||||||
progressSuccess: { color: "green" },
|
progressSuccess: { color: "green" },
|
||||||
progressError: { color: "red" },
|
progressError: { color: "red" },
|
||||||
|
hidden: { display: "none" },
|
||||||
|
clickable: { cursor: "pointer" },
|
||||||
|
fileDropArea: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
textAlign: "center",
|
||||||
|
color: "Black",
|
||||||
|
},
|
||||||
|
overHighlight: { background: "green" },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default useStyles;
|
export default useStyles;
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
// no actual monitoring with mock
|
const logger = {
|
||||||
|
error: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export { logger };
|
||||||
|
|||||||
@@ -1,30 +1,53 @@
|
|||||||
import { getAuthHeaderOptions, fetchRespHandler, addQueryParams } from "../utils/http";
|
import {
|
||||||
|
getAuthHeaderOptions,
|
||||||
|
fetchRespHandler,
|
||||||
|
addQueryParams,
|
||||||
|
} from "../utils/http";
|
||||||
|
|
||||||
const API_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL || "https://gw-dev.fiskerdps.com/ota_update";
|
const API_ENDPOINT =
|
||||||
|
process.env.REACT_APP_UPLOAD_SERVICE_URL ||
|
||||||
|
"https://gw-dev.fiskerdps.com/ota_update";
|
||||||
|
|
||||||
const manifestsAPI = {
|
const manifestsAPI = {
|
||||||
deleteManifest: async (manifest_id, token) => fetch(`${API_ENDPOINT}/manifest?id=${manifest_id}`, {
|
deleteManifest: async (manifest_id, token) =>
|
||||||
|
fetch(`${API_ENDPOINT}/manifest?id=${manifest_id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
headers: Object.assign(
|
||||||
})
|
{ "Content-Type": "application/json" },
|
||||||
.then(fetchRespHandler),
|
getAuthHeaderOptions(token)
|
||||||
|
),
|
||||||
|
}).then(fetchRespHandler),
|
||||||
|
|
||||||
getManifests: async (search, token) => {
|
getManifests: async (search, token) => {
|
||||||
const u = addQueryParams(`${API_ENDPOINT}/manifests`, search);
|
const u = addQueryParams(`${API_ENDPOINT}/manifests`, search);
|
||||||
return fetch(u, {
|
return fetch(u, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
headers: Object.assign(
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
})
|
getAuthHeaderOptions(token)
|
||||||
.then(fetchRespHandler);
|
),
|
||||||
|
}).then(fetchRespHandler);
|
||||||
},
|
},
|
||||||
|
|
||||||
createManifest: async (data, token) => fetch(`${API_ENDPOINT}/manifest`, {
|
createManifest: async (data, token) =>
|
||||||
|
fetch(`${API_ENDPOINT}/manifest`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
headers: Object.assign(
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
getAuthHeaderOptions(token)
|
||||||
|
),
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
})
|
}).then(fetchRespHandler),
|
||||||
.then(fetchRespHandler),
|
|
||||||
|
createManifestECU: async (data, token) =>
|
||||||
|
fetch(`${API_ENDPOINT}/manifestecu`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: Object.assign(
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
getAuthHeaderOptions(token)
|
||||||
|
),
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}).then(fetchRespHandler),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default manifestsAPI;
|
export default manifestsAPI;
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import { getAuthHeaderOptions, fetchRespHandler, addQueryParams } from "../utils/http"
|
|
||||||
|
|
||||||
const API_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL || "https://gw-dev.fiskerdps.com/ota_update";
|
|
||||||
|
|
||||||
const vehiclesAPI = {
|
|
||||||
addVehicle: async (vehicle, token) => fetch(`${API_ENDPOINT}/vehicle`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
|
||||||
body: JSON.stringify(vehicle),
|
|
||||||
})
|
|
||||||
.then(fetchRespHandler),
|
|
||||||
|
|
||||||
getConnections: async (vins, token) => {
|
|
||||||
const u = `${API_ENDPOINT}/carsconnected?vins=${vins.join(",")}`;
|
|
||||||
return fetch(u, {
|
|
||||||
method: "GET",
|
|
||||||
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
|
||||||
})
|
|
||||||
.then(fetchRespHandler)
|
|
||||||
},
|
|
||||||
|
|
||||||
getECUs: async (search, token) => {
|
|
||||||
const u = addQueryParams(`${API_ENDPOINT}/vehicleecus`, search);
|
|
||||||
return fetch(u, {
|
|
||||||
method: "GET",
|
|
||||||
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
|
||||||
})
|
|
||||||
.then(fetchRespHandler)
|
|
||||||
},
|
|
||||||
|
|
||||||
getModels: async (token) => fetch(`${API_ENDPOINT}/vehiclemodels`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
|
||||||
})
|
|
||||||
.then(fetchRespHandler),
|
|
||||||
|
|
||||||
getLocations: async (token) => fetch(`${API_ENDPOINT}/carslocations`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
|
||||||
})
|
|
||||||
.then(fetchRespHandler),
|
|
||||||
|
|
||||||
getState: async (token, vin) => fetch(`${API_ENDPOINT}/carstate?vin=${vin}`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
|
||||||
})
|
|
||||||
.then(fetchRespHandler),
|
|
||||||
|
|
||||||
getVehicles: async (search, token) => {
|
|
||||||
const u = addQueryParams(`${API_ENDPOINT}/vehicles`, search);
|
|
||||||
return fetch(u, {
|
|
||||||
method: "GET",
|
|
||||||
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
|
||||||
})
|
|
||||||
.then(fetchRespHandler)
|
|
||||||
},
|
|
||||||
|
|
||||||
getYears: async (token) => fetch(`${API_ENDPOINT}/vehicleyears`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
|
||||||
})
|
|
||||||
.then(fetchRespHandler),
|
|
||||||
|
|
||||||
sendCommand: async (vins, command, parameters, token) => fetch(`${API_ENDPOINT}/vehiclecommand`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
|
||||||
body: JSON.stringify({
|
|
||||||
vins, command, parameters
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then(fetchRespHandler),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default vehiclesAPI;
|
|
||||||
106
src/services/vehiclesAPI.js
Normal file
106
src/services/vehiclesAPI.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import {
|
||||||
|
getAuthHeaderOptions,
|
||||||
|
fetchRespHandler,
|
||||||
|
addQueryParams,
|
||||||
|
} from "../utils/http";
|
||||||
|
|
||||||
|
const API_ENDPOINT =
|
||||||
|
process.env.REACT_APP_UPLOAD_SERVICE_URL ||
|
||||||
|
"https://gw-dev.fiskerdps.com/ota_update";
|
||||||
|
|
||||||
|
const vehiclesAPI = {
|
||||||
|
addVehicle: async (vehicle, token) =>
|
||||||
|
fetch(`${API_ENDPOINT}/vehicle`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: Object.assign(
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
getAuthHeaderOptions(token)
|
||||||
|
),
|
||||||
|
body: JSON.stringify(vehicle),
|
||||||
|
}).then(fetchRespHandler),
|
||||||
|
|
||||||
|
getConnections: async (vins, token) => {
|
||||||
|
const u = `${API_ENDPOINT}/carsconnected?vins=${vins.join(",")}`;
|
||||||
|
return fetch(u, {
|
||||||
|
method: "GET",
|
||||||
|
headers: Object.assign(
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
getAuthHeaderOptions(token)
|
||||||
|
),
|
||||||
|
}).then(fetchRespHandler);
|
||||||
|
},
|
||||||
|
|
||||||
|
getECUs: async (search, token) => {
|
||||||
|
const u = addQueryParams(`${API_ENDPOINT}/vehicleecus`, search);
|
||||||
|
return fetch(u, {
|
||||||
|
method: "GET",
|
||||||
|
headers: Object.assign(
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
getAuthHeaderOptions(token)
|
||||||
|
),
|
||||||
|
}).then(fetchRespHandler);
|
||||||
|
},
|
||||||
|
|
||||||
|
getModels: async (token) =>
|
||||||
|
fetch(`${API_ENDPOINT}/vehiclemodels`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: Object.assign(
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
getAuthHeaderOptions(token)
|
||||||
|
),
|
||||||
|
}).then(fetchRespHandler),
|
||||||
|
|
||||||
|
getLocations: async (token) =>
|
||||||
|
fetch(`${API_ENDPOINT}/carslocations`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: Object.assign(
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
getAuthHeaderOptions(token)
|
||||||
|
),
|
||||||
|
}).then(fetchRespHandler),
|
||||||
|
|
||||||
|
getState: async (token, vin) =>
|
||||||
|
fetch(`${API_ENDPOINT}/carstate?vin=${vin}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: Object.assign(
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
getAuthHeaderOptions(token)
|
||||||
|
),
|
||||||
|
}).then(fetchRespHandler),
|
||||||
|
|
||||||
|
getVehicles: async (search, token) => {
|
||||||
|
const u = addQueryParams(`${API_ENDPOINT}/vehicles`, search);
|
||||||
|
return fetch(u, {
|
||||||
|
method: "GET",
|
||||||
|
headers: Object.assign(
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
getAuthHeaderOptions(token)
|
||||||
|
),
|
||||||
|
}).then(fetchRespHandler);
|
||||||
|
},
|
||||||
|
|
||||||
|
getYears: async (token) =>
|
||||||
|
fetch(`${API_ENDPOINT}/vehicleyears`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: Object.assign(
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
getAuthHeaderOptions(token)
|
||||||
|
),
|
||||||
|
}).then(fetchRespHandler),
|
||||||
|
|
||||||
|
sendCommand: async (vins, command, parameters, token) =>
|
||||||
|
fetch(`${API_ENDPOINT}/vehiclecommand`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: Object.assign(
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
getAuthHeaderOptions(token)
|
||||||
|
),
|
||||||
|
body: JSON.stringify({
|
||||||
|
vins,
|
||||||
|
command,
|
||||||
|
parameters,
|
||||||
|
}),
|
||||||
|
}).then(fetchRespHandler),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default vehiclesAPI;
|
||||||
101
src/utils/manifestValidation.js
Normal file
101
src/utils/manifestValidation.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
export const validateManifest = (data, accessToken) => {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
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) {
|
||||||
|
errors.push("name");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.version || data.version.length === 0) {
|
||||||
|
errors.push("version");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.description || data.description.length === 0) {
|
||||||
|
errors.push("description");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.releasenotes || data.releasenotes.length === 0) {
|
||||||
|
errors.push("release notes");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0)
|
||||||
|
throw new Error(`package ${errors.join(", ")} required`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateManifestECUs = (ecus) => {
|
||||||
|
const ecuKeys = {};
|
||||||
|
|
||||||
|
if (!ecus || ecus.length === 0) throw new Error("ECUs required");
|
||||||
|
|
||||||
|
ecus.forEach((ecu) => validateManifestECU(ecu, ecuKeys));
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateManifestECU = (data, keys) => {
|
||||||
|
const errors = [];
|
||||||
|
const fileKeys = {};
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
throw new Error("manifest ECU data required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.name || data.name.length === 0) {
|
||||||
|
errors.push("name");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.version || data.version.length === 0) {
|
||||||
|
errors.push("version");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.part_number || data.part_number.length === 0) {
|
||||||
|
errors.push("part number");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.files || data.files.length === 0) {
|
||||||
|
errors.push("files");
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `${data.name} ${data.version} ${data.part_number}`;
|
||||||
|
if (!keys[key]) {
|
||||||
|
keys[key] = true;
|
||||||
|
} else {
|
||||||
|
throw new Error(`ECU ${key} already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0)
|
||||||
|
throw new Error(`ECU ${data.name} ${errors.join(", ")} required`);
|
||||||
|
|
||||||
|
data.files.forEach((file) => validateFile(data.name, file, fileKeys));
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateFile = (ecu, file, keys) => {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
throw new Error("File data required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.filename || file.filename.length === 0) {
|
||||||
|
errors.push("filename");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.offset || file.offset.length === 0) {
|
||||||
|
errors.push("offset");
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `${file.filename} ${file.version}`;
|
||||||
|
if (!keys[key]) {
|
||||||
|
keys[key] = true;
|
||||||
|
} else {
|
||||||
|
throw new Error(`${ecu} ${key} already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0)
|
||||||
|
throw new Error(`${ecu} ${file.filename} ${errors.join(", ")} required`);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user