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:
John Wu
2021-10-14 12:23:16 -07:00
committed by GitHub
parent ba7611d6aa
commit 86eeaab869
32 changed files with 2293 additions and 866 deletions

10
.github/CODEOWNERS vendored
View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
jest.mock("../../services/vehicles"); jest.mock("../../services/vehiclesAPI");
import { import {
render, render,

View 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(),
});

View File

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

View File

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

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

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

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

View 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"],
];

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1,6 @@
// no actual monitoring with mock const logger = {
error: jest.fn(),
warn: jest.fn(),
};
export { logger };

View File

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

View File

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

View 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`);
};