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/superset");
jest.mock("../../services/suppliersAPI"); jest.mock("../../services/suppliersAPI");
jest.mock("../../services/issueAPI"); jest.mock("../../services/issueAPI");
jest.mock("../TransformModal");
import { import {
act, cleanup, render, act, cleanup, render,

View File

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

View File

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

View File

@@ -14,12 +14,14 @@ import { RoleWrap } from "../../Controls/RoleWrap";
import SearchField from "../../Controls/SearchField"; import SearchField from "../../Controls/SearchField";
import DropDownButton from "../../Controls/DropDownButton"; import DropDownButton from "../../Controls/DropDownButton";
import TransformModal from "../../TransformModal"; import TransformModal from "../../TransformModal";
import { useLocalStorage } from "../../useLocalStorage";
import useStyles from "../../useStyles"; import useStyles from "../../useStyles";
import TaskRunner from "../../../utils/taskRunner"; import TaskRunner from "../../../utils/taskRunner";
import GeneralConfirmation from "../../GeneralConfirmation";
const MainForm = () => { const MainForm = () => {
const classes = useStyles(); const classes = useStyles();
const [search, setSearch] = useState(""); const [search, setSearch] = useLocalStorage("VEHICLE_SEARCH", "");
const [online, setOnline] = useState(false); const [online, setOnline] = useState(false);
const [onlineHMI, setOnlineHMI] = useState(false); const [onlineHMI, setOnlineHMI] = useState(false);
const [selectedVins, setSelectedVins] = useState([]); const [selectedVins, setSelectedVins] = useState([]);
@@ -29,8 +31,15 @@ const MainForm = () => {
type: "boolean", type: "boolean",
value: false 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 { setTitle, setSitePath, setMessage } = useStatusContext();
const { const {
token: { token: {
@@ -64,7 +73,7 @@ const MainForm = () => {
const handleUploadConfig = (fn) => { const handleUploadConfig = (fn) => {
const taskRunner = new TaskRunner(5); const taskRunner = new TaskRunner(5);
const request = (vin, i) => { const request = (vin, i) => {
const messagePrefix = `${i+1}/${selectedVins.length} "${vin}":`; const messagePrefix = `${i + 1}/${selectedVins.length} "${vin}":`;
return async () => { return async () => {
const result = await fn(vin, config.force.value, token) const result = await fn(vin, config.force.value, token)
.then(() => { .then(() => {
@@ -79,11 +88,44 @@ const MainForm = () => {
selectedVins.forEach((vin, i) => taskRunner.push(request(vin, i))) 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 = [ const actions = [
{ {
name: "Update Configs", name: "Update Configs",
disabled: selectedVins.length === 0, 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]} /> <DropDownButton actions={actions} payload={[selectedVins]} />
</Grid> </Grid>
<Grid item md={4} className={classes.textCenterAlign}> <Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} /> <SearchField classes={classes} onSearch={handleSearch} savedSearchValue={search} />
</Grid> </Grid>
<Grid item md={2} className={clsx(classes.textJustifyAlign, classes.actionsBar)}> <Grid item md={2} className={clsx(classes.textJustifyAlign, classes.actionsBar)}>
<OptionsDropdown listId="filter-menu"> <OptionsDropdown listId="filter-menu">
@@ -149,17 +191,33 @@ const MainForm = () => {
onSelectAll={handleSelectAll} onSelectAll={handleSelectAll}
/> />
<VehicleConsumer> <VehicleConsumer>
{(context) => ( {(context) => (<>
<TransformModal <TransformModal
open={showUpdateConfigModal} open={activeModal === "updateConfig"}
close={() => setShowUpdateConfigModal(false)} close={() => setActiveModal(null)}
title="Update Configs" title="Update Configs"
body={`You are updating the config for the following VINs: ${selectedVins.join(", ")}.`} body={`You are updating the config for the following VINs: ${selectedVins.join(", ")}.`}
data={config} data={config}
setData={setConfig} setData={setConfig}
submit={() => handleUploadConfig(context.uploadConfig)} 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> </VehicleConsumer>
</div> </div>
); );

View File

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

View File

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

View File

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

View File

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

View File

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

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) => { const getConnections = async (vins, token) => {
try { try {
setBusy(true); setBusy(true);
@@ -301,6 +317,7 @@ export const VehicleProvider = ({ children }) => {
getFleets, getFleets,
getVersionLog, getVersionLog,
uploadConfig, uploadConfig,
addTags,
}} }}
> >
{children} {children}

View File

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

View File

@@ -2,12 +2,12 @@ import {
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableContainer,
TableFooter, TableFooter,
TablePagination, TablePagination,
TableRow, TableRow,
Tooltip Tooltip
} from "@material-ui/core"; } from "@material-ui/core";
import clsx from "clsx";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { logger } from "../../../services/monitoring"; import { logger } from "../../../services/monitoring";
@@ -122,7 +122,7 @@ const CarECUsTable = ({ vin, token, classes }) => {
}; };
return ( return (
<div className={clsx(classes.paper, classes.tableSize)}> <TableContainer>
<Table> <Table>
<TableHeaderSortable <TableHeaderSortable
classes={classes} classes={classes}
@@ -166,7 +166,7 @@ const CarECUsTable = ({ vin, token, classes }) => {
</TableRow> </TableRow>
</TableFooter> </TableFooter>
</Table> </Table>
</div> </TableContainer>
); );
}; };

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { import {
FormControl, FormControl,
IconButton, IconButton,
@@ -10,7 +10,7 @@ import SearchIcon from "@material-ui/icons/Search";
import clsx from "clsx"; import clsx from "clsx";
const SearchField = (props) => { const SearchField = (props) => {
const { classes, onSearch } = props; const { classes, onSearch, savedSearchValue } = props;
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const handleChange = (e) => { const handleChange = (e) => {
setSearchTerm(e.target.value); setSearchTerm(e.target.value);
@@ -29,6 +29,12 @@ const SearchField = (props) => {
} }
}; };
useEffect(() => {
if (savedSearchValue) {
setSearchTerm(savedSearchValue);
}
}, [savedSearchValue]);
return ( return (
<FormControl className={clsx(classes.margin, classes.fullWidth)}> <FormControl className={clsx(classes.margin, classes.fullWidth)}>
<InputLabel htmlFor="search">Search</InputLabel> <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 LOCKED = "Locked";
const UNLOCKED = "Unlocked"; const UNLOCKED = "Unlocked";
const appendUnits = (value, units) => {
if (value || value === 0) return `${value}${units}`;
return UNKNOWN;
}
const keyValueTemplate = (key, value) => ( const keyValueTemplate = (key, value) => (
<p key={key}> <p key={key}>
<b>{key}</b>: {value} <b>{key}</b>: {value}
@@ -37,16 +42,17 @@ const DigitalTwin = (props) => {
{(battery || max_range) && ( {(battery || max_range) && (
<div className={classes.popupSection}> <div className={classes.popupSection}>
<h3>Battery</h3> <h3>Battery</h3>
{keyValueTemplate("Percentage", `${battery?.percent || 0}%`)} {keyValueTemplate("Percentage", appendUnits(battery?.percent || 0, "%"))}
{keyValueTemplate("Total Mileage", `${battery?.total_mileage_odometer} km` || UNKNOWN)} {keyValueTemplate("Total Mileage", appendUnits(battery?.total_mileage_odometer, " km"))}
{keyValueTemplate("Max Range", `${max_range?.max_miles} km` || UNKNOWN)} {keyValueTemplate("Voltage", appendUnits(battery?.battery_voltage, " V"))}
{keyValueTemplate("Max Range", appendUnits(max_range?.max_miles, " km"))}
</div> </div>
)} )}
{(vcu0x260 || charging_metrics) && ( {(vcu0x260 || charging_metrics) && (
<div className={classes.popupSection}> <div className={classes.popupSection}>
<h3>Charging</h3> <h3>Charging</h3>
{keyValueTemplate("Charge Type", vcu0x260?.charge_type || UNKNOWN)} {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> </div>
)} )}
{doors && ( {doors && (
@@ -95,9 +101,9 @@ const DigitalTwin = (props) => {
return keyValueTemplate(value[0], "Invalid") return keyValueTemplate(value[0], "Invalid")
} }
if (value[0] === "altitude") { if (value[0] === "altitude") {
return keyValueTemplate(value[0], `${value[1]} m`); return keyValueTemplate(value[0], appendUnits(value[1], " m"));
} else { } else {
return keyValueTemplate(value[0], `${value[1]}°`); return keyValueTemplate(value[0], appendUnits(value[1], "°"));
} }
})} })}
</div> </div>
@@ -124,7 +130,7 @@ const DigitalTwin = (props) => {
)} )}
{vehicle_speed && ( {vehicle_speed && (
<div className={classes.popupSection}> <div className={classes.popupSection}>
{keyValueTemplate("Vehicle Speed", `${vehicle_speed.speed} km/h`)} {keyValueTemplate("Vehicle Speed", appendUnits(vehicle_speed?.speed, " km/h"))}
</div> </div>
)} )}
</div> </div>

View File

@@ -86,6 +86,23 @@ exports[`FleetDetailsTab Render 1`] = `
: :
3 3
</p> </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>
<div <div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12" 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 [showDeleteModal, setShowDeleteModal] = useState(false)
const { token: { idToken: { jwtToken: token } } } = useUserContext(); const { token: { idToken: { jwtToken: token } } } = useUserContext();
const showDebugMask = (process.env.REACT_APP_ENABLE_DEBUGMASK === "1");
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
@@ -70,6 +72,14 @@ const MainForm = ({ name }) => {
<p><b>Enabled</b>: {fleet.canbus.data_logger_enabled.toString()}</p> <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>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>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>
)} )}
<Grid item md={12} className={classes.textCenterAlign}> <Grid item md={12} className={classes.textCenterAlign}>
@@ -85,7 +95,7 @@ const MainForm = ({ name }) => {
</Tooltip> </Tooltip>
</Grid> </Grid>
</Grid> </Grid>
<DeleteConfirmation message={name} open={showDeleteModal} close={()=> setShowDeleteModal(false)} deleteFunction={onDelete}/> <DeleteConfirmation message={name} open={showDeleteModal} close={() => setShowDeleteModal(false)} deleteFunction={onDelete} />
</div > </div >
); );
}; };

View File

@@ -94,6 +94,23 @@ exports[`DetailsTab Render 1`] = `
: :
3 3
</p> </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>
<div <div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12" class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"

View File

@@ -182,6 +182,23 @@ exports[`FleetStatus Render 1`] = `
: :
3 3
</p> </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>
<div <div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12" class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-12"

View File

@@ -1,12 +1,13 @@
import React, {useEffect, useState} from "react"; import React, { useEffect } from "react";
import {Link} from 'react-router-dom'; import { Link } from 'react-router-dom';
import {Grid,} from "@material-ui/core"; import { Grid, } from "@material-ui/core";
import AddCircleIcon from "@material-ui/icons/AddCircle"; import AddCircleIcon from "@material-ui/icons/AddCircle";
import clsx from "clsx"; import clsx from "clsx";
import {useUserContext} from "../../Contexts/UserContext" import { useUserContext } from "../../Contexts/UserContext"
import {useStatusContext} from "../../Contexts/StatusContext"; import { useStatusContext } from "../../Contexts/StatusContext";
import {FleetProvider} from "../../Contexts/FleetContext" import { FleetProvider } from "../../Contexts/FleetContext"
import { useLocalStorage } from "../../useLocalStorage";
import useStyles from "../../useStyles"; import useStyles from "../../useStyles";
import SearchField from "../../Controls/SearchField"; import SearchField from "../../Controls/SearchField";
import FleetSelectionTable from "../../Controls/FleetSelectionTable"; import FleetSelectionTable from "../../Controls/FleetSelectionTable";
@@ -14,9 +15,9 @@ import FleetSelectionTable from "../../Controls/FleetSelectionTable";
const MainForm = () => { const MainForm = () => {
const classes = useStyles(); const classes = useStyles();
const [search, setSearch] = useState(""); const [search, setSearch] = useLocalStorage("FLEET_SEARCH", "");
const {setSitePath, setTitle} = useStatusContext(); const { setSitePath, setTitle } = useStatusContext();
const {token: {idToken: {jwtToken: token}}} = useUserContext(); const { token: { idToken: { jwtToken: token } } } = useUserContext();
const handleSearch = (query) => { const handleSearch = (query) => {
setSearch(query); setSearch(query);
@@ -33,18 +34,18 @@ const MainForm = () => {
<Grid container className={classes.root} spacing={2}> <Grid container className={classes.root} spacing={2}>
<Grid item md={4} className={classes.textJustifyAlign}> <Grid item md={4} className={classes.textJustifyAlign}>
<Link to={"/fleet-add"}> <Link to={"/fleet-add"}>
<AddCircleIcon fontSize="large"/> <AddCircleIcon fontSize="large" />
</Link> </Link>
</Grid> </Grid>
<Grid item md={4} className={classes.textCenterAlign}> <Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch}/> <SearchField classes={classes} onSearch={handleSearch} savedSearchValue={search} />
</Grid> </Grid>
<Grid item md={4} className={classes.textRightAlign}></Grid> <Grid item md={4} className={classes.textRightAlign}></Grid>
</Grid> </Grid>
<FleetSelectionTable <FleetSelectionTable
token={token} token={token}
classes={classes} classes={classes}
search={{search}} search={{ search }}
multiSelect={false} multiSelect={false}
/> />
</div> </div>

View File

@@ -565,6 +565,81 @@ exports[`FleetUpdate Render 1`] = `
</fieldset> </fieldset>
</div> </div>
</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 <button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-0 MuiButton-containedPrimary MuiButton-fullWidth" class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-0 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0" 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 { Redirect } from "react-router";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { import {
@@ -38,6 +38,10 @@ const MainForm = () => {
const [dataLoggerEnabled, setDataLoggerEnabled] = useState(false); const [dataLoggerEnabled, setDataLoggerEnabled] = useState(false);
const [maxMemBufferSize, setMaxMemBufferSize] = useState(0); const [maxMemBufferSize, setMaxMemBufferSize] = useState(0);
const [maxDiskBufferSize, setMaxDiskBufferSize] = 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(() => { useEffect(() => {
setTitle("Update Fleet"); setTitle("Update Fleet");
@@ -78,7 +82,13 @@ const MainForm = () => {
setMaxDiskBufferSize( setMaxDiskBufferSize(
fleet.canbus.max_disk_buffer_size ?? maxDiskBufferSize 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 // eslint-disable-next-line react-hooks/exhaustive-deps
}, [fleet]); }, [fleet]);
@@ -98,6 +108,10 @@ const MainForm = () => {
setDataLoggerEnabled(event.target.checked); setDataLoggerEnabled(event.target.checked);
}; };
const onDtcEnabledChange = (event) => {
setDTCEnabled(event.target.checked);
}
const onMaxMemBufferSizeChange = (event) => { const onMaxMemBufferSizeChange = (event) => {
setMaxMemBufferSize(event.target.value); setMaxMemBufferSize(event.target.value);
}; };
@@ -116,11 +130,10 @@ const MainForm = () => {
enabled: canbusEnabled, enabled: canbusEnabled,
data_logger_enabled: canbusEnabled ? dataLoggerEnabled : false, data_logger_enabled: canbusEnabled ? dataLoggerEnabled : false,
max_mem_buffer_size: canbusEnabled ? parseInt(maxMemBufferSize) : 0, max_mem_buffer_size: canbusEnabled ? parseInt(maxMemBufferSize) : 0,
max_disk_buffer_size: max_disk_buffer_size: canbusEnabled && dataLoggerEnabled ? parseInt(maxDiskBufferSize) : 0,
canbusEnabled && dataLoggerEnabled dtc_enabled: dtcEnabled
? parseInt(maxDiskBufferSize)
: 0,
}, },
debug_mask: debugMaskEl.current?.value
}; };
const result = await updateFleet(oldName, formData, token); const result = await updateFleet(oldName, formData, token);
@@ -230,6 +243,30 @@ const MainForm = () => {
required required
fullWidth 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 <Button
type="submit" type="submit"
disabled={busy} disabled={busy}

View File

@@ -90,8 +90,8 @@ const MainForm = () => {
const [pageIndex, setPageIndex] = useState(0); const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("id"); const [orderBy, setOrderBy] = useState("id");
const [order, setOrder] = useState("asc"); const [order, setOrder] = useState("asc");
const [search, setSearch] = useState(""); const [search, setSearch] = useLocalStorage("DEPLOYMENT_SEARCH", "");
const [active, setActive] = useState(true); const [active, setActive] = useLocalStorage("DEPLOYMENT_ACTIVE", "true");
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deleteId, setDeleteId] = useState(""); const [deleteId, setDeleteId] = useState("");
@@ -130,6 +130,7 @@ const MainForm = () => {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
handleActiveChange(null, active);
await getManifests( await getManifests(
{ {
limit: pageSize, limit: pageSize,
@@ -164,7 +165,7 @@ const MainForm = () => {
}; };
const handleActiveChange = (event, newAlignment) => { const handleActiveChange = (event, newAlignment) => {
if (newAlignment !== null){ if (newAlignment !== null) {
setActive(newAlignment) setActive(newAlignment)
} }
} }
@@ -246,7 +247,7 @@ const MainForm = () => {
<Grid container className={classes.root} spacing={2}> <Grid container className={classes.root} spacing={2}>
<Grid item md={4} className={classes.textJustifyAlign}></Grid> <Grid item md={4} className={classes.textJustifyAlign}></Grid>
<Grid item md={4} className={classes.textCenterAlign}> <Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} /> <SearchField classes={classes} onSearch={handleSearch} savedSearchValue={search} />
<RoleWrap <RoleWrap
groups={groups} groups={groups}
providers={providers} providers={providers}
@@ -258,8 +259,8 @@ const MainForm = () => {
aria-label="Active" aria-label="Active"
onChange={handleActiveChange} onChange={handleActiveChange}
> >
<ToggleButton value={true}>Active</ToggleButton> <ToggleButton value={"true"}>Active</ToggleButton>
<ToggleButton value={false}>Archived</ToggleButton> <ToggleButton value={"false"}>Archived</ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
</RoleWrap> </RoleWrap>
</Grid> </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, FormGroup,
FormControlLabel, FormControlLabel,
} from '@material-ui/core'; } from '@material-ui/core';
import TextInputList from "../Controls/TextInputList";
const TransformModal = ({ const TransformModal = ({
open, open,
@@ -25,16 +26,24 @@ const TransformModal = ({
submit(); submit();
}; };
const handleChange = (key) => { const handleChange = (key, value) => {
setData((data) => { setData((data) => {
const {[key]: toChange, ...rest} = data; const {[key]: toChange, ...rest} = data;
switch (data[key].type) {
case "boolean":
toChange.value = !toChange.value; toChange.value = !toChange.value;
break;
case "list.string":
toChange.value = value;
break;
default:
}
return { return {
[key]: toChange, [key]: toChange,
...rest ...rest
}; };
}); });
} };
return ( return (
<Dialog <Dialog
@@ -64,6 +73,14 @@ const TransformModal = ({
} }
/> />
) )
case "list.string":
return (
<TextInputList
key={key}
label={value.label}
onChange={(list) => handleChange(key, list)}
/>
)
default: default:
return <></>; return <></>;
} }
@@ -72,6 +89,7 @@ const TransformModal = ({
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button <Button
label="Test"
onClick={close} onClick={close}
> >
Cancel Cancel

View File

@@ -267,7 +267,12 @@ const useStyles = makeStyles((theme) => ({
textDecoration: "inherit", textDecoration: "inherit",
color: "inherit", color: "inherit",
}, },
tableSize: { height: 700, width: "100%" }, tableSize: {
width: "100%",
},
tabContainer: {
maxWidth: "100%",
},
whiteBackground: { backgroundColor: "White" }, whiteBackground: { backgroundColor: "White" },
progressIcon: { width: 40, height: 40 }, progressIcon: { width: 40, height: 40 },
progressSuccess: { color: "green" }, progressSuccess: { color: "green" },
@@ -288,7 +293,7 @@ const useStyles = makeStyles((theme) => ({
}, },
tableHeader: { tableHeader: {
textDecorationStyle: "solid", textDecorationStyle: "solid",
fontWeight:500, fontWeight: 500,
}, },
limitWidthTableCell: { limitWidthTableCell: {
maxWidth: "200px", maxWidth: "200px",

View File

@@ -17,6 +17,18 @@ const vehiclesAPI = {
.then(fetchRespHandler) .then(fetchRespHandler)
.catch(errorHandler), .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) => deleteVehicle: async (vin, token) =>
fetch(`${API_ENDPOINT}/vehicle/${vin}`, { fetch(`${API_ENDPOINT}/vehicle/${vin}`, {
method: "DELETE", method: "DELETE",