CEC-3577: fetch T.Rex log from the cloud (#283)

* CEC-3577: fetch T.Rex log from the cloud

* tabs?

* CSS

* smells

* fix smells and warnings

* suggestions
This commit is contained in:
Eduard Voronkin
2023-02-17 14:53:36 -08:00
committed by GitHub
parent f2aa5d00fb
commit 749f1672da
7 changed files with 397 additions and 4 deletions

View File

@@ -9851,7 +9851,7 @@ exports[`App Route /vehicle-status authenticated 1`] = `
<span
class="MuiTab-wrapper"
>
ECUs
T.Rex logs
</span>
<span
class="MuiTouchRipple-root"
@@ -9869,7 +9869,7 @@ exports[`App Route /vehicle-status authenticated 1`] = `
<span
class="MuiTab-wrapper"
>
Remote Commands
ECUs
</span>
<span
class="MuiTouchRipple-root"
@@ -9883,6 +9883,24 @@ exports[`App Route /vehicle-status authenticated 1`] = `
role="tab"
tabindex="-1"
type="button"
>
<span
class="MuiTab-wrapper"
>
Remote Commands
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
<button
aria-controls="tabpanel-8"
aria-selected="false"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit"
id="tab-8"
role="tab"
tabindex="-1"
type="button"
>
<span
class="MuiTab-wrapper"
@@ -10124,6 +10142,12 @@ exports[`App Route /vehicle-status authenticated 1`] = `
id="tabpanel-7"
role="tabpanel"
/>
<div
aria-labelledby="tab-8"
hidden=""
id="tabpanel-8"
role="tabpanel"
/>
</div>
</main>
</main>

View File

@@ -0,0 +1,36 @@
import { Typography } from "@material-ui/core";
import clsx from "clsx";
import React from "react";
import { useParams } from "react-router";
import { useUserContext } from "../../Contexts/UserContext";
import { VehicleProvider } from "../../Contexts/VehicleContext";
import TRexLogsTable from "../../Controls/TRexLogs";
import useStyles from "../../useStyles";
const MainForm = () => {
const { vin } = useParams();
const classes = useStyles();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Typography variant="h6" className={classes.labelInline}>
T.Rex Logs
</Typography>
<TRexLogsTable vin={vin} token={token} classes={classes} />
</div>
);
};
const TRexLogsTab = () => (
<VehicleProvider>
<MainForm />
</VehicleProvider>
);
export default TRexLogsTab;

View File

@@ -135,7 +135,7 @@ exports[`CarStatus Render 1`] = `
<span
class="MuiTab-wrapper"
>
ECUs
T.Rex logs
</span>
<span
class="MuiTouchRipple-root"
@@ -153,7 +153,7 @@ exports[`CarStatus Render 1`] = `
<span
class="MuiTab-wrapper"
>
Remote Commands
ECUs
</span>
<span
class="MuiTouchRipple-root"
@@ -167,6 +167,24 @@ exports[`CarStatus Render 1`] = `
role="tab"
tabindex="-1"
type="button"
>
<span
class="MuiTab-wrapper"
>
Remote Commands
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
<button
aria-controls="tabpanel-8"
aria-selected="false"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit"
id="tab-8"
role="tab"
tabindex="-1"
type="button"
>
<span
class="MuiTab-wrapper"
@@ -352,6 +370,12 @@ exports[`CarStatus Render 1`] = `
id="tabpanel-7"
role="tabpanel"
/>
<div
aria-labelledby="tab-8"
hidden=""
id="tabpanel-8"
role="tabpanel"
/>
</div>
</div>
</div>

View File

@@ -17,6 +17,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 +43,10 @@ const TabViews = [
label: "CAN Signals",
component: CANSignalsTab,
},
{
label: "T.Rex logs",
component: TRexLogsTab,
},
{
label: "ECUs",
component: ECUsTab,

View File

@@ -0,0 +1,258 @@
import React, { useEffect, useState } from "react";
import api from "../../../services/vehiclesAPI";
import {
Table,
TableBody,
TableCell,
TableFooter,
TablePagination,
TableRow,
} from "@material-ui/core";
import {
MuiPickersUtilsProvider,
KeyboardDatePicker,
} from '@material-ui/pickers';
import clsx from "clsx";
import { useStatusContext } from "../../Contexts/StatusContext";
import { logger } from "../../../services/monitoring";
import { TableHead } from "@mui/material";
import DateFnsUtils from '@date-io/date-fns';
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
};
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%",
},
];
//read at least 30000 bytes per one API request
const ONE_READ_SIZE = 30000
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 [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();
const readBlob = async (offset, count) => {
console.log(`reading from offset: ${offset}`)
return await api.getTRexLogs(vin, fromatDateForRequest(selectedDate), offset, count, "UP", token)
}
const getDesiredSize = () => {
return pageSize * pageIndex + pageSize
}
const getReadPercentage = () => {
if (blobSize === 0) {
return `No logs for ${fromatDateForRequest(selectedDate)}`
}
return `Read ${(currentOffset * 100 / blobSize).toFixed(2)}% of logs`
}
const fetchLogs = async (desiredSize) => {
let fetched = []
let offset = currentOffset
let readSize = ONE_READ_SIZE
do {
const result = await readBlob(offset, readSize)
if (result.error) {
fetched.error = result.error
break
}
console.log(`ret.RealOffset ${result.RealOffset}\nret.bytesRead ${result.bytesRead}\ndesired offset ${offset}\ndesired read size ${readSize}\nblobsize: ${result.blobSize}`)
setBlobSize(result.blobSize)
//if read two times less then neccessary, then adjust read size
if (fetched.length < desiredSize / 2) {
readSize *= 2
console.log(`new read size: ${readSize}`)
}
offset = result.RealOffset + result.bytesRead
fetched = transformLogs(result.data).concat(fetched)
if (offset >= result.blobSize) {
setMessage(`All log for ${fromatDateForRequest(selectedDate)} fetched`)
setAllLogsFetched(true)
break
}
} while (fetched.length <= desiredSize)
if (fetched.length === 0) {
if (logs.length !== 0) {
setMessage(`No more T.Rex logs for ${fromatDateForRequest(selectedDate)}`)
return
}
setTotal(0)
const msg = `Can not fetch logs for ${fromatDateForRequest(selectedDate)}`
setMessage(msg)
console.log(`${msg}, Cloud error:\n${fetched.error}`);
return
}
setCurrentOffset(offset)
return fetched
}
useEffect(() => {
(async () => {
try {
if (!vin || !token) return;
const desiredSize = getDesiredSize()
console.log(`desired size: ${desiredSize}, actual size: ${logs.length}`)
if (desiredSize < logs.length || allLogsFetched) {
console.log(`enough logs in cache`)
return
}
let fetched = await fetchLogs(desiredSize)
if (!fetched || fetched.length === 0) {
return
}
fetched = fetched.concat(logs)
setLogs(fetched);
setTotal(fetched.length);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vin, token, pageIndex, pageSize, selectedDate]);
const handleChangePageSize = (event) => {
setPageSize(parseInt(event.target.value, 10));
setPageIndex(0);
};
const handleNewDate = (newValue) => {
setPageIndex(0);
setCurrentOffset(0)
setLogs([])
setAllLogsFetched(false)
setBlobSize(0)
setSelectedDate(newValue)
};
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Table
style={{ tableLayout: "fixed", minWidth: "1400px" }}
>
<TableRow>
<TableCell>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDatePicker
disableToolbar
variant="inline"
format="yyyy/MM/dd"
margin="normal"
id="date-picker-inline"
label="Choose date"
value={selectedDate}
onChange={handleNewDate}
KeyboardButtonProps={{
'aria-label': 'change date',
}}
/>
</MuiPickersUtilsProvider>
</TableCell>
<TableCell align="left">
{getReadPercentage()}
</TableCell>
</TableRow>
</Table>
<Table
style={{ tableLayout: "fixed", minWidth: "1400px" }}
>
<TableHead
classes={classes}>
<TableRow>
{tableColumns.map((column) => (
<TableCell style={{ width: column.width }} align="center" key={column.label || "none"}>{column.label}</TableCell>
))} </TableRow>
</TableHead >
<TableBody>
{logs.slice(-getDesiredSize(), (pageIndex === 0 ? undefined : -(pageSize * pageIndex))).map((log, i) => (
<TableRow key={log.trex_timestamp + log.cloud_timestamp} >
<TableCell align="center">{log.level}</TableCell>
<TableCell align="center">{log.trex_timestamp}</TableCell>
<TableCell align="center">{log.cloud_timestamp}</TableCell>
<TableCell align="center">{log.line_number}</TableCell>
<TableCell align="center" style={{ wordBreak: "break-all" }}>{log.filename}</TableCell>
<TableCell align="left" style={{ wordBreak: "break-all" }}>{log.msg}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[25, 100, 1000, 10000]}
colSpan={6}
count={total}
rowsPerPage={pageSize}
page={!pageIndex || pageIndex <= 0 ? 0 : pageIndex}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onPageChange={(_, newValue) => setPageIndex(newValue)}
onRowsPerPageChange={handleChangePageSize}
/>
</TableRow>
</TableFooter>
</Table>
</div >
);
};
export default TRexLogsTable;

View File

@@ -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": [
{

View File

@@ -173,6 +173,17 @@ const vehiclesAPI = {
.then(fetchRespHandler)
.catch(errorHandler),
getTRexLogs: async (vin, date, offset, count, direction, token) =>
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)
),
})
.then(fetchRespHandler)
.catch(errorHandler),
getVersionLog: async ({vin, ...search}, token) => {
const u = addQueryParams(`${API_ENDPOINT}/vehicle/${vin}/version/logs`, search);
return fetch(u, {