CEC-1450 Show Trex version (#169)

* CEC-1450 Show Trex version

* Code smells

* Clean up

* Fixes

* Optimize test
This commit is contained in:
John Wu
2022-07-26 09:19:48 -07:00
committed by GitHub
parent 058edb63ba
commit b70afa5312
10 changed files with 479 additions and 91 deletions

View File

@@ -7484,6 +7484,24 @@ exports[`App Route /vehicle-status authenticated 1`] = `
class="MuiTouchRipple-root" class="MuiTouchRipple-root"
/> />
</button> </button>
<button
aria-controls="tabpanel-3"
aria-selected="false"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit"
id="tab-3"
role="tab"
tabindex="-1"
type="button"
>
<span
class="MuiTab-wrapper"
>
Digital Twin
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div> </div>
<span <span
class="PrivateTabIndicator-root-1424 PrivateTabIndicator-colorSecondary-1426 MuiTabs-indicator" class="PrivateTabIndicator-root-1424 PrivateTabIndicator-colorSecondary-1426 MuiTabs-indicator"
@@ -7634,6 +7652,12 @@ exports[`App Route /vehicle-status authenticated 1`] = `
id="tabpanel-2" id="tabpanel-2"
role="tabpanel" role="tabpanel"
/> />
<div
aria-labelledby="tab-3"
hidden=""
id="tabpanel-3"
role="tabpanel"
/>
</div> </div>
</main> </main>
</main> </main>

View File

@@ -0,0 +1,64 @@
import React, { useEffect, useState } from "react";
import clsx from "clsx";
import { Typography } from "@material-ui/core";
import useStyles from "../../useStyles";
import DigitalTwin from "../../DigitalTwin";
import {
useVehicleContext,
VehicleProvider,
} from "../../Contexts/VehicleContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import { logger } from "../../../services/monitoring";
const Main = (props) => {
const { getState } = useVehicleContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const { setMessage } = useStatusContext();
const classes = useStyles();
const [carState, setCarState] = useState(null);
const { vin } = props;
useEffect(() => {
if (!vin) return;
(async () => {
try {
const result = await getState(token, vin);
setCarState(result.data);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vin]);
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Typography variant="h6" style={{ paddingBottom: "10px" }}>
Digital Twin
</Typography>
{carState && (
<>
<div>
<b>Connected</b>: {carState.online.toString()}
</div>
<DigitalTwin {...carState} />
</>
)}
</div>
);
};
const DigitalTwinTab = (props) => (
<VehicleProvider>
<Main {...props} />
</VehicleProvider>
);
export default DigitalTwinTab;

View File

@@ -0,0 +1,36 @@
jest.mock("../../Contexts/VehicleContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../Contexts/UserContext");
jest.mock("@material-ui/core/utils/unstable_useId", () =>
jest.fn().mockReturnValue("mui-test-id")
);
import React from "react";
import { render, waitFor } from "@testing-library/react";
import { StatusProvider } from "../../Contexts/StatusContext";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
import DigitalTwinTab from "./DigitalTwinTab";
const renderDetailsTab = async () => {
const { container } = render(
<StatusProvider>
<UserProvider>
<DigitalTwinTab vin="TESTVIN1234567890" />
</UserProvider>
</StatusProvider>
);
await waitFor(() => {
/* render */
});
return container;
};
describe("DigitalTwinTab", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderDetailsTab();
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,143 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DigitalTwinTab Render 1`] = `
<div>
<div
data-testid="mocked-statusprovider"
>
<div
data-testid="mocked-userprovider"
>
<div
data-testid="mocked-vehicleprovider"
>
<div
class="makeStyles-paper-3 makeStyles-tableSize-53"
>
<h6
class="MuiTypography-root MuiTypography-h6"
style="padding-bottom: 10px;"
>
Digital Twin
</h6>
<div>
<b>
Connected
</b>
:
false
</div>
<div>
<p>
<b>
Battery
</b>
:
95%
</p>
<div
class="makeStyles-popupSection-40"
>
<h3>
Doors
</h3>
<p>
<b>
hood
</b>
:
closed
</p>
<p>
<b>
left_front
</b>
:
closed
</p>
<p>
<b>
left_rear
</b>
:
closed
</p>
<p>
<b>
right_front
</b>
:
closed
</p>
<p>
<b>
right_rear
</b>
:
closed
</p>
<p>
<b>
trunk
</b>
:
closed
</p>
</div>
<div
class="makeStyles-popupSection-40"
>
<h3>
Location
</h3>
<p>
<b>
altitude
</b>
:
17
</p>
<p>
<b>
longitude
</b>
:
-122.414°
</p>
<p>
<b>
latitude
</b>
:
37.764°
</p>
</div>
<div
class="makeStyles-popupSection-40"
>
<p>
<b>
Trex Version
</b>
:
1000000
</p>
</div>
<div
class="makeStyles-popupSection-40"
>
<p>
<b>
Updated at
</b>
:
7/26/2022 12:26:38 AM
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -83,6 +83,24 @@ exports[`CarStatus Render 1`] = `
class="MuiTouchRipple-root" class="MuiTouchRipple-root"
/> />
</button> </button>
<button
aria-controls="tabpanel-3"
aria-selected="false"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit"
id="tab-3"
role="tab"
tabindex="-1"
type="button"
>
<span
class="MuiTab-wrapper"
>
Digital Twin
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div> </div>
<span <span
class="PrivateTabIndicator-root-63 PrivateTabIndicator-colorSecondary-65 MuiTabs-indicator" class="PrivateTabIndicator-root-63 PrivateTabIndicator-colorSecondary-65 MuiTabs-indicator"
@@ -179,6 +197,12 @@ exports[`CarStatus Render 1`] = `
id="tabpanel-2" id="tabpanel-2"
role="tabpanel" role="tabpanel"
/> />
<div
aria-labelledby="tab-3"
hidden=""
id="tabpanel-3"
role="tabpanel"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,15 +7,12 @@ import { Box, Tab, Tabs } from "@material-ui/core";
import CarDetailsTab from "./DetailsTab"; import CarDetailsTab from "./DetailsTab";
import CarUpdatesTab from "./CarUpdatesTab"; import CarUpdatesTab from "./CarUpdatesTab";
import CANFiltersTab from "./CANFiltersTab"; import CANFiltersTab from "./CANFiltersTab";
import DigitalTwinTab from "./DigitalTwinTab";
import TabPanel from "../../Controls/TabPanel"; import TabPanel from "../../Controls/TabPanel";
import { useStatusContext } from "../../Contexts/StatusContext"; import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles"; import useStyles from "../../useStyles";
const tabHashes = [ const tabHashes = ["details", "updates", "filters"];
"details",
"updates",
"filters"
]
const CarStatus = () => { const CarStatus = () => {
const { vin } = useParams(); const { vin } = useParams();
@@ -25,8 +22,8 @@ const CarStatus = () => {
const [tabIndex, setTabIndex] = React.useState(0); const [tabIndex, setTabIndex] = React.useState(0);
useEffect(() => { useEffect(() => {
const key = hash.replace("#", "") const key = hash.replace("#", "");
const index = tabHashes.findIndex(element => element === key); const index = tabHashes.findIndex((element) => element === key);
if (index >= 0) setTabIndex(index); if (index >= 0) setTabIndex(index);
}, [hash]); }, [hash]);
@@ -51,11 +48,20 @@ const CarStatus = () => {
return ( return (
<div className={clsx(classes.paper, classes.tableSize)}> <div className={clsx(classes.paper, classes.tableSize)}>
<Box className={classes.tableToolbar} sx={{ borderBottom: 1, borderColor: 'divider' }}> <Box
<Tabs value={tabIndex} onChange={handleTabChange} aria-label="car tabs" indicatorColor="secondary"> className={classes.tableToolbar}
sx={{ borderBottom: 1, borderColor: "divider" }}
>
<Tabs
value={tabIndex}
onChange={handleTabChange}
aria-label="car tabs"
indicatorColor="secondary"
>
<Tab label="Details" {...tabProps(0)} /> <Tab label="Details" {...tabProps(0)} />
<Tab label="Car Updates" {...tabProps(1)} /> <Tab label="Car Updates" {...tabProps(1)} />
<Tab label="CAN Filters" {...tabProps(2)} /> <Tab label="CAN Filters" {...tabProps(2)} />
<Tab label="Digital Twin" {...tabProps(3)} />
</Tabs> </Tabs>
</Box> </Box>
@@ -70,14 +76,18 @@ const CarStatus = () => {
<TabPanel value={tabIndex} index={2}> <TabPanel value={tabIndex} index={2}>
<CANFiltersTab /> <CANFiltersTab />
</TabPanel> </TabPanel>
</div >
<TabPanel value={tabIndex} index={3}>
<DigitalTwinTab vin={vin} />
</TabPanel>
</div>
); );
}; };
function tabProps(index) { function tabProps(index) {
return { return {
id: `tab-${index}`, id: `tab-${index}`,
"aria-controls": `tabpanel-${index}` "aria-controls": `tabpanel-${index}`,
}; };
} }

View File

@@ -5,17 +5,17 @@ let busy = false;
const filters = [ const filters = [
{ {
can_id: "123-456", can_id: "123-456",
interval: 789 interval: 789,
}, },
{ {
can_id: "1", can_id: "1",
interval: 1000 interval: 1000,
}, },
{ {
can_id: "1000", can_id: "1000",
interval: 1 interval: 1,
} },
] ];
let vehicle = { let vehicle = {
vin: "3C4PDCBG0ET127145", vin: "3C4PDCBG0ET127145",
@@ -24,8 +24,55 @@ let vehicle = {
trim: "Basic", trim: "Basic",
ecu_list: "ECUA 2.0.0, ECUB 2.1.1", ecu_list: "ECUA 2.0.0, ECUB 2.1.1",
log_level: "info", log_level: "info",
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: filters }, canbus: {
enabled: true,
data_logger_enabled: true,
max_mem_buffer_size: 1,
max_disk_buffer_size: 2,
filters: filters,
},
}; };
let vehicleState = {
data: {
online: false,
battery: {
percent: 95,
},
max_range: {
max_miles: 577,
},
doors: {
hood: false,
left_front: false,
left_rear: false,
right_front: false,
right_rear: false,
trunk: false,
},
location: {
altitude: 17,
longitude: -122.414,
latitude: 37.764,
},
door_locks: {
driver: false,
all: false,
},
sunroof: {
sunroof: 0,
},
cabin_climate: {
cabin_temperature: 0,
internal_temperature: 29,
},
ambient_temperature: {
temperature: 26,
},
trex_version: "1000000",
updated: "2022-07-26T00:26:38.880381Z",
},
};
let vehicles = []; let vehicles = [];
let models = ["Ocean", "PEAR"]; let models = ["Ocean", "PEAR"];
let years = [2023, 2024]; let years = [2023, 2024];
@@ -83,7 +130,7 @@ export const useVehicleContext = () => ({
getModels: jest.fn(() => { getModels: jest.fn(() => {
models = ["Ocean", "PEAR"]; models = ["Ocean", "PEAR"];
}), }),
getState: jest.fn(), getState: jest.fn(() => vehicleState),
getYears: jest.fn(() => { getYears: jest.fn(() => {
years = [2023, 2024]; years = [2023, 2024];
}), }),

View File

@@ -0,0 +1,60 @@
import React from "react";
import useStyles from "../useStyles";
import { LocalDateTimeString } from "../../utils/dates";
const keyValueTemplate = (key, value) => (
<p key={key}>
<b>{key}</b>: {value}
</p>
);
const openCloseState = (value) => (value ? "open" : "closed");
const mapOpenCloseState = (value) =>
keyValueTemplate(value[0], openCloseState(value[1]));
const DigitalTwin = (props) => {
const classes = useStyles();
const { battery, doors, location, trex_version, updated, windows } = props;
return (
<div>
{battery != null && keyValueTemplate("Battery", `${battery.percent}%`)}
{doors != null && (
<div className={classes.popupSection}>
<h3>Doors</h3>
{Object.entries(doors).map(mapOpenCloseState)}
</div>
)}
{windows != null && (
<div className={classes.popupSection}>
<h3>Windows</h3>
{Object.entries(windows).map(mapOpenCloseState)}
</div>
)}
{location != null && (
<div className={classes.popupSection}>
<h3>Location</h3>
{Object.entries(location).map((value) => {
if (value[0] === "altitude") {
return keyValueTemplate(value[0], value[1]);
} else {
return keyValueTemplate(value[0], `${value[1]}°`);
}
})}
</div>
)}
{trex_version && (
<div className={classes.popupSection}>
{keyValueTemplate("Trex Version", trex_version)}
</div>
)}
{updated != null && (
<div className={classes.popupSection}>
{keyValueTemplate("Updated at", LocalDateTimeString(updated))}
</div>
)}
</div>
);
};
export default DigitalTwin;

View File

@@ -179,7 +179,8 @@ const Component = () => {
doors={carState.doors} doors={carState.doors}
location={carState.location} location={carState.location}
windows={carState.windows} windows={carState.windows}
updatedAt={carState.updated} trex_version={carState.trex_version}
updated={carState.updated}
className={classes.popup} className={classes.popup}
onClose={handleClose} onClose={handleClose}
/> />

View File

@@ -1,16 +1,16 @@
import React from "react"; import React from "react";
import Dialog from '@material-ui/core/Dialog'; import Dialog from "@material-ui/core/Dialog";
import MuiDialogTitle from '@material-ui/core/DialogTitle'; import MuiDialogTitle from "@material-ui/core/DialogTitle";
import IconButton from '@material-ui/core/IconButton'; import IconButton from "@material-ui/core/IconButton";
import CloseIcon from '@material-ui/icons/Close'; import CloseIcon from "@material-ui/icons/Close";
import Typography from '@material-ui/core/Typography'; import Typography from "@material-ui/core/Typography";
import useStyles from "../useStyles"; import useStyles from "../useStyles";
import { LocalDateTimeString } from "../../utils/dates"; import DigitalTwin from "../DigitalTwin";
const VehiclePopUp = (props) => { const VehiclePopUp = (props) => {
const classes = useStyles(); const classes = useStyles();
const { vin, online, battery, doors, location, updatedAt, windows, onClose } = props; const { vin, online, battery, doors, location, windows, onClose } = props;
return ( return (
<Dialog <Dialog
@@ -19,50 +19,21 @@ const VehiclePopUp = (props) => {
open={true} open={true}
onClose={onClose} onClose={onClose}
> >
<DialogTitle align="center" onClose={onClose}>{vin}</DialogTitle> <DialogTitle align="center" onClose={onClose}>
{vin}
</DialogTitle>
<div align="center" className={classes.popUpContent}> <div align="center" className={classes.popUpContent}>
<p><b>Connected</b>: {online.toString()}</p> <p>
{online && ( <b>Connected</b>: {online.toString()}
<div> </p>
{battery != null && ( {online && <DigitalTwin {...props} />}
<p><b>Battery</b>: {battery.percent}%</p> {(!online ||
)} (battery == null &&
{doors != null && ( doors == null &&
<div className={classes.popupSection}> location == null &&
<h3>Doors</h3> windows == null)) && <p>No vehicle data to display.</p>}
{Object.entries(doors).map((value) => (<p key={value[0]}><b>{value[0]}</b>: {value[1] ? "open" : "closed"}</p>))}
</div> </div>
)} </Dialog>
{windows != null && (
<div className={classes.popupSection}>
<h3>Windows</h3>
{Object.entries(windows).map((value) => (<p key={value[0]}><b>{value[0]}</b>: {value[1] ? "open" : "closed"}</p>))}
</div>
)}
{location != null && (
<div className={classes.popupSection}>
<h3>Location</h3>
{Object.entries(location).map((value) => {
if (value[0] === "altitude") {
return (<p key={value[0]}><b>{value[0]}</b>: {value[1]}</p>);
} else {
return (<p key={value[0]}><b>{value[0]}</b>: {value[1]}°</p>)
}
})}
</div>
)}
{updatedAt != null && (
<div className={classes.popupSection}>
<p><b>Updated at</b>: {LocalDateTimeString(updatedAt)}</p>
</div>
)}
</div>
)}
{(!online || (battery == null && doors == null && location == null && windows == null)) && (
<p>No vehicle data to display.</p>
)}
</div>
</Dialog >
); );
}; };
@@ -70,10 +41,18 @@ const DialogTitle = (props) => {
const { children, onClose, ...other } = props; const { children, onClose, ...other } = props;
const classes = useStyles(); const classes = useStyles();
return ( return (
<MuiDialogTitle disableTypography className={classes.ppopUpTItle} {...other}> <MuiDialogTitle
disableTypography
className={classes.ppopUpTItle}
{...other}
>
<Typography variant="h6">{children}</Typography> <Typography variant="h6">{children}</Typography>
{onClose ? ( {onClose ? (
<IconButton aria-label="close" className={classes.closeButton} onClick={onClose}> <IconButton
aria-label="close"
className={classes.closeButton}
onClick={onClose}
>
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
) : null} ) : null}