diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap
index c2d990a..02bd570 100644
--- a/src/components/App/__snapshots__/App.test.js.snap
+++ b/src/components/App/__snapshots__/App.test.js.snap
@@ -4445,7 +4445,7 @@ exports[`App Route /package-deploy authenticated 1`] = `
class="MuiIconButton-label"
>
{
+ const closeLabel = hideSubmit ? "Close" : "Cancel";
+
return (
)
diff --git a/src/components/BulkActions/actions/Diagnostic.jsx b/src/components/BulkActions/actions/Diagnostic.jsx
new file mode 100644
index 0000000..d00476b
--- /dev/null
+++ b/src/components/BulkActions/actions/Diagnostic.jsx
@@ -0,0 +1,120 @@
+import { forwardRef, useImperativeHandle, useState, useEffect } from "react";
+import {
+ FormControl,
+ InputLabel,
+ Select,
+} from "@material-ui/core";
+import api from "../../../services/vehiclesAPI";
+import TaskRunner from "../../../utils/taskRunner";
+import { AllECUsCommand } from "../../Controls/SendDiagnosticCommand";
+import useStyles from "../../useStyles";
+import { useStatusContext } from "../../Contexts/StatusContext";
+import { useUserContext } from "../../Contexts/UserContext";
+import unionIntersect from "../../../utils/unionIntersect";
+
+const commands = [
+ { val: "remote_reset", displayname: "Remote Reset" },
+];
+
+async function getECUsByVINs(vins, token) {
+ return new Promise((resolve, reject) => {
+ const taskRunner = new TaskRunner(10, vins.length);
+
+ const task = (vin) => {
+ return async () => api.getECUs({ vin, unique: true }, token)
+ .then((result) => {
+ if (result.total === 0) {
+ reject([]);
+ }
+ return result.data.map(({ ecu }) => ecu);
+ })
+ .catch(() => reject([]));
+ }
+
+ vins.forEach((vin) => {
+ taskRunner.push(task(vin));
+ });
+
+ taskRunner.onComplete().then((results) => {
+ const ecus = unionIntersect(...results);
+ resolve(ecus.map(ecu => ({ ecu })));
+ });
+ });
+}
+
+export default forwardRef(({
+ ids,
+ idCSV,
+}, ref) => {
+ const [ecus, setECUs] = useState([{ ecu: "TBOX" }]);
+ const [currentECU, setCurrentECU] = useState("");
+ const [validateECUs, setValidateECUs] = useState(false);
+ const [command, setCommand] = useState("");
+ const classes = useStyles();
+ const { setMessage } = useStatusContext();
+ const { token: { idToken: { jwtToken: token } } } = useUserContext();
+
+ useImperativeHandle(ref, () => ({
+ async submit() {
+ if (!validateECUs) {
+ return Promise.reject("Invalid ECUs found, cannot submit");
+ }
+
+ return api.sendDiagnosticCommand(ids, {
+ command,
+ ecu_name: currentECU,
+ }, token)
+ .then(() => {
+ setMessage(`Sent ${command} command to ${ids.length} vehicles.`);
+ })
+ .catch(() => {
+ setMessage(`Failed to send ${command} command.`);
+ });
+ }
+ }));
+
+ const handleSelectCommand = (e) => {
+ setCommand(e.target.value);
+ };
+
+ useEffect(() => {
+ async function fetchData() {
+ setValidateECUs(false);
+ const ecus = await getECUsByVINs(ids, token);
+ setECUs(() => [{ ecu: "TBOX" }, ...ecus]); // TBOX is a hardcoded ECU
+ }
+ fetchData();
+ }, [ids, token]);
+
+ useEffect(() => {
+ setValidateECUs(true);
+ }, [ecus]);
+
+ return (
+
+
+ Attempt to send a vehicle diagnostic command to the following VINs: {idCSV}.
+
+
+
+
+
+ Diagnostic Command
+
+
+
+
+ );
+});
\ No newline at end of file
diff --git a/src/components/BulkActions/actions/Diagnostic.test.jsx b/src/components/BulkActions/actions/Diagnostic.test.jsx
new file mode 100644
index 0000000..9fb3617
--- /dev/null
+++ b/src/components/BulkActions/actions/Diagnostic.test.jsx
@@ -0,0 +1,42 @@
+jest.mock("../../Contexts/UserContext");
+jest.mock("../../Contexts/StatusContext");
+jest.mock("../../../services/vehiclesAPI");
+
+import React from "react";
+import {
+ render,
+ act,
+} from "@testing-library/react";
+import { UserProvider, setToken } from "../../Contexts/UserContext";
+import { StatusProvider } from "../../Contexts/StatusContext";
+import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing";
+import Diagnostic from "./Diagnostic";
+import vehiclesAPI from "../../../services/vehiclesAPI";
+
+describe("BulkActions/DeleteVehicles", () => {
+ beforeAll(() => {
+ setToken(TEST_AUTH_OBJECT_FISKER);
+ });
+
+ it("makes request to send remote command", async () => {
+ const sendDiagnosticCommand = jest.spyOn(vehiclesAPI, "sendDiagnosticCommand");
+ const getECUs = jest.spyOn(vehiclesAPI, "getECUs");
+ const ref = React.createRef();
+
+ render(
+
+
+
+
+
+ );
+
+ await act(async () => ref.current.submit());
+ expect(sendDiagnosticCommand).toHaveBeenCalledTimes(1);
+ expect(getECUs).toHaveBeenCalledTimes(3);
+ });
+});
diff --git a/src/components/BulkActions/actions/RemoteCommand.jsx b/src/components/BulkActions/actions/RemoteCommand.jsx
new file mode 100644
index 0000000..cf8b947
--- /dev/null
+++ b/src/components/BulkActions/actions/RemoteCommand.jsx
@@ -0,0 +1,20 @@
+import { forwardRef } from "react";
+import { VehicleProvider } from "../../Contexts/VehicleContext";
+import SendCommand from "../../Controls/SendCommand";
+
+export default forwardRef(({
+ ids,
+ idCSV,
+}) => {
+
+ return (
+
+
+ Send a remote command to the following VINs: {idCSV}.
+
+
+
+
+
+ );
+});
\ No newline at end of file
diff --git a/src/components/BulkActions/index.jsx b/src/components/BulkActions/index.jsx
index 2b50dd8..23d82e0 100644
--- a/src/components/BulkActions/index.jsx
+++ b/src/components/BulkActions/index.jsx
@@ -14,6 +14,8 @@ const UpdateConfig = lazy(() => import("./actions/UpdateConfig"));
const SendSMS = lazy(() => import("./actions/SendSMS"));
const Cancel = lazy(() => import("./actions/Cancel"));
const Redeploy = lazy(() => import("./actions/Redeploy"));
+const RemoteCommand = lazy(() => import("./actions/RemoteCommand"));
+const Diagnostic = lazy(() => import("./actions/Diagnostic"));
export default function BulkActions({
ids = [],
@@ -22,6 +24,7 @@ export default function BulkActions({
const [open, setOpen] = useState(false);
const [title, setTitle] = useState("Action");
const [active, setActive] = useState(null);
+ const [embedded, setEmbedded] = useState(false); // If the "submit" is embedded in the linked component
const activeRef = useRef();
const { groups, providers } = useUserContext();
@@ -67,6 +70,19 @@ export default function BulkActions({
name: "Redploy Updates",
disabled: false,
trigger: () => setActive("redeploy"),
+ },
+ {
+ id: "remoteCommand",
+ name: "Send Command",
+ disabled: false,
+ trigger: () => setActive("remoteCommand"),
+ embedded: true,
+ },
+ {
+ id: "diagnostic",
+ name: "Send Diagnostic",
+ disabled: false, // TODO set role
+ trigger: () => setActive("diagnostic"),
}
].filter((action) => actions.includes(action.id));
@@ -77,16 +93,20 @@ export default function BulkActions({
};
const handleClose = () => {
- setOpen(false).then(() => setActive(null));
+ setOpen(false);
}
const handleSubmit = () => {
- activeRef.current.submit();
+ if (activeRef.current.submit) {
+ activeRef.current.submit();
+ }
handleClose();
}
useEffect(() => {
- setTitle(filteredActions.find((action) => active === action.id)?.name || "Action");
+ const action = filteredActions.find((action) => active === action.id);
+ setTitle(action?.name || "Action");
+ setEmbedded(action?.embedded);
}, [active, filteredActions]);
if (!ids || ids.length === 0) return <>>;
@@ -99,6 +119,7 @@ export default function BulkActions({
open={open}
close={handleClose}
submit={handleSubmit}
+ hideSubmit={embedded}
>
Loading...}>
@@ -109,6 +130,8 @@ export default function BulkActions({
{active === "sms" && }
{active === "cancel" && }
{active === "redeploy" && }
+ {active === "remoteCommand" && }
+ {active === "diagnostic" && }
diff --git a/src/components/Cars/List/__snapshots__/index.test.jsx.snap b/src/components/Cars/List/__snapshots__/index.test.jsx.snap
index 1c5b5f3..2b53f6d 100644
--- a/src/components/Cars/List/__snapshots__/index.test.jsx.snap
+++ b/src/components/Cars/List/__snapshots__/index.test.jsx.snap
@@ -150,7 +150,7 @@ exports[`VehicleTable Render 1`] = `
class="MuiIconButton-label"
>
{
);
};
-const AllECUsCommand = ({ classes, ecus, currentECU, setCurrentECU }) => {
+export const AllECUsCommand = ({ classes, ecus, currentECU, setCurrentECU }) => {
return (