From 9ae3ef0e2ee164ea0ae2198fbd69df87c34d0cf1 Mon Sep 17 00:00:00 2001 From: Paul Adamsen <117673433+pauladamseniii@users.noreply.github.com> Date: Fri, 16 Jun 2023 11:10:47 -0400 Subject: [PATCH 1/6] CEC-4538 - Fleet/Vehicle/Deployment search saved (#363) * CEC-4538 - Fleet/Vehicle/Deployment search saved * save archived --- src/components/Cars/List/index.jsx | 7 +++--- src/components/Controls/SearchField/index.jsx | 10 ++++++-- src/components/Fleets/Table/index.jsx | 25 ++++++++++--------- src/components/Manifest/List/index.jsx | 13 +++++----- 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/components/Cars/List/index.jsx b/src/components/Cars/List/index.jsx index 5a4ec2c..f4a332d 100644 --- a/src/components/Cars/List/index.jsx +++ b/src/components/Cars/List/index.jsx @@ -14,12 +14,13 @@ import { RoleWrap } from "../../Controls/RoleWrap"; import SearchField from "../../Controls/SearchField"; import DropDownButton from "../../Controls/DropDownButton"; import TransformModal from "../../TransformModal"; +import { useLocalStorage } from "../../useLocalStorage"; import useStyles from "../../useStyles"; import TaskRunner from "../../../utils/taskRunner"; const MainForm = () => { const classes = useStyles(); - const [search, setSearch] = useState(""); + const [search, setSearch] = useLocalStorage("VEHICLE_SEARCH", ""); const [online, setOnline] = useState(false); const [onlineHMI, setOnlineHMI] = useState(false); const [selectedVins, setSelectedVins] = useState([]); @@ -64,7 +65,7 @@ const MainForm = () => { const handleUploadConfig = (fn) => { const taskRunner = new TaskRunner(5); const request = (vin, i) => { - const messagePrefix = `${i+1}/${selectedVins.length} "${vin}":`; + const messagePrefix = `${i + 1}/${selectedVins.length} "${vin}":`; return async () => { const result = await fn(vin, config.force.value, token) .then(() => { @@ -113,7 +114,7 @@ const MainForm = () => { - + diff --git a/src/components/Controls/SearchField/index.jsx b/src/components/Controls/SearchField/index.jsx index b6868cc..3fb72b7 100644 --- a/src/components/Controls/SearchField/index.jsx +++ b/src/components/Controls/SearchField/index.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { FormControl, IconButton, @@ -10,7 +10,7 @@ import SearchIcon from "@material-ui/icons/Search"; import clsx from "clsx"; const SearchField = (props) => { - const { classes, onSearch } = props; + const { classes, onSearch, savedSearchValue } = props; const [searchTerm, setSearchTerm] = useState(""); const handleChange = (e) => { setSearchTerm(e.target.value); @@ -29,6 +29,12 @@ const SearchField = (props) => { } }; + useEffect(() => { + if (savedSearchValue) { + setSearchTerm(savedSearchValue); + } + }, [savedSearchValue]); + return ( Search diff --git a/src/components/Fleets/Table/index.jsx b/src/components/Fleets/Table/index.jsx index 2a34897..1d3f0b9 100644 --- a/src/components/Fleets/Table/index.jsx +++ b/src/components/Fleets/Table/index.jsx @@ -1,12 +1,13 @@ -import React, {useEffect, useState} from "react"; -import {Link} from 'react-router-dom'; -import {Grid,} from "@material-ui/core"; +import React, { useEffect } from "react"; +import { Link } from 'react-router-dom'; +import { Grid, } from "@material-ui/core"; import AddCircleIcon from "@material-ui/icons/AddCircle"; import clsx from "clsx"; -import {useUserContext} from "../../Contexts/UserContext" -import {useStatusContext} from "../../Contexts/StatusContext"; -import {FleetProvider} from "../../Contexts/FleetContext" +import { useUserContext } from "../../Contexts/UserContext" +import { useStatusContext } from "../../Contexts/StatusContext"; +import { FleetProvider } from "../../Contexts/FleetContext" +import { useLocalStorage } from "../../useLocalStorage"; import useStyles from "../../useStyles"; import SearchField from "../../Controls/SearchField"; import FleetSelectionTable from "../../Controls/FleetSelectionTable"; @@ -14,9 +15,9 @@ import FleetSelectionTable from "../../Controls/FleetSelectionTable"; const MainForm = () => { const classes = useStyles(); - const [search, setSearch] = useState(""); - const {setSitePath, setTitle} = useStatusContext(); - const {token: {idToken: {jwtToken: token}}} = useUserContext(); + const [search, setSearch] = useLocalStorage("FLEET_SEARCH", ""); + const { setSitePath, setTitle } = useStatusContext(); + const { token: { idToken: { jwtToken: token } } } = useUserContext(); const handleSearch = (query) => { setSearch(query); @@ -33,18 +34,18 @@ const MainForm = () => { - + - + diff --git a/src/components/Manifest/List/index.jsx b/src/components/Manifest/List/index.jsx index e41d28f..089e2a3 100644 --- a/src/components/Manifest/List/index.jsx +++ b/src/components/Manifest/List/index.jsx @@ -90,8 +90,8 @@ const MainForm = () => { const [pageIndex, setPageIndex] = useState(0); const [orderBy, setOrderBy] = useState("id"); const [order, setOrder] = useState("asc"); - const [search, setSearch] = useState(""); - const [active, setActive] = useState(true); + const [search, setSearch] = useLocalStorage("DEPLOYMENT_SEARCH", ""); + const [active, setActive] = useLocalStorage("DEPLOYMENT_ACTIVE", "true"); const [showDeleteModal, setShowDeleteModal] = useState(false); const [deleteId, setDeleteId] = useState(""); @@ -130,6 +130,7 @@ const MainForm = () => { useEffect(() => { (async () => { try { + handleActiveChange(null, active); await getManifests( { limit: pageSize, @@ -164,7 +165,7 @@ const MainForm = () => { }; const handleActiveChange = (event, newAlignment) => { - if (newAlignment !== null){ + if (newAlignment !== null) { setActive(newAlignment) } } @@ -246,7 +247,7 @@ const MainForm = () => { - + { aria-label="Active" onChange={handleActiveChange} > - Active - Archived + Active + Archived From 7c358a6052601d8bed4fb9a2da97c083f44dfdf2 Mon Sep 17 00:00:00 2001 From: Tristan Timblin Date: Fri, 16 Jun 2023 11:48:48 -0700 Subject: [PATCH 2/6] CEC-4525: add support for /tags endpoint and implement a new action for it (#361) * add action for adding tags --- src/components/App/App.test.js | 1 + .../App/__snapshots__/App.test.js.snap | 6 ++ src/components/Cars/List/index.jsx | 41 ++++++-- src/components/Contexts/VehicleContext.jsx | 17 ++++ .../__snapshots__/index.test.jsx.snap | 58 +++++++++++ .../Controls/TextInputList/index.jsx | 97 +++++++++++++++++++ .../Controls/TextInputList/index.test.jsx | 57 +++++++++++ .../TransformModal/__mocks__/index.jsx | 5 + src/components/TransformModal/index.jsx | 24 ++++- src/services/vehiclesAPI.js | 12 +++ 10 files changed, 308 insertions(+), 10 deletions(-) create mode 100644 src/components/Controls/TextInputList/__snapshots__/index.test.jsx.snap create mode 100644 src/components/Controls/TextInputList/index.jsx create mode 100644 src/components/Controls/TextInputList/index.test.jsx create mode 100644 src/components/TransformModal/__mocks__/index.jsx diff --git a/src/components/App/App.test.js b/src/components/App/App.test.js index f665b6e..7d64b94 100644 --- a/src/components/App/App.test.js +++ b/src/components/App/App.test.js @@ -9,6 +9,7 @@ jest.mock("../../services/vehiclesAPI"); jest.mock("../../services/superset"); jest.mock("../../services/suppliersAPI"); jest.mock("../../services/issueAPI"); +jest.mock("../TransformModal"); import { act, cleanup, render, diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap index 28e0e1e..7205472 100644 --- a/src/components/App/__snapshots__/App.test.js.snap +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -12652,6 +12652,12 @@ exports[`App Route /vehicles authenticated 1`] = ` +
+
diff --git a/src/components/Cars/List/index.jsx b/src/components/Cars/List/index.jsx index f4a332d..41485da 100644 --- a/src/components/Cars/List/index.jsx +++ b/src/components/Cars/List/index.jsx @@ -30,8 +30,15 @@ const MainForm = () => { type: "boolean", value: false }, - }) - const [showUpdateConfigModal, setShowUpdateConfigModal] = useState(false); + }); + const [tagsToAdd, setTagsToAdd] = useState({ + tags: { + label: "Tags", + type: "list.string", + value: [], + }, + }); + const [activeModal, setActiveModal] = useState(null); const { setTitle, setSitePath, setMessage } = useStatusContext(); const { token: { @@ -80,12 +87,23 @@ const MainForm = () => { selectedVins.forEach((vin, i) => taskRunner.push(request(vin, i))) } + const handleAddTags = async (fn) => { + await fn(selectedVins, tagsToAdd.tags.value, token) + .then(() => setMessage(`Added ${tagsToAdd.tags.value.length} tags to ${selectedVins.length} vehicles.`)) + .catch((error) => setMessage(error.message)); + }; + const actions = [ { name: "Update Configs", disabled: selectedVins.length === 0, - trigger: () => setShowUpdateConfigModal(true), + trigger: () => setActiveModal("updateConfig"), }, + { + name: "Add Tags", + disabled: selectedVins.length === 0, + trigger: () => setActiveModal("addTags"), + } ]; const handleOnlineHMI = (event) => { @@ -150,17 +168,26 @@ const MainForm = () => { onSelectAll={handleSelectAll} /> - {(context) => ( + {(context) => (<> setShowUpdateConfigModal(false)} + open={activeModal === "updateConfig"} + close={() => setActiveModal(null)} title="Update Configs" body={`You are updating the config for the following VINs: ${selectedVins.join(", ")}.`} data={config} setData={setConfig} submit={() => handleUploadConfig(context.uploadConfig)} /> - )} + setActiveModal(null)} + title="Add Tags" + body={`You are adding tags for the following VINs: ${selectedVins.join(", ")}.`} + data={tagsToAdd} + setData={setTagsToAdd} + submit={() => handleAddTags(context.addTags)} + /> + )} ); diff --git a/src/components/Contexts/VehicleContext.jsx b/src/components/Contexts/VehicleContext.jsx index 6a45be8..7bb2fae 100644 --- a/src/components/Contexts/VehicleContext.jsx +++ b/src/components/Contexts/VehicleContext.jsx @@ -61,6 +61,22 @@ export const VehicleProvider = ({ children }) => { } }; + const addTags = async (vins, tags, token) => { + try { + setBusy(true); + vins.forEach(vin => validateVIN(vin)); + const validateTags = tags.every(tag => typeof tag === "string"); + if (!validateTags) + throw new Error("Invalid Tag"); + + const result = await api.addTags(vins, tags, token) + if (result.error) + throw new Error(`Add tags error. ${result.message}`); + } finally { + setBusy(false) + } + } + const getConnections = async (vins, token) => { try { setBusy(true); @@ -301,6 +317,7 @@ export const VehicleProvider = ({ children }) => { getFleets, getVersionLog, uploadConfig, + addTags, }} > {children} diff --git a/src/components/Controls/TextInputList/__snapshots__/index.test.jsx.snap b/src/components/Controls/TextInputList/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..d1eb692 --- /dev/null +++ b/src/components/Controls/TextInputList/__snapshots__/index.test.jsx.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DropDownButton Render 1`] = ` +
+
+
+ +
+ + +
+
+
+
+
+`; diff --git a/src/components/Controls/TextInputList/index.jsx b/src/components/Controls/TextInputList/index.jsx new file mode 100644 index 0000000..a3fcb94 --- /dev/null +++ b/src/components/Controls/TextInputList/index.jsx @@ -0,0 +1,97 @@ +import { useState, useEffect } from "react"; +import { Cancel } from "@mui/icons-material"; +import { TextField } from "@mui/material"; +import { Box } from "@mui/system"; +import { useStatusContext } from "../../Contexts/StatusContext"; + +const TextInput = ({ text, handleDelete }) => { + return ( + + {text} + handleDelete(text)} + /> + + ); +} + +const TextInputList = ({ + onChange = () => {}, + validate = () => {}, + label +}) => { + const [textList, setTextList] = useState([]); + const [input, setInput] = useState(""); + + const { setMessage } = useStatusContext(); + + const handleDelete = (textToDelete) => { + setTextList(textList => textList.filter(text => text !== textToDelete)); + } + + const handleOnChange = (event) => { + const char = event.nativeEvent.data; + if (char === ",") { + try { + if (validate) validate(input); + setTextList(textList => [...textList, input]); + setInput(""); + } catch { + setMessage(`"${input}" is not valid.`); + } + } else { + setInput(event.target.value); + } + } + + useEffect(() => { + onChange(input.length ? [...textList, input] : [...textList]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [textList, input]); + + return ( +
+ + + + {textList.map((text) => ( + + ))} + +
+ ); +}; + +export default TextInputList; \ No newline at end of file diff --git a/src/components/Controls/TextInputList/index.test.jsx b/src/components/Controls/TextInputList/index.test.jsx new file mode 100644 index 0000000..4ce17b1 --- /dev/null +++ b/src/components/Controls/TextInputList/index.test.jsx @@ -0,0 +1,57 @@ +jest.mock("../../Contexts/StatusContext"); + +import React from "react"; +import { render, waitFor } from "@testing-library/react"; +import userEvent from '@testing-library/user-event'; + +import TextInputList from "."; +import addSnapshotSerializer from "../../../utils/snapshot"; + +describe("DropDownButton", () => { + beforeAll(() => { + addSnapshotSerializer(expect); + }); + + it("Render", async () => { + const { container } = render( + + ); + await waitFor(() => { + /* render */ + }); + expect(container).toMatchSnapshot(); + }); + + it("properly adds tag after comma", async () => { + const { getByText, getByTestId } = render( + + ); + + const [inputEl] = getByTestId("text-input-list").getElementsByTagName("input"); + userEvent.type(inputEl, "tag1"); + userEvent.type(inputEl, ","); + expect(getByText("tag1").nodeName).toBe("DIV"); + }); + + it("properly passes payload to callback", async () => { + const mockCallback = jest.fn(); + + const { getByTestId } = render( + + ); + + const [inputEl] = getByTestId("text-input-list").getElementsByTagName("input"); + userEvent.type(inputEl, "tag1"); + userEvent.type(inputEl, ","); + userEvent.type(inputEl, "tag2"); + expect(mockCallback).toHaveBeenCalledWith(["tag1", "tag2"]); + }); +}); diff --git a/src/components/TransformModal/__mocks__/index.jsx b/src/components/TransformModal/__mocks__/index.jsx new file mode 100644 index 0000000..dc2966e --- /dev/null +++ b/src/components/TransformModal/__mocks__/index.jsx @@ -0,0 +1,5 @@ +const TransformModalMock = jest.fn().mockImplementation(() => { + return
+}); + +export default TransformModalMock; diff --git a/src/components/TransformModal/index.jsx b/src/components/TransformModal/index.jsx index 7aae75b..8a58a0c 100644 --- a/src/components/TransformModal/index.jsx +++ b/src/components/TransformModal/index.jsx @@ -10,6 +10,7 @@ import { FormGroup, FormControlLabel, } from '@material-ui/core'; +import TextInputList from "../Controls/TextInputList"; const TransformModal = ({ open, @@ -25,16 +26,24 @@ const TransformModal = ({ submit(); }; - const handleChange = (key) => { + const handleChange = (key, value) => { setData((data) => { const {[key]: toChange, ...rest} = data; - toChange.value = !toChange.value; + switch (data[key].type) { + case "boolean": + toChange.value = !toChange.value; + break; + case "list.string": + toChange.value = value; + break; + default: + } return { [key]: toChange, ...rest }; }); - } + }; return ( ) + case "list.string": + return ( + handleChange(key, list)} + /> + ) default: return <>; } @@ -72,6 +89,7 @@ const TransformModal = ({
+
diff --git a/src/components/Cars/List/index.jsx b/src/components/Cars/List/index.jsx index 41485da..e2adbb4 100644 --- a/src/components/Cars/List/index.jsx +++ b/src/components/Cars/List/index.jsx @@ -17,6 +17,7 @@ import TransformModal from "../../TransformModal"; import { useLocalStorage } from "../../useLocalStorage"; import useStyles from "../../useStyles"; import TaskRunner from "../../../utils/taskRunner"; +import GeneralConfirmation from "../../GeneralConfirmation"; const MainForm = () => { const classes = useStyles(); @@ -93,6 +94,23 @@ const MainForm = () => { .catch((error) => setMessage(error.message)); }; + const handleDelete = async (fn) => { + const taskRunner = new TaskRunner(5); + const request = (vin) => { + return async () => { + return fn(vin, token) + .then(() => { + setMessage(`Deleted ${selectedVins.length} vehicles`); + setSelectedVins([]); + }) + .catch((error) => { + setMessage(error.message); + }) + } + } + selectedVins.forEach((vin) => taskRunner.push(request(vin))); + }; + const actions = [ { name: "Update Configs", @@ -103,7 +121,12 @@ const MainForm = () => { name: "Add Tags", disabled: selectedVins.length === 0, trigger: () => setActiveModal("addTags"), - } + }, + { + name: "Delete", + disabled: selectedVins.length === 0, + trigger: () => setActiveModal("delete"), + }, ]; const handleOnlineHMI = (event) => { @@ -187,6 +210,13 @@ const MainForm = () => { setData={setTagsToAdd} submit={() => handleAddTags(context.addTags)} /> + setActiveModal(null)} + title="Delete" + message={`You are about to delete the following VINs: ${selectedVins.join(", ")}`} + actionFunction={() => handleDelete(context.deleteVehicle)} + /> )} From 5120c271875ee1cd2aeecc97495031ed7d858a69 Mon Sep 17 00:00:00 2001 From: John Wu <76966357+jwu-fisker@users.noreply.github.com> Date: Tue, 20 Jun 2023 14:46:00 -0700 Subject: [PATCH 4/6] CEC-4581 Show battery voltage in digital twin (#365) * CEC-4581 Show battery voltage in digital twin * Fix warning --- .../DigitalTwinTab.test.jsx.snap | 7 +++++++ .../Contexts/__mocks__/VehicleContext.jsx | 1 + src/components/DigitalTwin/index.js | 20 ++++++++++++------- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/components/Cars/Status/__snapshots__/DigitalTwinTab.test.jsx.snap b/src/components/Cars/Status/__snapshots__/DigitalTwinTab.test.jsx.snap index 2f68f73..f12a979 100644 --- a/src/components/Cars/Status/__snapshots__/DigitalTwinTab.test.jsx.snap +++ b/src/components/Cars/Status/__snapshots__/DigitalTwinTab.test.jsx.snap @@ -55,6 +55,13 @@ exports[`DigitalTwinTab Render 1`] = ` : 12000 km

+

+ + Voltage + + : + 12.5 V +

Max Range diff --git a/src/components/Contexts/__mocks__/VehicleContext.jsx b/src/components/Contexts/__mocks__/VehicleContext.jsx index cfa6f0d..8e6034a 100644 --- a/src/components/Contexts/__mocks__/VehicleContext.jsx +++ b/src/components/Contexts/__mocks__/VehicleContext.jsx @@ -39,6 +39,7 @@ let vehicleState = { battery: { total_mileage_odometer: 12000, percent: 95, + battery_voltage: 12.5, }, max_range: { max_miles: 577, diff --git a/src/components/DigitalTwin/index.js b/src/components/DigitalTwin/index.js index 8548dc5..21b1709 100644 --- a/src/components/DigitalTwin/index.js +++ b/src/components/DigitalTwin/index.js @@ -8,6 +8,11 @@ const UNKNOWN = "unknown"; const LOCKED = "Locked"; const UNLOCKED = "Unlocked"; +const appendUnits = (value, units) => { + if (value || value === 0) return `${value}${units}`; + return UNKNOWN; +} + const keyValueTemplate = (key, value) => (

{key}: {value} @@ -37,16 +42,17 @@ const DigitalTwin = (props) => { {(battery || max_range) && (

Battery

- {keyValueTemplate("Percentage", `${battery?.percent || 0}%`)} - {keyValueTemplate("Total Mileage", `${battery?.total_mileage_odometer} km` || UNKNOWN)} - {keyValueTemplate("Max Range", `${max_range?.max_miles} km` || UNKNOWN)} + {keyValueTemplate("Percentage", appendUnits(battery?.percent || 0, "%"))} + {keyValueTemplate("Total Mileage", appendUnits(battery?.total_mileage_odometer, " km"))} + {keyValueTemplate("Voltage", appendUnits(battery?.battery_voltage, " V"))} + {keyValueTemplate("Max Range", appendUnits(max_range?.max_miles, " km"))}
)} {(vcu0x260 || charging_metrics) && (

Charging

{keyValueTemplate("Charge Type", vcu0x260?.charge_type || UNKNOWN)} - {keyValueTemplate("Remaining Time", `${charging_metrics?.remaining_charging_time} min` || UNKNOWN)} + {keyValueTemplate("Remaining Time", appendUnits(charging_metrics?.remaining_charging_time, " min"))}
)} {doors && ( @@ -95,9 +101,9 @@ const DigitalTwin = (props) => { return keyValueTemplate(value[0], "Invalid") } if (value[0] === "altitude") { - return keyValueTemplate(value[0], `${value[1]} m`); + return keyValueTemplate(value[0], appendUnits(value[1], " m")); } else { - return keyValueTemplate(value[0], `${value[1]}°`); + return keyValueTemplate(value[0], appendUnits(value[1], "°")); } })} @@ -124,7 +130,7 @@ const DigitalTwin = (props) => { )} {vehicle_speed && (
- {keyValueTemplate("Vehicle Speed", `${vehicle_speed.speed} km/h`)} + {keyValueTemplate("Vehicle Speed", appendUnits(vehicle_speed?.speed, " km/h"))}
)} From 224b4b2157251304de6073daa72c934574b154d7 Mon Sep 17 00:00:00 2001 From: Tristan Timblin Date: Wed, 21 Jun 2023 13:41:40 -0400 Subject: [PATCH 5/6] CEC-4545: fix responsive ecu table (#367) * CEC-4545: fix responsive ecu table --- src/components/App/__snapshots__/App.test.js.snap | 10 ++++++++++ src/components/Cars/Status/ECUsTab.jsx | 6 +++--- .../Cars/Status/__snapshots__/ECUsTab.test.jsx.snap | 4 ++-- .../Cars/Status/__snapshots__/index.test.jsx.snap | 10 ++++++++++ src/components/Cars/Status/index.jsx | 9 +++++++-- src/components/Controls/CarECUsTable/index.jsx | 6 +++--- src/components/useStyles.jsx | 9 +++++++-- 7 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap index 00e9ac4..5a5eea9 100644 --- a/src/components/App/__snapshots__/App.test.js.snap +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -11276,6 +11276,7 @@ exports[`App Route /vehicle-status authenticated 1`] = `
@@ -11528,54 +11529,63 @@ exports[`App Route /vehicle-status authenticated 1`] = `