Release/0.0.3 to main (#254)

* CEC-2628 - Display IP in digital twin in portal (#251)

* CEC-3453 Update security dll instructions (#252)

* CEC-2752-Add-Mobile-Issue-Tracker (#250)

* first commit

* removed comments

* remove more comments

* fix build issues

* fix unused vars

* update snapshot

* fix test

* Fix connect ECONNREFUSED 127.0.0.1:80

* Test Magna side menu

* attempt to pass test

* fix test

* remove comments

* fix some code smells

* fix test

* resolve comments

* fix bug

* resolved comments

* resolve comments

* resolve comments

* update snapshot

* resolved comments

Co-authored-by: jwu-fisker <jwu@fiskerinc.com>

* Cec 2752 small fix (#253)

* first commit

* removed comments

* remove more comments

* fix build issues

* fix unused vars

* update snapshot

* fix test

* Fix connect ECONNREFUSED 127.0.0.1:80

* Test Magna side menu

* attempt to pass test

* fix test

* remove comments

* fix some code smells

* fix test

* resolve comments

* fix bug

* resolved comments

* resolve comments

* resolve comments

* update snapshot

* resolved comments

* small fix

Co-authored-by: jwu-fisker <jwu@fiskerinc.com>

Co-authored-by: Paul Adamsen <117673433+pauladamseniii@users.noreply.github.com>
Co-authored-by: das31 <31259710+das31@users.noreply.github.com>
This commit is contained in:
John Wu
2023-01-10 18:30:39 -08:00
committed by GitHub
parent cf6ff831d7
commit b80a2cb8bf
29 changed files with 3056 additions and 140 deletions

View File

@@ -3,9 +3,12 @@ jest.mock("../Contexts/FileUploadContext");
jest.mock("../Contexts/VehicleContext");
jest.mock("../Contexts/ManifestsContext");
jest.mock("../Contexts/UserContext");
jest.mock("../Contexts/IssueContext");
jest.mock("../../services/monitoring");
jest.mock("../../services/vehiclesAPI");
jest.mock("../../services/superset")
jest.mock("../../services/superset");
jest.mock("../../services/suppliersAPI");
jest.mock("../../services/issueAPI");
import {
act, cleanup, render,
@@ -39,13 +42,14 @@ const check = async (path, selector, compare) => {
const sleepAndCheck = async (path, selector, compare) => {
const container = await renderRoute(path);
await waitFor(() => {});
await waitFor(() => { });
expect(container.querySelector(selector).innerHTML).toEqual(compare);
expect(container).toMatchSnapshot();
};
describe("App", () => {
beforeAll(() => {
global.URL.createObjectURL = jest.fn();
addSnapshotSerializer(expect);
}, 60000);
@@ -78,6 +82,14 @@ describe("App", () => {
await check("/vehicle-add", "span.MuiButton-label", "Sign In");
});
it("Route /issues unauthenticated", async () => {
await check("/issues", "span.MuiButton-label", "Sign In");
});
it("Route /issue-info unauthenticated", async () => {
await check("/issue-info/FISKER123", "span.MuiButton-label", "Sign In");
});
it("Route /vehicles unauthenticated", async () => {
await check("/vehicles", "span.MuiButton-label", "Sign In");
});
@@ -158,6 +170,17 @@ describe("App", () => {
await check("/vehicles", "h6", "Vehicles");
});
it("Route /issues authenticated", async () => {
setToken(TEST_AUTH_OBJECT_FISKER);
await check("/issues", "h6", "Issues");
});
it("Route /issue-info authenticated", async () => {
setToken(TEST_AUTH_OBJECT_FISKER);
await check("/issue-info/FISKER123", "h6", "Issue FISKER123 Details");
});
it("Route /vehicle-status authenticated", async () => {
setToken(TEST_AUTH_OBJECT_FISKER);
await check("/vehicle-status/FISKER123", "h6", "Vehicle FISKER123 Details");

File diff suppressed because it is too large Load Diff

View File

@@ -164,121 +164,9 @@ exports[`CarUpdatesTab Render 1`] = `
<tr
class="MuiTableRow-root MuiTableRow-footer"
>
<td
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
colspan="6"
>
<div
class="MuiToolbar-root MuiToolbar-regular MuiTablePagination-toolbar MuiToolbar-gutters"
>
<div
class="MuiTablePagination-spacer"
/>
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
Rows per page:
<p>
No Car Updates found
</p>
<div
class="MuiInputBase-root MuiTablePagination-input MuiTablePagination-selectRoot"
>
<select
aria-label="rows per page"
class="MuiSelect-root MuiSelect-select MuiTablePagination-select MuiInputBase-input"
>
<option
class="MuiTablePagination-menuItem"
value="5"
>
5
</option>
<option
class="MuiTablePagination-menuItem"
value="10"
>
10
</option>
<option
class="MuiTablePagination-menuItem"
value="25"
>
25
</option>
<option
class="MuiTablePagination-menuItem"
value="100"
>
100
</option>
</select>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSelect-icon MuiTablePagination-selectIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
</div>
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
0-0 of 0
</p>
<div
class="MuiTablePagination-actions"
>
<button
aria-label="Previous page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Previous page"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z"
/>
</svg>
</span>
</button>
<button
aria-label="Next page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Next page"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"
/>
</svg>
</span>
</button>
</div>
</div>
</td>
</tr>
</tfoot>
</table>

View File

@@ -130,6 +130,17 @@ exports[`DigitalTwinTab Render 1`] = `
1000000
</p>
</div>
<div
class="makeStyles-popupSection-0"
>
<p>
<b>
Trex IP
</b>
:
172.20.0.17:49850
</p>
</div>
<div
class="makeStyles-popupSection-0"
>

View File

@@ -0,0 +1,55 @@
import React, { useContext, useState, useMemo, useCallback } from "react";
import api from "../../services/issueAPI";
const IssueContext = React.createContext();
export const IssueProvider = ({ children }) => {
const [issue, setIssue] = useState({});
const [issues, setIssues] = useState([]);
const [totalIssues, setTotalIssues] = useState(0);
const getIssue = useCallback(async (id, token) => {
const result = await api.getIssue(id, token);
if (result.error) throw new Error(`Get issue error. ${result.message}`);
setIssue(result.data ?? []);
return result;
}, []);
const getIssues = useCallback(async (search,token) => {
const result = await api.getIssues(search,token);
if (result.error) {
setIssues([]);
throw new Error(`Get issues error. ${result.message}`);
}
setIssues(result.data ?? []);
if (result.total) {
setTotalIssues(result.total);
}
}, []);
const deleteIssue = useCallback(async (id, token) => {
const result = await api.deleteIssue(id, token);
if (result.error)
throw new Error(`Delete issue error. ${result.message}`);
return result;
}, []);
const value = useMemo(() => ({
totalIssues,
issue,
issues,
deleteIssue,
getIssue,
getIssues,
}), [totalIssues, issue, issues, deleteIssue, getIssue, getIssues]);
return (
<IssueContext.Provider value={value}>
{children}
</IssueContext.Provider>
);
};
export const useIssueContext = () => useContext(IssueContext);

View File

@@ -0,0 +1,102 @@
jest.mock("../../services/issueAPI");
import {
render,
cleanup,
screen,
fireEvent,
waitFor,
} from "@testing-library/react";
import { IssueProvider, useIssueContext } from "./IssueContext";
const checkIssueResult = (issue) => {
expect(screen.getByTestId("issue").innerHTML).toEqual(issue);
};
const checkIssuesResult = (issues) => {
expect(screen.getByTestId("issues").innerHTML).toEqual(issues);
};
describe("IssueContext", () => {
describe("getIssues", () => {
beforeEach(() => {
const TestComp = () => {
const { issues, getIssues } = useIssueContext();
return (
<>
<div data-testid="issues">{JSON.stringify(issues)}</div>
<button
data-testid="getIssues"
onClick={() => getIssues()}
/>
</>
);
};
render(
<IssueProvider>
<TestComp />
</IssueProvider>
);
});
afterEach(() => {
cleanup();
});
it("Initial state", () => {
checkIssuesResult("[]");
});
it("getIssues", async () => {
fireEvent.click(screen.getByTestId("getIssues"));
await waitFor(() =>
expect(screen.getByTestId("issues").innerHTML).not.toBe([])
);
checkIssuesResult(JSON.stringify(expectedIssuesData));
});
});
describe("getIssue", () => {
beforeEach(() => {
const TestComp = () => {
const { issue, getIssue } = useIssueContext();
return (
<>
<div data-testid="issue">{JSON.stringify(issue)}</div>
<button
data-testid="getIssue"
onClick={() => getIssue("1")}
/>
</>
);
};
render(
<IssueProvider>
<TestComp />
</IssueProvider>
);
});
afterEach(() => {
cleanup();
});
it("Initial state", () => {
checkIssueResult("{}");
});
it("getIssue", async () => {
fireEvent.click(screen.getByTestId("getIssue"));
await waitFor(() =>
expect(screen.getByTestId("issue").innerHTML).not.toBe("{}")
);
checkIssueResult(JSON.stringify(expectedIssueData));
});
});
});
const expectedIssuesData = [{ "id": 18, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" }, { "id": 19, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" }, { "id": 20, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" }, { "id": 21, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" }, { "id": 22, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" }, { "id": 25, "vin": "1GNGC26RXXJ407648", "title": "Example HMI Problem", "description": "HMI blue screen", "driver_id": "0b6b1930-b20a-4fce-967a-efac6a01fd10", "timestamp": "2022-12-19T22:25:03.848855Z" }, { "id": 26, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" }, { "id": 27, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" }, { "id": 28, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" }]
const expectedIssueData = { "id": 18, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z", "images": [{ "id": 15, "image": "SGVsbG8x", "issue_id": 18 }] }

View File

@@ -0,0 +1,35 @@
import React from "react";
let issue = {
"id": 1,
"vin": "1GNGC26RXXJ407648",
"title": "sometitle",
"description": "2343242",
"driver_id": "valid-cognito-id-1",
"timestamp": "2022-12-09T23:16:38.074858Z",
"images": [
{
"id": 1,
"image": "SGVsbG8x",
"issue_id": 1
}
]
}
let issues = [
{ "id": 15, "vin": "4Y1SL65848Z411439", "title": "sometitle", "description": "2343242", "driver_id": "12345", "timestamp": "2022-12-09T23:16:38.074858Z" },
{ "id": 17, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" },
];
export const IssueProvider = ({ children }) => {
return <div data-testid="mocked-issueprovider">{children}</div>;
};
export const useIssueContext = () => ({
issue,
issues,
deleteIssue: jest.fn(),
getIssues: jest.fn().mockReturnValue(issues),
getIssue: jest.fn(),
});

View File

@@ -70,6 +70,7 @@ let vehicleState = {
temperature: 26,
},
trex_version: "1000000",
ip: "172.20.0.17:49850",
updated: "2022-07-26T00:26:38.880381Z",
},
};
@@ -146,10 +147,12 @@ export const useVehicleContext = () => ({
command,
parameters,
})),
getFleets: jest.fn((vin, search,_token) => {return {
getFleets: jest.fn((vin, search, _token) => {
return {
data: ["fleet1", "fleet2"],
total: 2,
}}),
}
}),
});
export const setBusy = (val) => {

View File

@@ -197,6 +197,9 @@ const MainForm = ({ vin, token }) => {
</TableBody>
<TableFooter>
<TableRow>
{totalCarUpdates === 0 ? (
<p>No Car Updates found</p>
) : (
<TablePagination
rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={6}
@@ -209,7 +212,7 @@ const MainForm = ({ vin, token }) => {
}}
onPageChange={handleChangePageIndex}
onRowsPerPageChange={handleChangePageSize}
/>
/>)}
</TableRow>
</TableFooter>
</Table>

View File

@@ -0,0 +1,217 @@
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,
} from "@material-ui/core";
import clsx from "clsx";
import { useIssueContext } from "../../Contexts/IssueContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import { LocalDateTimeString } from "../../../utils/dates";
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 = [
{
id: "id",
label: "Id",
},
{
id: "vin",
label: "VIN",
},
{
id: "title",
label: "Title",
},
{
id: "description",
label: "Description",
},
{
id: "driver_id",
label: "Driver ID",
},
{
id: "created_at",
label: "Created",
},
{
id: "",
label: "",
},
];
const PAGE_SIZE = "ISSUE_SELECTION_TABLE_PAGE_SIZE";
const IssueSelectionTable = (props) => {
const {
token,
classes,
search,
multiSelect,
selected,
onSelectAll,
} = props;
const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10);
const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("created_at");
const [order, setOrder] = useState("asc");
const { getIssues, issues, totalIssues } = useIssueContext();
const { groups, providers } = useUserContext();
const { setMessage } = useStatusContext();
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 handleSelectAll = (event) => {
if (!onSelectAll) return;
const newSelected = [];
if (event.target.checked) {
issues.forEach((car) => {
newSelected.push(car.vin);
});
}
onSelectAll(newSelected);
};
useEffect(() => {
(async () => {
try {
if (!token) return;
await getIssues(
{
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
},
token
);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pageIndex, pageSize, orderBy, order, search, token]);
useEffect(() => {
setPageIndex(0);
}, [search]);
const { deleteIssue } = useIssueContext();
const handleDelete = (id) => {
deleteIssue(id, token).then(() => {
getIssues(token)
});
};
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Table>
<TableHeaderSortable
classes={classes}
orderBy={orderBy}
order={order}
columnData={tableColumns}
onSortRequest={handleSort}
multiSelect={multiSelect}
onSelectAll={handleSelectAll}
selectCount={selected ? selected.length : 0}
rowCount={issues ? issues.length : 0}
/>
<TableBody>
{issues.map((row) => {
return (
<TableRow key={row.id}>
<TableCell align="center">
<Link to={`/issue-info/${row.id}`}>{row.id}</Link>
</TableCell>
<TableCell align="center">{row.vin}</TableCell>
<TableCell align="center">{row.title}</TableCell>
<TableCell align="center">{row.description || ""}</TableCell>
<TableCell align="center">{row.driver_id}</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.timestamp)}
</TableCell>
<RoleWrap
groups={groups}
providers={providers}
rolesPerProvider={Permissions.FiskerDelete}
>
<TableCell>
<Button onClick={() => handleDelete(row.id)}>Delete</Button>
</TableCell>
</RoleWrap>
</TableRow>
);
})}
</TableBody>
<TableFooter>
<TableRow>
{totalIssues === 0 ? (
<p>No issues found</p>
) : (
<TablePagination
rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={7}
count={totalIssues}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onPageChange={handleChangePageIndex}
onRowsPerPageChange={handleChangePageSize}
/>
)}
</TableRow>
</TableFooter>
</Table>
</div>
);
};
IssueSelectionTable.propTypes = {
token: PropTypes.string.isRequired,
classes: PropTypes.object.isRequired,
multiSelect: PropTypes.bool,
selected: PropTypes.array,
onSelect: PropTypes.func,
onSelectAll: PropTypes.func,
};
export default IssueSelectionTable;

View File

@@ -1,10 +1,10 @@
import { embedDashboard } from "@superset-ui/embedded-sdk";
import React, { useEffect } from "react";
import { useHistory } from "react-router-dom";
import api from "../../services/superset";
import { useStatusContext } from "../Contexts/StatusContext";
import { useUserContext } from "../Contexts/UserContext";
import './index.css'
import { embedDashboard } from "@superset-ui/embedded-sdk";
import { useHistory } from "react-router-dom";
import './index.css';
const Dashboard = () => {

View File

@@ -14,7 +14,7 @@ const mapOpenCloseState = (value) =>
const DigitalTwin = (props) => {
const classes = useStyles();
const { battery, doors, location, trex_version, updated, windows } = props;
const { battery, doors, location, trex_version, ip, updated, windows } = props;
return (
<div>
@@ -51,6 +51,11 @@ const DigitalTwin = (props) => {
{keyValueTemplate("Trex Version", trex_version)}
</div>
)}
{ip && (
<div className={classes.popupSection}>
{keyValueTemplate("Trex IP", ip)}
</div>
)}
{updated != null && (
<div className={classes.popupSection}>
{keyValueTemplate("Updated at", LocalDateTimeString(updated))}

View File

@@ -0,0 +1,79 @@
import { Grid } from "@material-ui/core";
import clsx from "clsx";
import React, { useEffect } from "react";
import { logger } from "../../../../services/monitoring";
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();
const { issue, getIssue } = useIssueContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
useEffect(() => {
(async () => {
try {
if (!id || !token) return;
await getIssue(id, token);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
if (issue) {
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Grid container className={classes.root} spacing={2}>
<Grid item md={12} className={classes.textCenterAlign}>
<p>
<b>ID</b>: {id}
</p>
<p>
<b>VIN</b>: {issue.vin}
</p>
<p>
<b>Title</b>: {issue.title}
</p>
<p>
<b>Description</b>: {issue.description}
</p>
<p>
<b>timestamp</b>: {issue.timestamp}
</p>
{issue.images && issue.images.map((image, index) => (
<img key={image.id} src={`data:image/png;base64, ${image.image}`} alt="Issue images" />
))}
</Grid>
</Grid>
</div>
);
} else {
return <p>Loading...</p>;
}
};
const IssueDetails = (props) => (
<IssueProvider>
<MainForm {...props} />
</IssueProvider>
);
export default IssueDetails;

View File

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

View File

@@ -0,0 +1,104 @@
import { Box, Tab, Tabs } from "@material-ui/core";
import clsx from "clsx";
import React, { useEffect, useState } 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`;
setTitle(title);
setSitePath([
{
label: "Issues",
link: "/issues",
},
{
label: title,
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
const handleTabChange = (_event, newIndex) => {
setTabIndex(newIndex);
};
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Box
className={classes.tableToolbar}
sx={{ borderBottom: 1, borderColor: "divider" }}
>
<Tabs
value={tabIndex}
onChange={handleTabChange}
aria-label="issue tabs"
indicatorColor="secondary">
{tabs.map((item, index) => <Tab key={index} label={item.label} {...tabProps(index)} />)}
</Tabs>
</Box>
{tabs.map((item, index) => (
<TabPanel key={index} value={tabIndex} index={index}>
<item.component id={id} />
</TabPanel>
))}
</div>
);
};
function tabProps(index) {
return {
id: `tab-${index}`,
"aria-controls": `tabpanel-${index}`,
};
}
export default IssueInfo;

View File

@@ -0,0 +1,51 @@
import { Grid } from "@material-ui/core";
import clsx from "clsx";
import React, { useEffect } from "react";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import { IssueProvider } from "../../Contexts/IssueContext";
import IssueSelectionTable from "../../Controls/IssueSelectionTable";
import useStyles from "../../useStyles";
const MainForm = () => {
const classes = useStyles();
const { setTitle, setSitePath } = useStatusContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
useEffect(() => {
setTitle("Issues");
setSitePath([]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Grid container className={classes.root} spacing={2}>
<Grid item md={4} className={classes.textJustifyAlign}>
</Grid>
<Grid item md={2} className={classes.textRightAlign} />
</Grid>
<IssueSelectionTable
classes={classes}
token={token}
multiSelect={false}
/>
</div>
);
};
const IssuesList = () => (
<IssueProvider>
<MainForm />
</IssueProvider>
);
export default IssuesList;

View File

@@ -4,6 +4,7 @@ import BuildIcon from "@material-ui/icons/Build";
import CloudDownloadIcon from "@material-ui/icons/CloudDownload";
import CommuteIcon from "@material-ui/icons/Commute";
import DirectionsCarIcon from "@material-ui/icons/DirectionsCar";
import BugReportIcon from "@material-ui/icons/BugReport";
import HomeIcon from "@material-ui/icons/Home";
import SettingsInputCompositeIcon from "@material-ui/icons/SettingsInputComposite";
import { default as React, useEffect, useState } from "react";
@@ -32,6 +33,12 @@ const menuData = [
icon: <DirectionsCarIcon />,
rolesPerProvider: Permissions.FiskerMagnaRead,
},
{
label: "Issues",
to: "/issues",
icon: <BugReportIcon />,
rolesPerProvider: Permissions.FiskerRead,
},
{
label: "Fleets",
to: "/fleets",

View File

@@ -4,7 +4,7 @@ jest.mock("../../services/superset");
import { render, screen, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import addSnapshotSerializer from "../../utils/snapshot";
import { TEST_AUTH_OBJECT_FISKER } from "../../utils/testing";
import { TEST_AUTH_OBJECT_FISKER, TEST_AUTH_OBJECT_MAGNA } from "../../utils/testing";
import { setToken, UserProvider } from "../Contexts/UserContext";
import SideMenu from "./SideMenu";
@@ -39,4 +39,13 @@ describe("SideMenu", () => {
});
expect(container).toMatchSnapshot();
});
it("Magna Authenticated", async () => {
setToken(TEST_AUTH_OBJECT_MAGNA);
const container = await renderMenu();
await waitFor(() => {
expect(screen.getByText("Tools")).toBeInTheDocument();
});
expect(container).toMatchSnapshot();
});
});

View File

@@ -116,6 +116,42 @@ exports[`SideMenu Authenticated 1`] = `
/>
</a>
</li>
<li>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
href="/issues"
role="button"
tabindex="0"
>
<div
class="MuiListItemIcon-root"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"
/>
</svg>
</div>
<div
class="MuiListItemText-root"
>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
Issues
</span>
</div>
<span
class="MuiTouchRipple-root"
/>
</a>
</li>
<li>
<a
aria-disabled="false"
@@ -340,6 +376,191 @@ exports[`SideMenu Authenticated 1`] = `
</div>
`;
exports[`SideMenu Magna Authenticated 1`] = `
<div>
<div
data-testid="mocked-userprovider"
>
<ul
class="MuiList-root MuiList-padding"
>
<li>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
href="/home"
role="button"
tabindex="0"
>
<div
class="MuiListItemIcon-root"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"
/>
</svg>
</div>
<div
class="MuiListItemText-root"
>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
Home
</span>
</div>
<span
class="MuiTouchRipple-root"
/>
</a>
</li>
<li>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
href="/packages"
role="button"
tabindex="0"
>
<div
class="MuiListItemIcon-root"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z"
/>
</svg>
</div>
<div
class="MuiListItemText-root"
>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
Deployments
</span>
</div>
<span
class="MuiTouchRipple-root"
/>
</a>
</li>
<li>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
href="/vehicles"
role="button"
tabindex="0"
>
<div
class="MuiListItemIcon-root"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M18.92 6.01C18.72 5.42 18.16 5 17.5 5h-11c-.66 0-1.21.42-1.42 1.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z"
/>
</svg>
</div>
<div
class="MuiListItemText-root"
>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
Vehicles
</span>
</div>
<span
class="MuiTouchRipple-root"
/>
</a>
</li>
<span>
<li>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
href="/tools/certificates/add"
role="button"
tabindex="0"
>
<div
class="MuiListItemIcon-root"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"
/>
</svg>
</div>
<div
class="MuiListItemText-root"
>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
Tools
</span>
</div>
<span
class="MuiTouchRipple-root"
/>
</a>
</li>
</span>
<ul
style="margin-left: 50px;"
>
<li>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
href="/tools/certificates/add"
role="button"
tabindex="0"
>
<div
class="MuiListItemText-root"
>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
Certificate
</span>
</div>
<span
class="MuiTouchRipple-root"
/>
</a>
</li>
</ul>
</ul>
</div>
</div>
`;
exports[`SideMenu Unauthenticated 1`] = `
<div>
<div

View File

@@ -4,11 +4,13 @@ exports[`Magna Security DLL page Security DLL Result page 1`] = `
<div>
<form>
<p>
Click to download your certificates and security.dll.
<br />
Please click the links below to download the certificate.pem, key.pem and the security.dll (either the 32-bit or 64-bit version to match your application). Install all files in the same folder, and then run your application of choice to connect to the Fisker cloud.
</p>
<p>
<strong>
DLL will not work unless certificate.pem and key.pem are placed next to the DLL
Important Note:
</strong>
Certificates expire in one month from the time they are generated. They have to be re-downloaded again to connect to the Fisker cloud.
</p>
<ul>
<li>

View File

@@ -8,7 +8,8 @@ const CertMimeType = "application/x-pem-file";
const Result = ({ public_key, private_key }) => (
<>
<form>
<p>Click to download your certificates and security.dll.<br /><strong>DLL will not work unless certificate.pem and key.pem are placed next to the DLL</strong></p>
<p>Please click the links below to download the certificate.pem, key.pem and the security.dll (either the 32-bit or 64-bit version to match your application). Install all files in the same folder, and then run your application of choice to connect to the Fisker cloud.</p>
<p><strong>Important Note:</strong> Certificates expire in one month from the time they are generated. They have to be re-downloaded again to connect to the Fisker cloud.</p>
<ul>
<li>
<DownloadFileLink

View File

@@ -8,6 +8,8 @@ import { AuthRoute, TYPES } from "../Routes/AuthRoute";
const CANFilterCreate = React.lazy(() => import("../CANFilter/Add"));
const CANFilterUpdate = React.lazy(() => import("../CANFilter/Update"));
const IssuesList = React.lazy(() => import("../Issues/List"))
const IssueInfo = React.lazy(() => import("../Issues/Info"))
const CarsList = React.lazy(() => import("../Cars/List"));
const CarStatus = React.lazy(() => import("../Cars/Status"));
const CarUpdateStatus = React.lazy(() => import("../Cars/UpdateStatus"));
@@ -175,6 +177,24 @@ const SiteRoutes = () => {
rolesPerGroup={Permissions.FiskerRead}
providers={providers}
/>
<AuthRoute
path="/issues"
render={() => <IssuesList />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
rolesPerGroup={Permissions.FiskerRead}
providers={providers}
/>
<AuthRoute
path="/issue-info/:id"
render={() => <IssueInfo />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
rolesPerGroup={Permissions.FiskerRead}
providers={providers}
/>
<AuthRoute
path="/vehicles"
render={() => <CarsList />}

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useUserContext } from "../Contexts/UserContext";
import supersetAPI from "../../services/superset";
@@ -29,10 +29,11 @@ const SupersetDashboardList = () => {
internalEffect(token)
}
return () => {
setDashboardList([]);
}
}, [groups, token])
return (
<ul style={{ marginLeft: 50 }}>
{dashboardList.map((subitem, index) => (

View File

@@ -193,6 +193,7 @@ const Component = () => {
location={carState.location}
windows={carState.windows}
trex_version={carState.trex_version}
ip={carState.ip}
updated={carState.updated}
className={classes.popup}
onClose={handleClose}

View File

@@ -0,0 +1,11 @@
const issueAPI = {
getIssues: async (token) => {
return { "data": [{ "id": 18, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" }, { "id": 19, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" }, { "id": 20, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" }, { "id": 21, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" }, { "id": 22, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" }, { "id": 25, "vin": "1GNGC26RXXJ407648", "title": "Example HMI Problem", "description": "HMI blue screen", "driver_id": "0b6b1930-b20a-4fce-967a-efac6a01fd10", "timestamp": "2022-12-19T22:25:03.848855Z" }, { "id": 26, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" }, { "id": 27, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" }, { "id": 28, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z" }], "total": 9 }
},
getIssue: async (token) => {
return { "data": { "id": 18, "vin": "1GNGC26RXXJ407648", "title": "sometitle", "description": "2343242", "driver_id": "valid-cognito-id-1", "timestamp": "2022-12-09T23:16:38.074858Z", "images": [{ "id": 15, "image": "SGVsbG8x", "issue_id": 18 }] } }
}
}
export default issueAPI;

View File

@@ -1,4 +1,7 @@
const SupersetAPI = {
getGuestToken: async () => {
return ""
},
getEmbeddedDashboards: async () => {
return [{
title: "test title",
@@ -7,7 +10,8 @@ const SupersetAPI = {
},
SupersetDashboardID: () => {
return "11111100-0000-1111-1111-000000000000"
}
},
SupersetDashboardURL: () => (null),
}
export default SupersetAPI

View File

@@ -59,6 +59,9 @@ const suppliersAPI = {
if (index >= 0) data[index] = supplier;
return supplier;
},
getManufactureCert: async () => {
return {public_key:"-----BEGIN CERTIFICATE-----\nTEST\n-----END CERTIFICATE-----",private_key:"-----BEGIN RSA PRIVATE KEY-----\nTEST\n-----END RSA PRIVATE KEY-----","serial_number":"66:c8:45:20:bd:75:04:79:e8:5e:0e:46:5b:5c:1a:21:8b:ea:81:9f","type":"rsa"}
},
};
export default suppliersAPI;

45
src/services/issueAPI.js Normal file
View File

@@ -0,0 +1,45 @@
import {
addQueryParams, errorHandler, fetchRespHandler, getAuthHeaderOptions
} from "../utils/http";
const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL;
const issuesAPI = {
deleteIssue: async (id, token) =>
fetch(`${API_ENDPOINT}/issues/${id}`, {
method: "DELETE",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
})
.then(fetchRespHandler)
.catch(errorHandler),
getIssue: async (id, token) =>
fetch(`${API_ENDPOINT}/issues/${id}`, {
method: "GET",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
})
.then(fetchRespHandler)
.catch(errorHandler),
getIssues: async (search, token) => {
const u = addQueryParams(`${API_ENDPOINT}/issues`, search);
return fetch(u, {
method: "GET",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
})
.then(fetchRespHandler)
.catch(errorHandler);
},
};
export default issuesAPI;