Car ECUs
@@ -25,9 +25,9 @@ const MainForm = ({ vin }) => {
);
};
-const CarUpdatesTab = ({vin}) => (
+const CarUpdatesTab = ({ vin }) => (
-
+
);
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/Cars/Status/__snapshots__/ECUsTab.test.jsx.snap b/src/components/Cars/Status/__snapshots__/ECUsTab.test.jsx.snap
index 8ec2646..b37ef2e 100644
--- a/src/components/Cars/Status/__snapshots__/ECUsTab.test.jsx.snap
+++ b/src/components/Cars/Status/__snapshots__/ECUsTab.test.jsx.snap
@@ -9,7 +9,7 @@ exports[`ECUsTab Render 1`] = `
data-testid="mocked-userprovider"
>
@@ -418,54 +419,63 @@ exports[`CarStatus Render 1`] = `
{
{tabs.map((item, index) => (
-
-
+
+
))}
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/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/Controls/CarECUsTable/index.jsx b/src/components/Controls/CarECUsTable/index.jsx
index 95d9b29..4c3fae0 100644
--- a/src/components/Controls/CarECUsTable/index.jsx
+++ b/src/components/Controls/CarECUsTable/index.jsx
@@ -2,12 +2,12 @@ import {
Table,
TableBody,
TableCell,
+ TableContainer,
TableFooter,
TablePagination,
TableRow,
Tooltip
} from "@material-ui/core";
-import clsx from "clsx";
import React, { useEffect, useState } from "react";
import { logger } from "../../../services/monitoring";
@@ -122,7 +122,7 @@ const CarECUsTable = ({ vin, token, classes }) => {
};
return (
-
+
);
};
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/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/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"))}
)}
diff --git a/src/components/Fleets/Status/Details/__snapshots__/index.test.jsx.snap b/src/components/Fleets/Status/Details/__snapshots__/index.test.jsx.snap
index 1019037..b66d659 100644
--- a/src/components/Fleets/Status/Details/__snapshots__/index.test.jsx.snap
+++ b/src/components/Fleets/Status/Details/__snapshots__/index.test.jsx.snap
@@ -86,6 +86,23 @@ exports[`FleetDetailsTab Render 1`] = `
:
3
+
+
+ DTC Enabled
+
+ :
+ false
+
+
+
{
const [showDeleteModal, setShowDeleteModal] = useState(false)
const { token: { idToken: { jwtToken: token } } } = useUserContext();
+ const showDebugMask = (process.env.REACT_APP_ENABLE_DEBUGMASK === "1");
+
useEffect(() => {
(async () => {
try {
@@ -70,6 +72,14 @@ const MainForm = ({ name }) => {
Enabled: {fleet.canbus.data_logger_enabled.toString()}
Max Disk Buffer Size: {fleet.canbus.max_disk_buffer_size ?? "Default"}
Filters: {fleet.canbus.filters ? fleet.canbus.filters.length : 0}
+
DTC Enabled: {(fleet.canbus.dtc_enabled || false).toString()}
+
+ )}
+ {showDebugMask && (
+
+
+ Debug Mask: {fleet.debug_mask}
+
)}
@@ -85,7 +95,7 @@ const MainForm = ({ name }) => {
-
setShowDeleteModal(false)} deleteFunction={onDelete}/>
+ 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 fd28656..b62b7fd 100644
--- a/src/components/Fleets/Status/__snapshots__/DetailsTab.test.jsx.snap
+++ b/src/components/Fleets/Status/__snapshots__/DetailsTab.test.jsx.snap
@@ -94,6 +94,23 @@ exports[`DetailsTab Render 1`] = `
:
3
+
+
+ DTC Enabled
+
+ :
+ false
+
+
+
+
+
+ DTC Enabled
+
+ :
+ false
+
+
+
{
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/Fleets/Update/__snapshots__/index.test.jsx.snap b/src/components/Fleets/Update/__snapshots__/index.test.jsx.snap
index 257cb6d..0b7ef53 100644
--- a/src/components/Fleets/Update/__snapshots__/index.test.jsx.snap
+++ b/src/components/Fleets/Update/__snapshots__/index.test.jsx.snap
@@ -565,6 +565,81 @@ exports[`FleetUpdate Render 1`] = `
+
+
+
+
+
+
+
+