Merge branch 'release/0.9.0'

This commit is contained in:
jwu-fisker
2023-06-21 20:24:18 -07:00
29 changed files with 614 additions and 58 deletions

View File

@@ -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,

View File

@@ -11276,6 +11276,7 @@ exports[`App Route /vehicle-status authenticated 1`] = `
</div>
<div
aria-labelledby="tab-0"
class="makeStyles-tabContainer-0"
id="tabpanel-0"
role="tabpanel"
>
@@ -11528,54 +11529,63 @@ exports[`App Route /vehicle-status authenticated 1`] = `
</div>
<div
aria-labelledby="tab-1"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-1"
role="tabpanel"
/>
<div
aria-labelledby="tab-2"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-2"
role="tabpanel"
/>
<div
aria-labelledby="tab-3"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-3"
role="tabpanel"
/>
<div
aria-labelledby="tab-4"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-4"
role="tabpanel"
/>
<div
aria-labelledby="tab-5"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-5"
role="tabpanel"
/>
<div
aria-labelledby="tab-6"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-6"
role="tabpanel"
/>
<div
aria-labelledby="tab-7"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-7"
role="tabpanel"
/>
<div
aria-labelledby="tab-8"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-8"
role="tabpanel"
/>
<div
aria-labelledby="tab-9"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-9"
role="tabpanel"
@@ -12652,6 +12662,13 @@ exports[`App Route /vehicles authenticated 1`] = `
</tfoot>
</table>
</div>
<div
data-testid="transform-modal"
/>
<div
data-testid="transform-modal"
/>
<div />
</div>
</div>
</main>

View File

@@ -482,6 +482,7 @@ exports[`VehicleTable Render 1`] = `
</tfoot>
</table>
</div>
<div />
</div>
</div>
</div>

View File

@@ -14,12 +14,14 @@ 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";
import GeneralConfirmation from "../../GeneralConfirmation";
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([]);
@@ -29,8 +31,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: {
@@ -79,11 +88,44 @@ 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 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",
disabled: selectedVins.length === 0,
trigger: () => setShowUpdateConfigModal(true),
trigger: () => setActiveModal("updateConfig"),
},
{
name: "Add Tags",
disabled: selectedVins.length === 0,
trigger: () => setActiveModal("addTags"),
},
{
name: "Delete",
disabled: selectedVins.length === 0,
trigger: () => setActiveModal("delete"),
},
];
@@ -113,7 +155,7 @@ const MainForm = () => {
<DropDownButton actions={actions} payload={[selectedVins]} />
</Grid>
<Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} />
<SearchField classes={classes} onSearch={handleSearch} savedSearchValue={search} />
</Grid>
<Grid item md={2} className={clsx(classes.textJustifyAlign, classes.actionsBar)}>
<OptionsDropdown listId="filter-menu">
@@ -149,17 +191,33 @@ const MainForm = () => {
onSelectAll={handleSelectAll}
/>
<VehicleConsumer>
{(context) => (
{(context) => (<>
<TransformModal
open={showUpdateConfigModal}
close={() => 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)}
/>
)}
<TransformModal
open={activeModal === "addTags"}
close={() => setActiveModal(null)}
title="Add Tags"
body={`You are adding tags for the following VINs: ${selectedVins.join(", ")}.`}
data={tagsToAdd}
setData={setTagsToAdd}
submit={() => handleAddTags(context.addTags)}
/>
<GeneralConfirmation
open={activeModal === "delete"}
close={() => setActiveModal(null)}
title="Delete"
message={`You are about to delete the following VINs: ${selectedVins.join(", ")}`}
actionFunction={() => handleDelete(context.deleteVehicle)}
/>
</>)}
</VehicleConsumer>
</div>
);

View File

@@ -16,7 +16,7 @@ const MainForm = ({ vin }) => {
} = useUserContext();
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<div className={clsx(classes.tableSize, classes.textCenterAlign)}>
<Typography variant="h6" className={classes.labelInline}>
Car ECUs
</Typography>

View File

@@ -55,6 +55,13 @@ exports[`DigitalTwinTab Render 1`] = `
:
12000 km
</p>
<p>
<b>
Voltage
</b>
:
12.5 V
</p>
<p>
<b>
Max Range

View File

@@ -9,7 +9,7 @@ exports[`ECUsTab Render 1`] = `
data-testid="mocked-userprovider"
>
<div
class="makeStyles-paper-0 makeStyles-tableSize-0"
class="makeStyles-tableSize-0 makeStyles-textCenterAlign-0"
>
<h6
class="MuiTypography-root makeStyles-labelInline-0 MuiTypography-h6"
@@ -17,7 +17,7 @@ exports[`ECUsTab Render 1`] = `
Car ECUs
</h6>
<div
class="makeStyles-paper-0 makeStyles-tableSize-0"
class="MuiTableContainer-root"
>
<table
class="MuiTable-root"

View File

@@ -223,6 +223,7 @@ exports[`CarStatus Render 1`] = `
</div>
<div
aria-labelledby="tab-0"
class="makeStyles-tabContainer-0"
id="tabpanel-0"
role="tabpanel"
>
@@ -418,54 +419,63 @@ exports[`CarStatus Render 1`] = `
</div>
<div
aria-labelledby="tab-1"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-1"
role="tabpanel"
/>
<div
aria-labelledby="tab-2"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-2"
role="tabpanel"
/>
<div
aria-labelledby="tab-3"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-3"
role="tabpanel"
/>
<div
aria-labelledby="tab-4"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-4"
role="tabpanel"
/>
<div
aria-labelledby="tab-5"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-5"
role="tabpanel"
/>
<div
aria-labelledby="tab-6"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-6"
role="tabpanel"
/>
<div
aria-labelledby="tab-7"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-7"
role="tabpanel"
/>
<div
aria-labelledby="tab-8"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-8"
role="tabpanel"
/>
<div
aria-labelledby="tab-9"
class="makeStyles-tabContainer-0"
hidden=""
id="tabpanel-9"
role="tabpanel"

View File

@@ -147,7 +147,12 @@ const CarStatus = () => {
</Tabs>
</Box>
{tabs.map((item, index) => (
<TabPanel key={index} value={tabIndex} index={index}>
<TabPanel
key={index}
value={tabIndex}
index={index}
className={classes.tabContainer}
>
<item.component vin={vin} />
</TabPanel>
))}

View File

@@ -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}

View File

@@ -39,6 +39,7 @@ let vehicleState = {
battery: {
total_mileage_odometer: 12000,
percent: 95,
battery_voltage: 12.5,
},
max_range: {
max_miles: 577,

View File

@@ -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 (
<div className={clsx(classes.paper, classes.tableSize)}>
<TableContainer>
<Table>
<TableHeaderSortable
classes={classes}
@@ -166,7 +166,7 @@ const CarECUsTable = ({ vin, token, classes }) => {
</TableRow>
</TableFooter>
</Table>
</div>
</TableContainer>
);
};

View File

@@ -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 (
<FormControl className={clsx(classes.margin, classes.fullWidth)}>
<InputLabel htmlFor="search">Search</InputLabel>

View File

@@ -0,0 +1,58 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DropDownButton Render 1`] = `
<div>
<div>
<div
class="MuiFormControl-root MuiFormControl-marginNormal MuiFormControl-fullWidth MuiTextField-root css-17vbkzs-MuiFormControl-root-MuiTextField-root"
data-testid="text-input-list"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeSmall MuiInputLabel-outlined MuiFormLabel-colorPrimary Mui-required MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeSmall MuiInputLabel-outlined css-1pysi21-MuiFormLabel-root-MuiInputLabel-root"
data-shrink="false"
for="mui-0"
id="mui-0-label"
>
The input label (use commas to add multiple)
<span
aria-hidden="true"
class="MuiFormLabel-asterisk MuiInputLabel-asterisk css-wgai2y-MuiFormLabel-asterisk"
>
*
</span>
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-colorPrimary MuiInputBase-fullWidth MuiInputBase-formControl MuiInputBase-sizeSmall css-md26zr-MuiInputBase-root-MuiOutlinedInput-root"
>
<input
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputSizeSmall css-1n4twyu-MuiInputBase-input-MuiOutlinedInput-input"
id="mui-0"
name="text"
required=""
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="MuiOutlinedInput-notchedOutline css-1d3z3hw-MuiOutlinedInput-notchedOutline"
>
<legend
class="css-yjsfm1"
>
<span>
The input label (use commas to add multiple)
*
</span>
</legend>
</fieldset>
</div>
</div>
<div
class="MuiBox-root css-6km64s"
/>
</div>
</div>
`;

View File

@@ -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 (
<Box sx={{
display: "flex",
alignItems: "center",
gap: "2px",
p: "2px 4px",
backgroundColor: "#ccc",
borderRadius: 1,
}}>
{text}
<Cancel
sx={{
color: "#666",
fontSize: "16px",
cursor: "pointer",
}}
onClick={() => handleDelete(text)}
/>
</Box>
);
}
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 (
<div>
<TextField
data-testid="text-input-list"
name="text"
label={`${label} (use commas to add multiple)`}
value={input}
variant="outlined"
margin="normal"
required
size="small"
fullWidth
onChange={handleOnChange}
/>
<Box sx={{
display: "flex",
flexWrap: "wrap",
gap: 1,
pr: 1,
}}>
{textList.map((text) => (
<TextInput
text={text}
handleDelete={handleDelete}
key={text}
/>
))}
</Box>
</div>
);
};
export default TextInputList;

View File

@@ -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(
<TextInputList
label={"The input label"}
/>
);
await waitFor(() => {
/* render */
});
expect(container).toMatchSnapshot();
});
it("properly adds tag after comma", async () => {
const { getByText, getByTestId } = render(
<TextInputList
label={"The input label"}
payload={[]}
/>
);
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(
<TextInputList
label={"The input label"}
onChange={mockCallback}
/>
);
const [inputEl] = getByTestId("text-input-list").getElementsByTagName("input");
userEvent.type(inputEl, "tag1");
userEvent.type(inputEl, ",");
userEvent.type(inputEl, "tag2");
expect(mockCallback).toHaveBeenCalledWith(["tag1", "tag2"]);
});
});

View File

@@ -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) => (
<p key={key}>
<b>{key}</b>: {value}
@@ -37,16 +42,17 @@ const DigitalTwin = (props) => {
{(battery || max_range) && (
<div className={classes.popupSection}>
<h3>Battery</h3>
{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"))}
</div>
)}
{(vcu0x260 || charging_metrics) && (
<div className={classes.popupSection}>
<h3>Charging</h3>
{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"))}
</div>
)}
{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], "°"));
}
})}
</div>
@@ -124,7 +130,7 @@ const DigitalTwin = (props) => {
)}
{vehicle_speed && (
<div className={classes.popupSection}>
{keyValueTemplate("Vehicle Speed", `${vehicle_speed.speed} km/h`)}
{keyValueTemplate("Vehicle Speed", appendUnits(vehicle_speed?.speed, " km/h"))}
</div>
)}
</div>

View File

@@ -86,6 +86,23 @@ exports[`FleetDetailsTab Render 1`] = `
:
3
</p>
<p>
<b>
DTC Enabled
</b>
:
false
</p>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
>
<p>
<b>
Debug Mask
</b>
:
</p>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"

View File

@@ -24,6 +24,8 @@ const MainForm = ({ name }) => {
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 }) => {
<p><b>Enabled</b>: {fleet.canbus.data_logger_enabled.toString()}</p>
<p><b>Max Disk Buffer Size</b>: {fleet.canbus.max_disk_buffer_size ?? "Default"}</p>
<p><b>Filters</b>: {fleet.canbus.filters ? fleet.canbus.filters.length : 0}</p>
<p><b>DTC Enabled</b>: {(fleet.canbus.dtc_enabled || false).toString()}</p>
</Grid>
)}
{showDebugMask && (
<Grid item md={12} className={classes.textCenterAlign}>
<p>
<b>Debug Mask</b>: {fleet.debug_mask}
</p>
</Grid>
)}
<Grid item md={12} className={classes.textCenterAlign}>

View File

@@ -94,6 +94,23 @@ exports[`DetailsTab Render 1`] = `
:
3
</p>
<p>
<b>
DTC Enabled
</b>
:
false
</p>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
>
<p>
<b>
Debug Mask
</b>
:
</p>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"

View File

@@ -182,6 +182,23 @@ exports[`FleetStatus Render 1`] = `
:
3
</p>
<p>
<b>
DTC Enabled
</b>
:
false
</p>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"
>
<p>
<b>
Debug Mask
</b>
:
</p>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"

View File

@@ -1,4 +1,4 @@
import React, {useEffect, useState} from "react";
import React, { useEffect } from "react";
import { Link } from 'react-router-dom';
import { Grid, } from "@material-ui/core";
import AddCircleIcon from "@material-ui/icons/AddCircle";
@@ -7,6 +7,7 @@ import clsx from "clsx";
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,7 +15,7 @@ import FleetSelectionTable from "../../Controls/FleetSelectionTable";
const MainForm = () => {
const classes = useStyles();
const [search, setSearch] = useState("");
const [search, setSearch] = useLocalStorage("FLEET_SEARCH", "");
const { setSitePath, setTitle } = useStatusContext();
const { token: { idToken: { jwtToken: token } } } = useUserContext();
@@ -37,7 +38,7 @@ const MainForm = () => {
</Link>
</Grid>
<Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch}/>
<SearchField classes={classes} onSearch={handleSearch} savedSearchValue={search} />
</Grid>
<Grid item md={4} className={classes.textRightAlign}></Grid>
</Grid>

View File

@@ -565,6 +565,81 @@ exports[`FleetUpdate Render 1`] = `
</fieldset>
</div>
</div>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-0 MuiCheckbox-root MuiCheckbox-colorSecondary PrivateSwitchBase-checked-0 Mui-checked MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
checked=""
class="PrivateSwitchBase-input-0"
data-indeterminate="false"
type="checkbox"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
DTC Enabled
</span>
</label>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined"
data-shrink="true"
for="debug_mask"
id="debug_mask-label"
>
Debug Mask
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
>
<input
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input"
id="debug_mask"
maxlength="255"
name="debug_mask"
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-0 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-0 PrivateNotchedOutline-legendNotched-0"
>
<span>
Debug Mask
</span>
</legend>
</fieldset>
</div>
</div>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-0 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { Redirect } from "react-router";
import { useLocation } from "react-router-dom";
import {
@@ -38,6 +38,10 @@ const MainForm = () => {
const [dataLoggerEnabled, setDataLoggerEnabled] = useState(false);
const [maxMemBufferSize, setMaxMemBufferSize] = useState(0);
const [maxDiskBufferSize, setMaxDiskBufferSize] = useState(0);
const [dtcEnabled, setDTCEnabled] = useState(true);
const debugMaskEl = useRef(null);
const showDebugMask = (process.env.REACT_APP_ENABLE_DEBUGMASK === "1");
useEffect(() => {
setTitle("Update Fleet");
@@ -78,7 +82,13 @@ const MainForm = () => {
setMaxDiskBufferSize(
fleet.canbus.max_disk_buffer_size ?? maxDiskBufferSize
);
setDTCEnabled(fleet.canbus.dtc_enabled ?? dtcEnabled);
}
if (showDebugMask) {
debugMaskEl.current.value = fleet.debug_mask ?? ""
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fleet]);
@@ -98,6 +108,10 @@ const MainForm = () => {
setDataLoggerEnabled(event.target.checked);
};
const onDtcEnabledChange = (event) => {
setDTCEnabled(event.target.checked);
}
const onMaxMemBufferSizeChange = (event) => {
setMaxMemBufferSize(event.target.value);
};
@@ -116,11 +130,10 @@ const MainForm = () => {
enabled: canbusEnabled,
data_logger_enabled: canbusEnabled ? dataLoggerEnabled : false,
max_mem_buffer_size: canbusEnabled ? parseInt(maxMemBufferSize) : 0,
max_disk_buffer_size:
canbusEnabled && dataLoggerEnabled
? parseInt(maxDiskBufferSize)
: 0,
max_disk_buffer_size: canbusEnabled && dataLoggerEnabled ? parseInt(maxDiskBufferSize) : 0,
dtc_enabled: dtcEnabled
},
debug_mask: debugMaskEl.current?.value
};
const result = await updateFleet(oldName, formData, token);
@@ -230,6 +243,30 @@ const MainForm = () => {
required
fullWidth
/>
<FormControlLabel control={
<Checkbox
checked={dtcEnabled}
onChange={onDtcEnabledChange}
/>
} label="DTC Enabled" />
{showDebugMask && (
<TextField
id="debug_mask"
name="debug_mask"
label="Debug Mask"
InputLabelProps={{
shrink: true
}}
defaultValue=""
variant="outlined"
margin="normal"
inputProps={{
maxLength: "255",
}}
fullWidth
inputRef={debugMaskEl}
/>
)}
<Button
type="submit"
disabled={busy}

View File

@@ -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,
@@ -246,7 +247,7 @@ const MainForm = () => {
<Grid container className={classes.root} spacing={2}>
<Grid item md={4} className={classes.textJustifyAlign}></Grid>
<Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} />
<SearchField classes={classes} onSearch={handleSearch} savedSearchValue={search} />
<RoleWrap
groups={groups}
providers={providers}
@@ -258,8 +259,8 @@ const MainForm = () => {
aria-label="Active"
onChange={handleActiveChange}
>
<ToggleButton value={true}>Active</ToggleButton>
<ToggleButton value={false}>Archived</ToggleButton>
<ToggleButton value={"true"}>Active</ToggleButton>
<ToggleButton value={"false"}>Archived</ToggleButton>
</ToggleButtonGroup>
</RoleWrap>
</Grid>

View File

@@ -0,0 +1,5 @@
const TransformModalMock = jest.fn().mockImplementation(() => {
return <div data-testid="transform-modal" />
});
export default TransformModalMock;

View File

@@ -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;
switch (data[key].type) {
case "boolean":
toChange.value = !toChange.value;
break;
case "list.string":
toChange.value = value;
break;
default:
}
return {
[key]: toChange,
...rest
};
});
}
};
return (
<Dialog
@@ -64,6 +73,14 @@ const TransformModal = ({
}
/>
)
case "list.string":
return (
<TextInputList
key={key}
label={value.label}
onChange={(list) => handleChange(key, list)}
/>
)
default:
return <></>;
}
@@ -72,6 +89,7 @@ const TransformModal = ({
</DialogContent>
<DialogActions>
<Button
label="Test"
onClick={close}
>
Cancel

View File

@@ -267,7 +267,12 @@ const useStyles = makeStyles((theme) => ({
textDecoration: "inherit",
color: "inherit",
},
tableSize: { height: 700, width: "100%" },
tableSize: {
width: "100%",
},
tabContainer: {
maxWidth: "100%",
},
whiteBackground: { backgroundColor: "White" },
progressIcon: { width: 40, height: 40 },
progressSuccess: { color: "green" },

View File

@@ -17,6 +17,18 @@ const vehiclesAPI = {
.then(fetchRespHandler)
.catch(errorHandler),
addTags: async (vins, tags, token) =>
fetch(`${API_ENDPOINT}/tags`, {
method: "PUT",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token),
),
body: JSON.stringify({ vins, tags }),
})
.then(fetchRespHandler)
.catch(errorHandler),
deleteVehicle: async (vin, token) =>
fetch(`${API_ENDPOINT}/vehicle/${vin}`, {
method: "DELETE",