diff --git a/package-lock.json b/package-lock.json
index 5dd6166..aa4c44c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@emotion/styled": "^11.8.1",
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
+ "@material-ui/lab": "^4.0.0-alpha.61",
"@material-ui/pickers": "^3.3.10",
"@mui/material": "^5.10.14",
"@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": {
"version": "3.3.10",
"resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.10.tgz",
@@ -20962,6 +20990,18 @@
"@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": {
"version": "3.3.10",
"resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.10.tgz",
diff --git a/package.json b/package.json
index f90fa4c..04f2a47 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"@emotion/styled": "^11.8.1",
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
+ "@material-ui/lab": "^4.0.0-alpha.61",
"@material-ui/pickers": "^3.3.10",
"@mui/material": "^5.10.14",
"@superset-ui/embedded-sdk": "^0.1.0-alpha.8",
diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap
index 2f67ceb..ebabefe 100644
--- a/src/components/App/__snapshots__/App.test.js.snap
+++ b/src/components/App/__snapshots__/App.test.js.snap
@@ -11229,6 +11229,24 @@ exports[`App Route /vehicle-status authenticated 1`] = `
class="MuiTouchRipple-root"
/>
+
+
diff --git a/src/components/Cars/Status/__snapshots__/index.test.jsx.snap b/src/components/Cars/Status/__snapshots__/index.test.jsx.snap
index 2036b7d..9caaa1f 100644
--- a/src/components/Cars/Status/__snapshots__/index.test.jsx.snap
+++ b/src/components/Cars/Status/__snapshots__/index.test.jsx.snap
@@ -213,6 +213,24 @@ exports[`CarStatus Render 1`] = `
class="MuiTouchRipple-root"
/>
+
+
diff --git a/src/components/Cars/Status/index.jsx b/src/components/Cars/Status/index.jsx
index 98f223a..1cde8dd 100644
--- a/src/components/Cars/Status/index.jsx
+++ b/src/components/Cars/Status/index.jsx
@@ -19,6 +19,7 @@ import ECUsTab from "./ECUsTab";
import FleetsTab from "./FleetsTab";
import RemoteCommandsTab from "./RemoteCommandsTab";
import TRexLogsTab from "./TRexLogsTab";
+import DTCTimeline from "../../DTCTimeline/DTCTimeline";
const tabHashes = ["details", "updates", "filters"];
@@ -72,7 +73,13 @@ const TabViews = [
label: "CAN Signal Export",
component: SelfServeTab,
rolesPerProvider: Permissions.FiskerRead,
+ },
+ {
+ label: "DTC Timeline",
+ component: DTCTimeline,
+ rolesPerProvider: Permissions.FiskerMagnaRead,
}
+
];
const filterTabs = (data, groups, providers) => {
diff --git a/src/components/Contexts/DTCTimelineContext.jsx b/src/components/Contexts/DTCTimelineContext.jsx
new file mode 100644
index 0000000..9649182
--- /dev/null
+++ b/src/components/Contexts/DTCTimelineContext.jsx
@@ -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 (
+
+ {children}
+
+ );
+};
+
+export const useDTCTimelineContext = () => useContext(DTCTimelineContext);
diff --git a/src/components/DTCTimeline/DTCTimeline/index.jsx b/src/components/DTCTimeline/DTCTimeline/index.jsx
new file mode 100644
index 0000000..2af0b2b
--- /dev/null
+++ b/src/components/DTCTimeline/DTCTimeline/index.jsx
@@ -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 (
+
+
+
+
+ {
+ setSelectedECU(searchValue);
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(dtcData || []).map((dtc, index) => (
+
+
+ {dtc.id}
+
+ {dtc.vin}
+ {dtc.ecu_name}
+ {dtc.dtc}
+ {formatDate(dtc.epoch_usec)}
+
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+const DTCTimeline = (props) => (
+
+
+
+);
+
+export default DTCTimeline;
\ No newline at end of file
diff --git a/src/components/DTCTimeline/DTCTimelineTab.jsx b/src/components/DTCTimeline/DTCTimelineTab.jsx
new file mode 100644
index 0000000..e8d7352
--- /dev/null
+++ b/src/components/DTCTimeline/DTCTimelineTab.jsx
@@ -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 (
+
+ DTC Timeline
+
+
+ );
+};
+
+export default SelfServeTab;
\ No newline at end of file
diff --git a/src/services/DTCTimelineAPI.js b/src/services/DTCTimelineAPI.js
new file mode 100644
index 0000000..6468b2d
--- /dev/null
+++ b/src/services/DTCTimelineAPI.js
@@ -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;
\ No newline at end of file