CEC-3943-display-ecu-vin-timeline (#303)

* first push

* fix test

* Fix test

* resolve comments

* fix undefined

* align items and change search field

* fix dates

* resolve comments

* fix lint

* fix test

---------

Co-authored-by: jwu-fisker <jwu@fiskerinc.com>
This commit is contained in:
das31
2023-03-27 12:05:40 -04:00
committed by GitHub
parent 234252a100
commit 8ece05a4b9
9 changed files with 420 additions and 0 deletions

40
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"@emotion/styled": "^11.8.1", "@emotion/styled": "^11.8.1",
"@material-ui/core": "^4.12.4", "@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3", "@material-ui/icons": "^4.11.3",
"@material-ui/lab": "^4.0.0-alpha.61",
"@material-ui/pickers": "^3.3.10", "@material-ui/pickers": "^3.3.10",
"@mui/material": "^5.10.14", "@mui/material": "^5.10.14",
"@superset-ui/embedded-sdk": "^0.1.0-alpha.8", "@superset-ui/embedded-sdk": "^0.1.0-alpha.8",
@@ -4237,6 +4238,33 @@
} }
} }
}, },
"node_modules/@material-ui/lab": {
"version": "4.0.0-alpha.61",
"resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.61.tgz",
"integrity": "sha512-rSzm+XKiNUjKegj8bzt5+pygZeckNLOr+IjykH8sYdVk7dE9y2ZuUSofiMV2bJk3qU+JHwexmw+q0RyNZB9ugg==",
"deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.",
"dependencies": {
"@babel/runtime": "^7.4.4",
"@material-ui/utils": "^4.11.3",
"clsx": "^1.0.4",
"prop-types": "^15.7.2",
"react-is": "^16.8.0 || ^17.0.0"
},
"engines": {
"node": ">=8.0.0"
},
"peerDependencies": {
"@material-ui/core": "^4.12.1",
"@types/react": "^16.8.6 || ^17.0.0",
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@material-ui/pickers": { "node_modules/@material-ui/pickers": {
"version": "3.3.10", "version": "3.3.10",
"resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.10.tgz", "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.10.tgz",
@@ -20962,6 +20990,18 @@
"@babel/runtime": "^7.4.4" "@babel/runtime": "^7.4.4"
} }
}, },
"@material-ui/lab": {
"version": "4.0.0-alpha.61",
"resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.61.tgz",
"integrity": "sha512-rSzm+XKiNUjKegj8bzt5+pygZeckNLOr+IjykH8sYdVk7dE9y2ZuUSofiMV2bJk3qU+JHwexmw+q0RyNZB9ugg==",
"requires": {
"@babel/runtime": "^7.4.4",
"@material-ui/utils": "^4.11.3",
"clsx": "^1.0.4",
"prop-types": "^15.7.2",
"react-is": "^16.8.0 || ^17.0.0"
}
},
"@material-ui/pickers": { "@material-ui/pickers": {
"version": "3.3.10", "version": "3.3.10",
"resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.10.tgz", "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.10.tgz",

View File

@@ -10,6 +10,7 @@
"@emotion/styled": "^11.8.1", "@emotion/styled": "^11.8.1",
"@material-ui/core": "^4.12.4", "@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3", "@material-ui/icons": "^4.11.3",
"@material-ui/lab": "^4.0.0-alpha.61",
"@material-ui/pickers": "^3.3.10", "@material-ui/pickers": "^3.3.10",
"@mui/material": "^5.10.14", "@mui/material": "^5.10.14",
"@superset-ui/embedded-sdk": "^0.1.0-alpha.8", "@superset-ui/embedded-sdk": "^0.1.0-alpha.8",

View File

@@ -11229,6 +11229,24 @@ exports[`App Route /vehicle-status authenticated 1`] = `
class="MuiTouchRipple-root" class="MuiTouchRipple-root"
/> />
</button> </button>
<button
aria-controls="tabpanel-10"
aria-selected="false"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit"
id="tab-10"
role="tab"
tabindex="-1"
type="button"
>
<span
class="MuiTab-wrapper"
>
DTC Timeline
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div> </div>
<span <span
class="PrivateTabIndicator-root-0 PrivateTabIndicator-colorSecondary-0 MuiTabs-indicator" class="PrivateTabIndicator-root-0 PrivateTabIndicator-colorSecondary-0 MuiTabs-indicator"
@@ -11472,6 +11490,12 @@ exports[`App Route /vehicle-status authenticated 1`] = `
id="tabpanel-9" id="tabpanel-9"
role="tabpanel" role="tabpanel"
/> />
<div
aria-labelledby="tab-10"
hidden=""
id="tabpanel-10"
role="tabpanel"
/>
</div> </div>
</main> </main>
</main> </main>

View File

@@ -213,6 +213,24 @@ exports[`CarStatus Render 1`] = `
class="MuiTouchRipple-root" class="MuiTouchRipple-root"
/> />
</button> </button>
<button
aria-controls="tabpanel-10"
aria-selected="false"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit"
id="tab-10"
role="tab"
tabindex="-1"
type="button"
>
<span
class="MuiTab-wrapper"
>
DTC Timeline
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div> </div>
<span <span
class="PrivateTabIndicator-root-0 PrivateTabIndicator-colorSecondary-0 MuiTabs-indicator" class="PrivateTabIndicator-root-0 PrivateTabIndicator-colorSecondary-0 MuiTabs-indicator"
@@ -400,6 +418,12 @@ exports[`CarStatus Render 1`] = `
id="tabpanel-9" id="tabpanel-9"
role="tabpanel" role="tabpanel"
/> />
<div
aria-labelledby="tab-10"
hidden=""
id="tabpanel-10"
role="tabpanel"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -19,6 +19,7 @@ import ECUsTab from "./ECUsTab";
import FleetsTab from "./FleetsTab"; import FleetsTab from "./FleetsTab";
import RemoteCommandsTab from "./RemoteCommandsTab"; import RemoteCommandsTab from "./RemoteCommandsTab";
import TRexLogsTab from "./TRexLogsTab"; import TRexLogsTab from "./TRexLogsTab";
import DTCTimeline from "../../DTCTimeline/DTCTimeline";
const tabHashes = ["details", "updates", "filters"]; const tabHashes = ["details", "updates", "filters"];
@@ -72,7 +73,13 @@ const TabViews = [
label: "CAN Signal Export", label: "CAN Signal Export",
component: SelfServeTab, component: SelfServeTab,
rolesPerProvider: Permissions.FiskerRead, rolesPerProvider: Permissions.FiskerRead,
},
{
label: "DTC Timeline",
component: DTCTimeline,
rolesPerProvider: Permissions.FiskerMagnaRead,
} }
]; ];
const filterTabs = (data, groups, providers) => { const filterTabs = (data, groups, providers) => {

View File

@@ -0,0 +1,43 @@
import React, { useContext, useState } from "react";
import api from "../../services/DTCTimelineAPI";
const DTCTimelineContext = React.createContext();
export const DTCTimelineProvider = ({ children }) => {
const [busy, setBusy] = useState(false);
const [dtcData, setDTCData] = useState([]);
const [total, setTotal] = useState(0)
const getDTCData = async (vin, ecu, startDate, endDate, search,token) => {
try {
setBusy(true);
const result = await api.getDTCData(vin, ecu, startDate, endDate, search,token);
if (result.error) {
throw new Error(`Get DTC data error. ${result.message}`);
}
setDTCData(result.data ?? []);
if (result.total){
setTotal(result.total)
}
return result;
} finally {
setBusy(false);
}
};
return (
<DTCTimelineContext.Provider
value={{
busy,
total,
dtcData,
getDTCData
}}
>
{children}
</DTCTimelineContext.Provider>
);
};
export const useDTCTimelineContext = () => useContext(DTCTimelineContext);

View File

@@ -0,0 +1,228 @@
import DateFnsUtils from '@date-io/date-fns';
import { Button, CircularProgress, Grid, TableFooter, TablePagination, TableCell, Table, TableRow, TableBody} from "@material-ui/core";
import { KeyboardDatePicker, MuiPickersUtilsProvider } from '@material-ui/pickers';
import React, { useEffect, useState } from "react";
import { logger } from "../../../services/monitoring";
import { DTCTimelineProvider, useDTCTimelineContext } from '../../Contexts/DTCTimelineContext';
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useLocalStorage } from "../../useLocalStorage";
import clsx from "clsx";
import TableHeaderSortable from "../../Table/HeaderSortable";
import SearchField from '../../Controls/SearchField';
import useStyles from "../../useStyles";
const MainForm = ({ vin }) => {
const classes = useStyles();
const PAGE_SIZE = "DTC_TABLE_PAGE_SIZE";
const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10);
const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("epoch_usec");
const [order, setOrder] = useState("desc");
const { dtcData, getDTCData, total=0 } = useDTCTimelineContext();
const [selectedStartDate, setSelectedStartDate] = useState(new Date(Date.now() - 24 * 60 * 60 * 1000));
const [selectedEndDate, setSelectedEndDate] = useState(new Date());
const [selectedECU, setSelectedECU] = useState("");
const [loading, setLoading] = useState(false);
const { setMessage } = useStatusContext();
const tableColumns = [
{
id: "id",
label: "Id",
},
{
id: "vin",
label: "VIN",
},
{
id: "ecu",
label: "ECU",
},
{
id: "dtc",
label: "DTC",
},
{
id: "epoch_usec",
label: "Date",
},
];
const handleSort = (_event, property) => {
if (property === orderBy) {
if (order === "asc") {
setOrder("desc");
} else {
setOrder("asc");
}
} else {
setOrderBy(property);
setOrder("desc");
}
};
const handleChangePageIndex = (_event, newIndex) => {
setPageIndex(newIndex);
};
const handleChangePageSize = (event) => {
setPageSize(parseInt(event.target.value, 10));
setPageIndex(0);
};
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const fetchDTCData = async () => {
setLoading(true);
try {
let start_date = new Date(selectedStartDate);
start_date.setHours(0, 0, 0, 0);
start_date = start_date.toISOString();
let end_date = new Date(selectedEndDate);
end_date.setHours(23, 59, 59, 999);
end_date = end_date.toISOString();
const search = {
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
}
await getDTCData(vin, selectedECU, start_date, end_date, search, token);
// setDTCData(data);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
setLoading(false);
};
function formatDate(microseconds) {
const date = new Date(microseconds / 1000);
return date.toLocaleString();
}
useEffect(() => {
fetchDTCData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vin, selectedECU, selectedStartDate, selectedEndDate, pageIndex, pageSize, order, orderBy]);
return (
<div className={classes.paper}>
<Grid container spacing={3} alignItems="flex-end">
<Grid item xs={6} md={3}>
<div style={{ marginBottom: '8px' }}>
<SearchField
classes={classes}
onSearch={(searchValue) => {
setSelectedECU(searchValue);
}}
/>
</div>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDatePicker
disableToolbar
variant="inline"
format="MM/dd/yyyy"
margin="normal"
label="Start Date"
value={selectedStartDate}
onChange={setSelectedStartDate}
KeyboardButtonProps={{
'aria-label': 'change date',
}}
fullWidth
/>
</MuiPickersUtilsProvider>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDatePicker
disableToolbar
variant="inline"
format="MM/dd/yyyy"
margin="normal"
label="End Date"
value={selectedEndDate}
onChange={setSelectedEndDate}
KeyboardButtonProps={{
'aria-label': 'change date',
}}
fullWidth
/>
</MuiPickersUtilsProvider>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Button
variant="contained"
color="primary"
onClick={fetchDTCData}
disabled={loading}
fullWidth
style={{ marginTop: '15px' }}
>
{loading ? <CircularProgress size={24} /> : "Filter"}
</Button>
</Grid>
</Grid>
<div className={clsx(classes.paper, classes.tableSize)}>
<Table aria-label="dtc table">
<TableHeaderSortable
classes={classes}
orderBy={orderBy}
order={order}
columnData={tableColumns}
onSortRequest={handleSort}
/>
<TableBody>
{(dtcData || []).map((dtc, index) => (
<TableRow key={index}>
<TableCell component="th" scope="row">
{dtc.id}
</TableCell>
<TableCell>{dtc.vin}</TableCell>
<TableCell>{dtc.ecu_name}</TableCell>
<TableCell>{dtc.dtc}</TableCell>
<TableCell>{formatDate(dtc.epoch_usec)}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={6}
count={total}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onPageChange={handleChangePageIndex}
onRowsPerPageChange={handleChangePageSize}
/>
</TableRow>
</TableFooter>
</Table>
</div>
</div>
);
};
const DTCTimeline = (props) => (
<DTCTimelineProvider>
<MainForm {...props} />
</DTCTimelineProvider>
);
export default DTCTimeline;

View File

@@ -0,0 +1,21 @@
import { Typography } from "@material-ui/core";
import clsx from "clsx";
import React from "react";
import { useParams } from "react-router";
import useStyles from "../useStyles";
import DTCTimeline from "./DTCTimeline";
const SelfServeTab = () => {
const { vin } = useParams();
const classes = useStyles();
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Typography variant="h6">DTC Timeline</Typography>
<DTCTimeline id={vin} classes={classes} />
</div >
);
};
export default SelfServeTab;

View File

@@ -0,0 +1,32 @@
import {
addQueryParams,
errorHandler,
fetchRespHandler,
getAuthHeaderOptions,
} from "../utils/http";
const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL;
const DTCTimelineAPI = {
getDTCData: async (vin, ecu, startDate, endDate,search,token) => {
const queryParams = {
ecu,
start_time: startDate,
end_time: endDate,
...search,
};
const url = addQueryParams(`${API_ENDPOINT}/ecus/${vin}`, queryParams);
console.log(url)
return fetch(url, {
method: "GET",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
})
.then(fetchRespHandler)
.catch(errorHandler);
},
};
export default DTCTimelineAPI;