Merge branch 'main' into CEC-5542

This commit is contained in:
Paul Adamsen
2024-05-02 09:57:09 -04:00
committed by GitHub
15 changed files with 357 additions and 102 deletions

View File

@@ -34,6 +34,7 @@ export const VehicleProvider = ({ children }) => {
const [totalFlashpacks, setTotalFlashpacks] = useState(0); const [totalFlashpacks, setTotalFlashpacks] = useState(0);
const [flashpackECUMappings, setFlashpackECUMappings] = useState([]) const [flashpackECUMappings, setFlashpackECUMappings] = useState([])
const [totalFlashpackECUMappings, setTotalFlashpackECUMappings] = useState(0) const [totalFlashpackECUMappings, setTotalFlashpackECUMappings] = useState(0)
const [osVersions, setOSVersions] = useState([{ "value": "", "label": "None" }])
const addConnections = async (cars, token) => { const addConnections = async (cars, token) => {
try { try {
@@ -331,7 +332,7 @@ export const VehicleProvider = ({ children }) => {
} }
}; };
const addFlashpackVersion = async (model, trim, year, flashpack, carFlashpackVersions, token) => { const addFlashpackVersion = async (model, trim, year, flashpack, osVersion, carFlashpackVersions, token) => {
try { try {
setBusy(true); setBusy(true);
@@ -340,6 +341,7 @@ export const VehicleProvider = ({ children }) => {
"car_trim": trim, "car_trim": trim,
"car_year": year, "car_year": year,
"flashpack": flashpack, "flashpack": flashpack,
"os_version": osVersion,
"ecu_versions": carFlashpackVersions, "ecu_versions": carFlashpackVersions,
} }
@@ -424,6 +426,30 @@ export const VehicleProvider = ({ children }) => {
} finally { } finally {
setBusy(false); setBusy(false);
} }
};
const getOSVersions = async (token) => {
try {
setBusy(true);
const result = await api.getOSVersions(token);
if (result.error) {
throw new Error(`Get OS versions error. ${result.message}`);
}
var data = [{ "value": "", "label": "None" }]
for (let i = 0; i < result.data.length; i++) {
data.push({
"value": result.data[i],
"label": result.data[i]
});
}
setOSVersions(data);
} finally {
setBusy(false);
}
} }
return ( return (
@@ -466,6 +492,8 @@ export const VehicleProvider = ({ children }) => {
deleteFlashpackVersion, deleteFlashpackVersion,
deleteFlashpackVersionECUMapping, deleteFlashpackVersionECUMapping,
getCarFlashpackVersionInfo, getCarFlashpackVersionInfo,
osVersions,
getOSVersions,
}} }}
> >
{children} {children}

View File

@@ -1,5 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { import {
Checkbox, Checkbox,
@@ -17,9 +16,9 @@ import { useStatusContext } from "../../Contexts/StatusContext";
import { LocalDateTimeString } from "../../../utils/dates"; import { LocalDateTimeString } from "../../../utils/dates";
import TableHeaderSortable from "../../Table/HeaderSortable"; import TableHeaderSortable from "../../Table/HeaderSortable";
import { logger } from "../../../services/monitoring"; import { logger } from "../../../services/monitoring";
import ConnectedIcon from "../../Controls/ConnectedIcon";
import ECUList from "../../Controls/ECUList"; import ECUList from "../../Controls/ECUList";
import { useLocalStorage } from "../../useLocalStorage"; import { useLocalStorage } from "../../useLocalStorage";
import { VehicleTeaser } from "../../VehicleTeaser";
const tableColumns = [ const tableColumns = [
{ {
@@ -164,12 +163,11 @@ const CarSelectionTable = (props) => {
</TableCell> </TableCell>
)} )}
<TableCell align="center"> <TableCell align="center">
<ConnectedIcon <VehicleTeaser
connected={row.connected} vin={row.vin}
connectedHMI={row.connectedHMI} trex={row.connected}
style={{ marginRight: 3 }} icc={row.connectedHMI}
/> />
<Link to={`/vehicle-status/${row.vin}`}>{row.vin}</Link>
{row.ecu_list && ( {row.ecu_list && (
<> <>
<br /> <br />

View File

@@ -1,36 +1,78 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import CheckCircleIcon from "@material-ui/icons/CheckCircle"; import DoneAllIcon from "@material-ui/icons/DoneAll";
import CheckBoxIcon from "@material-ui/icons/CheckBox"; import ClearIcon from "@material-ui/icons/Clear";
import DoneIcon from "@material-ui/icons/Done";
import { Tooltip } from "@material-ui/core"; import { Tooltip } from "@material-ui/core";
const ConnectedIcon = (props) => { const ConnectedIcon = (props) => {
if (props.connected || props.connectedHMI) { let title = tooltip(props.connected, props.connectedHMI);
const content = () => {
if (props.connected && props.connectedHMI) {
return ( return (
<span style={props.style}> <Tooltip title={title}>
{props.connected && ( <DoneAllIcon
<Tooltip title="TBOX"> fontSize="small"
<CheckCircleIcon style={{ color: "green" }}
style={{ color: "Green", fontSize: 12, marginRight: 3 }}
/> />
</Tooltip> </Tooltip>
)}
{props.connectedHMI && (
<Tooltip title="ICC">
<CheckBoxIcon
style={{ color: "Blue", fontSize: 12, marginRight: 3 }}
/>
</Tooltip>
)}
</span>
); );
} }
return null; if (props.connected || props.connectedHMI) {
let color = "blue";
if (props.connected) {
color = "green"
}
return (
<Tooltip title={title}>
<DoneIcon
fontSize="small"
style={{ color }}
/>
</Tooltip>
);
}
return (
<Tooltip title={title}>
<ClearIcon
fontSize="small"
style={{ color: "red" }}
/>
</Tooltip>
);
};
return (
<span style={props.style}>
{content()}
</span>
)
}; };
ConnectedIcon.propTypes = { ConnectedIcon.propTypes = {
connected: PropTypes.bool.isRequired, connected: PropTypes.bool.isRequired,
}; };
function tooltip(trex, icc) {
const status = [];
if (trex) {
status.push("TREX");
}
if (icc) {
status.push("ICC");
}
if (!status.length) {
return "OFFLINE";
}
return status.join(" & ");
}
export default ConnectedIcon; export default ConnectedIcon;

View File

@@ -91,11 +91,6 @@ exports[`FlashpackAdd Render 1`] = `
class="MuiSelect-root MuiSelect-select MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input" class="MuiSelect-root MuiSelect-select MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input"
required="" required=""
> >
<option
value="Base"
>
Base
</option>
<option <option
value="Sport" value="Sport"
> >
@@ -244,6 +239,50 @@ exports[`FlashpackAdd Render 1`] = `
</fieldset> </fieldset>
</div> </div>
</div> </div>
<div
class="MuiFormControl-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root makeStyles-whiteBackground-0 MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined"
data-shrink="false"
>
OS Version
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-formControl"
required=""
>
<select
aria-invalid="false"
class="MuiSelect-root MuiSelect-select MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input"
required=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSelect-icon MuiSelect-iconOutlined"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-0 MuiOutlinedInput-notchedOutline"
style="padding-left: 8px;"
>
<legend
class="PrivateNotchedOutline-legend-0"
style="width: 0.01px;"
>
<span>
</span>
</legend>
</fieldset>
</div>
</div>
<div <div
class="container" class="container"
> >

View File

@@ -27,14 +27,17 @@ const MainForm = () => {
const [redirect, setRedirect] = useState(null); const [redirect, setRedirect] = useState(null);
const { setMessage, setTitle, setSitePath } = useStatusContext(); const { setMessage, setTitle, setSitePath } = useStatusContext();
const [carModel, setCarModel] = useLocalStorage("FLASHPACK_ADD_MODEL", "Ocean"); const [carModel, setCarModel] = useLocalStorage("FLASHPACK_ADD_MODEL", "Ocean");
const [carTrim, setCarTrim] = useLocalStorage("FLASHPACK_ADD_TRIM", "Base"); const [carTrim, setCarTrim] = useLocalStorage("FLASHPACK_ADD_TRIM", "Sport");
const [carYear, setCarYear] = useLocalStorage("FLASHPACK_ADD_YEAR", 2024); const [carYear, setCarYear] = useLocalStorage("FLASHPACK_ADD_YEAR", 2024);
const [trims, setTrims] = useLocalStorage("FLASHPACK_ADD_TRIMS", modelsTrimsYears.oceanTrims); const [trims, setTrims] = useLocalStorage("FLASHPACK_ADD_TRIMS", modelsTrimsYears.oceanTrims);
const [years, setYears] = useLocalStorage("FLASHPACK_ADD_YEARS", modelsTrimsYears.oceanYears); const [years, setYears] = useLocalStorage("FLASHPACK_ADD_YEARS", modelsTrimsYears.oceanYears);
const [flashpack, setFlashpack] = useState(); const [flashpack, setFlashpack] = useState("");
const [osVersion, setOSVersion] = useState("");
const [mappingInputs, setMappingInputs] = useState([{ ecuName: "", ecuVersion: "" }]); const [mappingInputs, setMappingInputs] = useState([{ ecuName: "", ecuVersion: "" }]);
const { const {
addFlashpackVersion, addFlashpackVersion,
getOSVersions,
osVersions,
busy, busy,
} = useVehicleContext(); } = useVehicleContext();
@@ -56,6 +59,19 @@ const MainForm = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
useEffect(() => {
(async () => {
try {
if (!token) return;
await getOSVersions(token);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
const onCarModelChange = (event) => { const onCarModelChange = (event) => {
let newModel = event.target.value let newModel = event.target.value
@@ -87,6 +103,10 @@ const MainForm = () => {
setFlashpack(event.target.value); setFlashpack(event.target.value);
} }
const onOSVersionChange = (event) => {
setOSVersion(event.target.value);
}
const onSubmit = async (event) => { const onSubmit = async (event) => {
try { try {
event.preventDefault(); event.preventDefault();
@@ -100,7 +120,7 @@ const MainForm = () => {
}) })
} }
const result = await addFlashpackVersion(carModel, carTrim, parseInt(carYear), flashpack, carFlashpackVersions, token); const result = await addFlashpackVersion(carModel, carTrim, parseInt(carYear), flashpack, osVersion, carFlashpackVersions, token);
if (!result || result.error) return; if (!result || result.error) return;
setMessage(`Added ${carYear} ${carModel} ${carTrim} ${flashpack}`); setMessage(`Added ${carYear} ${carModel} ${carTrim} ${flashpack}`);
@@ -157,6 +177,7 @@ const MainForm = () => {
onChange={onFlashpackChange} onChange={onFlashpackChange}
type="number" type="number"
/> />
<DropDownList fullWidth required label="OS Version" data={osVersions} classes={classes} onChange={onOSVersionChange} value={osVersion} />
<div className="container"> <div className="container">
{mappingInputs.map((item, index) => ( {mappingInputs.map((item, index) => (
<div className="input_container" key={index}> <div className="input_container" key={index}>

View File

@@ -79,11 +79,6 @@ exports[`Flashpack Render 1`] = `
aria-invalid="false" aria-invalid="false"
class="MuiSelect-root MuiSelect-select MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input" class="MuiSelect-root MuiSelect-select MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input"
> >
<option
value="Base"
>
Base
</option>
<option <option
value="Sport" value="Sport"
> >
@@ -245,6 +240,29 @@ exports[`Flashpack Render 1`] = `
</svg> </svg>
</span> </span>
</th> </th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Part of OS Version
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th <th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col" scope="col"

View File

@@ -30,6 +30,10 @@ const tableColumns = [
id: "flashpack", id: "flashpack",
label: "Flashpack Number", label: "Flashpack Number",
}, },
{
id: "os_version",
label: "Part of OS Version",
},
{ {
id: "car_model", id: "car_model",
label: "Model", label: "Model",
@@ -58,7 +62,7 @@ const MainForm = () => {
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [rowToDelete, setRowToDelete] = useState({}); const [rowToDelete, setRowToDelete] = useState({});
const [model, setModel] = useLocalStorage("FLASHPACKS_MODEL", "Ocean"); const [model, setModel] = useLocalStorage("FLASHPACKS_MODEL", "Ocean");
const [trim, setTrim] = useLocalStorage("FLASHPACKS_TRIM", "Base"); const [trim, setTrim] = useLocalStorage("FLASHPACKS_TRIM", "Sport");
const [year, setYear] = useLocalStorage("FLASHPACKS_YEAR", 2024); const [year, setYear] = useLocalStorage("FLASHPACKS_YEAR", 2024);
const [trims, setTrims] = useLocalStorage("FLASHPACKS_TRIMS", modelsTrimsYears.oceanTrims); const [trims, setTrims] = useLocalStorage("FLASHPACKS_TRIMS", modelsTrimsYears.oceanTrims);
const [years, setYears] = useLocalStorage("FLASHPACKS_YEARS", modelsTrimsYears.oceanYears); const [years, setYears] = useLocalStorage("FLASHPACKS_YEARS", modelsTrimsYears.oceanYears);
@@ -210,6 +214,9 @@ const MainForm = () => {
{row.flashpack} {row.flashpack}
</Link> </Link>
</TableCell> </TableCell>
<TableCell align="center">
{row.os_version}
</TableCell>
<TableCell align="center"> <TableCell align="center">
{row.car_model} {row.car_model}
</TableCell> </TableCell>
@@ -245,7 +252,7 @@ const MainForm = () => {
) : ( ) : (
<TablePagination <TablePagination
rowsPerPageOptions={[5, 10, 25, 100]} rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={5} colSpan={6}
count={totalFlashpacks} count={totalFlashpacks}
rowsPerPage={pageSize} rowsPerPage={pageSize}
page={pageIndex} page={pageIndex}

View File

@@ -6,10 +6,6 @@
} }
], ],
"oceanTrims": [ "oceanTrims": [
{
"value": "Base",
"label": "Base"
},
{ {
"value": "Sport", "value": "Sport",
"label": "Sport" "label": "Sport"

View File

@@ -191,7 +191,7 @@ exports[`FleetVehiclesTable Render 1`] = `
role="button" role="button"
tabindex="0" tabindex="0"
> >
VIN Vehicle
<svg <svg
aria-hidden="true" aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc" class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"

View File

@@ -23,18 +23,19 @@ import {
} from "../../../../Contexts/FleetContext"; } from "../../../../Contexts/FleetContext";
import { useStatusContext } from "../../../../Contexts/StatusContext"; import { useStatusContext } from "../../../../Contexts/StatusContext";
import { useUserContext } from "../../../../Contexts/UserContext"; import { useUserContext } from "../../../../Contexts/UserContext";
import { VehicleProvider } from "../../../../Contexts/VehicleContext";
import SearchField from "../../../../Controls/SearchField"; import SearchField from "../../../../Controls/SearchField";
import TableHeaderSortable from "../../../../Table/HeaderSortable"; import TableHeaderSortable from "../../../../Table/HeaderSortable";
import { useLocalStorage } from "../../../../useLocalStorage"; import { useLocalStorage } from "../../../../useLocalStorage";
import ConnectedIcon from "../../../../Controls/ConnectedIcon";
import BulkActions from "../../../../BulkActions"; import BulkActions from "../../../../BulkActions";
import Battery from "../../../../Battery"; import Battery from "../../../../Battery";
import { VehicleTeaserController as VehicleTeaser } from "../../../../VehicleTeaser";
import useStyles from "../../../../useStyles"; import useStyles from "../../../../useStyles";
const tableColumns = [ const tableColumns = [
{ {
id: "vin", id: "vehicle",
label: "VIN", label: "Vehicle",
}, },
{ {
id: "trex_version", id: "trex_version",
@@ -213,6 +214,7 @@ const MainForm = ({ name }) => {
selectCount={selected.length} selectCount={selected.length}
rowCount={fleetVehicles.length} rowCount={fleetVehicles.length}
/> />
<VehicleProvider>
<TableBody> <TableBody>
{fleetVehicles && fleetVehicles.map((car) => { {fleetVehicles && fleetVehicles.map((car) => {
const isSelected = selected.includes(car.vin); const isSelected = selected.includes(car.vin);
@@ -224,15 +226,12 @@ const MainForm = ({ name }) => {
/> />
</TableCell> </TableCell>
<TableCell key={"cell" + car.vin} align="center"> <TableCell key={"cell" + car.vin} align="center">
{(car.connected || car.connectedHMI) && <VehicleTeaser
<ConnectedIcon vin={car.vin}
key={"icon" + car.vin} icc={car.connectedHMI}
connected={car.connected} trex={car.connected}
connectedHMI={car.connectedHMI} token={token}
style={{ marginRight: 3 }}
/> />
}
<Link key={"link" + car.vin} to={`/vehicle-status/${car.vin}`}>{car.vin}</Link>
</TableCell> </TableCell>
<TableCell key={"cell2" + car.vin} align="center">{car.trex_version}</TableCell> <TableCell key={"cell2" + car.vin} align="center">{car.trex_version}</TableCell>
<TableCell key={"cell3" + car.car_update_name}> <TableCell key={"cell3" + car.car_update_name}>
@@ -261,6 +260,7 @@ const MainForm = ({ name }) => {
) )
})} })}
</TableBody> </TableBody>
</VehicleProvider>
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TablePagination <TablePagination

View File

@@ -190,7 +190,7 @@ exports[`VehiclesTab Render 1`] = `
role="button" role="button"
tabindex="0" tabindex="0"
> >
VIN Vehicle
<svg <svg
aria-hidden="true" aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc" class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"

View File

@@ -0,0 +1,72 @@
import { useRef, useState, useEffect } from "react";
import { Link } from "react-router-dom";
import Chip from '@mui/material/Chip';
import Stack from '@mui/material/Stack';
import ConnectedIcon from "../Controls/ConnectedIcon";
import { useVehicleContext } from "../Contexts/VehicleContext";
import useStyles from "../useStyles";
import { useIntersectObserver } from "../../hooks";
// Prevent fetching missing data by not including `token` prop
export function VehicleTeaserController(props) {
const el = useRef(null);
const isVisible = useIntersectObserver(el, "0px", true);
const [isMissingData, setIsMissingData] = useState(false);
const { getVehicle, vehicle } = useVehicleContext();
const classes = useStyles();
useEffect(() => {
if (props.trim === undefined) {
setIsMissingData(true);
}
}, [isVisible, props.trim]);
useEffect(() => {
if (isVisible && props.token) {
getVehicle(props.vin, props.token)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMissingData, isVisible, props.vin, props.token]);
if (!props.vin) {
return (
<div className={classes.alignBaseline}>
Missing VIN
</div>
);
}
return (
<div ref={el}>
<VehicleTeaser
vin={props.vin}
trex={props.trex}
icc={props.icc}
trim={props.trim || vehicle.trim}
/>
</div>
);
}
export function VehicleTeaser(props) {
const classes = useStyles();
return (
<div className={classes.alignBaseline}>
<ConnectedIcon
connected={props.trex}
connectedHMI={props.icc}
style={{ display: "flex", marginRight: 3 }}
/>
<Link to={`/vehicle-status/${props.vin}`}>
{props.vin}
</Link>
<Stack direction="row" spacing={1} style={{ marginLeft: 3 }}>
{props.trim && <Chip label={props.trim} size="small" />}
</Stack>
</div>
)
}

View File

@@ -1,2 +1,4 @@
export { useTimeoutState } from "./useTimeoutState"; export { useTimeoutState } from "./useTimeoutState";
export { useUpdateManifest } from "./useUpdateManifest"; export { useUpdateManifest } from "./useUpdateManifest";
export { useIntersectObserver } from "./useIntersectObserver";

View File

@@ -0,0 +1,21 @@
import { useEffect, useState } from "react";
export function useIntersectObserver(element, offset, once) {
const [isInViewport, setIsInViewport] = useState(false);
useEffect(() => {
const current = element?.current;
const observer = new IntersectionObserver(([entry]) => {
setIsInViewport(entry.isIntersecting);
if (entry.isIntersecting && once) {
observer.unobserve(current);
}
}, { rootMargin: offset });
current && observer.observe(current);
return () => current && observer.unobserve(current);
}, [element, offset, once]);
return isInViewport;
}

View File

@@ -360,6 +360,17 @@ const vehiclesAPI = {
}).then(fetchRespHandler) }).then(fetchRespHandler)
.catch(errorHandler) .catch(errorHandler)
}, },
getOSVersions: async (token) => {
return fetch(`${API_ENDPOINT}/manifests_active_os_versions`, {
method: "GET",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler)
.catch(errorHandler)
},
}; };
export default vehiclesAPI; export default vehiclesAPI;