From a68c00b4ad7fea63e43a70ed3c42242ced4fd821 Mon Sep 17 00:00:00 2001
From: Paul Adamsen <117673433+pauladamseniii@users.noreply.github.com>
Date: Tue, 13 Jun 2023 15:54:04 -0400
Subject: [PATCH 1/5] CEC-4241 - Add trouble code to DTC search (#356)
---
.../Contexts/DTCTimelineContext.jsx | 6 ++--
.../__snapshots__/index.test.jsx.snap | 36 +++++++++++++++++++
.../DTCTimeline/DTCTimeline/index.jsx | 18 +++++++++-
src/services/DTCTimelineAPI.js | 5 +--
4 files changed, 59 insertions(+), 6 deletions(-)
diff --git a/src/components/Contexts/DTCTimelineContext.jsx b/src/components/Contexts/DTCTimelineContext.jsx
index 9649182..dded562 100644
--- a/src/components/Contexts/DTCTimelineContext.jsx
+++ b/src/components/Contexts/DTCTimelineContext.jsx
@@ -9,15 +9,15 @@ export const DTCTimelineProvider = ({ children }) => {
const [dtcData, setDTCData] = useState([]);
const [total, setTotal] = useState(0)
- const getDTCData = async (vin, ecu, startDate, endDate, search,token) => {
+ const getDTCData = async (vin, ecu, troubleCode, startDate, endDate, search, token) => {
try {
setBusy(true);
- const result = await api.getDTCData(vin, ecu, startDate, endDate, search,token);
+ const result = await api.getDTCData(vin, ecu, troubleCode, startDate, endDate, search, token);
if (result.error) {
throw new Error(`Get DTC data error. ${result.message}`);
}
setDTCData(result.data ?? []);
- if (result.total){
+ if (result.total) {
setTotal(result.total)
}
return result;
diff --git a/src/components/DTCTimeline/DTCTimeline/__snapshots__/index.test.jsx.snap b/src/components/DTCTimeline/DTCTimeline/__snapshots__/index.test.jsx.snap
index 3cde435..156a9b6 100644
--- a/src/components/DTCTimeline/DTCTimeline/__snapshots__/index.test.jsx.snap
+++ b/src/components/DTCTimeline/DTCTimeline/__snapshots__/index.test.jsx.snap
@@ -377,6 +377,42 @@ exports[`Render Render 1`] = `
+
+
+
+
+
+
+
{
const [selectedStartDate, setSelectedStartDate] = useState(new Date(Date.now() - 24 * 60 * 60 * 1000));
const [selectedEndDate, setSelectedEndDate] = useState(new Date());
const [selectedECU, setSelectedECU] = useState("");
+ const [selectedTroubleCode, setSelectedTroubleCode] = useState("");
const [gmtTimezone, setGmtTimezone] = useState(false);
const [loading, setLoading] = useState(false);
const { setMessage } = useStatusContext();
@@ -101,7 +102,7 @@ const MainForm = ({ vin }) => {
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
}
- await getDTCData(vin, selectedECU, start_date, end_date, search, token);
+ await getDTCData(vin, selectedECU, selectedTroubleCode, start_date, end_date, search, token);
// setDTCData(data);
} catch (e) {
setMessage(e.message);
@@ -244,6 +245,21 @@ const MainForm = ({ vin }) => {
setSelectedECU(e.target.value);
}}
/>
+ {
+ setSelectedTroubleCode(e.target.value);
+ }}
+ />
diff --git a/src/services/DTCTimelineAPI.js b/src/services/DTCTimelineAPI.js
index 3fce6c6..96aef95 100644
--- a/src/services/DTCTimelineAPI.js
+++ b/src/services/DTCTimelineAPI.js
@@ -8,12 +8,13 @@ import {
const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL;
const DTCTimelineAPI = {
- getDTCData: async (vin, ecu, startDate, endDate,search,token) => {
+ getDTCData: async (vin, ecu, troubleCode, startDate, endDate, search, token) => {
const queryParams = {
ecu,
+ trouble_code: troubleCode,
start_time: startDate,
end_time: endDate,
- decode:true,
+ decode: true,
...search,
};
const url = addQueryParams(`${API_ENDPOINT}/dtcs/${vin}`, queryParams);
From de1a5dcd2d9990a62dc147724477a6fa2ab5d8cf Mon Sep 17 00:00:00 2001
From: Tristan Timblin
Date: Wed, 14 Jun 2023 13:53:32 -0400
Subject: [PATCH 2/5] CEC-4499: add bulk update configs support (#357)
* add taskRunner util
* add bulk update config flow
---
src/components/Cars/List/index.jsx | 78 ++++++++++++-
src/components/Contexts/VehicleContext.jsx | 4 +-
.../__snapshots__/index.test.jsx.snap | 103 +++++++++++++++++
.../Controls/DropDownButton/index.jsx | 109 ++++++++++++++++++
.../Controls/DropDownButton/index.test.jsx | 80 +++++++++++++
.../__snapshots__/index.test.jsx.snap | 7 ++
src/components/TransformModal/index.jsx | 92 +++++++++++++++
src/components/TransformModal/index.test.jsx | 56 +++++++++
src/components/useStyles.jsx | 5 +
src/utils/taskRunner.js | 36 ++++++
src/utils/taskRunner.test.js | 58 ++++++++++
11 files changed, 621 insertions(+), 7 deletions(-)
create mode 100644 src/components/Controls/DropDownButton/__snapshots__/index.test.jsx.snap
create mode 100644 src/components/Controls/DropDownButton/index.jsx
create mode 100644 src/components/Controls/DropDownButton/index.test.jsx
create mode 100644 src/components/TransformModal/__snapshots__/index.test.jsx.snap
create mode 100644 src/components/TransformModal/index.jsx
create mode 100644 src/components/TransformModal/index.test.jsx
create mode 100644 src/utils/taskRunner.js
create mode 100644 src/utils/taskRunner.test.js
diff --git a/src/components/Cars/List/index.jsx b/src/components/Cars/List/index.jsx
index c0ab155..05b5cc7 100644
--- a/src/components/Cars/List/index.jsx
+++ b/src/components/Cars/List/index.jsx
@@ -7,19 +7,31 @@ import { Link } from "react-router-dom";
import { Permissions } from "../../../utils/roles";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
-import { VehicleProvider } from "../../Contexts/VehicleContext";
+import { VehicleProvider, VehicleContext } from "../../Contexts/VehicleContext";
import CarSelectionTable from "../../Controls/CarSelectionTable";
import OptionsDropdown from "../../Controls/OptionsDropdown";
import { RoleWrap } from "../../Controls/RoleWrap";
import SearchField from "../../Controls/SearchField";
+import DropDownButton from "../../Controls/DropDownButton";
+import TransformModal from "../../TransformModal";
import useStyles from "../../useStyles";
+import TaskRunner from "../../../utils/taskRunner";
const MainForm = () => {
const classes = useStyles();
const [search, setSearch] = useState("");
const [online, setOnline] = useState(false);
const [onlineHMI, setOnlineHMI] = useState(false);
- const { setTitle, setSitePath } = useStatusContext();
+ const [selectedVins, setSelectedVins] = useState([]);
+ const [config, setConfig] = useState({
+ force: {
+ label: "Force push",
+ type: "boolean",
+ value: false
+ },
+ })
+ const [showUpdateConfigModal, setShowUpdateConfigModal] = useState(false);
+ const { setTitle, setSitePath, setMessage } = useStatusContext();
const {
token: {
idToken: { jwtToken: token },
@@ -36,6 +48,45 @@ const MainForm = () => {
setOnline(event.target.checked);
};
+ const handleSelectAll = (cars) => {
+ setSelectedVins(cars);
+ };
+
+ const handleSelect = (event, key) => {
+ setSelectedVins((selectedVins) => {
+ if (event.target.checked) {
+ return [...selectedVins, key];
+ }
+ return selectedVins.filter(vin => vin !== key);
+ });
+ };
+
+ const handleUploadConfig = (fn) => {
+ const taskRunner = new TaskRunner(5);
+ const request = (vin, i) => {
+ const messagePrefix = `${i+1}/${selectedVins.length} "${vin}":`;
+ return async () => {
+ const result = await fn(vin, config.force.value, token)
+ .then(() => {
+ setMessage(`${messagePrefix} updated.`);
+ })
+ .catch((error) => {
+ setMessage(`${messagePrefix} ${error.message}`);
+ });
+ return result;
+ }
+ }
+ selectedVins.forEach((vin, i) => taskRunner.push(request(vin, i)))
+ }
+
+ const actions = [
+ {
+ name: "Update Configs",
+ disabled: selectedVins.length === 0,
+ trigger: () => setShowUpdateConfigModal(true),
+ },
+ ];
+
const handleOnlineHMI = (event) => {
setOnlineHMI(event.target.checked);
};
@@ -49,7 +100,7 @@ const MainForm = () => {
return (
-
+
{
+
-
+
);
};
diff --git a/src/components/Contexts/VehicleContext.jsx b/src/components/Contexts/VehicleContext.jsx
index 2486828..6967a34 100644
--- a/src/components/Contexts/VehicleContext.jsx
+++ b/src/components/Contexts/VehicleContext.jsx
@@ -4,7 +4,7 @@ import { logger } from "../../services/monitoring";
import api from "../../services/vehiclesAPI";
import { validateVIN } from "../../utils/validationSupplier";
-const VehicleContext = React.createContext();
+export const VehicleContext = React.createContext();
const validateAdd = (vehicle) => {
if (vehicle == null) {
@@ -299,7 +299,7 @@ export const VehicleProvider = ({ children }) => {
updateVehicle,
getFleets,
getVersionLog,
- uploadConfig
+ uploadConfig,
}}
>
{children}
diff --git a/src/components/Controls/DropDownButton/__snapshots__/index.test.jsx.snap b/src/components/Controls/DropDownButton/__snapshots__/index.test.jsx.snap
new file mode 100644
index 0000000..a4d6165
--- /dev/null
+++ b/src/components/Controls/DropDownButton/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,103 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DownloadFileLink Render 1`] = `
+
+`;
+
+exports[`DropDownButton Render 1`] = `
+
+`;
diff --git a/src/components/Controls/DropDownButton/index.jsx b/src/components/Controls/DropDownButton/index.jsx
new file mode 100644
index 0000000..77a5562
--- /dev/null
+++ b/src/components/Controls/DropDownButton/index.jsx
@@ -0,0 +1,109 @@
+import { useRef, useState } from "react";
+import {
+ Button,
+ ButtonGroup,
+ ClickAwayListener,
+ Grow,
+ MenuItem,
+ MenuList,
+ Paper,
+ Popper,
+} from "@mui/material";
+import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
+
+const DropDownButton = ({ actions = [], payload = [] }) => {
+ const [open, setOpen] = useState(false);
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const anchorRef = useRef(null);
+
+ const handleClick = () => {
+ actions[selectedIndex].trigger(...payload);
+ };
+
+ const handleMenuItemClick = (event, index) => {
+ setSelectedIndex(index);
+ setOpen(false);
+ }
+
+ const handleToggle = () => {
+ setOpen(open => !open);
+ };
+
+ const handleClose = (event) => {
+ if (
+ anchorRef.current &&
+ anchorRef.current.contains(event.target)
+ ) {
+ return;
+ }
+
+ setOpen(false);
+ };
+
+ return (
+ <>
+
+
+
+
+
+ {({ TransitionProps, placement }) => (
+
+
+
+
+
+
+
+ )}
+
+ >
+ )
+}
+
+export default DropDownButton;
\ No newline at end of file
diff --git a/src/components/Controls/DropDownButton/index.test.jsx b/src/components/Controls/DropDownButton/index.test.jsx
new file mode 100644
index 0000000..570c51f
--- /dev/null
+++ b/src/components/Controls/DropDownButton/index.test.jsx
@@ -0,0 +1,80 @@
+import React from "react";
+import { fireEvent, render, waitFor, screen } from "@testing-library/react";
+
+import DropDownButton from ".";
+import addSnapshotSerializer from "../../../utils/snapshot";
+
+describe("DropDownButton", () => {
+ beforeAll(() => {
+ addSnapshotSerializer(expect);
+ });
+
+ it("Render", async () => {
+ const actions = [
+ {
+ name: "Action One",
+ disabled: false,
+ trigger: (paramOne, paramTwo) => {}
+ },
+ {
+ name: "Action Two",
+ disabled: false,
+ trigger: (paramOne, paramTwo) => {}
+ },
+ ];
+
+ const { container } = render(
+
+ );
+ await waitFor(() => {
+ /* render */
+ });
+ expect(container).toMatchSnapshot();
+ });
+
+ it("properly disables an action", async () => {
+ const actions = [
+ {
+ name: "Disabled Action",
+ disabled: true,
+ trigger: () => {}
+ },
+ ];
+
+ const { getByText } = render(
+
+ );
+ await waitFor(() => {
+ /* render */
+ });
+ const buttonEl = getByText("Disabled Action").parentElement;
+ expect(buttonEl).toHaveProperty("disabled", true);
+ });
+
+ it("properly passes payload to callback", async () => {
+ const actions = [
+ {
+ name: "Action One",
+ disabled: false,
+ trigger: jest.fn(),
+ },
+ ];
+
+ render(
+
+ );
+
+ const buttonEl = screen.getByText("Action One");
+ fireEvent.click(buttonEl);
+ expect(actions[0].trigger).toHaveBeenCalledWith("somePayload", "somePayload2");
+ });
+});
diff --git a/src/components/TransformModal/__snapshots__/index.test.jsx.snap b/src/components/TransformModal/__snapshots__/index.test.jsx.snap
new file mode 100644
index 0000000..c2a5f3f
--- /dev/null
+++ b/src/components/TransformModal/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TransformModal Render 1`] = `
+
+`;
diff --git a/src/components/TransformModal/index.jsx b/src/components/TransformModal/index.jsx
new file mode 100644
index 0000000..7aae75b
--- /dev/null
+++ b/src/components/TransformModal/index.jsx
@@ -0,0 +1,92 @@
+import React from "react";
+import {
+ Button,
+ Checkbox,
+ Dialog,
+ DialogTitle,
+ DialogContentText,
+ DialogActions,
+ DialogContent,
+ FormGroup,
+ FormControlLabel,
+} from '@material-ui/core';
+
+const TransformModal = ({
+ open,
+ close,
+ title,
+ body,
+ data,
+ setData,
+ submit
+}) => {
+ const handleClick = () => {
+ close();
+ submit();
+ };
+
+ const handleChange = (key) => {
+ setData((data) => {
+ const {[key]: toChange, ...rest} = data;
+ toChange.value = !toChange.value;
+ return {
+ [key]: toChange,
+ ...rest
+ };
+ });
+ }
+
+ return (
+
+ );
+}
+
+export default TransformModal;
\ No newline at end of file
diff --git a/src/components/TransformModal/index.test.jsx b/src/components/TransformModal/index.test.jsx
new file mode 100644
index 0000000..260c828
--- /dev/null
+++ b/src/components/TransformModal/index.test.jsx
@@ -0,0 +1,56 @@
+import React from "react";
+import { render, waitFor } from "@testing-library/react";
+
+import TransformModal from ".";
+import addSnapshotSerializer from "../../utils/snapshot";
+
+const data = {
+ test: {
+ label: "Test field",
+ value: false,
+ type: "boolean",
+ },
+}
+
+describe("TransformModal", () => {
+ beforeAll(() => {
+ addSnapshotSerializer(expect);
+ });
+
+ it("Render", async () => {
+
+ const { container } = render(
+ {}}
+ title="Title"
+ body="Body"
+ data={data}
+ setData={() => {}}
+ submit={() => {}}
+ />
+ );
+ await waitFor(() => {
+ /* render */
+ });
+ expect(container).toMatchSnapshot();
+ });
+
+ it("properly renders a checkbox for a boolean", async () => {
+ const { getByText } = render(
+ {}}
+ title="Title"
+ body="Body"
+ data={data}
+ setData={() => {}}
+ submit={() => {}}
+ />
+ );
+ await waitFor(() => {
+ /* render */
+ });
+ expect(getByText("Test field")).toBeTruthy();
+ });
+});
diff --git a/src/components/useStyles.jsx b/src/components/useStyles.jsx
index d3daf28..9cb706f 100644
--- a/src/components/useStyles.jsx
+++ b/src/components/useStyles.jsx
@@ -302,6 +302,11 @@ const useStyles = makeStyles((theme) => ({
width: "100%",
padding: "16px 0",
backgroundColor: "#fafafa",
+ },
+ actionsBar: {
+ display: "flex",
+ alignItems: "center",
+ gap: "12px",
}
}));
diff --git a/src/utils/taskRunner.js b/src/utils/taskRunner.js
new file mode 100644
index 0000000..baa8e6b
--- /dev/null
+++ b/src/utils/taskRunner.js
@@ -0,0 +1,36 @@
+export default class TaskRunner {
+ constructor(concurrencyLimit = 1) {
+ this.queue = [];
+ this.running = 0;
+ this.concurrencyLimit = concurrencyLimit;
+ }
+
+ execute() {
+ if (this.running >= this.concurrencyLimit || this.queue.length === 0) {
+ return;
+ }
+
+ const task = this.queue.shift();
+ this.running += 1;
+ task();
+ }
+
+ async push(fn) {
+ return new Promise((resolve, reject) => {
+ const task = async () => {
+ try {
+ const result = await fn();
+ resolve(result);
+ } catch (error) {
+ reject(error);
+ } finally {
+ this.running -= 1;
+ this.execute();
+ }
+ }
+
+ this.queue.push(task);
+ this.execute();
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/utils/taskRunner.test.js b/src/utils/taskRunner.test.js
new file mode 100644
index 0000000..7e8b301
--- /dev/null
+++ b/src/utils/taskRunner.test.js
@@ -0,0 +1,58 @@
+import TaskRunner from "./taskRunner";
+
+const mockPromise = async (id, ms) => {
+ await new Promise(resolve => setTimeout(resolve, ms));
+ return id;
+}
+
+const asyncFn1 = () => mockPromise(1, 200);
+const asyncFn2 = () => mockPromise(2, 100);
+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);
+ taskRunner.push(() => mockPromise(1, 300));
+ expect(taskRunner.running).toEqual(1);
+ });
+
+ it("keeps task in queue when at concurrency limit", () => {
+ const taskRunner = new TaskRunner(2);
+ 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);
+ });
+
+ it("runs queued tasks as space becomes available", async () => {
+ const taskRunner = new TaskRunner(2);
+ taskRunner.push(() => mockPromise(1, 600));
+ taskRunner.push(() => mockPromise(2, 300));
+ taskRunner.push(() => mockPromise(3, 100));
+ expect(taskRunner.queue.length).toEqual(1);
+ await new Promise(r => setTimeout(r, 301));
+ expect(taskRunner.queue.length).toEqual(0);
+ });
+
+ it("runs tasks in order", async () => {
+ const actual = [];
+ const taskRunner = new TaskRunner(2);
+ taskRunner.push(asyncFn1)
+ .then((id) => {
+ actual.push(id);
+ });
+ taskRunner.push(asyncFn2)
+ .then((id) => {
+ actual.push(id);
+ });
+ taskRunner.push(asyncFn3)
+ .then((id) => {
+ actual.push(id);
+ });
+ await new Promise(resolve => setTimeout(resolve, 500));
+ expect(actual).toEqual([2, 3, 1]);
+ });
+})
\ No newline at end of file
From 68ac95b33b1f7b76d6a6954aa5cd0ef2c9d751f3 Mon Sep 17 00:00:00 2001
From: Tristan Timblin
Date: Wed, 14 Jun 2023 18:08:45 -0400
Subject: [PATCH 3/5] add VehicleConsumer mock (#359)
---
.../App/__snapshots__/App.test.js.snap | 74 ++++++++++++++-
.../List/__snapshots__/index.test.jsx.snap | 74 ++++++++++++++-
src/components/Cars/List/index.jsx | 6 +-
src/components/Contexts/VehicleContext.jsx | 3 +-
.../Contexts/__mocks__/VehicleContext.jsx | 4 +
.../__snapshots__/index.test.jsx.snap | 91 ++++---------------
.../Controls/DropDownButton/index.test.jsx | 2 +-
7 files changed, 170 insertions(+), 84 deletions(-)
diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap
index 08771e4..7cc160d 100644
--- a/src/components/App/__snapshots__/App.test.js.snap
+++ b/src/components/App/__snapshots__/App.test.js.snap
@@ -12198,7 +12198,7 @@ exports[`App Route /vehicles authenticated 1`] = `
class="MuiGrid-root makeStyles-root-0 MuiGrid-container MuiGrid-spacing-xs-2"
>