From f73b5125a36a5890b8b2e20dbc956ccce8865fda Mon Sep 17 00:00:00 2001 From: Tristan Timblin Date: Wed, 21 Jun 2023 23:28:53 -0400 Subject: [PATCH 01/15] CEC-4594: add bulk actions to fleet (#368) * CEC-4594: add bulk actions to fleet * add reject case --- src/components/BulkActions/index.jsx | 84 +++++++++++++++++++ src/components/BulkActions/useAddTags.js | 22 +++++ src/components/BulkActions/useUpdateConfig.js | 40 +++++++++ .../Controls/DropDownButton/index.jsx | 4 + .../Details/__snapshots__/index.test.jsx.snap | 42 ++++++++++ .../Fleets/Status/Details/index.jsx | 4 + .../__snapshots__/DetailsTab.test.jsx.snap | 42 ++++++++++ .../Status/__snapshots__/index.test.jsx.snap | 42 ++++++++++ src/components/TransformModal/index.jsx | 2 +- 9 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 src/components/BulkActions/index.jsx create mode 100644 src/components/BulkActions/useAddTags.js create mode 100644 src/components/BulkActions/useUpdateConfig.js diff --git a/src/components/BulkActions/index.jsx b/src/components/BulkActions/index.jsx new file mode 100644 index 0000000..81f5f3c --- /dev/null +++ b/src/components/BulkActions/index.jsx @@ -0,0 +1,84 @@ +import { useEffect, useState } from "react"; +import TransformModal from "../TransformModal"; +import DropDownButton from "../Controls/DropDownButton"; +import { useUserContext } from "../Contexts/UserContext"; +import { useStatusContext } from "../Contexts/StatusContext"; +import useAddTags from "./useAddTags"; +import useUpdateConfig from "./useUpdateConfig"; + +const transformArrayToCSV = (arr) => arr.join(", "); + +export default function BulkActions({ + vins = [], +}) { + const [vinCSV, setVinCSV] = useState(transformArrayToCSV(vins)); + const [active, setActive] = useState(null); + const actions = [ + { + name: "Update Configs", + disabled: vins.length === 0, + trigger: () => setActive("updateConfig"), + }, + { + name: "Add Tags", + disabled: vins.length === 0, + trigger: () => setActive("addTags"), + }, + ]; + + const updateConfig = useUpdateConfig(); + const addTags = useAddTags(); + + const { setMessage } = useStatusContext(); + const { + token: { + idToken: { jwtToken: token }, + }, + } = useUserContext(); + + const handleUpdateConfig = () => { + updateConfig.submit(vins, token) + .then(() => { + setMessage(`${vins.length} vehicles updated.`); + }) + .catch((error) => { + setMessage(error.message); + }); + } + + const handleAddTags = () => { + addTags.submit(vins, token) + .then(() => setMessage(`Added ${addTags.data.tags.value.length} tags to ${vins.length} vehicles.`)) + .catch((error) => setMessage(error.message)); + } + + const handleClose = () => setActive(null); + + useEffect(() => { + setVinCSV(transformArrayToCSV(vins)); + }, [vins]); + + return ( + <> + + + + + ); +} diff --git a/src/components/BulkActions/useAddTags.js b/src/components/BulkActions/useAddTags.js new file mode 100644 index 0000000..7696595 --- /dev/null +++ b/src/components/BulkActions/useAddTags.js @@ -0,0 +1,22 @@ +import { useState } from "react"; +import vehiclesAPI from "../../services/vehiclesAPI"; + +export default function useAddTags() { + const [tags, setTags] = useState({ + tags: { + label: "Tags", + type: "list.string", + value: [], + }, + }); + + const submit = async (vins, token) => { + return vehiclesAPI.addTags(vins, tags.tags.value, token); + } + + return { + data: tags, + setData: setTags, + submit, + }; +} diff --git a/src/components/BulkActions/useUpdateConfig.js b/src/components/BulkActions/useUpdateConfig.js new file mode 100644 index 0000000..c648bd1 --- /dev/null +++ b/src/components/BulkActions/useUpdateConfig.js @@ -0,0 +1,40 @@ +import { useState } from "react"; +import TaskRunner from "../../utils/taskRunner"; +import vehiclesAPI from "../../services/vehiclesAPI"; + +export default function useUpdateConfig() { + const [config, setConfig] = useState({ + force: { + label: "Force Push", + type: "boolean", + value: false, + }, + }); + + const submit = async (vins, token) => { + return new Promise((resolve, reject) => { + const taskRunner = new TaskRunner(5); + + const task = (vin, isLast) => { + return async () => vehiclesAPI.updateConfig(vin, config.force.value, token) + .then((response) => { + if (isLast) { + if (response.error) { + reject(response); + } + resolve(response) + } + }) + .catch((error) => reject(error)); + } + + vins.forEach((vin, index) => taskRunner.push(task(vin, index === vins.length - 1))); + }); + } + + return { + data: config, + setData: setConfig, + submit, + }; +} \ No newline at end of file diff --git a/src/components/Controls/DropDownButton/index.jsx b/src/components/Controls/DropDownButton/index.jsx index 77a5562..6820877 100644 --- a/src/components/Controls/DropDownButton/index.jsx +++ b/src/components/Controls/DropDownButton/index.jsx @@ -40,6 +40,10 @@ const DropDownButton = ({ actions = [], payload = [] }) => { setOpen(false); }; + if (!actions.length) { + return <>; + } + return ( <> +
+
+ + +
+
diff --git a/src/components/Fleets/Status/Details/index.jsx b/src/components/Fleets/Status/Details/index.jsx index 8b81643..2926b38 100644 --- a/src/components/Fleets/Status/Details/index.jsx +++ b/src/components/Fleets/Status/Details/index.jsx @@ -15,6 +15,7 @@ import { FleetProvider, useFleetContext } from "../../../Contexts/FleetContext" import useStyles from "../../../useStyles"; import { logger } from "../../../../services/monitoring"; import DeleteConfirmation from "../../../DeleteConfirmation"; +import BulkActions from "../../../BulkActions"; const MainForm = ({ name }) => { const classes = useStyles(); @@ -94,6 +95,9 @@ const MainForm = ({ name }) => { + + + setShowDeleteModal(false)} deleteFunction={onDelete} /> diff --git a/src/components/Fleets/Status/__snapshots__/DetailsTab.test.jsx.snap b/src/components/Fleets/Status/__snapshots__/DetailsTab.test.jsx.snap index b62b7fd..b9ee191 100644 --- a/src/components/Fleets/Status/__snapshots__/DetailsTab.test.jsx.snap +++ b/src/components/Fleets/Status/__snapshots__/DetailsTab.test.jsx.snap @@ -151,6 +151,48 @@ exports[`DetailsTab Render 1`] = ` +
+
+ + +
+
diff --git a/src/components/Fleets/Status/__snapshots__/index.test.jsx.snap b/src/components/Fleets/Status/__snapshots__/index.test.jsx.snap index 19171b8..42fdb41 100644 --- a/src/components/Fleets/Status/__snapshots__/index.test.jsx.snap +++ b/src/components/Fleets/Status/__snapshots__/index.test.jsx.snap @@ -239,6 +239,48 @@ exports[`FleetStatus Render 1`] = ` +
+
+ + +
+
diff --git a/src/components/TransformModal/index.jsx b/src/components/TransformModal/index.jsx index 8a58a0c..ee0b13f 100644 --- a/src/components/TransformModal/index.jsx +++ b/src/components/TransformModal/index.jsx @@ -28,7 +28,7 @@ const TransformModal = ({ const handleChange = (key, value) => { setData((data) => { - const {[key]: toChange, ...rest} = data; + const { [key]: toChange, ...rest } = data; switch (data[key].type) { case "boolean": toChange.value = !toChange.value; From 3d4a07d8d8587317b368957697011d6b881b3a0b Mon Sep 17 00:00:00 2001 From: Paul Adamsen <117673433+pauladamseniii@users.noreply.github.com> Date: Thu, 22 Jun 2023 15:32:30 -0400 Subject: [PATCH 02/15] CEC-4561 - Add in_park to digital twin display (#371) --- .../Status/__snapshots__/DigitalTwinTab.test.jsx.snap | 11 +++++++++++ src/components/Contexts/__mocks__/VehicleContext.jsx | 3 +++ src/components/DigitalTwin/index.js | 9 ++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/Cars/Status/__snapshots__/DigitalTwinTab.test.jsx.snap b/src/components/Cars/Status/__snapshots__/DigitalTwinTab.test.jsx.snap index f12a979..c7e54ae 100644 --- a/src/components/Cars/Status/__snapshots__/DigitalTwinTab.test.jsx.snap +++ b/src/components/Cars/Status/__snapshots__/DigitalTwinTab.test.jsx.snap @@ -237,6 +237,17 @@ exports[`DigitalTwinTab Render 1`] = ` 77.7 km/h

+
+

+ + Parked + + : + Yes +

+
{ if (value || value === 0) return `${value}${units}`; @@ -32,7 +34,7 @@ const windowState = (value) => { const DigitalTwin = (props) => { const classes = useStyles(); - const { battery, doors, location, trex_version, ip, updated, windows, misc_windows, sunroof, dbc_version, door_locks, vcu0x260, charging_metrics, max_range, vehicle_speed } = props; + const { battery, doors, location, trex_version, ip, updated, windows, misc_windows, sunroof, dbc_version, door_locks, vcu0x260, charging_metrics, max_range, vehicle_speed, gear } = props; return (
@@ -133,6 +135,11 @@ const DigitalTwin = (props) => { {keyValueTemplate("Vehicle Speed", appendUnits(vehicle_speed?.speed, " km/h"))}
)} + {gear && ( +
+ {keyValueTemplate("Parked", gear.in_park ? PARKED : NOT_PARKED)} +
+ )}
); }; From fc7c1ea514df9e66ab0a749212351f0d0e039993 Mon Sep 17 00:00:00 2001 From: Eduard Voronkin <116690094+eduardvoronkin@users.noreply.github.com> Date: Fri, 23 Jun 2023 12:38:09 -0700 Subject: [PATCH 03/15] CEC-4517 reset TBOX remote command (#369) * CEC-4517 Diagnostic Commands tab * snapshot --- .../App/__snapshots__/App.test.js.snap | 27 +++- .../Cars/Status/RemoteDiagnosticCommands.jsx | 30 +++++ .../Status/__snapshots__/index.test.jsx.snap | 27 +++- src/components/Cars/Status/index.jsx | 6 + src/components/Contexts/VehicleContext.jsx | 15 ++- .../Controls/SendDiagnosticCommand/index.jsx | 119 ++++++++++++++++++ src/services/vehiclesAPI.js | 15 +++ 7 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 src/components/Cars/Status/RemoteDiagnosticCommands.jsx create mode 100644 src/components/Controls/SendDiagnosticCommand/index.jsx diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap index 5a5eea9..8dc8aac 100644 --- a/src/components/App/__snapshots__/App.test.js.snap +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -11242,7 +11242,7 @@ exports[`App Route /vehicle-status authenticated 1`] = ` - Fleets + Remote Diagnostic Commands + + Fleets + + + + + +
+ + {carState && carState.online ? "ONLINE" : "OFFLINE"} + +
+ + ); +}; + +export default SendDiagnosticCommand; diff --git a/src/services/vehiclesAPI.js b/src/services/vehiclesAPI.js index 3f2a5c2..17736d9 100644 --- a/src/services/vehiclesAPI.js +++ b/src/services/vehiclesAPI.js @@ -173,6 +173,21 @@ const vehiclesAPI = { .then(fetchRespHandler) .catch(errorHandler), + sendDiagnosticCommand: async (vins, command, token) => + fetch(`${API_ENDPOINT}/vehiclediagnosticcommand`, { + method: "POST", + headers: Object.assign( + { "Content-Type": "application/json" }, + getAuthHeaderOptions(token) + ), + body: JSON.stringify({ + vins, + ...command, + }), + }) + .then(fetchRespHandler) + .catch(errorHandler), + updateVehicle: async (vin, vehicle, token) => fetch(`${API_ENDPOINT}/vehicle/${vin}`, { method: "PUT", From ff7b7abadf163d76243d47f138758ce3a5625af7 Mon Sep 17 00:00:00 2001 From: Eduard Voronkin <116690094+eduardvoronkin@users.noreply.github.com> Date: Fri, 23 Jun 2023 12:51:38 -0700 Subject: [PATCH 04/15] CEC-4517 Diagnostic Command hot fix (#373) * CEC-4517 Diagnostic Command hot fix Fix null access. * disable if no car state --- src/components/Controls/SendDiagnosticCommand/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Controls/SendDiagnosticCommand/index.jsx b/src/components/Controls/SendDiagnosticCommand/index.jsx index 1e66d03..85fdd02 100644 --- a/src/components/Controls/SendDiagnosticCommand/index.jsx +++ b/src/components/Controls/SendDiagnosticCommand/index.jsx @@ -103,7 +103,7 @@ const SendDiagnosticCommand = ({ vin, token, classes }) => { color="primary" className={classes.submit} onClick={clickHandler} - disabled={!carState.online} + disabled={!carState ? true : !carState.online} > Send From 787bb122608cd53834666c6fa1adc688eefd87ec Mon Sep 17 00:00:00 2001 From: Paul Adamsen <117673433+pauladamseniii@users.noreply.github.com> Date: Mon, 26 Jun 2023 11:55:38 -0400 Subject: [PATCH 05/15] CEC-4595 - show online status of cars in fleet (#374) * CEC-4595 - show online status of cars in fleet * fix mocking --- src/components/Contexts/FleetContext.jsx | 18 +++++- src/components/Contexts/FleetContext.test.jsx | 19 ++++++- .../Contexts/__mocks__/FleetContext.jsx | 21 ++++++- .../Contexts/__mocks__/VehicleContext.jsx | 12 ++-- .../Table/__snapshots__/index.test.jsx.snap | 57 +------------------ .../Fleets/Status/Vehicles/Table/index.jsx | 20 +++++-- .../__snapshots__/VehiclesTab.test.jsx.snap | 57 +------------------ src/services/__mocks__/vehiclesAPI.js | 1 + 8 files changed, 79 insertions(+), 126 deletions(-) diff --git a/src/components/Contexts/FleetContext.jsx b/src/components/Contexts/FleetContext.jsx index dd62c6d..551a762 100644 --- a/src/components/Contexts/FleetContext.jsx +++ b/src/components/Contexts/FleetContext.jsx @@ -1,5 +1,6 @@ import React, { useContext, useState } from "react"; import api from "../../services/fleetsAPI"; +import vehiclesAPI from "../../services/vehiclesAPI"; import { validateCANID, validateFilter, validateVIN } from "../../utils/validationSupplier"; const FleetContext = React.createContext(); @@ -112,7 +113,22 @@ export const FleetProvider = ({ children }) => { throw new Error(`Get fleet vehicles error. ${result.message}`); } - setFleetVehicles(result.data) + const connectionsResult = await vehiclesAPI.getConnections(result.data, token) + if (result.error) { + setFleetVehicles([]) + throw new Error(`Get vehicles connections error. ${result.message}`); + } + + var cars = [] + result.data.forEach((vin) => { + cars.push({ + vin: vin, + connected: connectionsResult[vin] || false, + connectedHMI: connectionsResult[`2:${vin}`] || false + }) + }) + + setFleetVehicles(cars) if (result.total) { setTotalFleetVehicles(result.total); } diff --git a/src/components/Contexts/FleetContext.test.jsx b/src/components/Contexts/FleetContext.test.jsx index 93b88a1..b3b883d 100644 --- a/src/components/Contexts/FleetContext.test.jsx +++ b/src/components/Contexts/FleetContext.test.jsx @@ -1,4 +1,5 @@ jest.mock("../../services/fleetsAPI"); +jest.mock("../../services/vehiclesAPI"); import { render, @@ -800,9 +801,21 @@ const expectedFleetsData = [ ]; const expectedFleetVehiclesData = [ - "USWESTVIN12345678", - "USWESTVIN12345679", - "USWESTVIN12345670", + { + vin: "USWESTVIN12345678", + connected: true, + connectedHMI: false, + }, + { + vin: "USWESTVIN12345679", + connected: true, + connectedHMI: false, + }, + { + vin: "USWESTVIN12345670", + connected: true, + connectedHMI: false, + }, ]; const expectedFleetCANFiltersData = [ diff --git a/src/components/Contexts/__mocks__/FleetContext.jsx b/src/components/Contexts/__mocks__/FleetContext.jsx index 7c9c613..6b081c2 100644 --- a/src/components/Contexts/__mocks__/FleetContext.jsx +++ b/src/components/Contexts/__mocks__/FleetContext.jsx @@ -62,7 +62,26 @@ export const useFleetContext = () => ({ fleetVehicles, totalFleetVehicles, - getFleetVehicles: jest.fn(), + getFleetVehicles: jest.fn().mockImplementation((name, search, _token) => { + const result = [ + { + vin: "USWESTVIN12345678", + connected: false, + connectedHMI: false + }, + { + vin: "USWESTVIN12345679", + connected: true, + connectedHMI: true + }, + { + vin: "USWESTVIN12345670", + connected: false, + connectedHMI: false + }, + ]; + return Promise.resolve(result); + }), addFleetVehicles: jest.fn(), deleteFleetVehicle: jest.fn(), diff --git a/src/components/Contexts/__mocks__/VehicleContext.jsx b/src/components/Contexts/__mocks__/VehicleContext.jsx index 18cc9b0..52bd9fb 100644 --- a/src/components/Contexts/__mocks__/VehicleContext.jsx +++ b/src/components/Contexts/__mocks__/VehicleContext.jsx @@ -112,10 +112,14 @@ export const useVehicleContext = () => ({ addVehicle: jest.fn(), getConnections: jest .fn().mockImplementation((vins, _token) => { - const result = {}; - vins.forEach((vin) => { - result[vin] = true; - }); + const result = { + "USWESTVIN12345678": true, + "2:USWESTVIN12345678": false, + "USWESTVIN12345679": true, + "2:USWESTVIN12345679": false, + "USWESTVIN12345670": true, + "2:USWESTVIN12345670": false, + }; return Promise.resolve(result); }), getECUs: jest.fn(() => { diff --git a/src/components/Fleets/Status/Vehicles/Table/__snapshots__/index.test.jsx.snap b/src/components/Fleets/Status/Vehicles/Table/__snapshots__/index.test.jsx.snap index 9cad130..2c81f3a 100644 --- a/src/components/Fleets/Status/Vehicles/Table/__snapshots__/index.test.jsx.snap +++ b/src/components/Fleets/Status/Vehicles/Table/__snapshots__/index.test.jsx.snap @@ -137,62 +137,7 @@ exports[`FleetVehiclesTable Render 1`] = ` - - - - USWESTVIN12345678 - - - - No actions - - - - - - USWESTVIN12345679 - - - - No actions - - - - - - USWESTVIN12345670 - - - - No actions - - - + /> diff --git a/src/components/Fleets/Status/Vehicles/Table/index.jsx b/src/components/Fleets/Status/Vehicles/Table/index.jsx index 07dc489..1554689 100644 --- a/src/components/Fleets/Status/Vehicles/Table/index.jsx +++ b/src/components/Fleets/Status/Vehicles/Table/index.jsx @@ -26,6 +26,7 @@ import SearchField from "../../../../Controls/SearchField"; import DeleteConfirmation from "../../../../DeleteConfirmation"; import TableHeaderSortable from "../../../../Table/HeaderSortable"; import { useLocalStorage } from "../../../../useLocalStorage"; +import ConnectedIcon from "../../../../Controls/ConnectedIcon"; import useStyles from "../../../../useStyles"; const tableColumns = [ @@ -190,13 +191,22 @@ const MainForm = ({ name }) => { onSortRequest={handleSort} /> - {fleetVehicles.map((vin) => ( - - - {vin} + {fleetVehicles && fleetVehicles.map((car) => ( + (car.vin && + + {(car.connected || car.connectedHMI) && + + } + {car.vin} - {Actions(vin)} + {Actions(car.vin)} + ) ))} diff --git a/src/components/Fleets/Status/__snapshots__/VehiclesTab.test.jsx.snap b/src/components/Fleets/Status/__snapshots__/VehiclesTab.test.jsx.snap index aab2895..220b263 100644 --- a/src/components/Fleets/Status/__snapshots__/VehiclesTab.test.jsx.snap +++ b/src/components/Fleets/Status/__snapshots__/VehiclesTab.test.jsx.snap @@ -136,62 +136,7 @@ exports[`VehiclesTab Render 1`] = ` - - - - USWESTVIN12345678 - - - - No actions - - - - - - USWESTVIN12345679 - - - - No actions - - - - - - USWESTVIN12345670 - - - - No actions - - - + /> diff --git a/src/services/__mocks__/vehiclesAPI.js b/src/services/__mocks__/vehiclesAPI.js index afdc805..73da510 100644 --- a/src/services/__mocks__/vehiclesAPI.js +++ b/src/services/__mocks__/vehiclesAPI.js @@ -112,6 +112,7 @@ const vehiclesAPI = { vins.forEach((vin) => { result[vin] = true; + result["2:" + vin] = false; }); return result; From 60c1f414a6b03b0f3fce7f0ed08401766b58dd59 Mon Sep 17 00:00:00 2001 From: Tristan Timblin Date: Mon, 26 Jun 2023 12:35:17 -0400 Subject: [PATCH 06/15] CEC-4523: add bulk archive to /packages (#372) * CEC-4523: add bulk archive to /packages --- .../App/__snapshots__/App.test.js.snap | 105 +++++++++++++- src/components/Manifest/List/index.jsx | 135 ++++++++++++------ src/hooks/index.js | 1 + src/hooks/useArchiveManifests.js | 32 +++++ src/utils/taskRunner.js | 61 ++++++-- src/utils/taskRunner.test.js | 61 ++++++-- 6 files changed, 328 insertions(+), 67 deletions(-) create mode 100644 src/hooks/index.js create mode 100644 src/hooks/useArchiveManifests.js diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap index 8dc8aac..d9392bd 100644 --- a/src/components/App/__snapshots__/App.test.js.snap +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -6284,7 +6284,44 @@ exports[`App Route /packages authenticated 1`] = `
+ > +
+ + +
+
+
+ + + + + + + + + + + + + + + + + diff --git a/src/components/Manifest/List/index.jsx b/src/components/Manifest/List/index.jsx index 089e2a3..a115576 100644 --- a/src/components/Manifest/List/index.jsx +++ b/src/components/Manifest/List/index.jsx @@ -6,7 +6,8 @@ import { TableFooter, TablePagination, TableRow, - Tooltip + Tooltip, + Checkbox, } from "@material-ui/core"; import DeleteIcon from "@material-ui/icons/Delete"; import SendIcon from "@material-ui/icons/Send"; @@ -37,6 +38,8 @@ import DeleteConfirmation from "../../DeleteConfirmation"; import TableHeaderSortable from "../../Table/HeaderSortable"; import { useLocalStorage } from "../../useLocalStorage"; import useStyles from "../../useStyles"; +import { useArchiveManifests } from "../../../hooks"; +import DropDownButton from "../../Controls/DropDownButton"; const tableColumns = [ { @@ -92,12 +95,12 @@ const MainForm = () => { const [order, setOrder] = useState("asc"); const [search, setSearch] = useLocalStorage("DEPLOYMENT_SEARCH", ""); const [active, setActive] = useLocalStorage("DEPLOYMENT_ACTIVE", "true"); + const [selected, setSelected] = useState([]); const [showDeleteModal, setShowDeleteModal] = useState(false); - const [deleteId, setDeleteId] = useState(""); const [deleteRowName, setDeleteRowName] = useState(""); - const { getManifests, deleteManifest, manifests, totalManifests } = + const { getManifests, manifests, totalManifests } = useManifestsContext(); const { setMessage, setTitle, setSitePath } = useStatusContext(); const { @@ -107,6 +110,7 @@ const MainForm = () => { groups, providers, } = useUserContext(); + const { archive } = useArchiveManifests(token); const sortHandler = (event, property) => { if (property === orderBy) { @@ -170,21 +174,45 @@ const MainForm = () => { } } - const setDeletePopup = (id, row) => { - setDeleteId(id); - setDeleteRowName(`${row.name} ${row.version}`); + const handleSelectAll = () => { + setSelected((selected) => selected.length ? [] : manifests); + }; + + const handleSelect = (event, manifest) => { + setSelected((selected) => { + if (event.target.checked && selected.find(({ id }) => id === manifest.id)) { + return selected; + } else if (event.target.checked) { + return [...selected, manifest]; + } + return selected.filter(({ id }) => id !== manifest.id); + }); + }; + + const setDeletePopup = (row) => { + handleSelect({ target: { checked: true } }, row); setShowDeleteModal(true); }; - const onDelete = async (manifest_id) => { + const onDelete = async () => { try { - await deleteManifest(parseInt(manifest_id), token); + await archive(selected.map((manifest) => manifest.id)) + .then(({ summary }) => { + setMessage(summary); + }); } catch (e) { setMessage(e.message); logger.warn(e.stack); } }; + useEffect(() => { + setDeleteRowName(() => selected + .map((manifest) => `${manifest.name} ${manifest.version}`) + .join(", ") + ); + }, [selected]); + const Actions = (row) => { let actions = []; if (hasRole(groups, Permissions.FiskerMagnaRead, providers)) { @@ -232,7 +260,7 @@ const MainForm = () => { return ( - setDeletePopup(action.id, row)}> + setDeletePopup(row)}> {action.icon} @@ -264,7 +292,17 @@ const MainForm = () => { - + + setShowDeleteModal(true), + disabled: !selected.length, + } + ]} + /> + { order={order} columnData={tableColumns} onSortRequest={sortHandler} + multiSelect + onSelectAll={handleSelectAll} + selectCount={selected ? selected.length : 0} + rowCount={manifests ? manifests.length : 0} /> - {manifests.map((row) => ( - - {row.id} - - {row.name} - {row.ecu_list && ( - <> -
- - - )} -
- {row.version} - {row.sums} - - {formatManifestType(row.type)} - - - {LocalDateTimeString(row.created)} - - - {LocalDateTimeString(row.updated)} - - {Actions(row)} -
- ))} + {manifests.map((row) => { + const isSelected = selected + ? !!selected.find(({ id }) => id === row.id) + : false; + return ( + + + handleSelect(event, row)} + /> + + {row.id} + + {row.name} + {row.ecu_list && ( + <> +
+ + + )} +
+ {row.version} + {row.sums} + + {formatManifestType(row.type)} + + + {LocalDateTimeString(row.created)} + + + {LocalDateTimeString(row.updated)} + + {Actions(row)} +
+ ); + })}
@@ -328,7 +381,7 @@ const MainForm = () => { message={deleteRowName} open={showDeleteModal} close={() => setShowDeleteModal(false)} - deleteFunction={() => onDelete(deleteId)} + deleteFunction={() => onDelete()} /> ); diff --git a/src/hooks/index.js b/src/hooks/index.js new file mode 100644 index 0000000..7b34538 --- /dev/null +++ b/src/hooks/index.js @@ -0,0 +1 @@ +export { useArchiveManifests } from "./useArchiveManifests"; \ No newline at end of file diff --git a/src/hooks/useArchiveManifests.js b/src/hooks/useArchiveManifests.js new file mode 100644 index 0000000..dc6a2d2 --- /dev/null +++ b/src/hooks/useArchiveManifests.js @@ -0,0 +1,32 @@ +import manifestsAPI from "../services/manifestsAPI"; +import TaskRunner from "../utils/taskRunner"; + +export const useArchiveManifests = (token) => { + + const archive = async (ids) => { + return new Promise((resolve) => { + const taskRunner = new TaskRunner(5, ids.length); + let errorCount = 0; + + const task = (id) => { + return async () => manifestsAPI.deleteManifest(id, token); + } + + ids.forEach((id) => taskRunner.push(task(id)) + .then((response) => { + if (response.error) { + errorCount += 1; + } + }) + ); + taskRunner.onComplete().then((responses) => resolve({ + summary: `${ids.length - errorCount} out of ${ids.length} manifests were deleted.`, + responses, + })); + }); + } + + return { + archive, + }; +}; diff --git a/src/utils/taskRunner.js b/src/utils/taskRunner.js index baa8e6b..b13ccec 100644 --- a/src/utils/taskRunner.js +++ b/src/utils/taskRunner.js @@ -1,36 +1,67 @@ export default class TaskRunner { - constructor(concurrencyLimit = 1) { - this.queue = []; - this.running = 0; - this.concurrencyLimit = concurrencyLimit; + constructor(concurrencyLimit = 1, total) { + this._queue = []; + this._index = 0; + this._running = 0; + this._complete = 0; + this._concurrencyLimit = concurrencyLimit; + + if (total) { + this._total = total; + this._responses = new Array(total); + } + + this._onComplete = new Promise((resolve, reject) => { + this._onCompleteResolve = resolve; + this._onCompleteReject = reject; + }); } execute() { - if (this.running >= this.concurrencyLimit || this.queue.length === 0) { + if (this._running >= this._concurrencyLimit || this._queue.length === 0) { return; } - - const task = this.queue.shift(); - this.running += 1; - task(); + + const task = this._queue.shift(); + this._running += 1; + task(this._index); + this._index += 1; } async push(fn) { return new Promise((resolve, reject) => { - const task = async () => { + const task = async (index) => { try { - const result = await fn(); - resolve(result); + const response = await fn(); + if (this._responses) { + this._responses[index] = response; + } + resolve(response); } catch (error) { reject(error); } finally { - this.running -= 1; + this._running -= 1; + this.#progress(); this.execute(); } } - - this.queue.push(task); + + this._queue.push(task); this.execute(); }); } + + #progress() { + this._complete += 1; + if (this._complete === this._total) { + this._onCompleteResolve(this._responses); + } + } + + async onComplete() { + if (!this._total) { + this._onCompleteReject(new Error("Total is required to determine onComplete.")); + } + return this._onComplete; + } } \ No newline at end of file diff --git a/src/utils/taskRunner.test.js b/src/utils/taskRunner.test.js index 7e8b301..37749c3 100644 --- a/src/utils/taskRunner.test.js +++ b/src/utils/taskRunner.test.js @@ -4,6 +4,10 @@ const mockPromise = async (id, ms) => { await new Promise(resolve => setTimeout(resolve, ms)); return id; } +const mockPromiseError = async (id, ms) => { + await new Promise(resolve => setTimeout(resolve, ms)); + return new Error(`Task ${id} had an error`); +} const asyncFn1 = () => mockPromise(1, 200); const asyncFn2 = () => mockPromise(2, 100); @@ -12,19 +16,19 @@ const asyncFn3 = () => mockPromise(3, 50); describe("TaskRunner", () => { it("runs task added to queue, when space available", () => { const taskRunner = new TaskRunner(2); - expect(taskRunner.running).toEqual(0); + expect(taskRunner._running).toEqual(0); taskRunner.push(() => mockPromise(1, 300)); - expect(taskRunner.running).toEqual(1); + expect(taskRunner._running).toEqual(1); }); it("keeps task in queue when at concurrency limit", () => { const taskRunner = new TaskRunner(2); - expect(taskRunner.running).toEqual(0); + expect(taskRunner._running).toEqual(0); taskRunner.push(() => mockPromise(1, 100)); taskRunner.push(() => mockPromise(2, 25)); taskRunner.push(() => mockPromise(3, 10)); - expect(taskRunner.running).toEqual(2); - expect(taskRunner.queue.length).toEqual(1); + expect(taskRunner._running).toEqual(2); + expect(taskRunner._queue.length).toEqual(1); }); it("runs queued tasks as space becomes available", async () => { @@ -32,9 +36,9 @@ describe("TaskRunner", () => { taskRunner.push(() => mockPromise(1, 600)); taskRunner.push(() => mockPromise(2, 300)); taskRunner.push(() => mockPromise(3, 100)); - expect(taskRunner.queue.length).toEqual(1); + expect(taskRunner._queue.length).toEqual(1); await new Promise(r => setTimeout(r, 301)); - expect(taskRunner.queue.length).toEqual(0); + expect(taskRunner._queue.length).toEqual(0); }); it("runs tasks in order", async () => { @@ -52,7 +56,44 @@ describe("TaskRunner", () => { .then((id) => { actual.push(id); }); - await new Promise(resolve => setTimeout(resolve, 500)); - expect(actual).toEqual([2, 3, 1]); + await new Promise(resolve => setTimeout(resolve, 500)); + expect(actual).toEqual([2, 3, 1]); }); -}) \ No newline at end of file + + it("resolves a promise when all tasks are complete", async () => { + const taskRunner = new TaskRunner(2, 5); + taskRunner.push(() => mockPromise(1, 600)); + taskRunner.push(() => mockPromise(2, 300)); + taskRunner.push(() => mockPromise(3, 200)); + taskRunner.push(() => mockPromise(4, 600)); + taskRunner.push(() => mockPromise(5, 100)); + await taskRunner.onComplete().then((actual) => { + expect(actual).toStrictEqual([1, 2, 3, 4, 5]); + }); + }); + + it("resolves a promise when all tasks are complete, even if some fail", async () => { + const error = new Error(`Task 3 had an error`); + const taskRunner = new TaskRunner(2, 5); + taskRunner.push(() => mockPromise(1, 600)); + taskRunner.push(() => mockPromise(2, 300)); + taskRunner.push(() => mockPromiseError(3, 200)); + taskRunner.push(() => mockPromise(4, 600)); + taskRunner.push(() => mockPromise(5, 100)); + await taskRunner.onComplete().then((actual) => { + expect(actual).toStrictEqual([1, 2, error, 4, 5]); + }); + }); + + it("rejects a promise when the total number of tasks is unknown", async () => { + const taskRunner = new TaskRunner(2); + taskRunner.push(() => mockPromise(1, 600)); + taskRunner.push(() => mockPromise(2, 300)); + taskRunner.push(() => mockPromise(3, 200)); + taskRunner.push(() => mockPromise(4, 600)); + taskRunner.push(() => mockPromise(5, 100)); + await taskRunner.onComplete().catch((error) => { + expect(error.message).toBe("Total is required to determine onComplete."); + }); + }); +}); From 12bf0be05d92c905ac1f8fef337ad3200a1b91e7 Mon Sep 17 00:00:00 2001 From: Eduard Voronkin <116690094+eduardvoronkin@users.noreply.github.com> Date: Mon, 26 Jun 2023 13:18:02 -0700 Subject: [PATCH 07/15] CEC-4517 validate T.Rex version (#375) * CEC-4517 validate T.Rex version * suggestion + proper semver compare * use semver-compare package * use optional chaining --- package.json | 1 + .../Controls/SendDiagnosticCommand/index.jsx | 20 +++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5d58712..685bd6c 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "react-router-dom": "^5.3.0", "react-router-hash-link": "^2.4.3", "react-scripts": "5.0.0", + "semver-compare": "^1.0.0", "usehooks-ts": "^2.7.1", "web-vitals": "^2.1.4", "webpack": "^5.74.0" diff --git a/src/components/Controls/SendDiagnosticCommand/index.jsx b/src/components/Controls/SendDiagnosticCommand/index.jsx index 85fdd02..117583d 100644 --- a/src/components/Controls/SendDiagnosticCommand/index.jsx +++ b/src/components/Controls/SendDiagnosticCommand/index.jsx @@ -5,6 +5,7 @@ import Checkbox from '@mui/material/Checkbox'; import React, { useEffect, useState } from "react"; import { useStatusContext } from "../../Contexts/StatusContext"; import { logger } from "../../../services/monitoring"; +import cmp from "semver-compare"; import { @@ -48,6 +49,15 @@ const SendDiagnosticCommand = ({ vin, token, classes }) => { } }; + const isOnline = () => { + return carState && carState?.online; + }; + + const TREX_MIN_VER = "1.1.108"; + const isTBOXResetSupported = () => { + return !carState?.trex_version ? true : cmp(carState.trex_version, TREX_MIN_VER) === 1; + }; + const clickHandler = async (_) => { try { await sendDiagnosticCommand([vin], { body: { command: currentCommand, ecus: currentECUs } }, token); @@ -103,15 +113,21 @@ const SendDiagnosticCommand = ({ vin, token, classes }) => { color="primary" className={classes.submit} onClick={clickHandler} - disabled={!carState ? true : !carState.online} + disabled={!isOnline() || !isTBOXResetSupported()} > Send
- {carState && carState.online ? "ONLINE" : "OFFLINE"} + {isOnline() ? "ONLINE" : "OFFLINE"}
+
+ + {!isTBOXResetSupported() ? `TBOX Reset supported from ${TREX_MIN_VER}, current version ${carState.trex_version}` : ""} + +
+ ); }; From 26eb084da5c73e847d2d36055799e8d6a6aeb330 Mon Sep 17 00:00:00 2001 From: Paul Adamsen <117673433+pauladamseniii@users.noreply.github.com> Date: Mon, 26 Jun 2023 19:33:44 -0400 Subject: [PATCH 08/15] CEC-4543 - Add location to digital twin map (#376) --- package-lock.json | 11 +++++++ src/components/Contexts/VehicleContext.jsx | 2 +- .../Contexts/__mocks__/VehicleContext.jsx | 2 +- src/components/VehiclePathsMap/index.jsx | 31 ++++++++++++------- src/services/__mocks__/vehiclesAPI.js | 1 + 5 files changed, 34 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index b27b5b3..7d007ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "react-router-dom": "^5.3.0", "react-router-hash-link": "^2.4.3", "react-scripts": "5.0.0", + "semver-compare": "^1.0.0", "usehooks-ts": "^2.7.1", "web-vitals": "^2.1.4", "webpack": "^5.74.0" @@ -15130,6 +15131,11 @@ "node": ">=10" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==" + }, "node_modules/send": { "version": "0.17.2", "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", @@ -28020,6 +28026,11 @@ "lru-cache": "^6.0.0" } }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==" + }, "send": { "version": "0.17.2", "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", diff --git a/src/components/Contexts/VehicleContext.jsx b/src/components/Contexts/VehicleContext.jsx index 21cdada..261a2ab 100644 --- a/src/components/Contexts/VehicleContext.jsx +++ b/src/components/Contexts/VehicleContext.jsx @@ -105,7 +105,7 @@ export const VehicleProvider = ({ children }) => { setBusy(true); const result = await api.getLocations(token); if (result.error) - throw new Error(`Get locations vehicle paths error. ${result.message}`); + throw new Error(`Get locations error. ${result.message}`); return result; } finally { setBusy(false); diff --git a/src/components/Contexts/__mocks__/VehicleContext.jsx b/src/components/Contexts/__mocks__/VehicleContext.jsx index 52bd9fb..4387c84 100644 --- a/src/components/Contexts/__mocks__/VehicleContext.jsx +++ b/src/components/Contexts/__mocks__/VehicleContext.jsx @@ -153,9 +153,9 @@ export const useVehicleContext = () => ({ .fn() .mockResolvedValue({ // tests only pass without mocking the data here - // '3FAFP13P71R199267': [], // '3FAFP13P31R199430': [[16.891136999999986, 26.832352999999955], [56.891136999999986, 66.832352999999955], [26.891136999999986, 36.832352999999955]], // '3FAFP13P71R199060': [[36.891136999999986, 46.832352999999955], [76.891136999999986, 16.832352999999955]], + // '3FAFP13P61R199390': [], }), getModels: jest.fn(() => { models = ["Ocean", "PEAR"]; diff --git a/src/components/VehiclePathsMap/index.jsx b/src/components/VehiclePathsMap/index.jsx index 62449ee..10346e6 100644 --- a/src/components/VehiclePathsMap/index.jsx +++ b/src/components/VehiclePathsMap/index.jsx @@ -7,7 +7,7 @@ import useStyles from "../useStyles"; import GrayMarkerIcon from "../../assets/gray-marker.png"; import GreenMarkerIcon from "../../assets/green-marker.png"; import { logger } from "../../services/monitoring"; -import { ValidateLocationVehiclePathsData } from "../../utils/locations"; +import { ValidateLocationData, ValidateLocationVehiclePathsData } from "../../utils/locations"; import { useUserContext } from "../Contexts/UserContext"; import { useVehicleContext, VehicleProvider } from "../Contexts/VehicleContext"; import { VehiclePopUp } from "../VehicleMap/popup"; @@ -54,23 +54,32 @@ const ComponentVehiclePathsMap = (props) => { vinsParam += props.lookbackHours return getLocationsVehiclePaths(accessToken, vinsParam) - .then((result) => { + .then(async (result) => { let resultArray = Object.entries(result) const points = [] // validate each location for (let vinLocations of resultArray) { - // if there are points for the vin; skip if empty points array - if (vinLocations[0] && vinLocations[1] && vinLocations[1][0]) { - let path = [] - path[0] = vinLocations[0] - path[1] = [] - for (let location of vinLocations[1]) { - if (ValidateLocationVehiclePathsData(location) !== false) { - path[1].push(location); + if (vinLocations[0]) { + let path = []; + path[0] = vinLocations[0]; + path[1] = []; + if (vinLocations[1] && vinLocations[1][0]) { + for (let location of vinLocations[1]) { + if (ValidateLocationVehiclePathsData(location) !== false) { + path[1].push(location); + } } + } else { + await getState(token, vinLocations[0]).then((stateResult) => { + if (stateResult.data && stateResult.data.location) { + if (ValidateLocationData(stateResult.data.location) !== false) { + path[1].push([stateResult.data.location.latitude, stateResult.data.location.longitude]); + } + } + }); } - points.push(path) + points.push(path); } } diff --git a/src/services/__mocks__/vehiclesAPI.js b/src/services/__mocks__/vehiclesAPI.js index 73da510..a77a540 100644 --- a/src/services/__mocks__/vehiclesAPI.js +++ b/src/services/__mocks__/vehiclesAPI.js @@ -134,6 +134,7 @@ const vehiclesAPI = { return { '3FAFP13P31R199430': [[16.891136999999986, 26.832352999999955], [56.891136999999986, 66.832352999999955], [26.891136999999986, 36.832352999999955]], '3FAFP13P71R199060': [[36.891136999999986, 46.832352999999955], [76.891136999999986, 16.832352999999955]], + '3FAFP13P61R199390': [], }; }, getVehicle: async (vin) => { From df760fa73f8bca38f59a10efabbab96d7897370c Mon Sep 17 00:00:00 2001 From: Tristan Timblin Date: Tue, 27 Jun 2023 09:07:43 -0400 Subject: [PATCH 09/15] CEC-4576: update permission for updateDeploy action (#370) * CEC-4576: use new UpdateDeploy permission * update manifest deploy permission --- .env.cec-euprd | 1 + .env.cec-prd | 1 + .env.dev | 1 + .env.local | 1 + .env.prd | 1 + .env.stg | 1 + .env.template | 1 + .../App/__snapshots__/App.test.js.snap | 76 +------------------ .../Details/__snapshots__/index.test.jsx.snap | 58 +------------- src/components/Cars/Status/Details/index.jsx | 72 +++++++++--------- .../Cars/Status/Details/index.test.jsx | 28 ++++++- .../__snapshots__/DetailsTab.test.jsx.snap | 58 +------------- .../Status/__snapshots__/index.test.jsx.snap | 58 +------------- src/components/Controls/RoleWrap/index.js | 4 +- src/components/Manifest/List/index.jsx | 2 +- src/utils/roles.js | 4 + src/utils/roles.test.js | 9 +++ 17 files changed, 87 insertions(+), 289 deletions(-) diff --git a/.env.cec-euprd b/.env.cec-euprd index 74cb850..ae12566 100644 --- a/.env.cec-euprd +++ b/.env.cec-euprd @@ -15,6 +15,7 @@ REACT_APP_ROLE_GENERATE_CERTIFICATE=9af2d8c0-c26d-4d6d-bbd1-ac53cbd37ebc REACT_APP_ROLE_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69 REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8 REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f +REACT_APP_ROLE_UPDATE_DEPLOY=e4af2c4c-6c5e-4784-9097-7c18e776d7b6 REACT_APP_ECCKEY_ENV= REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":49.8327,"lng":9.8816,"zoom":4.5} REACT_APP_ENABLE_DEBUGMASK=1 \ No newline at end of file diff --git a/.env.cec-prd b/.env.cec-prd index 074549f..2f6cb8b 100644 --- a/.env.cec-prd +++ b/.env.cec-prd @@ -15,6 +15,7 @@ REACT_APP_ROLE_GENERATE_CERTIFICATE=9af2d8c0-c26d-4d6d-bbd1-ac53cbd37ebc REACT_APP_ROLE_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69 REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8 REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f +REACT_APP_ROLE_UPDATE_DEPLOY=e4af2c4c-6c5e-4784-9097-7c18e776d7b6 REACT_APP_ECCKEY_ENV= REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5} REACT_APP_ENABLE_DEBUGMASK=1 \ No newline at end of file diff --git a/.env.dev b/.env.dev index 7fdf5cb..4c0d651 100644 --- a/.env.dev +++ b/.env.dev @@ -15,6 +15,7 @@ REACT_APP_ROLE_GENERATE_CERTIFICATE=746f34b0-9ba0-4b5d-8d84-0256a9c8e390 REACT_APP_ROLE_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69 REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8 REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f +REACT_APP_ROLE_UPDATE_DEPLOY=3590ec3f-1c05-428b-81a4-40b00baf83de REACT_APP_ECCKEY_ENV=stage,prod REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5} REACT_APP_ENABLE_DEBUGMASK=1 \ No newline at end of file diff --git a/.env.local b/.env.local index 2311225..74b818d 100644 --- a/.env.local +++ b/.env.local @@ -15,6 +15,7 @@ REACT_APP_ROLE_GENERATE_CERTIFICATE=746f34b0-9ba0-4b5d-8d84-0256a9c8e390 REACT_APP_ROLE_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69 REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8 REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f +REACT_APP_ROLE_UPDATE_DEPLOY=3590ec3f-1c05-428b-81a4-40b00baf83de REACT_APP_ECCKEY_ENV=dev,stage,prod REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5} REACT_APP_ENABLE_DEBUGMASK=1 \ No newline at end of file diff --git a/.env.prd b/.env.prd index 878fe6b..634e327 100644 --- a/.env.prd +++ b/.env.prd @@ -15,6 +15,7 @@ REACT_APP_ROLE_GENERATE_CERTIFICATE=746f34b0-9ba0-4b5d-8d84-0256a9c8e390 REACT_APP_ROLE_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69 REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8 REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f +REACT_APP_ROLE_UPDATE_DEPLOY=e4af2c4c-6c5e-4784-9097-7c18e776d7b6 REACT_APP_ECCKEY_ENV=stage REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5} REACT_APP_ENABLE_DEBUGMASK=1 \ No newline at end of file diff --git a/.env.stg b/.env.stg index df9db72..10457a5 100644 --- a/.env.stg +++ b/.env.stg @@ -15,6 +15,7 @@ REACT_APP_ROLE_GENERATE_CERTIFICATE=746f34b0-9ba0-4b5d-8d84-0256a9c8e390 REACT_APP_ROLE_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69 REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8 REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f +REACT_APP_ROLE_UPDATE_DEPLOY=3590ec3f-1c05-428b-81a4-40b00baf83de REACT_APP_ECCKEY_ENV=prod REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5} REACT_APP_ENABLE_DEBUGMASK=1 \ No newline at end of file diff --git a/.env.template b/.env.template index 517bef2..291a90c 100644 --- a/.env.template +++ b/.env.template @@ -15,5 +15,6 @@ REACT_APP_ROLE_GENERATE_CERTIFICATE=746f34b0-9ba0-4b5d-8d84-0256a9c8e390 REACT_APP_ROLE_MANUFACTURE=3412e11a-a2d1-4355-be3e-ef9aa5065b69 REACT_APP_ROLE_SUPPLIER_APPROVER=a6c9805e-80b2-42b2-bfbb-9df52e5504d8 REACT_APP_ROLE_MANIFEST_MIGRATION=42798c8a-9fa7-4fb4-82c0-9582cabe364f +REACT_APP_ROLE_UPDATE_DEPLOY=3590ec3f-1c05-428b-81a4-40b00baf83de REACT_APP_ECCKEY_ENV=dev,stage,prod REACT_APP_HOME_MAP_DEFAULT_LOCATION={"lat":37.0902,"lng":-95.7129,"zoom":4.5} diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap index d9392bd..f3480ec 100644 --- a/src/components/App/__snapshots__/App.test.js.snap +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -6651,24 +6651,6 @@ exports[`App Route /packages authenticated 1`] = ` /> - - - @@ -11561,63 +11543,7 @@ exports[`App Route /vehicle-status authenticated 1`] = `
- - - - -
+ />
diff --git a/src/components/Cars/Status/Details/__snapshots__/index.test.jsx.snap b/src/components/Cars/Status/Details/__snapshots__/index.test.jsx.snap index 7cadd4e..24991b8 100644 --- a/src/components/Cars/Status/Details/__snapshots__/index.test.jsx.snap +++ b/src/components/Cars/Status/Details/__snapshots__/index.test.jsx.snap @@ -160,63 +160,7 @@ exports[`VehicleDetailsTab Render 1`] = `
- - - - -
+ />
diff --git a/src/components/Cars/Status/Details/index.jsx b/src/components/Cars/Status/Details/index.jsx index cb8a973..2b7bf04 100644 --- a/src/components/Cars/Status/Details/index.jsx +++ b/src/components/Cars/Status/Details/index.jsx @@ -115,38 +115,38 @@ const MainForm = ({ vin }) => { Info Source: {vehicle.info_source}

- Tags: {vehicle.tags ? vehicle.tags.join(", ") : "none" } + Tags: {vehicle.tags ? vehicle.tags.join(", ") : "none"}

- {vehicle.log_level != null && ( + {vehicle.log_level != null && (

Log Level: {vehicle.log_level}

)} - {vehicle.canbus && ( - <> -

- CANBus Enabled: {vehicle.canbus.enabled.toString()} -

-

- Max Memory Buffer Size: {vehicle.canbus.max_mem_buffer_size ?? "Default"} -

-

- Data Logger Enabled: {vehicle.canbus.data_logger_enabled.toString()} -

-

- Max Disk Buffer Size: {vehicle.canbus.max_disk_buffer_size ?? "Default"} -

-

- Filters: {vehicle.canbus.filters ? vehicle.canbus.filters.length : 0} -

-

- DTC Enabled: { (vehicle.canbus.dtc_enabled || false).toString() } -

- - )} -
+ {vehicle.canbus && ( + <> +

+ CANBus Enabled: {vehicle.canbus.enabled.toString()} +

+

+ Max Memory Buffer Size: {vehicle.canbus.max_mem_buffer_size ?? "Default"} +

+

+ Data Logger Enabled: {vehicle.canbus.data_logger_enabled.toString()} +

+

+ Max Disk Buffer Size: {vehicle.canbus.max_disk_buffer_size ?? "Default"} +

+

+ Filters: {vehicle.canbus.filters ? vehicle.canbus.filters.length : 0} +

+

+ DTC Enabled: {(vehicle.canbus.dtc_enabled || false).toString()} +

+ + )} + {showDebugMask && (

@@ -156,19 +156,19 @@ const MainForm = ({ vin }) => { )} + groups={groups} + providers={providers} + rolesPerProvider={Permissions.FiskerUpdateDeploy} + > - } + label="Force Config Update" + control={ + + } + /> setShowUploadConfigModal(true)} > diff --git a/src/components/Cars/Status/Details/index.test.jsx b/src/components/Cars/Status/Details/index.test.jsx index b571949..94f6310 100644 --- a/src/components/Cars/Status/Details/index.test.jsx +++ b/src/components/Cars/Status/Details/index.test.jsx @@ -2,24 +2,25 @@ jest.mock("../../../Contexts/VehicleContext"); jest.mock("../../../Contexts/StatusContext"); jest.mock("../../../Contexts/UserContext"); -import { render, waitFor } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import { BrowserRouter } from "react-router-dom"; import routeData from "react-router"; import { VehicleProvider } from "../../../Contexts/VehicleContext"; import { StatusProvider } from "../../../Contexts/StatusContext"; import { UserProvider, setToken } from "../../../Contexts/UserContext"; -import { TEST_AUTH_OBJECT_FISKER }from "../../../../utils/testing"; +import { TEST_AUTH_OBJECT_FISKER } from "../../../../utils/testing"; import MainForm from "./index"; import addSnapshotSerializer from "../../../../utils/snapshot"; +import * as Roles from "../../../../utils/roles"; const renderVehicleDetailsTab = async () => { const { container } = render( - + - + @@ -46,4 +47,23 @@ describe("VehicleDetailsTab", () => { const container = await renderVehicleDetailsTab(); expect(container).toMatchSnapshot(); }); + + it("renders update config control when required permission is present.", () => { + const hasRole = jest.spyOn(Roles, 'hasRole'); + hasRole.mockReturnValue(true); + render( + + + + + + + + + + ); + + expect(screen.getByLabelText("Force Config Update")).toBeTruthy(); + hasRole.mockRestore(); + }) }); diff --git a/src/components/Cars/Status/__snapshots__/DetailsTab.test.jsx.snap b/src/components/Cars/Status/__snapshots__/DetailsTab.test.jsx.snap index fedbdd8..92ea782 100644 --- a/src/components/Cars/Status/__snapshots__/DetailsTab.test.jsx.snap +++ b/src/components/Cars/Status/__snapshots__/DetailsTab.test.jsx.snap @@ -168,63 +168,7 @@ exports[`DetailsTab Render 1`] = `

- - - - -
+ />
diff --git a/src/components/Cars/Status/__snapshots__/index.test.jsx.snap b/src/components/Cars/Status/__snapshots__/index.test.jsx.snap index c65dfed..cae321f 100644 --- a/src/components/Cars/Status/__snapshots__/index.test.jsx.snap +++ b/src/components/Cars/Status/__snapshots__/index.test.jsx.snap @@ -349,63 +349,7 @@ exports[`CarStatus Render 1`] = `
- - - - -
+ />
diff --git a/src/components/Controls/RoleWrap/index.js b/src/components/Controls/RoleWrap/index.js index 024875c..5fcd7bd 100644 --- a/src/components/Controls/RoleWrap/index.js +++ b/src/components/Controls/RoleWrap/index.js @@ -2,9 +2,9 @@ import React from "react"; import { hasRole } from "../../../utils/roles"; export const RoleWrap = (props) => { - const {groups, rolesPerProvider, providers} = props; + const { groups, rolesPerProvider, providers } = props; - const eitherComponent = props["eitherComponent"] || null; + const eitherComponent = props["eitherComponent"] || null; if (!hasRole(groups, rolesPerProvider, providers)) { return eitherComponent != null ? eitherComponent : <>; diff --git a/src/components/Manifest/List/index.jsx b/src/components/Manifest/List/index.jsx index a115576..635ba1a 100644 --- a/src/components/Manifest/List/index.jsx +++ b/src/components/Manifest/List/index.jsx @@ -231,7 +231,7 @@ const MainForm = () => { icon: , }); } - if (hasRole(groups, Permissions.FiskerMagnaCreate, providers)) { + if (hasRole(groups, Permissions.FiskerUpdateDeploy, providers)) { actions.push({ tip: `Deploy "${row.name} ${row.version}"`, link: `/package-deploy/${row.id}`, diff --git a/src/utils/roles.js b/src/utils/roles.js index 69f1d18..e428a59 100644 --- a/src/utils/roles.js +++ b/src/utils/roles.js @@ -6,6 +6,7 @@ export const Roles = { DELETE: process.env.REACT_APP_ROLE_DELETE, CERTIFICATES: process.env.REACT_APP_ROLE_GENERATE_CERTIFICATE, APPROVESUPPLIERS: process.env.REACT_APP_ROLE_SUPPLIER_APPROVER, + UPDATEDEPLOY: process.env.REACT_APP_ROLE_UPDATE_DEPLOY, MANUFACTURE: process.env.REACT_APP_ROLE_MANUFACTURE, MAGNAGROUP: process.env.REACT_APP_MAGNA_GROUP_ID, MANIFEST_MIGRATION: process.env.REACT_APP_ROLE_MANIFEST_MIGRATION @@ -81,6 +82,9 @@ export const Permissions = { [Providers.FISKER_QA]: [Roles.MANUFACTURE], [Providers.MAGNA]: [Roles.MAGNAGROUP], }, + FiskerUpdateDeploy: { + [Providers.FISKER]: [Roles.UPDATEDEPLOY], + }, Magna: { [Providers.FISKER_QA]: [Roles.MANUFACTURE], [Providers.MAGNA]: [Roles.MAGNAGROUP], diff --git a/src/utils/roles.test.js b/src/utils/roles.test.js index c4198ac..9b40e97 100644 --- a/src/utils/roles.test.js +++ b/src/utils/roles.test.js @@ -68,6 +68,15 @@ describe("Roles Helper", () => { ).toEqual(true); }); + it("Check FiskerUpdateDeploy permission", () => { + expect( + hasRole([Roles.UPDATEDEPLOY], Permissions.FiskerUpdateDeploy, [Providers.FISKER]) + ).toEqual(true); + expect( + hasRole([Roles.UPDATEDEPLOY], Permissions.FiskerUpdateDeploy, [Providers.MAGNA]) + ).toEqual(false); + }); + it("Check Magna permission", () => { expect( hasRole([Roles.MAGNAGROUP], Permissions.Magna, [Providers.MAGNA]) From ab42480e31965a6a5522142e38b8380352e7d27e Mon Sep 17 00:00:00 2001 From: Paul Adamsen <117673433+pauladamseniii@users.noreply.github.com> Date: Fri, 30 Jun 2023 14:40:56 -0400 Subject: [PATCH 10/15] CEC-4543 - Add Aftersales filter to Deployments (#378) * CEC-4543 - Add location to digital twin map * CEC-4565 - Add Aftersales filter to Deployments * Comments --------- Co-authored-by: jwu-fisker --- .../App/__snapshots__/App.test.js.snap | 48 ++++++- src/components/Manifest/List/index.jsx | 125 ++++++++++++++---- src/utils/manifest_types.js | 2 + 3 files changed, 147 insertions(+), 28 deletions(-) diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap index f3480ec..4e55d17 100644 --- a/src/components/App/__snapshots__/App.test.js.snap +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -6261,9 +6261,9 @@ exports[`App Route /packages authenticated 1`] = ` class="MuiButtonBase-root MuiToggleButton-root Mui-selected MuiToggleButton-sizeMedium MuiToggleButton-standard MuiToggleButtonGroup-grouped MuiToggleButtonGroup-groupedHorizontal css-ueukts-MuiButtonBase-root-MuiToggleButton-root" tabindex="0" type="button" - value="true" + value="software" > - Active + Software @@ -6273,13 +6273,25 @@ exports[`App Route /packages authenticated 1`] = ` class="MuiButtonBase-root MuiToggleButton-root MuiToggleButton-sizeMedium MuiToggleButton-standard MuiToggleButtonGroup-grouped MuiToggleButtonGroup-groupedHorizontal css-ueukts-MuiButtonBase-root-MuiToggleButton-root" tabindex="0" type="button" - value="false" + value="archived" > Archived +
+
+ + Type + + + - Type + Update + @@ -6662,7 +6700,7 @@ exports[`App Route /packages authenticated 1`] = ` >
+
diff --git a/src/components/Manifest/List/index.jsx b/src/components/Manifest/List/index.jsx index ad52162..da6dd49 100644 --- a/src/components/Manifest/List/index.jsx +++ b/src/components/Manifest/List/index.jsx @@ -21,7 +21,6 @@ import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import EditIcon from "@material-ui/icons/Edit"; -import { useArchiveManifests } from "../../../hooks"; import { logger } from "../../../services/monitoring"; import { LocalDateTimeString } from "../../../utils/dates"; import { TYPE_MANIFEST_AFTERSALES, TYPE_MANIFEST_CONFIG, TYPE_MANIFEST_SOFTWARE } from "../../../utils/manifest_types"; @@ -40,6 +39,8 @@ import DeleteConfirmation from "../../DeleteConfirmation"; import TableHeaderSortable from "../../Table/HeaderSortable"; import { useLocalStorage } from "../../useLocalStorage"; import useStyles from "../../useStyles"; +import { useUpdateManifest } from "../../../hooks"; +import GeneralConfirmation from "../../GeneralConfirmation"; const tableColumns = [ { @@ -114,10 +115,10 @@ const MainForm = () => { const [order, setOrder] = useState("asc"); const [search, setSearch] = useLocalStorage("DEPLOYMENT_SEARCH", ""); const [active, setActive] = useLocalStorage("DEPLOYMENT_TAB_TOGGLE", "software"); - const [selected, setSelected] = useState([]); const [showDeleteModal, setShowDeleteModal] = useState(false); - const [deleteRowName, setDeleteRowName] = useState(""); + const [showArchiveModal, setShowArchiveModal] = useState(false); + const [archiveLabel, setArchiveLabel] = useState("Archive"); const { getManifests, manifests, totalManifests } = useManifestsContext(); @@ -129,7 +130,13 @@ const MainForm = () => { groups, providers, } = useUserContext(); - const { archive } = useArchiveManifests(token); + const { + remove, + archive, + updateManifestIds, + setUpdateManifestIds, + setMakeActive, + } = useUpdateManifest(token); const sortHandler = (event, property) => { if (property === orderBy) { @@ -227,7 +234,11 @@ const MainForm = () => { } })(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pageIndex, pageSize, token, orderBy, order, search, active]); + }, [pageIndex, pageSize, token, orderBy, order, search, active, updateManifestIds]); + + useEffect(() => { + setUpdateManifestIds([]); + }, [active, setUpdateManifestIds]); const handleChangePageIndex = (_event, newIndex) => { setPageIndex(newIndex); @@ -245,20 +256,28 @@ const MainForm = () => { const handleActiveChange = (event, newAlignment) => { if (newAlignment !== null) { - setActive(newAlignment) + setActive(newAlignment); + setMakeActive(newAlignment === 'archived'); + setArchiveLabel(() => { + if (newAlignment === "archived") { + return "Activate"; + } + + return "Archive"; + }); } } const handleSelectAll = () => { - setSelected((selected) => selected.length ? [] : manifests); + setUpdateManifestIds((selected) => selected.length ? [] : manifests.map((manifest) => manifest.id)); }; const handleSelect = (event, manifest) => { - setSelected((selected) => { - if (event.target.checked && selected.find(({ id }) => id === manifest.id)) { + setUpdateManifestIds((selected) => { + if (event.target.checked && selected.find((id) => id === manifest.id)) { return selected; } else if (event.target.checked) { - return [...selected, manifest]; + return [...selected, manifest.id]; } return selected.filter(({ id }) => id !== manifest.id); }); @@ -269,11 +288,12 @@ const MainForm = () => { setShowDeleteModal(true); }; - const onDelete = async () => { + const onArchive = async () => { try { - await archive(selected.map((manifest) => manifest.id)) - .then(({ summary }) => { - setMessage(summary); + await archive() + .then(({ message }) => { + setUpdateManifestIds([]); + setMessage(message); }); } catch (e) { setMessage(e.message); @@ -281,12 +301,18 @@ const MainForm = () => { } }; - useEffect(() => { - setDeleteRowName(() => selected - .map((manifest) => `${manifest.name} ${manifest.version}`) - .join(", ") - ); - }, [selected]); + const onDelete = async () => { + try { + await remove() + .then(({ summary }) => { + setUpdateManifestIds([]); + setMessage(summary); + }); + } catch (e) { + setMessage(e.message); + logger.warn(e.stack); + } + }; const Actions = (row) => { let actions = []; @@ -372,9 +398,9 @@ const MainForm = () => { setShowDeleteModal(true), - disabled: !selected.length, + name: archiveLabel, + trigger: () => setShowArchiveModal(true), + disabled: !updateManifestIds.length || active === "all", } ]} /> @@ -389,13 +415,13 @@ const MainForm = () => { onSortRequest={sortHandler} multiSelect onSelectAll={handleSelectAll} - selectCount={selected ? selected.length : 0} + selectCount={updateManifestIds ? updateManifestIds.length : 0} rowCount={manifests ? manifests.length : 0} /> {manifests.map((row) => { - const isSelected = selected - ? !!selected.find(({ id }) => id === row.id) + const isSelected = updateManifestIds + ? !!updateManifestIds.find((id) => id === row.id) : false; return ( @@ -457,11 +483,18 @@ const MainForm = () => {
setShowDeleteModal(false)} deleteFunction={() => onDelete()} /> + setShowArchiveModal(false)} + actionFunction={() => onArchive()} + /> ); }; diff --git a/src/components/Manifest/List/index.test.jsx b/src/components/Manifest/List/index.test.jsx new file mode 100644 index 0000000..e7f2ab4 --- /dev/null +++ b/src/components/Manifest/List/index.test.jsx @@ -0,0 +1,35 @@ +jest.mock("../../Contexts/ManifestsContext"); +jest.mock("../../Contexts/UserContext"); + +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { BrowserRouter } from "react-router-dom"; + +import { UserProvider, setToken } from "../../Contexts/UserContext"; +import { StatusProvider } from "../../Contexts/StatusContext"; +import ManifestList from "."; +import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing"; + +const Page = ( + + + + + + + +); + +describe("Manifest List Component", () => { + beforeAll(() => { + setToken(TEST_AUTH_OBJECT_FISKER); + }); + + it("adjusts the active state on switch to archived tab", async () => { + render(Page); + + const archiveActionEl = screen.getByText("Archive"); + fireEvent.click(screen.getByText("Archived")); + expect(archiveActionEl.innerHTML).toBe("Activate"); + }); +}); diff --git a/src/hooks/index.js b/src/hooks/index.js index 7b34538..5ee20bb 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -1 +1 @@ -export { useArchiveManifests } from "./useArchiveManifests"; \ No newline at end of file +export { useUpdateManifest } from "./useUpdateManifest"; \ No newline at end of file diff --git a/src/hooks/useArchiveManifests.js b/src/hooks/useArchiveManifests.js deleted file mode 100644 index dc6a2d2..0000000 --- a/src/hooks/useArchiveManifests.js +++ /dev/null @@ -1,32 +0,0 @@ -import manifestsAPI from "../services/manifestsAPI"; -import TaskRunner from "../utils/taskRunner"; - -export const useArchiveManifests = (token) => { - - const archive = async (ids) => { - return new Promise((resolve) => { - const taskRunner = new TaskRunner(5, ids.length); - let errorCount = 0; - - const task = (id) => { - return async () => manifestsAPI.deleteManifest(id, token); - } - - ids.forEach((id) => taskRunner.push(task(id)) - .then((response) => { - if (response.error) { - errorCount += 1; - } - }) - ); - taskRunner.onComplete().then((responses) => resolve({ - summary: `${ids.length - errorCount} out of ${ids.length} manifests were deleted.`, - responses, - })); - }); - } - - return { - archive, - }; -}; diff --git a/src/hooks/useUpdateManifest.js b/src/hooks/useUpdateManifest.js new file mode 100644 index 0000000..39b0b9b --- /dev/null +++ b/src/hooks/useUpdateManifest.js @@ -0,0 +1,47 @@ +import { useState } from "react"; +import manifestsAPI from "../services/manifestsAPI"; +import TaskRunner from "../utils/taskRunner"; + +export const useUpdateManifest = (token) => { + const [updateManifestIds, setUpdateManifestIds] = useState([]); + const [makeActive, setMakeActive] = useState(false); + + const remove = async () => { + return new Promise((resolve) => { + const taskRunner = new TaskRunner(5, updateManifestIds.length); + let errorCount = 0; + + const task = (id) => { + return async () => manifestsAPI.deleteManifest(id, token); + } + + updateManifestIds.forEach((id) => taskRunner.push(task(id)) + .then((response) => { + if (response.error) { + errorCount += 1; + } + }) + ); + taskRunner.onComplete().then((responses) => resolve({ + summary: `${updateManifestIds.length - errorCount} out of ${updateManifestIds.length} manifests were deleted.`, + responses, + })); + }); + } + + const archive = async () => { + return manifestsAPI.archiveManifest({ + ids: updateManifestIds, + active: makeActive, + }, token); + } + + return { + updateManifestIds, + setUpdateManifestIds, + makeActive, + setMakeActive, + archive, + remove, + }; +}; diff --git a/src/services/__mocks__/manifestsAPI.js b/src/services/__mocks__/manifestsAPI.js index 6404bc5..302e6d4 100644 --- a/src/services/__mocks__/manifestsAPI.js +++ b/src/services/__mocks__/manifestsAPI.js @@ -5,6 +5,10 @@ const manifestsAPI = { return data; }, + archiveManifest: async (data, token) => { + return { message: "Archived 1 update manifests" }; + }, + deleteManifest: async (manifest_id, token) => { return { message: "OK" }; }, diff --git a/src/services/manifestsAPI.js b/src/services/manifestsAPI.js index 8fbe7a8..25782f7 100644 --- a/src/services/manifestsAPI.js +++ b/src/services/manifestsAPI.js @@ -1,10 +1,22 @@ import { - addQueryParams, errorHandler, fetchRespHandler, getAuthHeaderOptions + addQueryParams, errorHandler, fetchRespHandler, getAuthHeaderOptions } from "../utils/http"; const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL; const manifestsAPI = { + archiveManifest: async (data, token) => + fetch(`${API_ENDPOINT}/vehicles/archive`, { + method: "PUT", + headers: Object.assign( + { "Content-Type": "application/json" }, + getAuthHeaderOptions(token) + ), + body: JSON.stringify(data) + }) + .then(fetchRespHandler) + .catch(errorHandler), + deleteManifest: async (manifest_id, token) => fetch(`${API_ENDPOINT}/manifest?id=${manifest_id}`, { method: "DELETE", @@ -79,8 +91,8 @@ const manifestsAPI = { .then(fetchRespHandler) .catch(errorHandler), - migrateManifest: async (manifest_id, token) => - fetch(`${API_ENDPOINT}/manifestmigrate/${manifest_id}`,{ + migrateManifest: async (manifest_id, token) => + fetch(`${API_ENDPOINT}/manifestmigrate/${manifest_id}`, { method: "POST", headers: Object.assign( { "Content-Type": "application/json" },