diff --git a/.github/workflows/blackduck.yml b/.github/workflows/blackduck.yml index f69b1a3..3fe8da2 100644 --- a/.github/workflows/blackduck.yml +++ b/.github/workflows/blackduck.yml @@ -1,40 +1,13 @@ name: Blackduck on: - schedule: - # run scans twice a month - - cron: "0 2 1,15 * *" + schedule: + # run scans twice a month + - cron: '0 2 1,15 * *' jobs: blackduck: - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v2 - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 - with: - node-version: "16" - cache: "npm" - - run: npm install - - run: npm run build - - # ota-admin-portal - - name: Run Synopsys Detect - ota-admin-portal - uses: synopsys-sig/detect-action@v0.3.2 - env: - DETECT_PROJECT_NAME: ota-admin-portal - DETECT_EXCLUDED_DIRECTORIES: node_modules - DETECT_PROJECT_VERSION_NAME: default - DETECT_NPM_INCLUDE_DEV_DEPENDENCIES: "FALSE" - # DETECT_DETECTOR_SEARCH_EXCLUSION_DEFAULTS: "true" - DETECT_DETECTOR_SEARCH_DEPTH: 0 - DETECT_DETECTOR_SEARCH_CONTINUE: "true" - - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - detect-version: 7.9.0 - blackduck-url: ${{ secrets.BLACKDUCK_URL }} - blackduck-api-token: ${{ secrets.BLACKDUCK_API_KEY }} - scan-mode: INTELLIGENT + name: Blackduck scan + uses: Fisker-Inc/github-actions/.github/workflows/blackduck.yml@main + with: + project: ota-admin-portal diff --git a/package-lock.json b/package-lock.json index 87ab3df..b14475f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@material-ui/icons": "^4.11.3", "@material-ui/pickers": "^3.3.10", "@mui/material": "^5.10.14", - "@superset-ui/embedded-sdk": "^0.1.0-alpha.7", + "@superset-ui/embedded-sdk": "^0.1.0-alpha.8", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^12.1.4", "@testing-library/user-event": "^13.5.0", @@ -4821,11 +4821,12 @@ } }, "node_modules/@superset-ui/embedded-sdk": { - "version": "0.1.0-alpha.7", - "resolved": "https://registry.npmjs.org/@superset-ui/embedded-sdk/-/embedded-sdk-0.1.0-alpha.7.tgz", - "integrity": "sha512-sBzfSnPvRw15D6A053t4gpKDydHaAjyn88By1Z3Vl3PWZScW6sTHh8n/5A69qMSJVorqFQ3g0IUquTF8sutGEQ==", + "version": "0.1.0-alpha.8", + "resolved": "https://registry.npmjs.org/@superset-ui/embedded-sdk/-/embedded-sdk-0.1.0-alpha.8.tgz", + "integrity": "sha512-X07s8uMbvQDEMe5GRyKXhVW5XzPm+yV4KQSo7WifWz7TdqLUT+rjNsL4jZAGAoO0/n0Fj7gFoDpT6bGlSULe5g==", "dependencies": { - "@superset-ui/switchboard": "^0.18.26-0" + "@superset-ui/switchboard": "^0.18.26-0", + "jwt-decode": "^3.1.2" } }, "node_modules/@superset-ui/switchboard": { @@ -21106,11 +21107,12 @@ } }, "@superset-ui/embedded-sdk": { - "version": "0.1.0-alpha.7", - "resolved": "https://registry.npmjs.org/@superset-ui/embedded-sdk/-/embedded-sdk-0.1.0-alpha.7.tgz", - "integrity": "sha512-sBzfSnPvRw15D6A053t4gpKDydHaAjyn88By1Z3Vl3PWZScW6sTHh8n/5A69qMSJVorqFQ3g0IUquTF8sutGEQ==", + "version": "0.1.0-alpha.8", + "resolved": "https://registry.npmjs.org/@superset-ui/embedded-sdk/-/embedded-sdk-0.1.0-alpha.8.tgz", + "integrity": "sha512-X07s8uMbvQDEMe5GRyKXhVW5XzPm+yV4KQSo7WifWz7TdqLUT+rjNsL4jZAGAoO0/n0Fj7gFoDpT6bGlSULe5g==", "requires": { - "@superset-ui/switchboard": "^0.18.26-0" + "@superset-ui/switchboard": "^0.18.26-0", + "jwt-decode": "^3.1.2" } }, "@superset-ui/switchboard": { diff --git a/package.json b/package.json index 0198a1f..f584165 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@material-ui/icons": "^4.11.3", "@material-ui/pickers": "^3.3.10", "@mui/material": "^5.10.14", - "@superset-ui/embedded-sdk": "^0.1.0-alpha.7", + "@superset-ui/embedded-sdk": "^0.1.0-alpha.8", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^12.1.4", "@testing-library/user-event": "^13.5.0", @@ -87,4 +87,4 @@ "lcov" ] } -} +} \ No newline at end of file diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap index 348551e..89e8f20 100644 --- a/src/components/App/__snapshots__/App.test.js.snap +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -1752,115 +1752,64 @@ exports[`App Route /issue-info authenticated 1`] = ` class="makeStyles-paper-0 makeStyles-tableSize-0" >
-
-
-
- -
- -
-
-
-
+ Issue Details +
-
- Issue Details -
-
-
-

- - ID - - : - FISKER123 -

-

- - VIN - - : - 1GNGC26RXXJ407648 -

-

- - Title - - : - sometitle -

-

- - Description - - : - 2343242 -

-

- - timestamp - - : - 2022-12-09T23:16:38.074858Z -

- Issue images -
-
+

+ + ID + + : + FISKER123 +

+

+ + VIN + + : + 1GNGC26RXXJ407648 +

+

+ + Title + + : + sometitle +

+

+ + Description + + : + 2343242 +

+

+ + Created + + : + 12/9/2022 11:16:38 PM +

+ Issue images
@@ -2472,29 +2421,6 @@ exports[`App Route /issues authenticated 1`] = ` - - - Description - - - sometitle - - 2343242 - @@ -2616,11 +2537,6 @@ exports[`App Route /issues authenticated 1`] = ` > sometitle - - 2343242 - @@ -2641,7 +2557,7 @@ exports[`App Route /issues authenticated 1`] = ` >
- 1-NaN of undefined + 0-0 of 0

@@ -9739,8 +9653,12 @@ exports[`App Route /vehicle-status authenticated 1`] = ` class="MuiTabs-root" >
+
- Remote Commands + ECUs + + Remote Commands + + + +
diff --git a/src/components/Cars/Status/__snapshots__/ECUsTab.test.jsx.snap b/src/components/Cars/Status/__snapshots__/ECUsTab.test.jsx.snap new file mode 100644 index 0000000..9766841 --- /dev/null +++ b/src/components/Cars/Status/__snapshots__/ECUsTab.test.jsx.snap @@ -0,0 +1,309 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ECUsTab Render 1`] = ` +
+
+
+
+
+ Car ECUs +
+
+ + + + + + + + + + + + + + + + + +
+ + ECU + + sorted descending + + + + + + SW Version + + + + + HW Version + + + + + Config + + + + + Created + + + + + Updated + + +
+
+
+
+
+
+`; diff --git a/src/components/Cars/Status/__snapshots__/index.test.jsx.snap b/src/components/Cars/Status/__snapshots__/index.test.jsx.snap index 23e1eba..e2731f0 100644 --- a/src/components/Cars/Status/__snapshots__/index.test.jsx.snap +++ b/src/components/Cars/Status/__snapshots__/index.test.jsx.snap @@ -21,8 +21,12 @@ exports[`CarStatus Render 1`] = ` class="MuiTabs-root" >
+
- Remote Commands + ECUs + + Remote Commands + + + +
diff --git a/src/components/Cars/Status/index.jsx b/src/components/Cars/Status/index.jsx index d2d8cec..1ea797f 100644 --- a/src/components/Cars/Status/index.jsx +++ b/src/components/Cars/Status/index.jsx @@ -14,6 +14,7 @@ import CANSignalsTab from "./CANSignalsTab"; import CarUpdatesTab from "./CarUpdatesTab"; import CarDetailsTab from "./DetailsTab"; import DigitalTwinTab from "./DigitalTwinTab"; +import ECUsTab from "./ECUsTab"; import FleetsTab from "./FleetsTab"; import RemoteCommandsTab from "./RemoteCommandsTab"; @@ -41,6 +42,10 @@ const TabViews = [ label: "CAN Signals", component: CANSignalsTab, }, + { + label: "ECUs", + component: ECUsTab, + }, { label: "Remote Commands", component: RemoteCommandsTab, @@ -110,6 +115,7 @@ const CarStatus = () => { value={tabIndex} onChange={handleTabChange} aria-label="car tabs" + variant="scrollable" indicatorColor="secondary"> {tabs.map((item, index) => )} diff --git a/src/components/Contexts/VehicleContext.jsx b/src/components/Contexts/VehicleContext.jsx index cf4062a..412c64e 100644 --- a/src/components/Contexts/VehicleContext.jsx +++ b/src/components/Contexts/VehicleContext.jsx @@ -233,6 +233,17 @@ export const VehicleProvider = ({ children }) => { } } + const getVersionLog = async (search, token) => { + try { + setBusy(true); + const result = await api.getVersionLog(search, token); + if (result.error) throw new Error(`Get version log error. ${result.message}`); + return result; + } finally { + setBusy(false); + } + }; + return ( { sendCommand, updateVehicle, getFleets, + getVersionLog, }} > {children} diff --git a/src/components/Contexts/__mocks__/VehicleContext.jsx b/src/components/Contexts/__mocks__/VehicleContext.jsx index f31d80c..c545745 100644 --- a/src/components/Contexts/__mocks__/VehicleContext.jsx +++ b/src/components/Contexts/__mocks__/VehicleContext.jsx @@ -70,6 +70,7 @@ let vehicleState = { temperature: 26, }, trex_version: "1000000", + dbc_version: "d439abd3662dd20099f49dd8f43f7b145202e961caa2b5aba2c6154c8096348b", ip: "172.20.0.17:49850", updated: "2022-07-26T00:26:38.880381Z", }, diff --git a/src/components/Controls/CarUpdatesTable/index.jsx b/src/components/Controls/CarUpdatesTable/index.jsx index 19882c9..4ea66ca 100644 --- a/src/components/Controls/CarUpdatesTable/index.jsx +++ b/src/components/Controls/CarUpdatesTable/index.jsx @@ -198,7 +198,7 @@ const MainForm = ({ vin, token }) => { {totalCarUpdates === 0 ? ( -

No Car Updates found

+ No Car Updates found ) : ( { + const [versions, setVersions] = useState([]); + const [total, setTotal] = useState(0); + const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10); + const [pageIndex, setPageIndex] = useState(0); + const [orderBy, setOrderBy] = useState("created_at"); + const [order, setOrder] = useState("desc"); + const { getVersionLog } = useVehicleContext(); + const { setMessage } = useStatusContext(); + + useEffect(() => { + (async () => { + try { + if (!vin || !token) return; + const result = await getVersionLog( + { + vin, + limit: pageSize, + offset: pageSize * pageIndex, + order: `${orderBy} ${order}`, + }, + token + ); + setVersions(result.data); + if (result.total > -1) setTotal(result.total); + } catch (e) { + setMessage(e.message); + logger.warn(e.stack); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [vin, token, pageIndex, pageSize, orderBy, order]); + + const handleChangePageIndex = (event, newIndex) => { + setPageIndex(newIndex); + }; + + const handleChangePageSize = (event) => { + setPageSize(parseInt(event.target.value, 10)); + setPageIndex(0); + }; + + const handleSort = (event, property) => { + try { + if (property === orderBy) { + if (order === "asc") { + setOrder("desc"); + } else { + setOrder("asc"); + } + } else { + setOrderBy(property); + setOrder("asc"); + } + } catch (e) { + logger.warn(e.stack); + } + }; + + return ( +
+ + + + {versions && versions.map((row, i) => ( + + {row.version_source} + {row.version} + {LocalDateTimeString(row.created_at)} + + ))} + + + + + + +
+
+ ); +}; + +export default CarVersionLogTable; diff --git a/src/components/Controls/IssueSelectionTable/index.jsx b/src/components/Controls/IssueSelectionTable/index.jsx index 3dc8c5f..9ef35d6 100644 --- a/src/components/Controls/IssueSelectionTable/index.jsx +++ b/src/components/Controls/IssueSelectionTable/index.jsx @@ -1,26 +1,26 @@ -import React, { useEffect, useState } from "react"; -import { Link } from "react-router-dom"; -import PropTypes from "prop-types"; import { Table, TableBody, TableCell, TableFooter, TablePagination, - TableRow, - Button, + TableRow } from "@material-ui/core"; +import DeleteIcon from "@material-ui/icons/Delete"; import clsx from "clsx"; +import PropTypes from "prop-types"; +import React, { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { logger } from "../../../services/monitoring"; +import { LocalDateTimeString } from "../../../utils/dates"; +import { Permissions } from "../../../utils/roles"; import { useIssueContext } from "../../Contexts/IssueContext"; import { useStatusContext } from "../../Contexts/StatusContext"; -import { LocalDateTimeString } from "../../../utils/dates"; +import { useUserContext } from "../../Contexts/UserContext"; import TableHeaderSortable from "../../Table/HeaderSortable"; -import { logger } from "../../../services/monitoring"; import { useLocalStorage } from "../../useLocalStorage"; import { RoleWrap } from "../RoleWrap"; -import { useUserContext } from "../../Contexts/UserContext"; -import { Permissions } from "../../../utils/roles"; const tableColumns = [ { @@ -35,10 +35,6 @@ const tableColumns = [ id: "title", label: "Title", }, - { - id: "description", - label: "Description", - }, { id: "driver_id", label: "Driver ID", @@ -69,7 +65,7 @@ const IssueSelectionTable = (props) => { const [pageIndex, setPageIndex] = useState(0); const [orderBy, setOrderBy] = useState("created_at"); const [order, setOrder] = useState("asc"); - const { getIssues, issues, totalIssues } = useIssueContext(); + const { deleteIssue, getIssues, issues, totalIssues = 0 } = useIssueContext(); const { groups, providers } = useUserContext(); const { setMessage } = useStatusContext(); @@ -132,12 +128,19 @@ const IssueSelectionTable = (props) => { setPageIndex(0); }, [search]); - const { deleteIssue } = useIssueContext(); const handleDelete = (id) => { deleteIssue(id, token).then(() => { - getIssues(token) + getIssues( + { + limit: pageSize, + offset: pageSize * pageIndex, + order: `${orderBy} ${order}`, + }, + token + ); }); }; + return (
@@ -161,7 +164,6 @@ const IssueSelectionTable = (props) => { {row.vin}{row.title} - {row.description || ""}{row.driver_id} {LocalDateTimeString(row.timestamp)} @@ -172,7 +174,9 @@ const IssueSelectionTable = (props) => { rolesPerProvider={Permissions.FiskerDelete} > - + handleDelete(row.id)}> + + @@ -181,12 +185,9 @@ const IssueSelectionTable = (props) => { - {totalIssues === 0 ? ( -

No issues found

- ) : ( { onPageChange={handleChangePageIndex} onRowsPerPageChange={handleChangePageSize} /> - )}
diff --git a/src/components/DigitalTwin/index.js b/src/components/DigitalTwin/index.js index 8129ce9..a0c1141 100644 --- a/src/components/DigitalTwin/index.js +++ b/src/components/DigitalTwin/index.js @@ -1,7 +1,7 @@ import React from "react"; -import useStyles from "../useStyles"; import { LocalDateTimeString } from "../../utils/dates"; +import useStyles from "../useStyles"; const keyValueTemplate = (key, value) => (

@@ -14,7 +14,7 @@ const mapOpenCloseState = (value) => const DigitalTwin = (props) => { const classes = useStyles(); - const { battery, doors, location, trex_version, ip, updated, windows } = props; + const { battery, doors, location, trex_version, ip, updated, windows, misc_windows, sunroof, dbc_version, door_locks } = props; return (

@@ -28,10 +28,55 @@ const DigitalTwin = (props) => { {Object.entries(doors).map(mapOpenCloseState)}
)} + {door_locks != null && ( +
+

Door Locks

+ {Object.entries(door_locks).map((value) => { + if (value[0] === "driver") { + return keyValueTemplate(value[0], value[1] ? "Open" : "Closed"); + } else { + return keyValueTemplate(value[0], value[1] ? "Unlocked" : "Locked"); + } + })} +
+ )} {windows != null && (

Windows

- {Object.entries(windows).map(mapOpenCloseState)} + {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`); + } + })} +
+ )} + {misc_windows != null && ( +
+

Misc Windows

+ {Object.entries(misc_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`); + } + })} +
+ )} + {sunroof != null && ( +
+

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`); + } + })}
)} {location != null && ( @@ -61,6 +106,11 @@ const DigitalTwin = (props) => { {keyValueTemplate("Updated at", LocalDateTimeString(updated))}
)} + {dbc_version != null && ( +
+ {keyValueTemplate("DBC version", dbc_version)} +
+ )}
); }; diff --git a/src/components/Issues/Info/Details/index.jsx b/src/components/Issues/Info/Details/index.jsx index c3031aa..307eafc 100644 --- a/src/components/Issues/Info/Details/index.jsx +++ b/src/components/Issues/Info/Details/index.jsx @@ -3,15 +3,14 @@ import clsx from "clsx"; import React, { useEffect } from "react"; import { logger } from "../../../../services/monitoring"; +import { LocalDateTimeString } from "../../../../utils/dates"; +import { + IssueProvider, useIssueContext +} from "../../../Contexts/IssueContext"; import { useStatusContext } from "../../../Contexts/StatusContext"; import { useUserContext } from "../../../Contexts/UserContext"; -import { - useIssueContext, - IssueProvider -} from "../../../Contexts/IssueContext"; import useStyles from "../../../useStyles"; - const MainForm = ({ id }) => { const classes = useStyles(); const { setMessage } = useStatusContext(); @@ -55,7 +54,7 @@ const MainForm = ({ id }) => { Description: {issue.description}

- timestamp: {issue.timestamp} + Created: {LocalDateTimeString(issue.timestamp)}

{issue.images && issue.images.map((image, index) => ( Issue images diff --git a/src/components/Issues/Info/index.jsx b/src/components/Issues/Info/index.jsx index bfa6deb..39942af 100644 --- a/src/components/Issues/Info/index.jsx +++ b/src/components/Issues/Info/index.jsx @@ -1,55 +1,15 @@ -import { Box, Tab, Tabs } from "@material-ui/core"; import clsx from "clsx"; -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { useParams } from "react-router"; -import { useLocation } from "react-router-dom"; -import { hasRole } from "../../../utils/roles"; import { useStatusContext } from "../../Contexts/StatusContext"; -import { useUserContext } from "../../Contexts/UserContext"; -import TabPanel from "../../Controls/TabPanel"; import useStyles from "../../useStyles"; import IssueDetailsTab from "./DetailsTab"; - -const tabHashes = ["details", "updates", "filters"]; - -const TabViews = [ - { - label: "Details", - component: IssueDetailsTab, - }, - -]; - -const filterTabs = (data, groups, providers) => { - return data.reduce((result, item) => { - if (hasRole(groups, item.rolesPerProvider, providers)) { - result.push(item); - } - return result; - }, []); -}; - const IssueInfo = () => { const { id } = useParams(); const classes = useStyles(); const { setTitle, setSitePath } = useStatusContext(); - const { hash } = useLocation(); - const [tabIndex, setTabIndex] = useState(0); - const [tabs, setTabs] = useState([]); - const { groups, providers } = useUserContext(); - - useEffect(() => { - const data = filterTabs(TabViews, groups, providers); - setTabs(data); - }, [groups, providers]); - - useEffect(() => { - const key = hash.replace("#", ""); - const index = tabHashes.findIndex((element) => element === key); - if (index >= 0) setTabIndex(index); - }, [hash]); useEffect(() => { const title = `Issue ${id} Details`; @@ -66,39 +26,12 @@ const IssueInfo = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [id]); - const handleTabChange = (_event, newIndex) => { - setTabIndex(newIndex); - }; - return (
- - - {tabs.map((item, index) => )} - - - {tabs.map((item, index) => ( - - - - ))} +
); }; -function tabProps(index) { - return { - id: `tab-${index}`, - "aria-controls": `tabpanel-${index}`, - }; -} - export default IssueInfo; diff --git a/src/components/VehicleMap/index.jsx b/src/components/VehicleMap/index.jsx index ff61861..2069e51 100644 --- a/src/components/VehicleMap/index.jsx +++ b/src/components/VehicleMap/index.jsx @@ -185,16 +185,7 @@ const Component = () => { {carState ? ( diff --git a/src/services/__mocks__/vehiclesAPI.js b/src/services/__mocks__/vehiclesAPI.js index ccd7537..d18a88b 100644 --- a/src/services/__mocks__/vehiclesAPI.js +++ b/src/services/__mocks__/vehiclesAPI.js @@ -116,7 +116,26 @@ const vehiclesAPI = { }, getCANSignals: async (vin, vehicle) => { return signals; - } + }, + getVersionLog: async (vin) => ({ + "data": [ + { + "id": 1, + "vin": "${vin}", + "version_source": "TREX", + "version": "0.9.56", + "created_at": "2023-01-13T02:11:33.327214Z" + }, + { + "id": 2, + "vin": "${vin}", + "version_source": "DBC", + "version": "386c18977a1be3cda60c953e5902c680dbe82b89523f2527e80cd9db863db991", + "created_at": "2023-01-13T02:11:33.330932Z" + } + ], + "total": 2 + }) }; export default vehiclesAPI; diff --git a/src/services/vehiclesAPI.js b/src/services/vehiclesAPI.js index 86a2bc8..9d8d0db 100644 --- a/src/services/vehiclesAPI.js +++ b/src/services/vehiclesAPI.js @@ -1,5 +1,5 @@ import { - addQueryParams, errorHandler, fetchRespHandler, getAuthHeaderOptions + addQueryParams, errorHandler, fetchRespHandler, getAuthHeaderOptions } from "../utils/http"; const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL; @@ -172,6 +172,19 @@ const vehiclesAPI = { }) .then(fetchRespHandler) .catch(errorHandler), + + getVersionLog: async ({vin, ...search}, token) => { + const u = addQueryParams(`${API_ENDPOINT}/vehicle/${vin}/version/logs`, search); + return fetch(u, { + method: "GET", + headers: Object.assign( + { "Content-Type": "application/json" }, + getAuthHeaderOptions(token) + ), + }) + .then(fetchRespHandler) + .catch(errorHandler) + }, }; export default vehiclesAPI;