diff --git a/src/components/Contexts/CarUpdatesContext.jsx b/src/components/Contexts/CarUpdatesContext.jsx index 3acffec..cad4a8b 100644 --- a/src/components/Contexts/CarUpdatesContext.jsx +++ b/src/components/Contexts/CarUpdatesContext.jsx @@ -32,6 +32,26 @@ const validateDeployFleetUpdates = (data) => { return validateDeployClosure(data, "fleet_names", "Fleets"); }; +export function downloadPercent(status) { + if (status.status === "install_succeeded") { + return 100; + } + + if (status.package_total === 0) { + return 0; + } + + return Math.floor((100 * status.package_current) / status.package_total); +} + +export function installPercent(status) { + if (status.total_files === 0) { + return 0; + } + + return Math.floor((100 * status.installed) / status.total_files); +} + export const CarUpdatesProvider = ({ children }) => { const [busy, setBusy] = useState(false); const [carUpdates, setCarUpdates] = useState([]); @@ -142,30 +162,10 @@ export const CarUpdatesProvider = ({ children }) => { return result; }; - const getDownloadProgress = (status) => { - const disabled = status.status === "install_succeeded"; - if (disabled) { - return -1; - } - - const calculated = status.package_total > 0; - if (calculated) { - return Math.floor((100 * status.package_current) / status.package_total); - } - - return 0; - }; - - const getInstallProgress = (status) => { - if (status.total_files > 0) - return Math.floor((100 * status.installed) / status.total_files); - return 0; - }; - const applyProgressStatus = (item, status) => { if (validateStatusMessage(status)) { if (status.msg === "downloading") { - item.progress = getDownloadProgress(status); + item.progress = downloadPercent(status); item.status = `${item.ecu || ""} downloading ${item.progress}%`.trim(); return; } else if (status.msg === "package_download_complete") { @@ -173,7 +173,7 @@ export const CarUpdatesProvider = ({ children }) => { item.status = "download complete"; return; } else if (status.msg === "installing") { - item.progress = getInstallProgress(status); + item.progress = installPercent(status); item.status = `${item.ecu || ""} installing ${item.progress}%`.trim(); return; } else if (status.msg === "package_install_complete") { diff --git a/src/components/Contexts/FleetContext.jsx b/src/components/Contexts/FleetContext.jsx index e8b329e..022efae 100644 --- a/src/components/Contexts/FleetContext.jsx +++ b/src/components/Contexts/FleetContext.jsx @@ -1,7 +1,10 @@ -import React, { useContext, useState } from "react"; +import React, { useContext, useState, useEffect, useRef } from "react"; import api from "../../services/fleetsAPI"; +import updatesApi from "../../services/updatesAPI"; import vehiclesAPI from "../../services/vehiclesAPI"; +import { downloadPercent, installPercent } from "./CarUpdatesContext"; import { validateCANID, validateFilter, validateVIN } from "../../utils/validationSupplier"; +import Polling from "../../utils/polling"; const FleetContext = React.createContext(); @@ -16,6 +19,10 @@ export const FleetProvider = ({ children }) => { const [fleetVehicles, setFleetVehicles] = useState([]); const [totalFleetVehicles, setTotalFleetVehicles] = useState(0); + const [carUpdateIds, setCarUpdateIds] = useState([]); + const carUpdateIdsRef = useRef(); + carUpdateIdsRef.current = carUpdateIds; + const [fleetCANFilters, setFleetCANFilters] = useState([]); const [totalFleetCANFilters, setTotalFleetCANFilters] = useState(0); @@ -115,7 +122,7 @@ export const FleetProvider = ({ children }) => { const vins = result.data.map(vehicle => vehicle.vin); const connectionsResult = await vehiclesAPI.getConnections({ "VINs": vins }, token) - if (result.error) { + if (connectionsResult.error) { setFleetVehicles([]) throw new Error(`Get vehicles connections error. ${result.message}`); } @@ -127,8 +134,12 @@ export const FleetProvider = ({ children }) => { connected: connectionsResult[vehicle.vin] || false, connectedHMI: connectionsResult[`2:${vehicle.vin}`] || false, trex_version: vehicle.carstate?.trex_version || "", - }) - }) + car_update_id: vehicle.carupdate?.id || "", + car_update_name: vehicle.carupdate?.updatemanifest?.name || "", + car_update_status: vehicle.carupdate?.status || "", + car_update_type: vehicle.carupdate?.updatemanifest?.type || "", + }); + }); setFleetVehicles(cars) if (result.total) { @@ -140,6 +151,52 @@ export const FleetProvider = ({ children }) => { } }; + const watchFleetVehicles = new Polling(async ({ token }) => { + const result = await updatesApi.getCarUpdateProgress( + carUpdateIdsRef.current.join(","), + token + ); + let pivot = result.statuses.length - 1; + setFleetVehicles((fleetVehicles) => fleetVehicles.map((vehicle) => { + result.statuses.find((status, i) => { + if (vehicle.car_update_id !== status.car_update_id) { + return false; + } + + switch (status.msg) { + case "downloading": + vehicle.car_update_progress = downloadPercent(); + vehicle.car_update_status = `${status.ecu} downloading ${vehicle.car_update_progress}%`.trim(); + break; + case "package_download_complete": + vehicle.car_update_progress = 100; + vehicle.car_update_status = `download complete`; + break; + case "installing": + vehicle.car_update_progress = installPercent(); + vehicle.car_update_status = `${status.ecu} installing ${vehicle.car_update_progress}%`.trim(); + break; + case "package_install_complete": + vehicle.car_update_progress = 100; + vehicle.car_update_status = `install complete`; + break; + default: + vehicle.car_update_progress = -1; + break; + } + + // Move found status' to end to reduce time complexity to Ologn + result.statuses[i] = result.statuses[pivot]; + result.statuses[pivot] = status; + pivot -= 1; + + return true; + }); + return vehicle; + })); + return Promise.resolve(); + }, 2000); + const addFleetVehicles = async (name, vehicles, token) => { try { setBusy(true); @@ -254,6 +311,12 @@ export const FleetProvider = ({ children }) => { } }; + useEffect(() => { + setCarUpdateIds(() => fleetVehicles + .filter((vehicle) => vehicle.car_update_status !== "installed") + .map((vehicle) => vehicle.car_update_id)); + }, [fleetVehicles, setCarUpdateIds]); + return ( { fleetVehicles, totalFleetVehicles, + watchFleetVehicles, getFleetVehicles, addFleetVehicles, deleteFleetVehicle, diff --git a/src/components/Contexts/FleetContext.test.jsx b/src/components/Contexts/FleetContext.test.jsx index c6384dd..ab4bc05 100644 --- a/src/components/Contexts/FleetContext.test.jsx +++ b/src/components/Contexts/FleetContext.test.jsx @@ -805,18 +805,30 @@ const expectedFleetVehiclesData = [ connected: true, connectedHMI: false, trex_version: "", + car_update_id: "", + car_update_name: "", + car_update_status: "", + car_update_type: "", }, { vin: "USWESTVIN12345679", connected: true, connectedHMI: false, trex_version: "", + car_update_id: "", + car_update_name: "", + car_update_status: "", + car_update_type: "", }, { vin: "USWESTVIN12345670", connected: true, connectedHMI: false, trex_version: "", + car_update_id: "", + car_update_name: "", + car_update_status: "", + car_update_type: "", }, ]; diff --git a/src/components/Contexts/__mocks__/FleetContext.jsx b/src/components/Contexts/__mocks__/FleetContext.jsx index 9d49e37..461f933 100644 --- a/src/components/Contexts/__mocks__/FleetContext.jsx +++ b/src/components/Contexts/__mocks__/FleetContext.jsx @@ -1,3 +1,5 @@ +import Polling from "../../../utils/polling"; + let busy = false; let fleetCANFilters = [ { @@ -66,6 +68,7 @@ export const useFleetContext = () => ({ fleetVehicles, totalFleetVehicles, + watchFleetVehicles: new Polling(jest.fn(), 0), getFleetVehicles: jest.fn().mockImplementation((name, search, _token) => { const result = [ { 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 016d927..5b065c6 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 @@ -150,6 +150,52 @@ exports[`FleetVehiclesTable Render 1`] = ` + + + Car Update + + + + + + Car Update Status + + + { const { fleetVehicles, totalFleetVehicles, + watchFleetVehicles, getFleetVehicles, deleteFleetVehicle, } = useFleetContext(); @@ -87,11 +97,15 @@ const MainForm = ({ name }) => { }, token ); + watchFleetVehicles.start({ token }); } catch (e) { setMessage(e.message); logger.warn(e.stack); } })(); + return () => { + watchFleetVehicles.end(); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [token, pageIndex, pageSize, orderBy, order, search]); @@ -209,7 +223,20 @@ const MainForm = ({ name }) => { {car.vin} {car.trex_version} - {Actions(car.vin)} + + + + {car.car_update_name} + + + + + {car.car_update_status} + {car.car_update_progress > -1 && ( + + )} + + {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 d84d351..c45ef81 100644 --- a/src/components/Fleets/Status/__snapshots__/VehiclesTab.test.jsx.snap +++ b/src/components/Fleets/Status/__snapshots__/VehiclesTab.test.jsx.snap @@ -149,6 +149,52 @@ exports[`VehiclesTab Render 1`] = ` + + + Car Update + + + + + + Car Update Status + + + ({ noWrap: { whiteSpace: "nowrap", }, + truncateCell: { + textOverflow: "ellipsis", + maxWidth: "200px", + overflow: "hidden", + display: "inline-block", + whiteSpace: "nowrap", + } })); export default useStyles; diff --git a/src/utils/polling.js b/src/utils/polling.js new file mode 100644 index 0000000..5337778 --- /dev/null +++ b/src/utils/polling.js @@ -0,0 +1,29 @@ +export default class Polling { + constructor(callback = () => Promise.resolve(), duration = 1000) { + this.callback = callback; + this.duration = duration; + this.timeout = undefined; + } + + #sleep() { + this.end(); + return new Promise((resolve) => { + this.timeout = setTimeout(resolve, this.duration); + }); + } + + async start(payload) { + this.callback(payload) + .then(() => { + this.#sleep().then(() => this.start(payload)); + }); + } + + end() { + if (!this.timeout) { + return; + } + + clearTimeout(this.timeout); + } +} \ No newline at end of file diff --git a/src/utils/polling.test.js b/src/utils/polling.test.js new file mode 100644 index 0000000..18c3fbf --- /dev/null +++ b/src/utils/polling.test.js @@ -0,0 +1,31 @@ +import Polling from "./polling"; + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +describe("Polling", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("starts and ends polling", async () => { + const callback = () => Promise.resolve(); + const poll = new Polling(callback, 100); + const spy = jest.spyOn(poll, "callback"); + poll.start(); + await sleep(500); + poll.end(); + await sleep(500); + expect(spy).toHaveBeenCalledTimes(5); + }); + + it("async polling", async () => { + const callback = () => sleep(100); + const poll = new Polling(callback, 100); + const spy = jest.spyOn(poll, "callback"); + poll.start(); + await sleep(1000); + expect(spy).toHaveBeenCalledTimes(5); + }); +});