diff --git a/src/components/BulkActions/index.jsx b/src/components/BulkActions/index.jsx
new file mode 100644
index 0000000..81f5f3c
--- /dev/null
+++ b/src/components/BulkActions/index.jsx
@@ -0,0 +1,84 @@
+import { useEffect, useState } from "react";
+import TransformModal from "../TransformModal";
+import DropDownButton from "../Controls/DropDownButton";
+import { useUserContext } from "../Contexts/UserContext";
+import { useStatusContext } from "../Contexts/StatusContext";
+import useAddTags from "./useAddTags";
+import useUpdateConfig from "./useUpdateConfig";
+
+const transformArrayToCSV = (arr) => arr.join(", ");
+
+export default function BulkActions({
+ vins = [],
+}) {
+ const [vinCSV, setVinCSV] = useState(transformArrayToCSV(vins));
+ const [active, setActive] = useState(null);
+ const actions = [
+ {
+ name: "Update Configs",
+ disabled: vins.length === 0,
+ trigger: () => setActive("updateConfig"),
+ },
+ {
+ name: "Add Tags",
+ disabled: vins.length === 0,
+ trigger: () => setActive("addTags"),
+ },
+ ];
+
+ const updateConfig = useUpdateConfig();
+ const addTags = useAddTags();
+
+ const { setMessage } = useStatusContext();
+ const {
+ token: {
+ idToken: { jwtToken: token },
+ },
+ } = useUserContext();
+
+ const handleUpdateConfig = () => {
+ updateConfig.submit(vins, token)
+ .then(() => {
+ setMessage(`${vins.length} vehicles updated.`);
+ })
+ .catch((error) => {
+ setMessage(error.message);
+ });
+ }
+
+ const handleAddTags = () => {
+ addTags.submit(vins, token)
+ .then(() => setMessage(`Added ${addTags.data.tags.value.length} tags to ${vins.length} vehicles.`))
+ .catch((error) => setMessage(error.message));
+ }
+
+ const handleClose = () => setActive(null);
+
+ useEffect(() => {
+ setVinCSV(transformArrayToCSV(vins));
+ }, [vins]);
+
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/src/components/BulkActions/useAddTags.js b/src/components/BulkActions/useAddTags.js
new file mode 100644
index 0000000..7696595
--- /dev/null
+++ b/src/components/BulkActions/useAddTags.js
@@ -0,0 +1,22 @@
+import { useState } from "react";
+import vehiclesAPI from "../../services/vehiclesAPI";
+
+export default function useAddTags() {
+ const [tags, setTags] = useState({
+ tags: {
+ label: "Tags",
+ type: "list.string",
+ value: [],
+ },
+ });
+
+ const submit = async (vins, token) => {
+ return vehiclesAPI.addTags(vins, tags.tags.value, token);
+ }
+
+ return {
+ data: tags,
+ setData: setTags,
+ submit,
+ };
+}
diff --git a/src/components/BulkActions/useUpdateConfig.js b/src/components/BulkActions/useUpdateConfig.js
new file mode 100644
index 0000000..c648bd1
--- /dev/null
+++ b/src/components/BulkActions/useUpdateConfig.js
@@ -0,0 +1,40 @@
+import { useState } from "react";
+import TaskRunner from "../../utils/taskRunner";
+import vehiclesAPI from "../../services/vehiclesAPI";
+
+export default function useUpdateConfig() {
+ const [config, setConfig] = useState({
+ force: {
+ label: "Force Push",
+ type: "boolean",
+ value: false,
+ },
+ });
+
+ const submit = async (vins, token) => {
+ return new Promise((resolve, reject) => {
+ const taskRunner = new TaskRunner(5);
+
+ const task = (vin, isLast) => {
+ return async () => vehiclesAPI.updateConfig(vin, config.force.value, token)
+ .then((response) => {
+ if (isLast) {
+ if (response.error) {
+ reject(response);
+ }
+ resolve(response)
+ }
+ })
+ .catch((error) => reject(error));
+ }
+
+ vins.forEach((vin, index) => taskRunner.push(task(vin, index === vins.length - 1)));
+ });
+ }
+
+ return {
+ data: config,
+ setData: setConfig,
+ submit,
+ };
+}
\ No newline at end of file
diff --git a/src/components/Cars/Status/Details/__snapshots__/index.test.jsx.snap b/src/components/Cars/Status/Details/__snapshots__/index.test.jsx.snap
index 7cadd4e..d245fc7 100644
--- a/src/components/Cars/Status/Details/__snapshots__/index.test.jsx.snap
+++ b/src/components/Cars/Status/Details/__snapshots__/index.test.jsx.snap
@@ -147,6 +147,13 @@ exports[`VehicleDetailsTab Render 1`] = `
:
false
+
+
+ DLT Logging Enabled
+
+ :
+ false
+
-
-
-
-
-
+ />
diff --git a/src/components/Cars/Status/Details/index.jsx b/src/components/Cars/Status/Details/index.jsx
index cb8a973..2ef5534 100644
--- a/src/components/Cars/Status/Details/index.jsx
+++ b/src/components/Cars/Status/Details/index.jsx
@@ -115,38 +115,41 @@ const MainForm = ({ vin }) => {
Info Source: {vehicle.info_source}
- Tags: {vehicle.tags ? vehicle.tags.join(", ") : "none" }
+ Tags: {vehicle.tags ? vehicle.tags.join(", ") : "none"}
- {vehicle.log_level != null && (
+ {vehicle.log_level != null && (
Log Level: {vehicle.log_level}
)}
- {vehicle.canbus && (
- <>
-
- CANBus Enabled: {vehicle.canbus.enabled.toString()}
-
-
- Max Memory Buffer Size: {vehicle.canbus.max_mem_buffer_size ?? "Default"}
-
-
- Data Logger Enabled: {vehicle.canbus.data_logger_enabled.toString()}
-
-
- Max Disk Buffer Size: {vehicle.canbus.max_disk_buffer_size ?? "Default"}
-
-
- Filters: {vehicle.canbus.filters ? vehicle.canbus.filters.length : 0}
-
-
- DTC Enabled: { (vehicle.canbus.dtc_enabled || false).toString() }
-
- >
- )}
-
+ {vehicle.canbus && (
+ <>
+
+ CANBus Enabled: {vehicle.canbus.enabled.toString()}
+
+
+ Max Memory Buffer Size: {vehicle.canbus.max_mem_buffer_size ?? "Default"}
+
+
+ Data Logger Enabled: {vehicle.canbus.data_logger_enabled.toString()}
+
+
+ Max Disk Buffer Size: {vehicle.canbus.max_disk_buffer_size ?? "Default"}
+
+
+ Filters: {vehicle.canbus.filters ? vehicle.canbus.filters.length : 0}
+
+
+ DTC Enabled: {(vehicle.canbus.dtc_enabled || false).toString()}
+
+
+ DLT Logging Enabled: {(vehicle.dlt_enabled || false).toString()}
+
+ >
+ )}
+
{showDebugMask && (
@@ -156,19 +159,19 @@ const MainForm = ({ vin }) => {
)}
+ groups={groups}
+ providers={providers}
+ rolesPerProvider={Permissions.FiskerUpdateDeploy}
+ >
- }
+ label="Force Config Update"
+ control={
+
+ }
+ />
setShowUploadConfigModal(true)} >
diff --git a/src/components/Cars/Status/Details/index.test.jsx b/src/components/Cars/Status/Details/index.test.jsx
index b571949..94f6310 100644
--- a/src/components/Cars/Status/Details/index.test.jsx
+++ b/src/components/Cars/Status/Details/index.test.jsx
@@ -2,24 +2,25 @@ jest.mock("../../../Contexts/VehicleContext");
jest.mock("../../../Contexts/StatusContext");
jest.mock("../../../Contexts/UserContext");
-import { render, waitFor } from "@testing-library/react";
+import { render, screen, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import routeData from "react-router";
import { VehicleProvider } from "../../../Contexts/VehicleContext";
import { StatusProvider } from "../../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../../Contexts/UserContext";
-import { TEST_AUTH_OBJECT_FISKER }from "../../../../utils/testing";
+import { TEST_AUTH_OBJECT_FISKER } from "../../../../utils/testing";
import MainForm from "./index";
import addSnapshotSerializer from "../../../../utils/snapshot";
+import * as Roles from "../../../../utils/roles";
const renderVehicleDetailsTab = async () => {
const { container } = render(
-
+
-
+
@@ -46,4 +47,23 @@ describe("VehicleDetailsTab", () => {
const container = await renderVehicleDetailsTab();
expect(container).toMatchSnapshot();
});
+
+ it("renders update config control when required permission is present.", () => {
+ const hasRole = jest.spyOn(Roles, 'hasRole');
+ hasRole.mockReturnValue(true);
+ render(
+
+
+
+
+
+
+
+
+
+ );
+
+ expect(screen.getByLabelText("Force Config Update")).toBeTruthy();
+ hasRole.mockRestore();
+ })
});
diff --git a/src/components/Cars/Status/RemoteDiagnosticCommands.jsx b/src/components/Cars/Status/RemoteDiagnosticCommands.jsx
new file mode 100644
index 0000000..cadf990
--- /dev/null
+++ b/src/components/Cars/Status/RemoteDiagnosticCommands.jsx
@@ -0,0 +1,30 @@
+import useStyles from "../../useStyles";
+import clsx from "clsx";
+import Typography from "@material-ui/core/Typography";
+import SendDiagnosticCommand from "../../Controls/SendDiagnosticCommand";
+import { useParams } from "react-router";
+
+import { useUserContext } from "../../Contexts/UserContext";
+
+import {VehicleProvider} from "../../Contexts/VehicleContext";
+
+const RemoteDiagnosticCommandsTab = (props) => {
+ const { vin } = useParams();
+ const classes = useStyles();
+ const {
+ token: {
+ idToken: { jwtToken: token },
+ },
+ } = useUserContext();
+
+ return (
+
+ Vehicle Diagnostic Commands
+
+
+
+
+ )
+}
+
+export default RemoteDiagnosticCommandsTab
diff --git a/src/components/Cars/Status/__snapshots__/DetailsTab.test.jsx.snap b/src/components/Cars/Status/__snapshots__/DetailsTab.test.jsx.snap
index fedbdd8..900b45a 100644
--- a/src/components/Cars/Status/__snapshots__/DetailsTab.test.jsx.snap
+++ b/src/components/Cars/Status/__snapshots__/DetailsTab.test.jsx.snap
@@ -155,6 +155,13 @@ exports[`DetailsTab Render 1`] = `
:
false
+
+
+ DLT Logging Enabled
+
+ :
+ false
+
-
-
-
-
-
+ />
diff --git a/src/components/Cars/Status/__snapshots__/DigitalTwinTab.test.jsx.snap b/src/components/Cars/Status/__snapshots__/DigitalTwinTab.test.jsx.snap
index f12a979..c7e54ae 100644
--- a/src/components/Cars/Status/__snapshots__/DigitalTwinTab.test.jsx.snap
+++ b/src/components/Cars/Status/__snapshots__/DigitalTwinTab.test.jsx.snap
@@ -237,6 +237,17 @@ exports[`DigitalTwinTab Render 1`] = `
77.7 km/h
+
-
-
-
-
-
+ />
diff --git a/src/components/Cars/Status/index.jsx b/src/components/Cars/Status/index.jsx
index fe7bd66..de180d4 100644
--- a/src/components/Cars/Status/index.jsx
+++ b/src/components/Cars/Status/index.jsx
@@ -18,6 +18,7 @@ import DigitalTwinTab from "./DigitalTwinTab";
import ECUsTab from "./ECUsTab";
import FleetsTab from "./FleetsTab";
import RemoteCommandsTab from "./RemoteCommandsTab";
+import RemoteDiagnosticCommandsTab from "./RemoteDiagnosticCommands";
import TRexLogsTab from "./TRexLogsTab";
const tabHashes = ["details", "updates", "filters"];
@@ -63,6 +64,11 @@ const TabViews = [
component: RemoteCommandsTab,
rolesPerProvider: Permissions.FiskerMagnaCreate,
},
+ {
+ label: "Remote Diagnostic Commands",
+ component: RemoteDiagnosticCommandsTab,
+ rolesPerProvider: Permissions.CarDiagnostic,
+ },
{
label: "Fleets",
component: FleetsTab,
diff --git a/src/components/Cars/Update/__snapshots__/index.test.jsx.snap b/src/components/Cars/Update/__snapshots__/index.test.jsx.snap
index 8bab667..6101f51 100644
--- a/src/components/Cars/Update/__snapshots__/index.test.jsx.snap
+++ b/src/components/Cars/Update/__snapshots__/index.test.jsx.snap
@@ -1006,6 +1006,43 @@ exports[`VehicleUpdate Render 1`] = `
DTC Enabled
+
diff --git a/src/components/Cars/Update/index.jsx b/src/components/Cars/Update/index.jsx
index 25ab193..65c2379 100644
--- a/src/components/Cars/Update/index.jsx
+++ b/src/components/Cars/Update/index.jsx
@@ -47,6 +47,7 @@ const MainForm = () => {
const [maxMemBufferSize, setMaxMemBufferSize] = useState(0);
const [maxDiskBufferSize, setMaxDiskBufferSize] = useState(0);
const [dtcEnabled, setDTCEnabled] = useState(true);
+ const [dltEnabled, setDLTEnabled] = useState(false);
const debugMaskEl = useRef(null);
const tagsEl = useRef(null);
@@ -99,6 +100,7 @@ const MainForm = () => {
setMaxDiskBufferSize(vehicle.canbus.max_disk_buffer_size ?? maxDiskBufferSize);
setDTCEnabled(vehicle.canbus.dtc_enabled ?? dtcEnabled);
}
+ setDLTEnabled(vehicle.dlt_enabled ?? dltEnabled);
if (showDebugMask) {
debugMaskEl.current.value = vehicle.debug_mask ?? ""
@@ -125,6 +127,10 @@ const MainForm = () => {
setDTCEnabled(event.target.checked);
}
+ const onDltEnabledChange = (event) => {
+ setDLTEnabled(event.target.checked);
+ }
+
const onMaxMemBufferSizeChange = (event) => {
setMaxMemBufferSize(event.target.value);
}
@@ -148,7 +154,7 @@ const MainForm = () => {
restraint: restraintEl.current.value,
body_type: bodyTypeEl.current.value,
log_level: selectedLogLevel,
- tags: tagsEl.current.value.split(",").map(function (word) {
+ tags: tagsEl.current.value.split(",").map(function(word) {
return word.trim();
}),
canbus: {
@@ -158,6 +164,7 @@ const MainForm = () => {
max_disk_buffer_size: canbusEnabled && dataLoggerEnabled ? parseInt(maxDiskBufferSize) : 0,
dtc_enabled: dtcEnabled
},
+ dlt_enabled: dltEnabled,
debug_mask: debugMaskEl.current?.value
};
@@ -423,6 +430,12 @@ const MainForm = () => {
onChange={onDtcEnabledChange}
/>
} label="DTC Enabled" />
+
+ } label="DLT Logging Enabled (supported from T.Rex 1.1.127)" />
{showDebugMask && (
{
throw new Error(`Get fleet vehicles error. ${result.message}`);
}
- setFleetVehicles(result.data)
+ const connectionsResult = await vehiclesAPI.getConnections(result.data, token)
+ if (result.error) {
+ setFleetVehicles([])
+ throw new Error(`Get vehicles connections error. ${result.message}`);
+ }
+
+ var cars = []
+ result.data.forEach((vin) => {
+ cars.push({
+ vin: vin,
+ connected: connectionsResult[vin] || false,
+ connectedHMI: connectionsResult[`2:${vin}`] || false
+ })
+ })
+
+ setFleetVehicles(cars)
if (result.total) {
setTotalFleetVehicles(result.total);
}
diff --git a/src/components/Contexts/FleetContext.test.jsx b/src/components/Contexts/FleetContext.test.jsx
index 93b88a1..b3b883d 100644
--- a/src/components/Contexts/FleetContext.test.jsx
+++ b/src/components/Contexts/FleetContext.test.jsx
@@ -1,4 +1,5 @@
jest.mock("../../services/fleetsAPI");
+jest.mock("../../services/vehiclesAPI");
import {
render,
@@ -800,9 +801,21 @@ const expectedFleetsData = [
];
const expectedFleetVehiclesData = [
- "USWESTVIN12345678",
- "USWESTVIN12345679",
- "USWESTVIN12345670",
+ {
+ vin: "USWESTVIN12345678",
+ connected: true,
+ connectedHMI: false,
+ },
+ {
+ vin: "USWESTVIN12345679",
+ connected: true,
+ connectedHMI: false,
+ },
+ {
+ vin: "USWESTVIN12345670",
+ connected: true,
+ connectedHMI: false,
+ },
];
const expectedFleetCANFiltersData = [
diff --git a/src/components/Contexts/VehicleContext.jsx b/src/components/Contexts/VehicleContext.jsx
index 7bb2fae..261a2ab 100644
--- a/src/components/Contexts/VehicleContext.jsx
+++ b/src/components/Contexts/VehicleContext.jsx
@@ -71,7 +71,7 @@ export const VehicleProvider = ({ children }) => {
const result = await api.addTags(vins, tags, token)
if (result.error)
- throw new Error(`Add tags error. ${result.message}`);
+ throw new Error(`Add tags error. ${result.message}`);
} finally {
setBusy(false)
}
@@ -105,7 +105,7 @@ export const VehicleProvider = ({ children }) => {
setBusy(true);
const result = await api.getLocations(token);
if (result.error)
- throw new Error(`Get locations vehicle paths error. ${result.message}`);
+ throw new Error(`Get locations error. ${result.message}`);
return result;
} finally {
setBusy(false);
@@ -202,6 +202,18 @@ export const VehicleProvider = ({ children }) => {
}
};
+ const sendDiagnosticCommand = async (vins, command, token) => {
+ try {
+ setBusy(true);
+ const result = await api.sendDiagnosticCommand(vins, command, token);
+ if (result.error)
+ throw new Error(`Send diagnostic command error. ${result.message}`);
+ return result;
+ } finally {
+ setBusy(false);
+ }
+ };
+
const updateVehicle = async (vin, v, token) => {
try {
setBusy(true);
@@ -313,6 +325,7 @@ export const VehicleProvider = ({ children }) => {
getVehicle,
getVehicles,
sendCommand,
+ sendDiagnosticCommand,
updateVehicle,
getFleets,
getVersionLog,
diff --git a/src/components/Contexts/__mocks__/FleetContext.jsx b/src/components/Contexts/__mocks__/FleetContext.jsx
index 7c9c613..6b081c2 100644
--- a/src/components/Contexts/__mocks__/FleetContext.jsx
+++ b/src/components/Contexts/__mocks__/FleetContext.jsx
@@ -62,7 +62,26 @@ export const useFleetContext = () => ({
fleetVehicles,
totalFleetVehicles,
- getFleetVehicles: jest.fn(),
+ getFleetVehicles: jest.fn().mockImplementation((name, search, _token) => {
+ const result = [
+ {
+ vin: "USWESTVIN12345678",
+ connected: false,
+ connectedHMI: false
+ },
+ {
+ vin: "USWESTVIN12345679",
+ connected: true,
+ connectedHMI: true
+ },
+ {
+ vin: "USWESTVIN12345670",
+ connected: false,
+ connectedHMI: false
+ },
+ ];
+ return Promise.resolve(result);
+ }),
addFleetVehicles: jest.fn(),
deleteFleetVehicle: jest.fn(),
diff --git a/src/components/Contexts/__mocks__/VehicleContext.jsx b/src/components/Contexts/__mocks__/VehicleContext.jsx
index 8e6034a..4387c84 100644
--- a/src/components/Contexts/__mocks__/VehicleContext.jsx
+++ b/src/components/Contexts/__mocks__/VehicleContext.jsx
@@ -78,6 +78,9 @@ let vehicleState = {
vehicle_speed: {
speed: 77.7,
},
+ gear: {
+ in_park: true,
+ }
},
};
@@ -109,10 +112,14 @@ export const useVehicleContext = () => ({
addVehicle: jest.fn(),
getConnections: jest
.fn().mockImplementation((vins, _token) => {
- const result = {};
- vins.forEach((vin) => {
- result[vin] = true;
- });
+ const result = {
+ "USWESTVIN12345678": true,
+ "2:USWESTVIN12345678": false,
+ "USWESTVIN12345679": true,
+ "2:USWESTVIN12345679": false,
+ "USWESTVIN12345670": true,
+ "2:USWESTVIN12345670": false,
+ };
return Promise.resolve(result);
}),
getECUs: jest.fn(() => {
@@ -146,9 +153,9 @@ export const useVehicleContext = () => ({
.fn()
.mockResolvedValue({
// tests only pass without mocking the data here
- // '3FAFP13P71R199267': [],
// '3FAFP13P31R199430': [[16.891136999999986, 26.832352999999955], [56.891136999999986, 66.832352999999955], [26.891136999999986, 36.832352999999955]],
// '3FAFP13P71R199060': [[36.891136999999986, 46.832352999999955], [76.891136999999986, 16.832352999999955]],
+ // '3FAFP13P61R199390': [],
}),
getModels: jest.fn(() => {
models = ["Ocean", "PEAR"];
diff --git a/src/components/Controls/DropDownButton/index.jsx b/src/components/Controls/DropDownButton/index.jsx
index 77a5562..6820877 100644
--- a/src/components/Controls/DropDownButton/index.jsx
+++ b/src/components/Controls/DropDownButton/index.jsx
@@ -40,6 +40,10 @@ const DropDownButton = ({ actions = [], payload = [] }) => {
setOpen(false);
};
+ if (!actions.length) {
+ return <>>;
+ }
+
return (
<>
{
- const {groups, rolesPerProvider, providers} = props;
+ const { groups, rolesPerProvider, providers } = props;
- const eitherComponent = props["eitherComponent"] || null;
+ const eitherComponent = props["eitherComponent"] || null;
if (!hasRole(groups, rolesPerProvider, providers)) {
return eitherComponent != null ? eitherComponent : <>>;
diff --git a/src/components/Controls/SendDiagnosticCommand/index.jsx b/src/components/Controls/SendDiagnosticCommand/index.jsx
new file mode 100644
index 0000000..117583d
--- /dev/null
+++ b/src/components/Controls/SendDiagnosticCommand/index.jsx
@@ -0,0 +1,135 @@
+import clsx from "clsx";
+
+import { Button, FormControl, InputLabel, Select, FormControlLabel, FormGroup } from "@material-ui/core";
+import Checkbox from '@mui/material/Checkbox';
+import React, { useEffect, useState } from "react";
+import { useStatusContext } from "../../Contexts/StatusContext";
+import { logger } from "../../../services/monitoring";
+import cmp from "semver-compare";
+
+
+import {
+ useVehicleContext
+} from "../../Contexts/VehicleContext";
+
+const commands = ["Reset"]
+const ecus = ["TBOX"]
+
+const SendDiagnosticCommand = ({ vin, token, classes }) => {
+
+ const { getState, sendDiagnosticCommand } = useVehicleContext();
+
+ const [carState, setCarState] = useState(null);
+ const { setMessage } = useStatusContext();
+
+ const [currentCommand, setCurrentCommand] = useState(commands[0].toLowerCase());
+ const [currentECUs] = useState([ecus[0]]);
+
+ const changeCommandHandler = (e) => {
+ setCurrentCommand(e.target.value);
+ };
+
+
+ //Update online/offline state
+ useEffect(() => {
+ if (!vin) return;
+ getCarState();
+ const interval = setInterval(getCarState, 5000);
+ return () => { clearInterval(interval); }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [vin]);
+
+ const getCarState = async () => {
+ try {
+ const result = await getState(token, vin);
+ setCarState(result.data);
+ } catch (e) {
+ setMessage(e.message);
+ logger.warn(e.stack);
+ }
+ };
+
+ const isOnline = () => {
+ return carState && carState?.online;
+ };
+
+ const TREX_MIN_VER = "1.1.108";
+ const isTBOXResetSupported = () => {
+ return !carState?.trex_version ? true : cmp(carState.trex_version, TREX_MIN_VER) === 1;
+ };
+
+ const clickHandler = async (_) => {
+ try {
+ await sendDiagnosticCommand([vin], { body: { command: currentCommand, ecus: currentECUs } }, token);
+ setMessage(`Sent diagnostic command to ${vin}`);
+ } catch (error) {
+ setMessage(error.message);
+ logger.error(error.stack);
+ }
+ };
+
+ return (
+
+
+
+ Diagnostic Command
+
+
+
+
+ {
+ ecus.map((ecu, idx) => {
+ return }
+ label={ecu}
+ value={ecu}
+ checked={true} />
+ })
+ }
+
+
+
+
+ {isOnline() ? "ONLINE" : "OFFLINE"}
+
+
+
+
+ {!isTBOXResetSupported() ? `TBOX Reset supported from ${TREX_MIN_VER}, current version ${carState.trex_version}` : ""}
+
+
+
+
+ );
+};
+
+export default SendDiagnosticCommand;
diff --git a/src/components/DigitalTwin/index.js b/src/components/DigitalTwin/index.js
index 21b1709..f705401 100644
--- a/src/components/DigitalTwin/index.js
+++ b/src/components/DigitalTwin/index.js
@@ -7,6 +7,8 @@ import useStyles from "../useStyles";
const UNKNOWN = "unknown";
const LOCKED = "Locked";
const UNLOCKED = "Unlocked";
+const PARKED = "Yes";
+const NOT_PARKED = "Not Parked";
const appendUnits = (value, units) => {
if (value || value === 0) return `${value}${units}`;
@@ -32,7 +34,7 @@ const windowState = (value) => {
const DigitalTwin = (props) => {
const classes = useStyles();
- const { battery, doors, location, trex_version, ip, updated, windows, misc_windows, sunroof, dbc_version, door_locks, vcu0x260, charging_metrics, max_range, vehicle_speed } = props;
+ const { battery, doors, location, trex_version, ip, updated, windows, misc_windows, sunroof, dbc_version, door_locks, vcu0x260, charging_metrics, max_range, vehicle_speed, gear } = props;
return (
@@ -133,6 +135,11 @@ const DigitalTwin = (props) => {
{keyValueTemplate("Vehicle Speed", appendUnits(vehicle_speed?.speed, " km/h"))}
)}
+ {gear && (
+
+ {keyValueTemplate("Parked", gear.in_park ? PARKED : NOT_PARKED)}
+
+ )}
);
};
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 b66d659..e674ba8 100644
--- a/src/components/Fleets/Status/Details/__snapshots__/index.test.jsx.snap
+++ b/src/components/Fleets/Status/Details/__snapshots__/index.test.jsx.snap
@@ -143,6 +143,48 @@ exports[`FleetDetailsTab Render 1`] = `
+
diff --git a/src/components/Fleets/Status/Details/index.jsx b/src/components/Fleets/Status/Details/index.jsx
index 8b81643..2926b38 100644
--- a/src/components/Fleets/Status/Details/index.jsx
+++ b/src/components/Fleets/Status/Details/index.jsx
@@ -15,6 +15,7 @@ import { FleetProvider, useFleetContext } from "../../../Contexts/FleetContext"
import useStyles from "../../../useStyles";
import { logger } from "../../../../services/monitoring";
import DeleteConfirmation from "../../../DeleteConfirmation";
+import BulkActions from "../../../BulkActions";
const MainForm = ({ name }) => {
const classes = useStyles();
@@ -94,6 +95,9 @@ const MainForm = ({ name }) => {
+