diff --git a/package-lock.json b/package-lock.json index a598d52..5dd6166 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,8 @@ "date-fns": "^2.29.2", "email-validator": "^2.0.4", "env-cmd": "^10.1.0", + "file-saver": "^2.0.5", + "filesaver": "^0.0.13", "jwt-decode": "^3.1.2", "leaflet": "^1.8.0", "material-ui-dropzone": "^3.5.0", @@ -6941,9 +6943,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001387", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001387.tgz", - "integrity": "sha512-fKDH0F1KOJvR+mWSOvhj8lVRr/Q/mc5u5nabU2vi1/sgvlSqEsE8dOq0Hy/BqVbDkCYQPRRHB1WRjW6PGB/7PA==", + "version": "1.0.30001458", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz", + "integrity": "sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w==", "funding": [ { "type": "opencollective", @@ -9424,6 +9426,11 @@ "webpack": "^4.0.0 || ^5.0.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "node_modules/file-selector": { "version": "0.1.19", "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.19.tgz", @@ -9443,6 +9450,15 @@ "minimatch": "^3.0.4" } }, + "node_modules/filesaver": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/filesaver/-/filesaver-0.0.13.tgz", + "integrity": "sha512-ay2iShYJKmzKRPk89cgb14foqtCXcJIe5i+qdlSPAouKfBv7F2VZ0lxk9GjpcODe9p2YrXfi3Q+4CRn7ZDmleQ==", + "dependencies": { + "mkdirp": "^0.5.0", + "safename": "0.0.4" + } + }, "node_modules/filesize": { "version": "8.0.7", "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", @@ -15612,6 +15628,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safename": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/safename/-/safename-0.0.4.tgz", + "integrity": "sha512-+n4TsvESZKTXbHxOTSyQ0Q1JCXRb6MohgrqC2fbdALzTNQP/IhPOnCNRA4JPtagQq+6DD5ZsQ3lKMy57BYvwJA==" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -22881,9 +22902,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001387", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001387.tgz", - "integrity": "sha512-fKDH0F1KOJvR+mWSOvhj8lVRr/Q/mc5u5nabU2vi1/sgvlSqEsE8dOq0Hy/BqVbDkCYQPRRHB1WRjW6PGB/7PA==" + "version": "1.0.30001458", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz", + "integrity": "sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w==" }, "case-sensitive-paths-webpack-plugin": { "version": "2.4.0", @@ -24710,6 +24731,11 @@ "schema-utils": "^3.0.0" } }, + "file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "file-selector": { "version": "0.1.19", "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.19.tgz", @@ -24726,6 +24752,15 @@ "minimatch": "^3.0.4" } }, + "filesaver": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/filesaver/-/filesaver-0.0.13.tgz", + "integrity": "sha512-ay2iShYJKmzKRPk89cgb14foqtCXcJIe5i+qdlSPAouKfBv7F2VZ0lxk9GjpcODe9p2YrXfi3Q+4CRn7ZDmleQ==", + "requires": { + "mkdirp": "^0.5.0", + "safename": "0.0.4" + } + }, "filesize": { "version": "8.0.7", "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", @@ -29080,6 +29115,11 @@ "is-regex": "^1.1.4" } }, + "safename": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/safename/-/safename-0.0.4.tgz", + "integrity": "sha512-+n4TsvESZKTXbHxOTSyQ0Q1JCXRb6MohgrqC2fbdALzTNQP/IhPOnCNRA4JPtagQq+6DD5ZsQ3lKMy57BYvwJA==" + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", diff --git a/package.json b/package.json index 68a020d..f90fa4c 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "date-fns": "^2.29.2", "email-validator": "^2.0.4", "env-cmd": "^10.1.0", + "file-saver": "^2.0.5", + "filesaver": "^0.0.13", "jwt-decode": "^3.1.2", "leaflet": "^1.8.0", "material-ui-dropzone": "^3.5.0", diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap index 8878484..7ec5bb8 100644 --- a/src/components/App/__snapshots__/App.test.js.snap +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -9851,7 +9851,7 @@ exports[`App Route /vehicle-status authenticated 1`] = ` - ECUs + T.Rex logs - Remote Commands + ECUs + + Remote Commands + + + + + +
- ECUs + T.Rex logs - Remote Commands + ECUs + + Remote Commands + + + + +
+ diff --git a/src/components/Cars/Status/index.jsx b/src/components/Cars/Status/index.jsx index 1ea797f..ffe3802 100644 --- a/src/components/Cars/Status/index.jsx +++ b/src/components/Cars/Status/index.jsx @@ -5,6 +5,7 @@ import { useParams } from "react-router"; import { useLocation } from "react-router-dom"; import { hasRole, Permissions } from "../../../utils/roles"; +import SelfServeTab from "../../CANSelfServe/SelfServeTab"; import { useStatusContext } from "../../Contexts/StatusContext"; import { useUserContext } from "../../Contexts/UserContext"; import TabPanel from "../../Controls/TabPanel"; @@ -17,6 +18,7 @@ import DigitalTwinTab from "./DigitalTwinTab"; import ECUsTab from "./ECUsTab"; import FleetsTab from "./FleetsTab"; import RemoteCommandsTab from "./RemoteCommandsTab"; +import TRexLogsTab from "./TRexLogsTab"; const tabHashes = ["details", "updates", "filters"]; @@ -42,6 +44,10 @@ const TabViews = [ label: "CAN Signals", component: CANSignalsTab, }, + { + label: "T.Rex logs", + component: TRexLogsTab, + }, { label: "ECUs", component: ECUsTab, @@ -55,6 +61,10 @@ const TabViews = [ component: FleetsTab, rolesPerProvider: Permissions.FiskerRead, }, + { + label: "CAN Signal Export", + component: SelfServeTab + } ]; const filterTabs = (data, groups, providers) => { diff --git a/src/components/Cars/Status/index.test.jsx b/src/components/Cars/Status/index.test.jsx index 21a32d6..739a284 100644 --- a/src/components/Cars/Status/index.test.jsx +++ b/src/components/Cars/Status/index.test.jsx @@ -6,15 +6,15 @@ jest.mock("@material-ui/core/utils/unstable_useId", () => ); import { render, waitFor } from "@testing-library/react"; -import { BrowserRouter } from "react-router-dom"; import routeData from "react-router"; +import { BrowserRouter } from "react-router-dom"; +import addSnapshotSerializer from "../../../utils/snapshot"; +import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing"; import { CANFiltersProvider } from "../../Contexts/CANFiltersContext"; import { StatusProvider } from "../../Contexts/StatusContext"; -import { UserProvider, setToken } from "../../Contexts/UserContext"; -import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing"; +import { setToken, UserProvider } from "../../Contexts/UserContext"; import CarStatus from "./index"; -import addSnapshotSerializer from "../../../utils/snapshot"; const renderCarStatus = async () => { const { container } = render( diff --git a/src/components/Contexts/CANFiltersContext.jsx b/src/components/Contexts/CANFiltersContext.jsx index b751827..4b2870d 100644 --- a/src/components/Contexts/CANFiltersContext.jsx +++ b/src/components/Contexts/CANFiltersContext.jsx @@ -1,6 +1,6 @@ import React, { useContext, useState } from "react"; import api from "../../services/CANFiltersAPI"; -import { validateCANID, validateFilter } from "../../utils/validationSupplier"; +import { validateCANID, validateFilter, validateVIN } from "../../utils/validationSupplier"; const CANFiltersContext = React.createContext(); @@ -100,10 +100,4 @@ export const CANFiltersProvider = ({ children }) => { ); }; -const validateVIN = (vin) => { - if (vin == null || vin.length !== 17) { - throw new Error("Invalid VIN"); - } -}; - export const useCANFiltersContext = () => useContext(CANFiltersContext); diff --git a/src/components/Contexts/CANSignalsExportContext.jsx b/src/components/Contexts/CANSignalsExportContext.jsx new file mode 100644 index 0000000..175c851 --- /dev/null +++ b/src/components/Contexts/CANSignalsExportContext.jsx @@ -0,0 +1,73 @@ +import React, { useContext, useState } from "react"; +import api from "../../services/CANSignalAPI"; +import { validateVIN } from "../../utils/validationSupplier"; + +const CANSignalsExportContext = React.createContext(); + +export const CANSignalsExportProvider = ({ children }) => { + const [busy, setBusy] = useState(false); + + const [canSignals, setCanSignals] = useState([]); + + const getCANSignalList = async (token) => { + try { + setBusy(true); + const result = await api.getCanSignalList(token); + if (result.error) { + throw new Error(`Get can signal list error. ${result.message}`); + } + setCanSignals(result.data ?? []); + return result; + } finally { + setBusy(false); + } + }; + + const getDynamicColumnCANSignals = async (vin, timestart, timeend, cansingals, token) => { + try { + setBusy(true) + if (!vin) return; + + validateVIN(vin); + if (timestart > timeend) throw new Error("Start time cannot be after end time"); + const result = await api.getCanSignalsVin(vin, timestart, timeend, cansingals, token); + if (result.error || !result.ok) + throw new Error(`Get CAN signals error. ${result.message}`); + + const blob = await result.blob(); + const reader = new FileReader(); + reader.onload = () => { + const csvData = reader.result; + const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8' }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'CAN_signals.csv'; + link.click(); + }; + reader.readAsText(blob); + } catch (e) { + throw new Error(e) + } + finally { + setBusy(false) + } + } + + return ( + + {children} + + ); +}; + +export const useCANSignalsExportContext = () => useContext(CANSignalsExportContext); + + + diff --git a/src/components/Contexts/VehicleContext.jsx b/src/components/Contexts/VehicleContext.jsx index 412c64e..31d0eb6 100644 --- a/src/components/Contexts/VehicleContext.jsx +++ b/src/components/Contexts/VehicleContext.jsx @@ -1,6 +1,8 @@ import React, { useContext, useState } from "react"; + import { logger } from "../../services/monitoring"; import api from "../../services/vehiclesAPI"; +import { validateVIN } from "../../utils/validationSupplier"; const VehicleContext = React.createContext(); @@ -285,10 +287,4 @@ const validateVehicle = (v) => { validateVIN(v.vin); }; -const validateVIN = (vin) => { - if (vin == null || vin.length !== 17) { - throw new Error("Invalid VIN"); - } -}; - export const useVehicleContext = () => useContext(VehicleContext); diff --git a/src/components/Contexts/__mocks__/CANSignalsExportContext.jsx b/src/components/Contexts/__mocks__/CANSignalsExportContext.jsx new file mode 100644 index 0000000..a69a596 --- /dev/null +++ b/src/components/Contexts/__mocks__/CANSignalsExportContext.jsx @@ -0,0 +1,23 @@ +let busy = false; +let canSignals = [ + { + signal_name: "123", + }, + { + signal_name: "456", + }, + { + signal_name: "789", + }, +]; + +export const CANSignalsExportProvider = ({ children }) => { + return
{children}
; +}; + +export const useCANSignalsExportContext= () => ({ + busy, + canSignals, + getCANSignalList: jest.fn(), + getDynamicColumnCANSignals: jest.fn(), +}); diff --git a/src/components/Controls/TRexLogs/index.jsx b/src/components/Controls/TRexLogs/index.jsx new file mode 100644 index 0000000..165c4ed --- /dev/null +++ b/src/components/Controls/TRexLogs/index.jsx @@ -0,0 +1,329 @@ +import React, { useEffect, useState } from "react"; + +import { + Table, + TableBody, + TableCell, + TableFooter, + TablePagination, + TableRow +} from "@material-ui/core"; +import Checkbox from '@mui/material/Checkbox'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormGroup from '@mui/material/FormGroup'; +import FormLabel from '@mui/material/FormLabel'; +import LinearProgress from '@mui/material/LinearProgress'; + +import { + KeyboardDatePicker, MuiPickersUtilsProvider +} from '@material-ui/pickers'; +import clsx from "clsx"; +import api from "../../../services/vehiclesAPI"; + +import DateFnsUtils from '@date-io/date-fns'; +import { TableHead } from "@mui/material"; +import { logger } from "../../../services/monitoring"; +import { useStatusContext } from "../../Contexts/StatusContext"; + +const tableColumns = [ + { + id: "level", + label: "Level", + width: "5%", + }, + { + id: "trex_timestamp", + label: "T.Rex Timestamp", + width: "10%", + }, + { + id: "cloud_timestamp", + label: "Cloud Timestamp", + width: "10%", + }, + { + id: "line_number", + label: "Line Number", + width: "5%", + }, + { + id: "filename", + label: "Filename", + width: "10%", + }, + { + id: "msg", + label: "Message", + width: "60%", + }, +]; + +const logLevelCheckBoxes = [ + { + "id": "trace", + "label": "Trace" + }, + { + "id": "debug", + "label": "Debug" + }, + { + "id": "info", + "label": "Info" + }, + { + "id": "warning", + "label": "Warning" + }, + { + "id": "error", + "label": "Error" + }, + { + "id": "critical", + "label": "Critical" + }, +] + +const transformLogs = (logs) => + logs.map((log) => { + const { level, timestamp, received_timestamp, line_number, filename, msg } = log; + //const trex_time = new Date(timestamp) + //const cloud_time = new Date(received_timestamp) + return { + level: level, + trex_timestamp: timestamp, + cloud_timestamp: received_timestamp, + line_number: line_number, + filename: filename, + msg: msg + }; + }) + .flat() + +const fromatDateForRequest = (date) => { + const getYear = date.toLocaleString("default", { year: "numeric" }); + const getMonth = date.toLocaleString("default", { month: "2-digit" }); + const getDay = date.toLocaleString("default", { day: "2-digit" }); + return getYear + "-" + getMonth + "-" + getDay +}; + +//read at least 50000 bytes per one API request +const ONE_READ_SIZE = 50000 +const DEFAULT_PAGE_SIZE = 25 + +const TRexLogsTable = ({ vin, token, classes }) => { + const [allLogsFetched, setAllLogsFetched] = useState(false) + const [blobSize, setBlobSize] = useState(0) + const [currentOffset, setCurrentOffset] = useState(0) + const [currectLogLevels, setCurrentLogLevels] = useState({ + trace: true, + debug: true, + info: true, + warning: true, + error: true, + critical: true + }) + const [logs, setLogs] = useState([]); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); + const [selectedDate, setSelectedDate] = useState(new Date()); + const [total, setTotal] = useState(0); + const { setMessage } = useStatusContext(); + + let controller = new AbortController() + const readBlob = async (offset, count) => { + return await api.getTRexLogs(vin, fromatDateForRequest(selectedDate), offset, count, "UP", token, controller) + } + const getDesiredSize = () => { + return pageSize * pageIndex + pageSize + } + const getReadPercentage = () => { + return (currentOffset * 100 / blobSize).toFixed(2); + } + const getFilteredLogs = (logs) => { + return logs.filter(log => currectLogLevels[log.level] === true) + } + const fetchAllLogs = async () => { + let fetched = [] + let offset = currentOffset + let readSize = ONE_READ_SIZE + for (; ;) { + const result = await readBlob(offset, readSize) + if (result.error) { + fetched.error = result.error + break + } + setBlobSize(result.blobSize) + readSize *= 2 + offset = result.RealOffset + result.bytesRead + setCurrentOffset(offset) + fetched = transformLogs(result.data).concat(fetched) + setLogs(fetched) + if (offset >= result.blobSize) { + setMessage(`All log for ${fromatDateForRequest(selectedDate)} fetched`) + setAllLogsFetched(true) + break + } + } + + if (fetched.length === 0) { + if (logs.length !== 0) { + setMessage(`No more T.Rex logs for ${fromatDateForRequest(selectedDate)}`) + return + } + setTotal(0) + const msg = `Cannot fetch logs for ${fromatDateForRequest(selectedDate)}` + setMessage(msg) + return + } + setCurrentOffset(offset) + return fetched + } + + useEffect(() => { + (async () => { + try { + if (!vin || !token) return; + const desiredSize = getDesiredSize() + if (desiredSize < logs.length || allLogsFetched) { + setTotal(getFilteredLogs(logs).length) + return + } + let fetched = await fetchAllLogs() + if (!fetched || fetched.length === 0) { + return + } + setTotal(getFilteredLogs(fetched).length); + } catch (e) { + setMessage(e.message); + logger.warn(e.stack); + } + })(); + return () => controller?.abort() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [vin, token, pageIndex, pageSize, selectedDate, currectLogLevels]); + + const handleChangePageSize = (event) => { + setPageSize(parseInt(event.target.value, 10)); + setTotal(getFilteredLogs(logs).length) + setPageIndex(0); + }; + + const handleNewDate = (newValue) => { + setPageIndex(0); + setCurrentOffset(0) + setLogs([]) + setAllLogsFetched(false) + setBlobSize(0) + setSelectedDate(newValue) + }; + + const handleNewFilter = (event) => { + setPageIndex(0) + setCurrentLogLevels({ + ...currectLogLevels, + [event.target.defaultValue]: event.target.checked, + }); + }; + return ( +
+ + + + + Log levels + + {logLevelCheckBoxes.map((box) => ( + } + label={box.label} + labelPlacement="bottom" + onChange={handleNewFilter} + /> + ))} + + + + + + + + + + + { + blobSize === 0 ? `No logs for ${fromatDateForRequest(selectedDate)}` : + `Read ${getReadPercentage()}% of logs` + } + { + + } + + +
+ + + + + {tableColumns.map((column) => ( + {column.label} + ))} + + + + {getFilteredLogs(logs).slice(-getDesiredSize(), (pageIndex === 0 ? undefined : -(pageSize * pageIndex))).map((log, i) => ( + + {log.level} + {log.trex_timestamp} + {log.cloud_timestamp} + {log.line_number} + {log.filename} + {log.msg} + + ))} + + + + setPageIndex(newValue)} + onRowsPerPageChange={handleChangePageSize} + /> + + +
+
+ ); +}; + +export default TRexLogsTable; diff --git a/src/components/DigitalTwin/index.js b/src/components/DigitalTwin/index.js index d581c9b..24c9ee6 100644 --- a/src/components/DigitalTwin/index.js +++ b/src/components/DigitalTwin/index.js @@ -13,6 +13,14 @@ const openCloseState = (value) => (value ? "open" : "closed"); const mapOpenCloseState = (value) => keyValueTemplate(value[0], openCloseState(value[1])); +const windowState = (value) => { + if (value[1] === 0 || value[1] > 100) { + return keyValueTemplate(value[0], `closed (${value[1]})`); + } else { + return keyValueTemplate(value[0], `${value[1]}% open`); + } +} + const DigitalTwin = (props) => { const classes = useStyles(); const { battery, doors, location, trex_version, ip, updated, windows, misc_windows, sunroof, dbc_version, door_locks } = props; @@ -45,12 +53,7 @@ const DigitalTwin = (props) => {

Windows

{Object.entries(windows).map((value) => { - if (value[1] === 0) { - return keyValueTemplate(value[0], "closed"); - } else { - const percentOpen = Math.min(value[1], 100); - return keyValueTemplate(value[0], `${percentOpen}% open`); - } + return windowState(value); })}
)} @@ -58,11 +61,7 @@ const DigitalTwin = (props) => {

Misc Windows

{Object.entries(misc_windows).map((value) => { - if (value[1] === 0 || value[1] > 100) { - return keyValueTemplate(value[0], `closed ${value[1]}%`); - } else { - return keyValueTemplate(value[0], `${value[1]}% open`); - } + return windowState(value); })}
)} @@ -70,12 +69,7 @@ const DigitalTwin = (props) => {

Sunroof

{Object.entries(sunroof).map((value) => { - if (value[1] === 0) { - return keyValueTemplate(value[0], "closed"); - } else { - const percentOpen = Math.min(value[1], 100); - return keyValueTemplate(value[0], `${percentOpen}% open`); - } + return windowState(value); })}
)} diff --git a/src/components/SMS/Send/__snapshots__/index.test.jsx.snap b/src/components/SMS/Send/__snapshots__/index.test.jsx.snap index d03c0ae..dbbf31c 100644 --- a/src/components/SMS/Send/__snapshots__/index.test.jsx.snap +++ b/src/components/SMS/Send/__snapshots__/index.test.jsx.snap @@ -1,3 +1,168 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`SMS Send Component Render 1`] = `undefined`; +exports[`SMS Send Component Render 1`] = ` +
+
+
+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ + +
+
+
+
+
+`; diff --git a/src/components/SMS/Send/index.jsx b/src/components/SMS/Send/index.jsx index 0d4005b..0abe10a 100644 --- a/src/components/SMS/Send/index.jsx +++ b/src/components/SMS/Send/index.jsx @@ -1,9 +1,9 @@ import React, { useEffect, useState } from "react"; -import { useSMSContext, SMSProvider } from "../../Contexts/SMSContext"; +import { logger } from "../../../services/monitoring"; +import { SMSProvider, useSMSContext } from "../../Contexts/SMSContext"; import { useStatusContext } from "../../Contexts/StatusContext"; import { useUserContext } from "../../Contexts/UserContext"; -import { logger } from "../../../services/monitoring"; import SendForm from "./SendForm"; import ViewResult from "./ViewResult"; diff --git a/src/components/SMS/Send/index.test.jsx b/src/components/SMS/Send/index.test.jsx index 29ce3dd..3e9b5b8 100644 --- a/src/components/SMS/Send/index.test.jsx +++ b/src/components/SMS/Send/index.test.jsx @@ -1,15 +1,15 @@ -import {TEST_AUTH_OBJECT_FISKER, TEST_TOKEN_FISKER} from "../../../utils/testing"; +import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing"; jest.mock("../../Contexts/SMSContext"); jest.mock("../../Contexts/UserContext"); -import SMSSend from "./index"; -import {BrowserRouter} from "react-router-dom"; -import {UserProvider, setToken} from "../../Contexts/UserContext"; -import {StatusProvider} from "../../Contexts/StatusContext"; +import { render, waitFor } from "@testing-library/react"; +import { BrowserRouter } from "react-router-dom"; import addSnapshotSerializer from "../../../utils/snapshot"; -import {render, waitFor} from "@testing-library/react"; +import { StatusProvider } from "../../Contexts/StatusContext"; +import { setToken, UserProvider } from "../../Contexts/UserContext"; +import SMSSend from "./index"; const renderSendSMS = async () => { @@ -38,7 +38,7 @@ describe("SMS Send Component", () => { it("Render", async () => { // setToken(TEST_AUTH_OBJECT_FISKER); - const {container} = await renderSendSMS(); + const container = await renderSendSMS(); expect(container).toMatchSnapshot(); }); }); \ No newline at end of file diff --git a/src/components/useStyles.jsx b/src/components/useStyles.jsx index 3b3a752..1931093 100644 --- a/src/components/useStyles.jsx +++ b/src/components/useStyles.jsx @@ -285,6 +285,10 @@ const useStyles = makeStyles((theme) => ({ color: "blue", textDecoration: "underline", }, + tableHeader: { + textDecorationStyle: "solid", + fontWeight:500, + } })); export default useStyles; diff --git a/src/services/CANSignalAPI.js b/src/services/CANSignalAPI.js new file mode 100644 index 0000000..6035dab --- /dev/null +++ b/src/services/CANSignalAPI.js @@ -0,0 +1,32 @@ +import { + addQueryParams, errorHandler, fetchRespHandler, getAuthHeaderOptions +} from "../utils/http"; + +const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL; + +const canSignalAPI = { + + getCanSignalsVin: async (vin, timestamp_start, timestamp_end, can_signals, token) => + fetch(addQueryParams(`${API_ENDPOINT}/can_signals_export`, { vin, timestamp_start, timestamp_end, can_signals }), { + method: "GET", + headers: Object.assign( + { "Content-Type": "application/json" }, + getAuthHeaderOptions(token) + ), + responseType: "blob" + }) + .catch(errorHandler), + + + getCanSignalList: async (token) => + fetch(addQueryParams(`${API_ENDPOINT}/can_signals_list`), { + method: "GET", + headers: Object.assign( + { "Content-Type": "application/json" }, + getAuthHeaderOptions(token) + ), + }) + .then(fetchRespHandler) + .catch(errorHandler), +}; +export default canSignalAPI; diff --git a/src/services/__mocks__/CANSignalAPI.js b/src/services/__mocks__/CANSignalAPI.js new file mode 100644 index 0000000..e088c01 --- /dev/null +++ b/src/services/__mocks__/CANSignalAPI.js @@ -0,0 +1,16 @@ +const canSignalList = [ + { signal_name: "123"}, + { signal_name: "456"}, + { signal_name: "789"} +]; + +const canSignalAPI = { + getCanSignalsVin: async (vin, timestamp_start, timestamp_end, can_signals, token) => { + return { data: "fake data" }; + }, + getCanSignalList: async (token) => { + return canSignalList; + } +}; + +export default canSignalAPI; \ No newline at end of file diff --git a/src/services/__mocks__/vehiclesAPI.js b/src/services/__mocks__/vehiclesAPI.js index d18a88b..168b539 100644 --- a/src/services/__mocks__/vehiclesAPI.js +++ b/src/services/__mocks__/vehiclesAPI.js @@ -58,6 +58,38 @@ const signals = {data:[ }, ]}; +const trexLogs = { + RealOffset: 0, + blobSize: 62072819, + bytesRead: 29874, + data: [ + { + "level": "info", + "timestamp": "2023-Feb-15 23:44:36.934746", + "line_number": 948, + "filename": "ota_service.cpp", + "msg": "ota::cleanup() Deleting directory \"/fisker/ota/1534\"", + "received_timestamp": "2023-Feb-15 23:44:37.203102197" + }, + { + "level": "debug", + "timestamp": "2023-Feb-15 23:44:36.970715", + "line_number": 992, + "filename": "ota_service.cpp", + "msg": "ota::end() manifest process aborted with error state", + "received_timestamp": "2023-Feb-15 23:44:37.203197597" + }, + { + "level": "debug", + "timestamp": "2023-Feb-15 23:44:36.971745", + "line_number": 91, + "filename": "vom_impl.cpp", + "msg": "vom::send_ota_result_fb() Sending 0x4F9->tbox_ota_res_fb (==OTA_error)", + "received_timestamp": "2023-Feb-15 23:44:37.203249997" + } + ] +} + const vehiclesAPI = { addVehicle: async (vehicle) => { data.push(vehicle); @@ -117,6 +149,9 @@ const vehiclesAPI = { getCANSignals: async (vin, vehicle) => { return signals; }, + getTRexLogs: async (vin, date, offset, count, direction, token) => { + return trexLogs; + }, getVersionLog: async (vin) => ({ "data": [ { diff --git a/src/services/vehiclesAPI.js b/src/services/vehiclesAPI.js index 9d8d0db..8d38faa 100644 --- a/src/services/vehiclesAPI.js +++ b/src/services/vehiclesAPI.js @@ -173,6 +173,18 @@ const vehiclesAPI = { .then(fetchRespHandler) .catch(errorHandler), + getTRexLogs: async (vin, date, offset, count, direction, token, controller) => + fetch(`${API_ENDPOINT}/vehicle/${vin}/trex-logs?date=${date}&offset=${offset}&count=${count}&direction=${direction}`, { + method: "GET", + headers: Object.assign( + { "Content-Type": "application/json" }, + getAuthHeaderOptions(token) + ), + signal: controller.signal + }) + .then(fetchRespHandler) + .catch(errorHandler), + getVersionLog: async ({vin, ...search}, token) => { const u = addQueryParams(`${API_ENDPOINT}/vehicle/${vin}/version/logs`, search); return fetch(u, { diff --git a/src/utils/http.js b/src/utils/http.js index 2d8b69a..5709cb4 100644 --- a/src/utils/http.js +++ b/src/utils/http.js @@ -6,7 +6,6 @@ export const addQueryParams = (url, params) => { const u = new URL(url); Object.keys(params).forEach((key) => u.searchParams.append(key, params[key])); - return u.toString(); }; diff --git a/src/utils/locations.js b/src/utils/locations.js index a308375..f4fdaba 100644 --- a/src/utils/locations.js +++ b/src/utils/locations.js @@ -1,3 +1,5 @@ +const invalidLocation = 99999; + export const ValidateLocationData = (location) => { if (Math.abs(location.latitude) > 90 || Math.abs(location.longitude) > 180) { return false; @@ -9,6 +11,7 @@ export const ValidateLocationData = (location) => { } export const ValidateLocationByParam = (parameter, value) => { + if (invalidLocation === value) return false; switch (parameter) { case "latitude": return Math.abs(value) <= 90;