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:
@@ -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
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
55
src/components/Contexts/IssueContext.jsx
Normal file
55
src/components/Contexts/IssueContext.jsx
Normal 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);
|
||||
102
src/components/Contexts/IssueContext.test.jsx
Normal file
102
src/components/Contexts/IssueContext.test.jsx
Normal 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 }] }
|
||||
35
src/components/Contexts/__mocks__/IssueContext.jsx
Normal file
35
src/components/Contexts/__mocks__/IssueContext.jsx
Normal 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(),
|
||||
});
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
217
src/components/Controls/IssueSelectionTable/index.jsx
Normal file
217
src/components/Controls/IssueSelectionTable/index.jsx
Normal 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;
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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))}
|
||||
|
||||
79
src/components/Issues/Info/Details/index.jsx
Normal file
79
src/components/Issues/Info/Details/index.jsx
Normal 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;
|
||||
21
src/components/Issues/Info/DetailsTab.jsx
Normal file
21
src/components/Issues/Info/DetailsTab.jsx
Normal 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;
|
||||
104
src/components/Issues/Info/index.jsx
Normal file
104
src/components/Issues/Info/index.jsx
Normal 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;
|
||||
51
src/components/Issues/List/index.jsx
Normal file
51
src/components/Issues/List/index.jsx
Normal 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;
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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}
|
||||
|
||||
11
src/services/__mocks__/issueAPI.js
Normal file
11
src/services/__mocks__/issueAPI.js
Normal 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;
|
||||
@@ -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
|
||||
@@ -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
45
src/services/issueAPI.js
Normal 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;
|
||||
Reference in New Issue
Block a user