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

View File

@@ -1,14 +1,22 @@
jest.mock("../Contexts/CarUpdatesContext");
jest.mock("../Contexts/FileUploadContext");
jest.mock("../Contexts/VehicleContext");
jest.mock("../Contexts/UserContext");
jest.mock("../Contexts/ManifestCreateContext");
jest.mock("../Contexts/ManifestsContext");
jest.mock("../Contexts/CarUpdatesContext");
jest.mock("../../services/monitoring");
jest.mock("../Contexts/UserContext");
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 { TEST_AUTH_OBJECT } from "../../utils/testing"
import { TEST_AUTH_OBJECT } from "../../utils/testing";
import App from ".";
const LOADING_STATUS = "Loading...";
@@ -30,26 +38,26 @@ const check = async (path, selector, compare) => {
const sleepAndCheck = async (path, selector, compare) => {
const container = await renderRoute(path);
await waitFor(() => { });
await waitFor(() => {});
expect(container.querySelector(selector).innerHTML).toEqual(compare);
expect(container).toMatchSnapshot();
};
describe("App", () => {
beforeAll(() => {
// Stablize Table Pagination control ids
// Stablize Table Pagination control ids
expect.addSnapshotSerializer({
test: function (val) {
return val && typeof val === "string" && val.indexOf("mui-") >= 0;
},
print: function (val) {
let str = val;
str = str.replace(/mui-[0-9]*/g, "mui-00000");
str = str.replace(/mui-\d*/g, "mui-00000");
return `"${str}"`;
}
},
});
});
}, 30000);
afterEach(() => {
setToken(null);
@@ -101,9 +109,13 @@ describe("App", () => {
});
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 () => {
await check("/page-not-found", "h1", "Page Not Found");
});
@@ -169,6 +181,10 @@ describe("App", () => {
it("Route /vehicle-status/vin/carupdateid authenticated", async () => {
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 api from "../../services/manifestsAPI";
import { uploadFile, getCancelToken } from "../../services/uploadFile";
const ManifestsContext = React.createContext();
export const ManifestsProvider = ({ children }) => {
const [busy, setBusy] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadStatus, setUploadStatus] = useState(null);
const [cancelUploadToken, setCancelUploadToken] = useState(null);
const [uploadFileIndex, setUploadFileIndex] = useState(0);
const [uploadedFiles, setUploadedFiles] = useState([]);
const [manifests, setManifests] = useState([]);
const [totalManifests, setTotalManifests] = useState(0);
@@ -54,164 +48,14 @@ export const ManifestsProvider = ({ children }) => {
return result;
};
const validateManifest = (data, accessToken) => {
const fileKeys = {};
if (!accessToken || accessToken.length === 0) {
throw new Error("Access token required");
}
if (!data) {
throw new Error("Missing manifest data");
}
if (!data.name || data.name.length === 0) {
throw new Error("Package name required");
}
if (!data.version || data.version.length === 0) {
throw new Error("Package version required");
}
if (!data.description || data.description.length === 0) {
throw new Error("Package description required");
}
if (!data.releasenotes || data.releasenotes.length === 0) {
throw new Error("Package release notes link required");
}
if (!data.files || data.files.length === 0) {
throw new Error("Package files required");
}
data.files.forEach((file) => validateFile(file, fileKeys));
};
const validateFile = (file, keys) => {
if (!file) {
throw new Error("File data required");
}
if (!file.filename || file.filename.length === 0) {
throw new Error("Filename required");
}
if (!file.name || file.name.length === 0) {
throw new Error(`${file.filename} ECU name required`);
}
if (!file.update_version || file.update_version.length === 0) {
throw new Error(`${file.filename} version required`);
}
if (!file.part_number || file.part_number.length === 0) {
throw new Error(`${file.filename} part number required`);
}
const key = `${file.name}, ${file.update_version}, ${file.filename}`;
if (!keys[key]) {
keys[key] = true;
} else {
throw new Error(`${key} already exists`);
}
};
const 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 (
<ManifestsContext.Provider
value={{
busy,
uploadProgress,
uploadStatus,
uploadFileIndex,
uploadedFiles,
manifests,
totalManifests,
getManifests,
deleteManifest,
createManifest,
cancelUpload,
}}
>
{children}

View File

@@ -150,16 +150,16 @@ export const UserProvider = ({ children }) => {
return (
<UserContext.Provider
value={{
fetching,
token,
groups,
error,
fetching,
groups,
token,
getAuthorizeURL,
getLogoutURL,
setError,
signIn,
signOut,
refresh,
getAuthorizeURL,
getLogoutURL,
}}
>
{children}

View File

@@ -1,6 +1,6 @@
import React, { useContext, useState } from "react";
import { logger } from "../../services/monitoring";
import api from "../../services/vehicles";
import api from "../../services/vehiclesAPI";
const VehicleContext = React.createContext();
@@ -163,10 +163,10 @@ export const VehicleProvider = ({ children }) => {
<VehicleContext.Provider
value={{
busy,
vehicles,
models,
years,
totalVehicles,
vehicles,
years,
addVehicle,
getConnections,
getECUs,

View File

@@ -1,4 +1,4 @@
jest.mock("../../services/vehicles");
jest.mock("../../services/vehiclesAPI");
import {
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 = () => ({
token,
fetching,
error,
fetching,
groups,
signIn: jest.fn(() => signInResp),
signOut: jest.fn(),
token,
getAuthorizeURL: jest.fn(() => authorizeURL),
getLogoutURL: jest.fn(() => logoutURL),
setError: jest.fn((value) => {
error = value;
}),
signIn: jest.fn(() => signInResp),
signOut: jest.fn(),
refresh: jest.fn(),
});
export const setToken = (val) => {

View File

@@ -13,9 +13,9 @@ export const VehicleProvider = ({ children }) => {
export const useVehicleContext = () => ({
busy,
vehicles,
totalVehicles,
models,
totalVehicles,
vehicles,
years,
addVehicle: jest.fn(),
getConnections: jest.fn((vins, token) => {
@@ -58,18 +58,19 @@ export const useVehicleContext = () => ({
total: 2,
};
}),
getModels: jest.fn(() => {
models = ["Ocean", "PEAR"];
}),
getLocations: jest
.fn()
.mockResolvedValue([
{ altitude: 5, longitude: 10, latitude: 15, vin: "TESTVIN123" },
]),
getVehicles: jest.fn(() => vehicles),
getModels: jest.fn(() => {
models = ["Ocean", "PEAR"];
}),
getState: jest.fn(),
getYears: jest.fn(() => {
years = [2023, 2024];
}),
getVehicles: jest.fn(() => vehicles),
sendCommand: jest.fn((vins, command, parameters, token) => ({
vins,
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) {
return `${data[option.field]}`;
} else if (option.delete) {
const idField = option.field || "data_id";
return (
<Link
to="#"
onClick={() => {
deleteHandler(data.data_id);
deleteHandler(data[idField]);
}}
>
<DeleteIcon />
@@ -43,7 +45,7 @@ const DataDisplay = ({ data, option, onDelete }) => {
name={option.field}
placeholder={option.label}
inputProps={option.inputProps}
requried={option.required}
requried={option.required ? "true" : "false"}
fullWidth
onChange={onChange}
value={text}

View File

@@ -1,117 +1,29 @@
import React, { useEffect, useRef, useState } from "react";
import { Redirect } from "react-router";
import {
Button,
Grid,
LinearProgress,
TextField,
Typography,
} from "@material-ui/core";
import { DropzoneArea } from "material-ui-dropzone";
import { Button, TextField, Typography } from "@material-ui/core";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import {
useManifestsContext,
ManifestsProvider,
} from "../../Contexts/ManifestsContext";
ManifestCreateProvider,
useManifestCreateContext,
} from "../../Contexts/ManifestCreateContext";
import useStyles from "../../useStyles";
import { logger } from "../../../services/monitoring";
import ECUFilesList from "../ECUFilesList";
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>
</>
);
};
import ManifestECUList from "../../Controls/ManifestECUList";
import ManifestUploadProgress from "../../Controls/ManifestUploadProgress";
const MainForm = () => {
const { createManifest, cancelUpload, busy } = useManifestsContext();
const { createManifest, cancelUpload, busy } = useManifestCreateContext();
const { token } = useUserContext();
const { setMessage, setTitle, setSitePath } = useStatusContext();
const [redirect, setRedirect] = useState(null);
const [fileIndex, setFileIndex] = useState(0);
const [ecuFiles, setECUFiles] = useState([]);
const classes = useStyles();
const packagenameEl = useRef(null);
const versionEl = useRef(null);
const descEl = useRef(null);
const releasenotesEl = useRef(null);
const getNewFile = (file) => {
setFileIndex(fileIndex + 1);
return Object.assign(
{ data_id: fileIndex, filename: file.name, file },
FileTemplate
);
};
const addFile = (file) => {
setECUFiles(ecuFiles.concat(getNewFile(file)));
};
useEffect(() => {
setTitle("Create Deployments");
setSitePath([
@@ -137,7 +49,6 @@ const MainForm = () => {
version: versionEl.current.value,
description: descEl.current.value,
releasenotes: releasenotesEl.current.value,
files: ecuFiles,
};
const manifest = await createManifest(formData, authToken);
@@ -216,25 +127,9 @@ const MainForm = () => {
inputRef={releasenotesEl}
/>
<Typography variant="h6">ECU Files</Typography>
<ECUFilesList data={ecuFiles} onChange={setECUFiles} />
<DropzoneArea
id="dropzone"
dropzoneText="Add Files"
maxFileSize={1e9}
filesLimit={1000}
showAlerts={false}
showPreviewsInDropzone={false}
onDrop={(files) => {
files.forEach((file) => {
addFile(file);
});
}}
onDropRejected={(files) => {
setMessage(`Rejected ${files[0].name} too large`);
}}
/>
<ManifestECUList />
{busy ? (
<UploadProgress onCancel={cancelUpload} />
<ManifestUploadProgress onCancel={cancelUpload} />
) : (
<Button
type="submit"
@@ -255,8 +150,8 @@ const MainForm = () => {
export default function FileUploadForm() {
return (
<ManifestsProvider>
<ManifestCreateProvider>
<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,
rowCount,
} = props;
const sortHandler = (property) => (event) => {
if (!onSortRequest) return;
onSortRequest(event, property);
};
const selectAllHandler = (event) => {
if (!onSelectAll) return;
onSelectAll(event);
};
const getOrder = (value) =>
value === "desc" ? "sorted descending" : "sorted ascending";
const ColumnLabel = (column) => {
if (column.id) {
return (
@@ -37,11 +43,9 @@ const HeaderSortable = (props) => {
onClick={sortHandler(column.id)}
>
{column.label}
{orderBy === column.id ? (
<span className={classes.hiddenSortSpan}>
{order === "desc" ? "sorted descending" : "sorted ascending"}
</span>
) : null}
{orderBy === column.id && (
<span className={classes.hiddenSortSpan}>{getOrder(order)}</span>
)}
</TableSortLabel>
);
}

View File

@@ -57,7 +57,7 @@ const Component = () => {
.catch((error) => logger.warn(error.stack));
};
const centerAroundMarkers = (markers) => {
const centerAroundMarkers = (points) => {
// if (markers == null) {
// markers = []
// }
@@ -77,8 +77,8 @@ const Component = () => {
if (!token) return;
if (markers.length > 0) {
const vins = markers.map((marker) => marker[2]);
getConnections(vins, token).then((connections) => {
setConnections(connections);
getConnections(vins, token).then((conns) => {
setConnections(conns);
});
}
// eslint-disable-next-line

View File

@@ -268,6 +268,15 @@ const useStyles = makeStyles((theme) => ({
progressIcon: { width: 40, height: 40 },
progressSuccess: { color: "green" },
progressError: { color: "red" },
hidden: { display: "none" },
clickable: { cursor: "pointer" },
fileDropArea: {
width: "100%",
height: "100%",
textAlign: "center",
color: "Black",
},
overHighlight: { background: "green" },
}));
export default useStyles;