From ab37cd598f2730e175fbe64beb2300eda36d45bb Mon Sep 17 00:00:00 2001 From: John Wu <76966357+jwu-fisker@users.noreply.github.com> Date: Fri, 25 Jun 2021 10:17:03 -0700 Subject: [PATCH 1/2] CEC-287 Add connection status to vehicles page (#63) * Add connection status to vehicles page * ConnectedIcon control * Handle Style --- .../Cars/CarSelectionTable/index.jsx | 14 +++++-------- src/components/Cars/List/index.jsx | 5 +++++ .../Controls/ConnectedIcon/index.jsx | 21 +++++++++++++++++++ 3 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 src/components/Controls/ConnectedIcon/index.jsx diff --git a/src/components/Cars/CarSelectionTable/index.jsx b/src/components/Cars/CarSelectionTable/index.jsx index 2550b8e..b37d2ba 100644 --- a/src/components/Cars/CarSelectionTable/index.jsx +++ b/src/components/Cars/CarSelectionTable/index.jsx @@ -10,13 +10,13 @@ import { TablePagination, TableRow, } from "@material-ui/core"; -import CheckCircleIcon from "@material-ui/icons/CheckCircle"; import { useVehicleContext } from "../../Contexts/VehicleContext"; import { useStatusContext } from "../../Contexts/StatusContext"; import { LocalDateTimeString } from "../../../utils/dates"; import TableHeaderSortable from "../../Table/HeaderSortable"; import { logger } from "../../../services/monitoring"; +import ConnectedIcon from "../../Controls/ConnectedIcon"; const tableColumns = [ { @@ -138,14 +138,10 @@ const CarSelectionTable = (props) => { /> - {row.connected && ( - <> - - - > - )} + {row.vin} {row.model} diff --git a/src/components/Cars/List/index.jsx b/src/components/Cars/List/index.jsx index 0eaec4e..957ebf7 100644 --- a/src/components/Cars/List/index.jsx +++ b/src/components/Cars/List/index.jsx @@ -21,6 +21,7 @@ import { LocalDateTimeString } from "../../../utils/dates"; import TableHeaderSortable from "../../Table/HeaderSortable"; import SearchField from "../../Controls/SearchField"; import { logger } from "../../../services/monitoring"; +import ConnectedIcon from "../../Controls/ConnectedIcon"; const tableColumns = [ { @@ -132,6 +133,10 @@ const MainForm = () => { {vehicles.map((row) => ( + {row.vin} {row.model} diff --git a/src/components/Controls/ConnectedIcon/index.jsx b/src/components/Controls/ConnectedIcon/index.jsx new file mode 100644 index 0000000..475892d --- /dev/null +++ b/src/components/Controls/ConnectedIcon/index.jsx @@ -0,0 +1,21 @@ +import React from "react"; +import PropTypes from "prop-types"; +import CheckCircleIcon from "@material-ui/icons/CheckCircle"; + +const ConnectedIcon = (props) => { + if (props.connected) { + return ( + + + + ); + } + + return null; +}; + +ConnectedIcon.propTypes = { + connected: PropTypes.bool.isRequired, +}; + +export default ConnectedIcon; From 83105fb7ca69ea9e23067e2379353e38f0a2df53 Mon Sep 17 00:00:00 2001 From: John Wu <76966357+jwu-fisker@users.noreply.github.com> Date: Fri, 16 Jul 2021 10:49:10 -0700 Subject: [PATCH 2/2] CEC-247, CEC-261 Manifest and ECU display (#65) * CEC-261 Add ECU list control * CEC-261 Update vehicle service mock * CEC-247 Manifest screens * Fix test * Remove dynamic dates from mocks * Remove timezone from mock dates * Fix test for date string timezone difference --- package-lock.json | 75 +- package.json | 9 +- src/components/App/App.test.js | 28 + .../App/__snapshots__/App.test.js.snap | 2610 +++++++++++++++-- .../Cars/CarSelectionTable/index.jsx | 9 +- src/components/Cars/List/index.jsx | 7 + src/components/Contexts/CarUpdatesContext.jsx | 176 ++ src/components/Contexts/ManifestsContext.jsx | 66 + .../Contexts/UpdatesContext.test.jsx | 2 +- .../Contexts/VehicleContext.test.jsx | 31 +- .../Contexts/__mocks__/CarUpdatesContext.jsx | 31 + .../Contexts/__mocks__/ManifestsContext.jsx | 27 + .../Contexts/__mocks__/UpdatesContext.jsx | 6 +- src/components/Controls/ECUList/index.jsx | 34 + src/components/Layouts/SideMenu.jsx | 5 + .../__snapshots__/SideMenu.test.jsx.snap | 22 + src/components/Manifest/Deploy/index.jsx | 161 + src/components/Manifest/List/index.jsx | 249 ++ src/components/Manifest/Status/index.jsx | 174 ++ src/components/Routes/SiteRoutes.jsx | 27 + src/components/UpdatePackages/List/index.jsx | 11 +- src/services/__mocks__/manifests.js | 59 + src/services/__mocks__/updates.js | 9 +- src/services/__mocks__/vehicles.js | 12 +- src/services/manifests.js | 23 + testEnv.js | 3 + 26 files changed, 3626 insertions(+), 240 deletions(-) create mode 100644 src/components/Contexts/CarUpdatesContext.jsx create mode 100644 src/components/Contexts/ManifestsContext.jsx create mode 100644 src/components/Contexts/__mocks__/CarUpdatesContext.jsx create mode 100644 src/components/Contexts/__mocks__/ManifestsContext.jsx create mode 100644 src/components/Controls/ECUList/index.jsx create mode 100644 src/components/Manifest/Deploy/index.jsx create mode 100644 src/components/Manifest/List/index.jsx create mode 100644 src/components/Manifest/Status/index.jsx create mode 100644 src/services/__mocks__/manifests.js create mode 100644 src/services/manifests.js create mode 100644 testEnv.js diff --git a/package-lock.json b/package-lock.json index 475765d..c06eaf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1275,9 +1275,9 @@ "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==" }, "@datadog/browser-core": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/@datadog/browser-core/-/browser-core-2.8.1.tgz", - "integrity": "sha512-pxY/jOtWGpWcs04LPKBSXd0bChzgQY0oiActomB+z7xdhmqGB/R0Fy50ZA1gtJXny3Pava1O7tIY51E/CiH0Vg==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@datadog/browser-core/-/browser-core-2.15.0.tgz", + "integrity": "sha512-qWTAysGYQXVpM5FOdstaqIF6B99nyQ2N/rJsi1ruPgFmU9yMM9tRdvqiJ7NZcy+OOsZWiinvFRFUMv9SOsHeUA==", "requires": { "tslib": "^1.10.0" }, @@ -1314,12 +1314,12 @@ } }, "@datadog/browser-rum": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/@datadog/browser-rum/-/browser-rum-2.8.1.tgz", - "integrity": "sha512-p/tp1869oyJXutOyr67Aip3e3tIJpSarO3RAwf2mt1evEbUfvopwNWfQPKifMzbtMRwRA26O9nmZ7wEKjMxvxA==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@datadog/browser-rum/-/browser-rum-2.15.0.tgz", + "integrity": "sha512-E9PmGHxGQEdn8SUA7DmUu2mf/ifWGXLuGm95Hes/+dqoXbIPryFdmPCFnHaVF2nZNIA7wwW23oqe60KKo2Qjaw==", "requires": { - "@datadog/browser-core": "2.8.1", - "@datadog/browser-rum-core": "2.8.1", + "@datadog/browser-core": "2.15.0", + "@datadog/browser-rum-core": "2.15.0", "tslib": "^1.10.0" }, "dependencies": { @@ -1331,11 +1331,11 @@ } }, "@datadog/browser-rum-core": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/@datadog/browser-rum-core/-/browser-rum-core-2.8.1.tgz", - "integrity": "sha512-5z95vUEWwugcokv/vTKxQ26oW50Uv5XxoNkg/sgwDZFrIcOpjMqzRwEaot/dUM2w+THobnRzufBBYJcXmj5HpQ==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@datadog/browser-rum-core/-/browser-rum-core-2.15.0.tgz", + "integrity": "sha512-XXEe3JpSyvSvYXpXz/MgrVqs5Rl4Zu2eJXmHfxafAxb3i+VxyA6vc/pLnXPaKeWVcO489MpBEr6Gv7HiOEFZNA==", "requires": { - "@datadog/browser-core": "2.8.1", + "@datadog/browser-core": "2.15.0", "tslib": "^1.10.0" }, "dependencies": { @@ -2075,16 +2075,16 @@ } }, "@testing-library/dom": { - "version": "7.30.4", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.30.4.tgz", - "integrity": "sha512-GObDVMaI4ARrZEXaRy4moolNAxWPKvEYNV/fa6Uc2eAzR/t4otS6A7EhrntPBIQLeehL9DbVhscvvv7gd6hWqA==", + "version": "7.31.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.31.2.tgz", + "integrity": "sha512-3UqjCpey6HiTZT92vODYLPxTBWlM8ZOOjr3LX5F37/VRipW2M1kX6I/Cm4VXzteZqfGfagg8yXywpcOgQBlNsQ==", "requires": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^4.2.0", "aria-query": "^4.2.2", "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.4", + "dom-accessibility-api": "^0.5.6", "lz-string": "^1.4.4", "pretty-format": "^26.6.2" }, @@ -2101,9 +2101,9 @@ } }, "@testing-library/jest-dom": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.12.0.tgz", - "integrity": "sha512-N9Y82b2Z3j6wzIoAqajlKVF1Zt7sOH0pPee0sUHXHc5cv2Fdn23r+vpWm0MBBoGJtPOly5+Bdx1lnc3CD+A+ow==", + "version": "5.14.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.14.1.tgz", + "integrity": "sha512-dfB7HVIgTNCxH22M1+KU6viG5of2ldoA5ly8Ar8xkezKHKXjRvznCdbMbqjYGgO2xjRbwnR+rR8MLUIqF3kKbQ==", "requires": { "@babel/runtime": "^7.9.2", "@types/testing-library__jest-dom": "^5.9.1", @@ -2111,14 +2111,15 @@ "chalk": "^3.0.0", "css": "^3.0.0", "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.5.6", "lodash": "^4.17.15", "redent": "^3.0.0" } }, "@testing-library/react": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.6.tgz", - "integrity": "sha512-TXMCg0jT8xmuU8BkKMtp8l7Z50Ykew5WNX8UoIKTaLFwKkP2+1YDhOLA2Ga3wY4x29jyntk7EWfum0kjlYiSjQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.7.tgz", + "integrity": "sha512-tzRNp7pzd5QmbtXNG/mhdcl7Awfu/Iz1RaVHY75zTdOkmHCuzMhRL83gWHSgOAcjS3CCbyfwUHMZgRJb4kAfpA==", "requires": { "@babel/runtime": "^7.12.5", "@testing-library/dom": "^7.28.1" @@ -2344,9 +2345,9 @@ "integrity": "sha512-0VBprVqfgFD7Ehb2vd8Lh9TG3jP98gvr8rgehQqzztZNI7o8zS8Ad4jyZneKELphpuE212D8J70LnSNQSyO6bQ==" }, "@types/testing-library__jest-dom": { - "version": "5.9.5", - "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.5.tgz", - "integrity": "sha512-ggn3ws+yRbOHog9GxnXiEZ/35Mow6YtPZpd7Z5mKDeZS/o7zx3yAle0ov/wjhVB5QT4N2Dt+GNoGCdqkBGCajQ==", + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.0.tgz", + "integrity": "sha512-l2P2GO+hFF4Liye+fAajT1qBqvZOiL79YMpEvgGs1xTK7hECxBI8Wz4J7ntACJNiJ9r0vXQqYovroXRLPDja6A==", "requires": { "@types/jest": "*" } @@ -2967,11 +2968,6 @@ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5109,9 +5105,9 @@ } }, "dom-accessibility-api": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz", - "integrity": "sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==" + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz", + "integrity": "sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw==" }, "dom-converter": { "version": "0.2.0", @@ -15968,11 +15964,18 @@ } }, "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", "requires": { "async-limiter": "~1.0.0" + }, + "dependencies": { + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + } } }, "yargs": { diff --git a/package.json b/package.json index b5ebb0e..82ebc79 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,11 @@ "private": true, "dependencies": { "@datadog/browser-logs": "^2.15.0", - "@datadog/browser-rum": "^2.8.1", + "@datadog/browser-rum": "^2.15.0", "@material-ui/core": "^4.11.4", "@material-ui/icons": "^4.11.2", - "@testing-library/jest-dom": "^5.12.0", - "@testing-library/react": "^11.2.6", + "@testing-library/jest-dom": "^5.14.1", + "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^12.8.3", "axios": "^0.21.1", "clsx": "^1.1.1", @@ -49,5 +49,8 @@ }, "devDependencies": { "react-test-renderer": "^17.0.2" + }, + "jest": { + "globalSetup": "./testEnv.js" } } diff --git a/src/components/App/App.test.js b/src/components/App/App.test.js index e4be513..d5d32cc 100644 --- a/src/components/App/App.test.js +++ b/src/components/App/App.test.js @@ -2,6 +2,8 @@ jest.mock("../Contexts/FileUploadContext"); jest.mock("../Contexts/VehicleContext"); jest.mock("../Contexts/UpdatesContext"); jest.mock("../Contexts/UserContext"); +jest.mock("../Contexts/ManifestsContext"); +jest.mock("../Contexts/CarUpdatesContext"); jest.mock("../../services/monitoring"); import { render, screen, cleanup, waitForElementToBeRemoved } from "@testing-library/react"; @@ -91,6 +93,18 @@ describe("App", () => { await check("/dashboard", "span.MuiButton-label", "Sign In"); }); + it("Route /manifests unauthenticated", async () => { + await check("/manifests", "span.MuiButton-label", "Sign In"); + }); + + it("Route /manifest-status unauthenticated", async () => { + await check("/manifest-status/1", "span.MuiButton-label", "Sign In"); + }); + + it("Route /manifest-deploy unauthenticated", async () => { + await check("/manifest-deploy/1", "span.MuiButton-label", "Sign In"); + }); + it("Route / authenticated", async () => { setToken(TEST_AUTH_OBJECT); await check("/", "h1", "Welcome John!"); @@ -155,4 +169,18 @@ describe("App", () => { await check("/dashboard", "h6", "Dashboard"); }); + it("Route /manifests authenticated", async () => { + setToken(TEST_AUTH_OBJECT); + await check("/manifests", "h6", "Deploy Manifest"); + }); + + it("Route /manifest-status authenticated", async () => { + setToken(TEST_AUTH_OBJECT); + await check("/manifest-status/1", "h6", "Manifest Test Manifest 1.0"); + }); + + it("Route /manifest-deploy authenticated", async () => { + setToken(TEST_AUTH_OBJECT); + await check("/manifest-deploy/1", "h6", "Deploy Test Manifest 1.0"); + }); }); diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap index f51c82a..83fb2a2 100644 --- a/src/components/App/__snapshots__/App.test.js.snap +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -6,10 +6,10 @@ exports[`App Route / authenticated 1`] = ` data-testid="mocked-userprovider" > @@ -34,17 +34,17 @@ exports[`App Route / authenticated 1`] = ` @@ -142,6 +142,28 @@ exports[`App Route / authenticated 1`] = ` /> + + + + + Deploy Manifest + + + + + @@ -327,17 +349,17 @@ exports[`App Route /carupdate-deploy authenticated 1`] = ` @@ -435,6 +457,28 @@ exports[`App Route /carupdate-deploy authenticated 1`] = ` /> + + + + + Deploy Manifest + + + + + Created - Invalid Date Invalid Date + 7/6/2021 11:48:19 PM . Description 0 Selected @@ -602,7 +646,7 @@ exports[`App Route /carupdate-deploy authenticated 1`] = ` style="text-align: right;" > VIN sorted ascending @@ -1010,10 +1054,10 @@ exports[`App Route /carupdate-status authenticated 1`] = ` data-testid="mocked-userprovider" > @@ -1040,17 +1084,17 @@ exports[`App Route /carupdate-status authenticated 1`] = ` @@ -1148,6 +1192,28 @@ exports[`App Route /carupdate-status authenticated 1`] = ` /> + + + + + Deploy Manifest + + + + + @@ -1503,17 +1569,17 @@ exports[`App Route /dashboard authenticated 1`] = ` @@ -1611,6 +1677,28 @@ exports[`App Route /dashboard authenticated 1`] = ` /> + + + + + Deploy Manifest + + + + + @@ -1851,17 +1939,17 @@ exports[`App Route /home authenticated 1`] = ` @@ -1959,6 +2047,28 @@ exports[`App Route /home authenticated 1`] = ` /> + + + + + Deploy Manifest + + + + + `; -exports[`App Route /package-upload authenticated 1`] = ` +exports[`App Route /manifest-deploy authenticated 1`] = ` - Create Update Package + Deploy Test Manifest 1.0 @@ -2144,17 +2254,17 @@ exports[`App Route /package-upload authenticated 1`] = ` @@ -2252,6 +2362,28 @@ exports[`App Route /package-upload authenticated 1`] = ` /> + + + + + Deploy Manifest + + + + + + + + + + + + + + Created + 7/1/2021 10:40:07 PM + . + + + + + + Search + + + + + + + + + + + + + + + + + 0 Selected + + + + + + Deploy + + + + + + + + + + + + + + + + + + + + + + VIN + + sorted ascending + + + + + + + + + Model + + + + + + + + Year + + + + + + + + Trim + + + + + + + + Created + + + + + + + + Updated + + + + + + + + + + + + + + + Rows per page: + + + + + 5 + + + 10 + + + 25 + + + 100 + + + + + + + + 0-0 of 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`App Route /manifest-deploy unauthenticated 1`] = ` + + + + + + + + + + + + + + Sign In + + + + + + + + + +`; + +exports[`App Route /manifest-status authenticated 1`] = ` + + + + + + + Manifest Test Manifest 1.0 + + + + Sign Out + + + + + + + + + + + + + + + + + Home + + + + + + + + + + Dashboard + + + + + + + + + + Deploy Packages + + + + + + + + + + Create Package + + + + + + + + + + Deploy Manifest + + + + + + + + + + View Vehicles + + + + + + + + + + Add Vehicle + + + + + + + + + + Send Command + + + + + + + + + + + + + + + + + + + ID + + + Vehicle + + + Status + + + Created + + + Updated + + + + + + + 1 + + + + 1G1FP87S3GN100062 + + + + downloaded + + + 7/1/2021 10:40:07 PM + + + 7/12/2021 6:22:13 PM + + + + + + + + + + Rows per page: + + + + + 5 + + + 10 + + + 25 + + + 100 + + + + + + + + 1-1 of 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`App Route /manifest-status unauthenticated 1`] = ` + + + + + + + + + + + + + + + Sign In + + + + + + + + + +`; + +exports[`App Route /manifests authenticated 1`] = ` + + + + + + + Deploy Manifest + + + + Sign Out + + + + + + + + + + + + + + + + + Home + + + + + + + + + + Dashboard + + + + + + + + + + Deploy Packages + + + + + + + + + + Create Package + + + + + + + + + + Deploy Manifest + + + + + + + + + + View Vehicles + + + + + + + + + + Add Vehicle + + + + + + + + + + Send Command + + + + + + + + + + + + + + + + + Search + + + + + + + + + + + + + + + + + + + + + + ID + + sorted ascending + + + + + + + + + Name + + + + + + + + Version + + + + + + + + Created + + + + + + + + Updated + + + + + + + Actions + + + + + + + 1 + + + Test Manifest + + + 1.0 + + + 7/1/2021 10:40:07 PM + + + 7/12/2021 6:22:13 PM + + + + + + + + + + + + + + + + + + + + + + + + + + + Rows per page: + + + + + 5 + + + 10 + + + 25 + + + 100 + + + + + + + + 1-1 of 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`App Route /manifests unauthenticated 1`] = ` + + + + + + + + + + + + + + + Sign In + + + + + + + + + +`; + +exports[`App Route /package-upload authenticated 1`] = ` + + + + + + + Create Update Package + + + + Sign Out + + + + + + + + + + + + + + + + + Home + + + + + + + + + + Dashboard + + + + + + + + + + Deploy Packages + + + + + + + + + + Create Package + + + + + + + + + + Deploy Manifest + + + + + + + + + + View Vehicles + + + + + + + + + + Add Vehicle + + + + + + + + + + Send Command + + + + + + + + + + @@ -2334,11 +4450,11 @@ exports[`App Route /package-upload authenticated 1`] = ` data-testid="mocked-fileuploadprovider" > Package name @@ -2420,10 +4536,10 @@ exports[`App Route /package-upload authenticated 1`] = ` /> Version @@ -2466,10 +4582,10 @@ exports[`App Route /package-upload authenticated 1`] = ` /> Description @@ -2513,10 +4629,10 @@ exports[`App Route /package-upload authenticated 1`] = ` /> Release Notes URL @@ -2558,7 +4674,7 @@ exports[`App Route /package-upload authenticated 1`] = ` @@ -2642,10 +4758,10 @@ exports[`App Route /page-not-found authenticated 1`] = ` data-testid="mocked-userprovider" > @@ -2670,17 +4786,17 @@ exports[`App Route /page-not-found authenticated 1`] = ` @@ -2778,6 +4894,28 @@ exports[`App Route /page-not-found authenticated 1`] = ` /> + + + + + Deploy Manifest + + + + + @@ -2952,17 +5090,17 @@ exports[`App Route /updates authenticated 1`] = ` @@ -3060,6 +5198,28 @@ exports[`App Route /updates authenticated 1`] = ` /> + + + + + Deploy Manifest + + + + + ID sorted descending @@ -3338,7 +5498,7 @@ exports[`App Route /updates authenticated 1`] = ` - Invalid Date Invalid Date + 7/1/2021 10:40:07 PM @@ -3625,17 +5785,17 @@ exports[`App Route /vehicle-add authenticated 1`] = ` @@ -3733,6 +5893,28 @@ exports[`App Route /vehicle-add authenticated 1`] = ` /> + + + + + Deploy Manifest + + + + + VIN @@ -3901,10 +6083,10 @@ exports[`App Route /vehicle-add authenticated 1`] = ` /> Model @@ -3948,10 +6130,10 @@ exports[`App Route /vehicle-add authenticated 1`] = ` /> Year @@ -3995,10 +6177,10 @@ exports[`App Route /vehicle-add authenticated 1`] = ` /> Trim @@ -4009,7 +6191,7 @@ exports[`App Route /vehicle-add authenticated 1`] = ` @@ -4093,10 +6275,10 @@ exports[`App Route /vehicle-status authenticated 1`] = ` data-testid="mocked-userprovider" > @@ -4123,17 +6305,17 @@ exports[`App Route /vehicle-status authenticated 1`] = ` @@ -4231,6 +6413,28 @@ exports[`App Route /vehicle-status authenticated 1`] = ` /> + + + + + Deploy Manifest + + + + + ID sorted descending @@ -4651,10 +6855,10 @@ exports[`App Route /vehicles authenticated 1`] = ` data-testid="mocked-userprovider" > @@ -4681,17 +6885,17 @@ exports[`App Route /vehicles authenticated 1`] = ` @@ -4789,6 +6993,28 @@ exports[`App Route /vehicles authenticated 1`] = ` /> + + + + + Deploy Manifest + + + + + VIN sorted ascending @@ -5282,10 +7508,10 @@ exports[`App Route /vehicles-command authenticated 1`] = ` data-testid="mocked-userprovider" > @@ -5312,17 +7538,17 @@ exports[`App Route /vehicles-command authenticated 1`] = ` @@ -5420,6 +7646,28 @@ exports[`App Route /vehicles-command authenticated 1`] = ` /> + + + + + Deploy Manifest + + + + + 0 Selected @@ -5572,10 +7820,10 @@ exports[`App Route /vehicles-command authenticated 1`] = ` style="text-align: right;" > @@ -5652,7 +7900,7 @@ exports[`App Route /vehicles-command authenticated 1`] = ` @@ -5749,7 +7997,7 @@ exports[`App Route /vehicles-command authenticated 1`] = ` VIN sorted ascending diff --git a/src/components/Cars/CarSelectionTable/index.jsx b/src/components/Cars/CarSelectionTable/index.jsx index b37d2ba..aba716e 100644 --- a/src/components/Cars/CarSelectionTable/index.jsx +++ b/src/components/Cars/CarSelectionTable/index.jsx @@ -17,6 +17,7 @@ import { LocalDateTimeString } from "../../../utils/dates"; import TableHeaderSortable from "../../Table/HeaderSortable"; import { logger } from "../../../services/monitoring"; import ConnectedIcon from "../../Controls/ConnectedIcon"; +import ECUList from "../../Controls/ECUList"; const tableColumns = [ { @@ -53,7 +54,7 @@ const CarSelectionTable = (props) => { const [order, setOrder] = useState("asc"); const { getVehicles, vehicles, totalVehicles } = useVehicleContext(); const { setMessage } = useStatusContext(); - + const { search: searchTerm } = search; const sortHandler = (event, property) => { if (property === orderBy) { if (order === "asc") { @@ -143,6 +144,12 @@ const CarSelectionTable = (props) => { style={{ marginRight: 5 }} /> {row.vin} + {row.ecu_list && ( + <> + + + > + )} {row.model} {row.year} diff --git a/src/components/Cars/List/index.jsx b/src/components/Cars/List/index.jsx index 957ebf7..f278851 100644 --- a/src/components/Cars/List/index.jsx +++ b/src/components/Cars/List/index.jsx @@ -22,6 +22,7 @@ import TableHeaderSortable from "../../Table/HeaderSortable"; import SearchField from "../../Controls/SearchField"; import { logger } from "../../../services/monitoring"; import ConnectedIcon from "../../Controls/ConnectedIcon"; +import ECUList from "../../Controls/ECUList"; const tableColumns = [ { @@ -138,6 +139,12 @@ const MainForm = () => { style={{ marginRight: 5 }} /> {row.vin} + {row.ecu_list && ( + <> + + + > + )} {row.model} {row.year} diff --git a/src/components/Contexts/CarUpdatesContext.jsx b/src/components/Contexts/CarUpdatesContext.jsx new file mode 100644 index 0000000..33f1f04 --- /dev/null +++ b/src/components/Contexts/CarUpdatesContext.jsx @@ -0,0 +1,176 @@ +import React, { useContext, useState } from "react"; + +import api from "../../services/updates"; + +const CarUpdatesContext = React.createContext(); + +const validateDeployCarUpdates = (data) => { + if (data === null) { + throw new Error("No car update data"); + } + + if (!data.manifest_id || data.manifest_id === 0) { + throw new Error("Manifest id required"); + } + + if (!data.vins || data.vins.length === 0) { + throw new Error("Cars are required"); + } +}; + +export const CarUpdatesProvider = ({ children }) => { + const [busy, setBusy] = useState(false); + const [carUpdates, setCarUpdates] = useState([]); + const [totalCarUpdates, setTotalCarUpdates] = useState(0); + const [delayCount, setDelayCount] = useState(0); + let progressTimer = 0; + + const deployCarUpdates = async (data, token) => { + let result; + + try { + setBusy(true); + validateDeployCarUpdates(data); + result = await api.createCarUpdates(data, token); + if (result.error) + throw new Error(`Deploy car updates error. ${result.message}`); + } finally { + setBusy(false); + } + + return result; + }; + + const getCarUpdates = async (search, token) => { + let result; + + try { + setBusy(true); + result = await api.getCarUpdates(search, token); + if (result.error) + throw new Error(`Get car updates error. ${result.message}`); + setCarUpdates(result.data); + if (search && search.offset === 0 && result.total) { + setTotalCarUpdates(result.total); + } + } finally { + setBusy(false); + } + + return result; + }; + + const getVINUpdates = async (vin, token) => { + let result; + + try { + setBusy(true); + result = await api.getVINUpdates(vin, token); + if (result.error) + throw new Error(`Get VIN updates error. ${result.message}`); + } finally { + setBusy(false); + } + + return result; + }; + + const applyProgressStatus = (item, status) => { + if (status.msg === "DONE") { + delete item.progress; + item.status = "downloaded"; + } else if (status.msg === "downloading" && status.total > 0) { + let progress = Math.floor((100 * status.bytes) / status.total); + if (progress > 99) progress = 0; + item.progress = progress; + item.status = `downloading ${progress}%`; + } else if (status.error > 0) { + item.status = "download error"; + } else { + item.status = "downloading"; + } + }; + + const applyProgressStatuses = (statuses) => { + let items = JSON.parse(JSON.stringify(carUpdates)); + + statuses.forEach((status) => { + let item = items.find((item) => status.id === item.id); + if (!item || status.id === 0) return; + applyProgressStatus(item, status); + }); + + setCarUpdates(items); + }; + + const updateStatusProgress = async (token) => { + stopMonitor(); + + if (!token || carUpdates.length === 0) return; + + try { + setBusy(true); + const carupdateids = carUpdates.reduce((accum, update) => { + if (update.status !== "downloaded") accum.push(update.id); + return accum; + }, []); + if (carupdateids.length === 0) return; + + const result = await api.getCarUpdateProgress( + carupdateids.join(","), + token + ); + if (result.error) + throw new Error(`Get update progress error. ${result.message}`); + + applyProgressStatuses(result.statuses); + } catch (e) { + } finally { + setBusy(false); + } + }; + + const getDelay = () => { + if (delayCount < 3) { + setDelayCount(delayCount + 1); + return 1000; + } + for (let i = 0, len = carUpdates.length; i < len; i++) { + if (carUpdates[i].status.indexOf("downloading") > -1) return 1000; + } + return 10000; + }; + + const startMonitor = async (token) => { + const delay = getDelay(); + stopMonitor(); + progressTimer = setTimeout(() => { + updateStatusProgress(token); + }, delay); + }; + + const stopMonitor = async () => { + if (progressTimer === 0) return; + clearTimeout(progressTimer); + progressTimer = 0; + }; + + return ( + + {children} + + ); +}; + +export const useCarUpdatesContext = () => useContext(CarUpdatesContext); diff --git a/src/components/Contexts/ManifestsContext.jsx b/src/components/Contexts/ManifestsContext.jsx new file mode 100644 index 0000000..f4f5274 --- /dev/null +++ b/src/components/Contexts/ManifestsContext.jsx @@ -0,0 +1,66 @@ +import React, { useContext, useState } from "react"; + +import api from "../../services/manifests"; + +const ManifestsContext = React.createContext(); + +export const ManifestsProvider = ({ children }) => { + const [busy, setBusy] = useState(false); + const [manifests, setManifests] = useState([]); + const [totalManifests, setTotalManifests] = useState(0); + + const getManifests = async (search, token) => { + let result; + + try { + setBusy(true); + result = await api.getManifests(search, token); + if (result.error) + throw new Error(`Get manifests error. ${result.message}`); + setManifests(result.data); + if (search && search.offset === 0 && result.total) { + setTotalManifests(result.total); + } + } finally { + setBusy(false); + } + + return result; + }; + + const deleteManifest = async (package_id, token) => { + let result; + + const index = manifests.findIndex((element) => { + return element.id === package_id; + }); + manifests.splice(index, 1); + + try { + setBusy(true); + result = await api.deleteManifest(package_id, token); + if (result.error) + throw new Error(`Delete manifest error. ${result.message}`); + } finally { + setBusy(false); + } + + return result; + }; + + return ( + + {children} + + ); +}; + +export const useManifestsContext = () => useContext(ManifestsContext); diff --git a/src/components/Contexts/UpdatesContext.test.jsx b/src/components/Contexts/UpdatesContext.test.jsx index bf033ad..29f63e2 100644 --- a/src/components/Contexts/UpdatesContext.test.jsx +++ b/src/components/Contexts/UpdatesContext.test.jsx @@ -13,7 +13,7 @@ import { TEST_AUTH_OBJECT } from "../../utils/testing"; describe("UpdatesContext", () => { describe("getPackages", () => { - const expectedData = `[{"id":1,"package_name":"Test","version":"1.0","link":"http://cloudfront.com/download"},{"id":2,"package_name":"Test","version":"1.1","link":"http://cloudfront.com/download"},{"id":3,"package_name":"Test","version":"1.2","link":"http://cloudfront.com/download"}]`; + const expectedData = `[{"id":1,"package_name":"Test","version":"1.0","link":"http://cloudfront.com/download","ecu_list":"ECU1 1.0.0,ECU2 1.0.2"},{"id":2,"package_name":"Test","version":"1.1","link":"http://cloudfront.com/download","ecu_list":"ECU1 1.0.1,ECU2 1.0.2"},{"id":3,"package_name":"Test","version":"1.2","link":"http://cloudfront.com/download","ecu_list":"ECU1 1.1.0,ECU2 1.1.2"}]`; const checkState = (busy, packages, message) => { expect(screen.getByTestId("busy").innerHTML).toEqual(busy); expect(screen.getByTestId("packages").innerHTML).toEqual(packages); diff --git a/src/components/Contexts/VehicleContext.test.jsx b/src/components/Contexts/VehicleContext.test.jsx index d45e782..c91be6e 100644 --- a/src/components/Contexts/VehicleContext.test.jsx +++ b/src/components/Contexts/VehicleContext.test.jsx @@ -134,11 +134,30 @@ describe("VehicleContext", () => { }); const expectedVehicleData = [ - { vin: "3C4PDCBG0ET127145", connected: true }, + { + vin: "3C4PDCBG0ET127145", + year: 2021, + model: "Ocean", + trim: "Basic", + ecu_list: "ECUA 2.0.0, ECUB 2.1.1", + connected: true, + }, { vin: "1G1FP87S3GN100062", connected: true }, - { vin: "1HGCG325XYA062256", connected: true }, - { vin: "1J4GZ78YXWC160024", connected: true }, - { vin: "2C3CCAAG8CH222800", connected: true }, - { vin: "KNADM4A39C6028108", connected: true }, - { vin: "1G11C5SL9FF153507", connected: true }, + { vin: "1HGCG325XYA062256", year: 2021, connected: true }, + { vin: "1J4GZ78YXWC160024", year: 2021, model: "Ocean", connected: true }, + { vin: "2C3CCAAG8CH222800", model: "Ocean", trim: "Basic", connected: true }, + { + vin: "KNADM4A39C6028108", + year: 2021, + model: "Ocean", + trim: "Basic", + connected: true, + }, + { + vin: "1G11C5SL9FF153507", + year: 2021, + model: "Ocean", + trim: "Basic", + connected: true, + }, ]; diff --git a/src/components/Contexts/__mocks__/CarUpdatesContext.jsx b/src/components/Contexts/__mocks__/CarUpdatesContext.jsx new file mode 100644 index 0000000..abc3cff --- /dev/null +++ b/src/components/Contexts/__mocks__/CarUpdatesContext.jsx @@ -0,0 +1,31 @@ +import React from "react"; + +const CarUpdatesContext = React.createContext(); + +let busy = false; +let carUpdates = [ + { + id: 1, + vin: "1G1FP87S3GN100062", + updatepackage_id: 18, + status: "downloaded", + created: "2021-07-01T22:40:07.778509Z", + updated: "2021-07-12T18:22:13.736755Z", + }, +]; +let totalCarUpdates = 1; + +export const CarUpdatesProvider = ({ children }) => { + return {children}; +}; + +export const useCarUpdatesContext = () => ({ + busy, + carUpdates, + totalCarUpdates, + deployCarUpdates: jest.fn((data) => data), + getCarUpdates: jest.fn(() => carUpdates), + getVINUpdates: jest.fn(() => carUpdates), + startMonitor: jest.fn(), + stopMonitor: jest.fn(), +}); diff --git a/src/components/Contexts/__mocks__/ManifestsContext.jsx b/src/components/Contexts/__mocks__/ManifestsContext.jsx new file mode 100644 index 0000000..057eccc --- /dev/null +++ b/src/components/Contexts/__mocks__/ManifestsContext.jsx @@ -0,0 +1,27 @@ +import React from "react"; + +const ManifestsContext = React.createContext(); + +let busy = false; +let manifests = [ + { + id: 1, + name: "Test Manifest", + version: "1.0", + created: "2021-07-01T22:40:07.778509Z", + updated: "2021-07-12T18:22:13.736755Z", + }, +]; +let totalManifests = 1; + +export const ManifestsProvider = ({ children }) => { + return {children}; +}; + +export const useManifestsContext = () => ({ + busy, + manifests, + totalManifests, + getManifests: jest.fn(() => manifests), + deleteManifest: jest.fn(), +}); diff --git a/src/components/Contexts/__mocks__/UpdatesContext.jsx b/src/components/Contexts/__mocks__/UpdatesContext.jsx index 054d6a8..3bf7a0e 100644 --- a/src/components/Contexts/__mocks__/UpdatesContext.jsx +++ b/src/components/Contexts/__mocks__/UpdatesContext.jsx @@ -10,9 +10,11 @@ const examplePackage = { version: "1.0", desc: "Description", release_notes: "https://www.google.com/", - created: Date.now().toString(), + timestamp: 1625615299, + created: "2021-07-01T22:40:07.778509Z", + updated: "2021-07-12T18:22:13.736755Z", }; -packages.push(examplePackage) +packages.push(examplePackage); let totalPackages = 0; let carUpdates = []; let totalCarUpdates = 0; diff --git a/src/components/Controls/ECUList/index.jsx b/src/components/Controls/ECUList/index.jsx new file mode 100644 index 0000000..fee3543 --- /dev/null +++ b/src/components/Controls/ECUList/index.jsx @@ -0,0 +1,34 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Chip } from "@material-ui/core"; + +const ECUList = ({ list, delimiter, search }) => { + if (!list) return null; + if (!delimiter) delimiter = ","; + + const items = list.split(delimiter); + + return items.map((item, index) => { + const match = search + ? item.toLowerCase().split(" ").indexOf(search.toLowerCase()) + : -1; + return ( + -1 ? "default" : "outlined"} + color={match > -1 ? "primary" : "default"} + style={{ margin: 1 }} + /> + ); + }); +}; + +ECUList.propTypes = { + list: PropTypes.string, + delimiter: PropTypes.string, + search: PropTypes.string, +}; + +export default ECUList; diff --git a/src/components/Layouts/SideMenu.jsx b/src/components/Layouts/SideMenu.jsx index 5b1ee7a..99ab277 100644 --- a/src/components/Layouts/SideMenu.jsx +++ b/src/components/Layouts/SideMenu.jsx @@ -26,6 +26,11 @@ const menuData = [ to: "/package-upload", roles: [Roles.CREATE], }, + { + label: "Deploy Manifest", + to: "/manifests", + roles: [Roles.CREATE, Roles.READ], + }, { label: "View Vehicles", to: "/vehicles", diff --git a/src/components/Layouts/__snapshots__/SideMenu.test.jsx.snap b/src/components/Layouts/__snapshots__/SideMenu.test.jsx.snap index e5897f8..ecb628b 100644 --- a/src/components/Layouts/__snapshots__/SideMenu.test.jsx.snap +++ b/src/components/Layouts/__snapshots__/SideMenu.test.jsx.snap @@ -96,6 +96,28 @@ exports[`SideMenu Authenticated 1`] = ` /> + + + + + Deploy Manifest + + + + + { + const { manifest_id } = useParams(); + const { getManifests, manifests, busy } = useManifestsContext(); + const { deployCarUpdates } = useCarUpdatesContext(); + const { + token: { + idToken: { jwtToken: token }, + }, + } = useUserContext(); + const { setMessage, setTitle } = useStatusContext(); + const [manifestName, setManifestName] = useState(""); + const [version, setVersion] = useState(""); + const [createDate, setCreateDate] = useState(""); + const [selected, setSelected] = useState([]); + const [search, setSearch] = useState(""); + const [redirect, setRedirect] = useState(""); + const classes = useStyles(); + + const handleSearch = (search) => { + setSelected([]); + setSearch(search); + }; + + const handleSelectAll = (cars) => { + setSelected(cars); + }; + + const handleSelect = (event, key) => { + try { + let newSelected; + if (event.target.checked) { + newSelected = [...selected]; + newSelected.push(key); + } else { + newSelected = selected.filter((vin) => vin !== key); + } + setSelected(newSelected); + } catch (e) { + logger.warn(e.stack); + } + }; + + const onSubmit = async (event) => { + try { + event.preventDefault(); + const data = { + manifest_id: parseInt(manifest_id), + vins: selected, + }; + await deployCarUpdates(data, token); + setMessage( + `Deployed ${manifestName} ${version} to ${selected.length} cars` + ); + setRedirect(`/manifest-status/${manifest_id}`); + } catch (e) { + setMessage(e.message); + logger.warn(e.stack); + } + }; + + const getData = async () => { + try { + getManifests({ id: parseInt(manifest_id) }, token); + } catch (e) { + setMessage(e.message); + logger.warn(e.stack); + } + }; + + useEffect(() => { + getData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [token]); + + useEffect(() => { + setTitle(`Deploy ${manifestName} ${version}`); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [manifestName, version]); + + useEffect(() => { + if (!manifests || manifests.length === 0) return; + var data = manifests[0]; + + setManifestName(data.name); + setVersion(data.version); + setCreateDate(LocalDateTimeString(data.created)); + }, [manifests]); + + if (redirect.length > 0) { + return ; + } + + return ( + + + Created {createDate}. + + + + {`${selected.length} Selected`} + + + + {busy ? "Deploying..." : "Deploy"} + + + + + + + ); +}; + +const ManifestDeployForm = () => ( + + + + + + + +); + +export default ManifestDeployForm; diff --git a/src/components/Manifest/List/index.jsx b/src/components/Manifest/List/index.jsx new file mode 100644 index 0000000..2b3b619 --- /dev/null +++ b/src/components/Manifest/List/index.jsx @@ -0,0 +1,249 @@ +import React, { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { + Table, + TableBody, + TableCell, + TableFooter, + TablePagination, + TableRow, + Toolbar, + Tooltip, +} from "@material-ui/core"; +import SendIcon from "@material-ui/icons/Send"; +import VisibilityIcon from "@material-ui/icons/Visibility"; +import DeleteIcon from "@material-ui/icons/Delete"; + +import { + useManifestsContext, + ManifestsProvider, +} from "../../Contexts/ManifestsContext"; +import { useUserContext } from "../../Contexts/UserContext"; +import { useStatusContext } from "../../Contexts/StatusContext"; +import useStyles from "../../useStyles"; +import { LocalDateTimeString } from "../../../utils/dates"; +import TableHeaderSortable from "../../Table/HeaderSortable"; +import SearchField from "../../Controls/SearchField"; +import { logger } from "../../../services/monitoring"; +import ECUList from "../../Controls/ECUList"; +import { Roles, hasRole } from "../../../utils/roles"; + +const tableColumns = [ + { + id: "id", + label: "ID", + }, + { + id: "name", + label: "Name", + }, + { + id: "version", + label: "Version", + }, + { + id: "created_at", + label: "Created", + }, + { + id: "updated_at", + label: "Updated", + }, + { + id: "", + label: "Actions", + }, +]; + +const MainForm = () => { + const classes = useStyles(); + const [pageSize, setPageSize] = useState(10); + const [pageIndex, setPageIndex] = useState(0); + const [orderBy, setOrderBy] = useState("id"); + const [order, setOrder] = useState("asc"); + const [search, setSearch] = useState(""); + const { getManifests, deleteManifest, manifests, totalManifests } = + useManifestsContext(); + const { setMessage, setTitle } = useStatusContext(); + const { + token: { + idToken: { jwtToken: token }, + }, + groups, + } = useUserContext(); + + const sortHandler = (event, property) => { + if (property === orderBy) { + if (order === "asc") { + setOrder("desc"); + } else { + setOrder("asc"); + } + } else { + setOrderBy(property); + setOrder("asc"); + } + }; + + useEffect(() => { + setTitle("Deploy Manifest"); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + (async () => { + try { + await getManifests( + { + limit: pageSize, + offset: pageSize * pageIndex, + order: `${orderBy} ${order}`, + search, + }, + token + ); + } catch (e) { + setMessage(e.message); + logger.warn(e.stack); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pageIndex, pageSize, token, orderBy, order, search]); + + const handleChangePageIndex = (event, newIndex) => { + setPageIndex(newIndex); + }; + + const handleChangePageSize = (event) => { + setPageSize(parseInt(event.target.value, 10)); + setPageIndex(0); + }; + + const handleSearch = (search) => { + setSearch(search); + }; + + const onDelete = async (manifest_id) => { + try { + await deleteManifest(parseInt(manifest_id), token); + } catch (e) { + setMessage(e.message); + logger.warn(e.stack); + } + }; + + const Actions = (row) => { + let actions = []; + if (hasRole([Roles.CREATE, Roles.READ], groups)) { + actions.push({ + tip: `Status "${row.name} ${row.version}"`, + link: `/manifest-status/${row.id}`, + icon: ( + + ), + }); + } + if (hasRole([Roles.CREATE], groups)) { + actions = actions.concat([ + { + tip: `Deploy "${row.name} ${row.version}"`, + link: `/manifest-deploy/${row.id}`, + icon: , + }, + { + tip: `Delete "${row.name} ${row.version}"`, + id: row.id, + icon: , + }, + ]); + } + + if (actions.length === 0) return "No actions"; + + return actions.map((action) => { + if (action.link != null) { + return ( + + + {action.icon} + + + ); + } else { + return ( + + onDelete(action.id)}> + {action.icon} + + + ); + } + }); + }; + + return ( + + + + + + + + {manifests.map((row) => ( + + {row.id} + + {row.name} + {row.ecu_list && ( + <> + + + > + )} + + {row.version} + + {LocalDateTimeString(row.created)} + + + {LocalDateTimeString(row.updated)} + + {Actions(row)} + + ))} + + + + + + + + + ); +}; + +const ManifestsList = () => ( + + + +); + +export default ManifestsList; diff --git a/src/components/Manifest/Status/index.jsx b/src/components/Manifest/Status/index.jsx new file mode 100644 index 0000000..95a611c --- /dev/null +++ b/src/components/Manifest/Status/index.jsx @@ -0,0 +1,174 @@ +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router"; +import { Link } from "react-router-dom"; +import { + LinearProgress, + Table, + TableBody, + TableCell, + TableFooter, + TableHead, + TablePagination, + TableRow, +} from "@material-ui/core"; + +import { + ManifestsProvider, + useManifestsContext, +} from "../../Contexts/ManifestsContext"; +import { + CarUpdatesProvider, + useCarUpdatesContext, +} from "../../Contexts/CarUpdatesContext"; +import { useUserContext } from "../../Contexts/UserContext"; +import { useStatusContext } from "../../Contexts/StatusContext"; +import useStyles from "../../useStyles"; +import { LocalDateTimeString } from "../../../utils/dates"; +import { logger } from "../../../services/monitoring"; + +const MainForm = () => { + const { manifest_id } = useParams(); + const classes = useStyles(); + const [pageSize, setPageSize] = useState(10); + const [pageIndex, setPageIndex] = useState(0); + const { getManifests, manifests } = useManifestsContext(); + const { + getCarUpdates, + carUpdates, + totalCarUpdates, + startMonitor, + stopMonitor, + } = useCarUpdatesContext(); + const { setMessage, setTitle } = useStatusContext(); + const { + token: { + idToken: { jwtToken: token }, + }, + } = useUserContext(); + + useEffect(() => { + (async () => { + try { + await getManifests({ id: manifest_id }, token); + } catch (e) { + setMessage(e.message); + logger.warn(e.stack); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [token]); + + useEffect(() => { + if (!manifests || manifests.length === 0) return; + setTitle(`Manifest ${manifests[0].name} ${manifests[0].version}`); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [manifests]); + + useEffect(() => { + (async () => { + try { + stopMonitor(); + await getCarUpdates( + { + manifest_id, + limit: pageSize, + offset: pageSize * pageIndex, + }, + token + ); + } catch (e) { + setMessage(e.message); + logger.warn(e.stack); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pageIndex, pageSize, token]); + + useEffect(() => { + try { + if (carUpdates.length === 0) return; + startMonitor(token); + } catch (e) { + setMessage(e.message); + logger.warn(e.stack); + } + return () => { + stopMonitor(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [carUpdates]); + + const handleChangePageIndex = (event, newIndex) => { + setPageIndex(newIndex); + }; + + const handleChangePageSize = (event) => { + setPageSize(parseInt(event.target.value, 10)); + setPageIndex(0); + }; + + return ( + + + + + ID + Vehicle + Status + Created + Updated + + + + {carUpdates.map((row) => ( + + {row.id} + + {row.vin} + + + {row.status} + {row.progress > 0 && ( + + )} + + + {LocalDateTimeString(row.created)} + + + {LocalDateTimeString(row.updated)} + + + ))} + + + + + + + + + ); +}; + +const ManifestStatus = () => ( + + + + + +); + +export default ManifestStatus; diff --git a/src/components/Routes/SiteRoutes.jsx b/src/components/Routes/SiteRoutes.jsx index 190ed66..eaa77f7 100644 --- a/src/components/Routes/SiteRoutes.jsx +++ b/src/components/Routes/SiteRoutes.jsx @@ -19,6 +19,9 @@ const CarUpdates = React.lazy(() => import("../Cars/Status")); const VehiclesList = React.lazy(() => import("../Cars/List")); const SendCommandBulk = React.lazy(() => import("../Cars/SendCommandBulk")); const Dashboard = React.lazy(() => import("../Dashboard")); +const Manifests = React.lazy(() => import("../Manifest/List")); +const ManifestDeploy = React.lazy(() => import("../Manifest/Deploy")); +const ManifestStatus = React.lazy(() => import("../Manifest/Status")); const SiteRoutes = () => { const { token, groups } = useUserContext(); @@ -119,6 +122,30 @@ const SiteRoutes = () => { groups={groups} roles={[Roles.READ, Roles.CREATE]} /> + } + type={TYPES.PROTECTED} + token={token} + groups={groups} + roles={[Roles.READ, Roles.CREATE]} + /> + } + type={TYPES.PROTECTED} + token={token} + groups={groups} + roles={[Roles.CREATE]} + /> + } + type={TYPES.PROTECTED} + token={token} + groups={groups} + roles={[Roles.READ, Roles.CREATE]} + /> diff --git a/src/components/UpdatePackages/List/index.jsx b/src/components/UpdatePackages/List/index.jsx index 4b46b3a..724edb8 100644 --- a/src/components/UpdatePackages/List/index.jsx +++ b/src/components/UpdatePackages/List/index.jsx @@ -25,6 +25,7 @@ import { Roles, hasRole } from "../../../utils/roles"; import TableHeaderSortable from "../../Table/HeaderSortable"; import SearchField from "../../Controls/SearchField"; import { logger } from "../../../services/monitoring"; +import ECUList from "../../Controls/ECUList"; const tableColumns = [ { @@ -195,7 +196,15 @@ const UpdatePackagesList = () => { {packages.map((row) => ( {row.id} - {row.package_name} + + {row.package_name} + {row.ecu_list && ( + <> + + + > + )} + {row.version} {LocalDateTimeString(row.created)} diff --git a/src/services/__mocks__/manifests.js b/src/services/__mocks__/manifests.js new file mode 100644 index 0000000..aeff668 --- /dev/null +++ b/src/services/__mocks__/manifests.js @@ -0,0 +1,59 @@ + + +const updatesAPI = { + createCarUpdates: async (data, token) => { + if (!data.id) data.id = 0; + data.id++; + return data; + }, + + getPackages: async (search, token) => { + return { + data: [ + { + id: 1, + package_name: "Test", + version: "1.0", + link: "http://cloudfront.com/download", + ecu_list: "ECU1 1.0.0,ECU2 1.0.2", + }, + { + id: 2, + package_name: "Test", + version: "1.1", + link: "http://cloudfront.com/download", + ecu_list: "ECU1 1.0.1,ECU2 1.0.2", + }, + { + id: 3, + package_name: "Test", + version: "1.2", + link: "http://cloudfront.com/download", + ecu_list: "ECU1 1.1.0,ECU2 1.1.2", + } + ] + } + }, + + updatePackage: async (data, token) => { + return data; + }, + + deployPackage: async (data, token) => { + return data; + }, + + getCarUpdates: async (filter, token) => { + return { data: [] }; + }, + + getVINUpdates: async (vin, token) => { + return { data: [] }; + }, + + getCarUpdateProgress: async (carupdateids, token) => { + return { statuses: [] }; + }, +}; + +export default updatesAPI; diff --git a/src/services/__mocks__/updates.js b/src/services/__mocks__/updates.js index 7da9b71..aeff668 100644 --- a/src/services/__mocks__/updates.js +++ b/src/services/__mocks__/updates.js @@ -14,19 +14,22 @@ const updatesAPI = { id: 1, package_name: "Test", version: "1.0", - link: "http://cloudfront.com/download" + link: "http://cloudfront.com/download", + ecu_list: "ECU1 1.0.0,ECU2 1.0.2", }, { id: 2, package_name: "Test", version: "1.1", - link: "http://cloudfront.com/download" + link: "http://cloudfront.com/download", + ecu_list: "ECU1 1.0.1,ECU2 1.0.2", }, { id: 3, package_name: "Test", version: "1.2", - link: "http://cloudfront.com/download" + link: "http://cloudfront.com/download", + ecu_list: "ECU1 1.1.0,ECU2 1.1.2", } ] } diff --git a/src/services/__mocks__/vehicles.js b/src/services/__mocks__/vehicles.js index 1f6662b..952b684 100644 --- a/src/services/__mocks__/vehicles.js +++ b/src/services/__mocks__/vehicles.js @@ -1,12 +1,12 @@ const data = [ - { vin: "3C4PDCBG0ET127145" }, + { vin: "3C4PDCBG0ET127145", year: 2021, model: "Ocean", trim: "Basic", ecu_list: "ECUA 2.0.0, ECUB 2.1.1" }, { vin: "1G1FP87S3GN100062" }, - { vin: "1HGCG325XYA062256" }, - { vin: "1J4GZ78YXWC160024" }, - { vin: "2C3CCAAG8CH222800" }, - { vin: "KNADM4A39C6028108" }, - { vin: "1G11C5SL9FF153507" }, + { vin: "1HGCG325XYA062256", year: 2021 }, + { vin: "1J4GZ78YXWC160024", year: 2021, model: "Ocean" }, + { vin: "2C3CCAAG8CH222800", model: "Ocean", trim: "Basic" }, + { vin: "KNADM4A39C6028108", year: 2021, model: "Ocean", trim: "Basic" }, + { vin: "1G11C5SL9FF153507", year: 2021, model: "Ocean", trim: "Basic" }, ]; const vehiclesAPI = { diff --git a/src/services/manifests.js b/src/services/manifests.js new file mode 100644 index 0000000..cc1abb1 --- /dev/null +++ b/src/services/manifests.js @@ -0,0 +1,23 @@ +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 manifestsAPI = { + deleteManifest: async (manifest_id, token) => fetch(`${API_ENDPOINT}/manifest?id=${manifest_id}`, { + method: "DELETE", + headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)), + }) + .then(fetchRespHandler), + + getManifests: async (search, token) => { + var u = addQueryParams(`${API_ENDPOINT}/manifests`, search); + return fetch(u, { + method: "GET", + headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)), + + }) + .then(fetchRespHandler); + }, +}; + +export default manifestsAPI; diff --git a/testEnv.js b/testEnv.js new file mode 100644 index 0000000..d222e71 --- /dev/null +++ b/testEnv.js @@ -0,0 +1,3 @@ +module.exports = async () => { + process.env.TZ = 'UTC'; +}; \ No newline at end of file
Created - Invalid Date Invalid Date + 7/6/2021 11:48:19 PM . Description
+ Created + 7/1/2021 10:40:07 PM + . +
+ Rows per page: +
+ 0-0 of 0 +
+ 1-1 of 1 +