CEC-1402 Merge to production (#148)
* Fix template function (#105) * CEC-638 Add EK test ECU (#106) * CEC-638 Add EK ECU * Update test * CEC-638 Should be EKS (#107) * Should be EKS * Update snapshot * CEC-624 Display update status info and ECU (#108) * Diplay ECU name in update status (#110) Optimize car update status progress control Remove car update status page test Replace with individual component tests * Handle case ECU is not in message (#111) * Refresh button label (#112) * Update ECU refresh button label * Update snapshot * remove * CEC-660 Fix release notes field (#113) * CEC-775 Manifest details component (#114) * CEC-775 Manifest details component * Code smells * Fix build warning * CEC-1050 New manifest format (#117) * CEC-1050 Manifest changes * Fix delete bug * Add approve update button * Code smell * Remove update approval * CEC-464 can filters forms (#118) * can filters forms and lists * unit tests * updating warnings and tests * merge develop * fixed snapshots * update jest mocks * updating tests * CEC-1050 Self download indicator (#119) * CEC-1160 Fix package warnings (#121) * CEC-1160 Last dependabot fix (#122) * CEC-1058 fleet forms (#123) * working fleets page * unit tests * snapshots * updating messages and snapshots * updating extraneous snaps * Update codeowners (#125) * CEC-1167 ota admin portal (#127) * Add test coverage script * Remove unnecessary check * CEC-1167 unit test and code coverage * included sonar job * updated the workflow * updated sonar properties * updated sonar properties * updated sonar properties * updated sonar properties * updated sonar properties * updated sonar properties * updated sonar properties * updated sonar properties Co-authored-by: jwu-fisker <jwu@fiskerinc.com> * CEC-1167 implementing ths coverage thresold (#128) * CEC-1216 Remove unused components (#129) * CEC-1216 Remove unused components * Remove import * CEC-1183/CEC-1201 fleet vehicles forms (#130) * working fleet vehicles forms * snapshots and api tests * CEC-1182 fleet filter forms (#131) * forms for fleet can filters * unit tests for fleet filters * removing warnings * updating regex * CEC-532 Display manifest file properties (#133) * CEC-532 Display update file properties * npm audit fix * CEC-1317 npm update (#134) * CEC-1320 Update for memory regions (#135) * CEC-1320 Update for memory regions * Clean up * CEC-1256/CEC-1330 data logger for vehicles/fleets and details tabs for vehicles/fleets (#136) * forms for fleet can filters * unit tests for fleet filters * removing warnings * updating regex * added fleet details page * fleet pages * smoothed out bugs * fleets done * working update, delete vehicles * finished mocks, still need snapshots and context tests * contexts done * snapshot tests * updating code smells * smells * CEC-1256/CEC-1330 fixing filters length function (#137) * fixing filters length function * adding filters testing * code smell * code smells * bug * CEC-1387 superset integration and removal of grafana (#138) * replace grafana with superset * updating snapshots * CEC-1316 azure migration (#140) * test portal azure * :doh: * runner * WIP * values * letsencrypt + docker cache * stg/prd * portal things * cleanup * split build/deploy + temp stage deploy * :doh: * try this * and prod * this works for now, can improve later * no need to specify azure anymore Co-authored-by: Drew Taylor <69828061+drew-fisker@users.noreply.github.com> * CEC-1369 Fix display of update error (#139) * CEC-1369 Fix display of update error * Update snapshot * CEC-749 Generate cert UI (#141) * Add Create Certificate page * Tests * Update permission check * Use Azure * CEC-1387 updating superset dns names (#142) * updating superset dns names * updating snapshots * Fix (#143) * CEC-749 Fix types (#144) * Merge branch 'develop' Co-authored-by: Drew Taylor <69828061+drew-fisker@users.noreply.github.com> Co-authored-by: venkats09 <97122017+venkats09@users.noreply.github.com> Co-authored-by: Rafi Greenberg <72412693+rafi-fisker@users.noreply.github.com>
This commit is contained in:
@@ -4,7 +4,6 @@ jest.mock("../Contexts/VehicleContext");
|
||||
jest.mock("../Contexts/ManifestCreateContext");
|
||||
jest.mock("../Contexts/ManifestsContext");
|
||||
jest.mock("../Contexts/UserContext");
|
||||
jest.mock("../../services/grafanaAPI");
|
||||
jest.mock("../../services/monitoring");
|
||||
jest.mock("../../services/vehiclesAPI");
|
||||
|
||||
@@ -44,16 +43,29 @@ const sleepAndCheck = async (path, selector, compare) => {
|
||||
};
|
||||
|
||||
describe("App", () => {
|
||||
const rxMakeStyles = /makeStyles-(\w+)-(\d+)/gi;
|
||||
|
||||
beforeAll(() => {
|
||||
// Stablize Table Pagination control ids
|
||||
expect.addSnapshotSerializer({
|
||||
test: function (val) {
|
||||
return val && typeof val === "string" && val.indexOf("mui-") >= 0;
|
||||
return val && typeof val === "string" && val.indexOf("mui-") > -1;
|
||||
},
|
||||
print: function (val) {
|
||||
let str = val;
|
||||
str = str.replace(/mui-\d*/g, "mui-00000");
|
||||
|
||||
return `"${str}"`;
|
||||
},
|
||||
});
|
||||
expect.addSnapshotSerializer({
|
||||
test: (val) => {
|
||||
return val && typeof val === "string" && val.search(rxMakeStyles) > -1;
|
||||
},
|
||||
print: function (val) {
|
||||
let str = val;
|
||||
str = str.replace(rxMakeStyles, "makeStyles-$1-0000");
|
||||
|
||||
return `"${str}"`;
|
||||
},
|
||||
});
|
||||
@@ -72,14 +84,6 @@ describe("App", () => {
|
||||
await sleepAndCheck("/home", "span.MuiButton-label", "Sign In");
|
||||
});
|
||||
|
||||
it("Route /datascope unauthenticated", async () => {
|
||||
await sleepAndCheck("/datascope", "span.MuiButton-label", "Sign In");
|
||||
});
|
||||
|
||||
it("Route /datascope/battery unauthenticated", async () => {
|
||||
await check("/datascope/battery", "span.MuiButton-label", "Sign In");
|
||||
});
|
||||
|
||||
it("Route /packages unauthenticated", async () => {
|
||||
await check("/packages", "span.MuiButton-label", "Sign In");
|
||||
});
|
||||
@@ -116,6 +120,10 @@ describe("App", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("Route /tools/certificates/add unauthenticated", async () => {
|
||||
await check("/tools/certificates/add", "span.MuiButton-label", "Sign In");
|
||||
});
|
||||
|
||||
it("Route /page-not-found unauthenticated", async () => {
|
||||
await check("/page-not-found", "h1", "Page Not Found");
|
||||
});
|
||||
@@ -134,16 +142,6 @@ describe("App", () => {
|
||||
await check("/page-not-found", "h1", "Page Not Found");
|
||||
});
|
||||
|
||||
it("Route /datascope authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await sleepAndCheck("/datascope", "h6", "Datascope");
|
||||
});
|
||||
|
||||
it("Route /datascope/battery authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/datascope/battery", "h6", "Battery");
|
||||
});
|
||||
|
||||
it("Route /packages authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/packages", "h6", "Deployments");
|
||||
@@ -178,4 +176,9 @@ describe("App", () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/vehicle-status/FISKER123", "h6", "Vehicle FISKER123 Details");
|
||||
});
|
||||
|
||||
it("Route /tools/certificates/add authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/tools/certificates/add", "h6", "Create Certificate");
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
177
src/components/CANFilter/Add/__snapshots__/index.test.jsx.snap
Normal file
177
src/components/CANFilter/Add/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,177 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CANFiltersAdd Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-canfiltersprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-canfiltersprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3"
|
||||
>
|
||||
<form
|
||||
action="{onSubmit}"
|
||||
class="makeStyles-form-5"
|
||||
novalidate=""
|
||||
>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||
data-shrink="false"
|
||||
for="vin"
|
||||
id="vin-label"
|
||||
>
|
||||
VIN
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="vin"
|
||||
maxlength="255"
|
||||
name="vin"
|
||||
readonly=""
|
||||
required=""
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64"
|
||||
>
|
||||
<span>
|
||||
VIN
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||
data-shrink="false"
|
||||
for="canId"
|
||||
id="canId-label"
|
||||
>
|
||||
CAN ID
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="canId"
|
||||
maxlength="255"
|
||||
name="canId"
|
||||
required=""
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64"
|
||||
>
|
||||
<span>
|
||||
CAN ID
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined"
|
||||
data-shrink="false"
|
||||
for="interval"
|
||||
id="interval-label"
|
||||
>
|
||||
Interval
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="interval"
|
||||
maxlength="255"
|
||||
name="interval"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64"
|
||||
>
|
||||
<span>
|
||||
Interval
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label"
|
||||
>
|
||||
Submit
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
f
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
127
src/components/CANFilter/Add/index.jsx
Normal file
127
src/components/CANFilter/Add/index.jsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Redirect } from "react-router";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { Button, TextField } from "@material-ui/core";
|
||||
|
||||
import {
|
||||
CANFiltersProvider,
|
||||
useCANFiltersContext,
|
||||
} from "../../Contexts/CANFiltersContext";
|
||||
import { useUserContext } from "../../Contexts/UserContext";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import useStyles from "../../useStyles";
|
||||
import { logger } from "../../../services/monitoring";
|
||||
|
||||
|
||||
const MainForm = () => {
|
||||
const { addFilter, busy } = useCANFiltersContext();
|
||||
const { token: { idToken: { jwtToken: token } } } = useUserContext();
|
||||
const { setMessage, setTitle, setSitePath } = useStatusContext();
|
||||
const [redirect, setRedirect] = useState(null);
|
||||
const classes = useStyles();
|
||||
const canIdEl = useRef(null);
|
||||
const intervalEl = useRef(null);
|
||||
const queries = new URLSearchParams(useLocation().search);
|
||||
const vin = queries.get("vin") ?? ""
|
||||
|
||||
useEffect(() => {
|
||||
setTitle("Create CAN Filter");
|
||||
setSitePath([
|
||||
{
|
||||
label: `Vehicle ${vin}`,
|
||||
link: "/vehicles",
|
||||
},
|
||||
{
|
||||
label: "Create CAN Filter",
|
||||
},
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (event) => {
|
||||
try {
|
||||
event.preventDefault();
|
||||
const formData = {
|
||||
can_id: canIdEl.current.value,
|
||||
interval: parseInt(intervalEl.current.value)
|
||||
};
|
||||
const result = await addFilter(vin, formData, token);
|
||||
if (!result || result.error) return;
|
||||
|
||||
setMessage(`Added filter`);
|
||||
setRedirect(`/vehicle-status/${vin}#filters`);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
};
|
||||
|
||||
if (redirect && redirect.length > 0) {
|
||||
return <Redirect to={redirect} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.paper}>
|
||||
<form className={classes.form} noValidate action="{onSubmit}">
|
||||
<TextField
|
||||
id="vin"
|
||||
name="vin"
|
||||
label="VIN"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "255",
|
||||
readOnly: true,
|
||||
}}
|
||||
value={vin}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
id="canId"
|
||||
name="canId"
|
||||
label="CAN ID"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "255",
|
||||
}}
|
||||
required
|
||||
fullWidth
|
||||
inputRef={canIdEl}
|
||||
/>
|
||||
<TextField
|
||||
id="interval"
|
||||
name="interval"
|
||||
label="Interval"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "255",
|
||||
}}
|
||||
fullWidth
|
||||
inputRef={intervalEl}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.submit}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CANFilterCreate = (props) => (
|
||||
<CANFiltersProvider>
|
||||
<MainForm {...props} />
|
||||
</CANFiltersProvider>
|
||||
);
|
||||
|
||||
export default CANFilterCreate;
|
||||
36
src/components/CANFilter/Add/index.test.jsx
Normal file
36
src/components/CANFilter/Add/index.test.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
jest.mock("../../Contexts/CANFiltersContext");
|
||||
jest.mock("../../Contexts/StatusContext");
|
||||
jest.mock("../../Contexts/UserContext");
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { CANFiltersProvider } from "../../Contexts/CANFiltersContext";
|
||||
import { StatusProvider } from "../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
|
||||
import MainForm from "./index"
|
||||
|
||||
const renderCANFiltersAdd = async () => {
|
||||
const { container } = render(
|
||||
<CANFiltersProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<MainForm />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>f
|
||||
</CANFiltersProvider>
|
||||
);
|
||||
await waitFor(() => { });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("CANFiltersAdd", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderCANFiltersAdd();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
453
src/components/CANFilter/Table/__snapshots__/index.test.jsx.snap
Normal file
453
src/components/CANFilter/Table/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,453 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CANFiltersTable Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-canfiltersprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-canfiltersprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3 makeStyles-tableSize-53"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textJustifyAlign-47 MuiGrid-item MuiGrid-grid-md-4"
|
||||
>
|
||||
<a
|
||||
class="makeStyles-labelInline-9"
|
||||
href="/filter-add?vin=undefined"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-8"
|
||||
>
|
||||
<div
|
||||
class="MuiFormControl-root makeStyles-margin-28 makeStyles-fullWidth-50"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated"
|
||||
data-shrink="false"
|
||||
for="search"
|
||||
>
|
||||
Search
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedEnd"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedEnd"
|
||||
id="search"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
class="MuiInputAdornment-root MuiInputAdornment-positionEnd"
|
||||
>
|
||||
<button
|
||||
aria-label="search"
|
||||
class="MuiButtonBase-root MuiIconButton-root"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table
|
||||
class="MuiTable-root"
|
||||
>
|
||||
<thead
|
||||
class="MuiTableHead-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root MuiTableRow-head"
|
||||
>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
CAN ID
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Interval (ms)
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="MuiTableBody-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
123
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
1000
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="/filter-update?vin=undefined&can_id=123&interval=1000"
|
||||
style="margin: 5px;"
|
||||
title="Update \\"123\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Update 123"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class=""
|
||||
href="/"
|
||||
title="Delete \\"123\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Delete 123"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="MuiTableRow-root"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
456-789
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
2000
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="/filter-update?vin=undefined&can_id=456-789&interval=2000"
|
||||
style="margin: 5px;"
|
||||
title="Update \\"456-789\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Update 456-789"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class=""
|
||||
href="/"
|
||||
title="Delete \\"456-789\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Delete 456-789"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="MuiTableRow-root"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
1
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
0
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="/filter-update?vin=undefined&can_id=1&interval=0"
|
||||
style="margin: 5px;"
|
||||
title="Update \\"1\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Update 1"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class=""
|
||||
href="/"
|
||||
title="Delete \\"1\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Delete 1"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot
|
||||
class="MuiTableFooter-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root MuiTableRow-footer"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
|
||||
colspan="8"
|
||||
>
|
||||
<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>
|
||||
<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"
|
||||
>
|
||||
1-3 of 3
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
210
src/components/CANFilter/Table/index.jsx
Normal file
210
src/components/CANFilter/Table/index.jsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Grid,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TablePagination,
|
||||
TableRow,
|
||||
Tooltip,
|
||||
} from "@material-ui/core";
|
||||
import AddCircleIcon from "@material-ui/icons/AddCircle";
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import clsx from "clsx";
|
||||
|
||||
import {
|
||||
CANFiltersProvider,
|
||||
useCANFiltersContext,
|
||||
} from "../../Contexts/CANFiltersContext";
|
||||
import TableHeaderSortable from "../../Table/HeaderSortable";
|
||||
import {
|
||||
useUserContext
|
||||
} from "../../Contexts/UserContext"
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import useStyles from "../../useStyles";
|
||||
import SearchField from "../../Controls/SearchField";
|
||||
import { logger } from "../../../services/monitoring";
|
||||
import { Roles, hasRole } from "../../../utils/roles";
|
||||
|
||||
const tableColumns = [
|
||||
{
|
||||
id: "can_id",
|
||||
label: "CAN ID"
|
||||
},
|
||||
{
|
||||
id: "interval",
|
||||
label: "Interval (ms)"
|
||||
},
|
||||
{
|
||||
id: "",
|
||||
label: "Actions"
|
||||
}
|
||||
];
|
||||
|
||||
const MainForm = ({ vin }) => {
|
||||
const classes = useStyles();
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [orderBy, setOrderBy] = useState("id");
|
||||
const [order, setOrder] = useState("desc");
|
||||
const { getFilters, deleteFilter, filters, totalFilters } = useCANFiltersContext();
|
||||
const { setMessage } = useStatusContext();
|
||||
const { token: { idToken: { jwtToken: token } }, groups } = useUserContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (!vin || !token) return;
|
||||
await getFilters(
|
||||
vin,
|
||||
{
|
||||
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
|
||||
}, [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);
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async (can_id) => {
|
||||
try {
|
||||
await deleteFilter(vin, can_id, token);
|
||||
setMessage(`Deleted ${can_id}`)
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
};
|
||||
|
||||
const Actions = (row) => {
|
||||
let actions = [];
|
||||
if (hasRole([Roles.CREATE], groups)) {
|
||||
actions.push({
|
||||
tip: `Update "${row.can_id}"`,
|
||||
link: `/filter-update?vin=${vin}&can_id=${row.can_id}&interval=${row.interval}`,
|
||||
icon: <EditIcon aria-label={`Update ${row.can_id}`} />
|
||||
});
|
||||
}
|
||||
if (hasRole([Roles.DELETE], groups)) {
|
||||
actions.push({
|
||||
tip: `Delete "${row.can_id}"`,
|
||||
id: row.can_id,
|
||||
icon: <DeleteIcon aria-label={`Delete ${row.can_id}`} />
|
||||
})
|
||||
}
|
||||
if (actions.length === 0) return ["No actions"];
|
||||
|
||||
return actions.map((action) => {
|
||||
if (action.link != null) {
|
||||
return (
|
||||
<Tooltip key={action.link} title={action.tip}>
|
||||
<Link to={action.link} style={{ margin: 5 }}>
|
||||
{action.icon}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tooltip key={`delete-${action.id}`} title={action.tip}>
|
||||
<Link to="#" onClick={() => onDelete(action.id)}>
|
||||
{action.icon}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.paper, classes.tableSize)}>
|
||||
<Grid container className={classes.root} spacing={2}>
|
||||
<Grid item md={4} className={classes.textJustifyAlign}>
|
||||
<Link to={`/filter-add?vin=${vin}`} className={classes.labelInline}>
|
||||
<AddCircleIcon fontSize="large" />
|
||||
</Link>
|
||||
</Grid>
|
||||
<Grid item md={8} className={classes.textCenterAlign}>
|
||||
<SearchField classes={classes} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Table>
|
||||
<TableHeaderSortable
|
||||
classes={classes}
|
||||
orderBy={orderBy}
|
||||
order={order}
|
||||
columnData={tableColumns}
|
||||
onSortRequest={handleSort}
|
||||
/>
|
||||
<TableBody>
|
||||
{filters.map((row) => (
|
||||
<TableRow key={row.can_id}>
|
||||
<TableCell align="center">{row.can_id}</TableCell>
|
||||
<TableCell align="center">{row.interval}</TableCell>
|
||||
<TableCell align="center">{Actions(row)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25, 100]}
|
||||
colSpan={8}
|
||||
count={totalFilters}
|
||||
rowsPerPage={pageSize}
|
||||
page={pageIndex}
|
||||
SelectProps={{
|
||||
inputProps: { "aria-label": "rows per page" },
|
||||
native: true,
|
||||
}}
|
||||
onPageChange={handleChangePageIndex}
|
||||
onRowsPerPageChange={handleChangePageSize}
|
||||
/>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
const CANFiltersTable = (props) => (
|
||||
<CANFiltersProvider>
|
||||
<MainForm {...props} />
|
||||
</CANFiltersProvider>
|
||||
);
|
||||
|
||||
export default CANFiltersTable;
|
||||
39
src/components/CANFilter/Table/index.test.jsx
Normal file
39
src/components/CANFilter/Table/index.test.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
jest.mock("../../Contexts/CANFiltersContext");
|
||||
jest.mock("../../Contexts/StatusContext");
|
||||
jest.mock("../../Contexts/UserContext");
|
||||
jest.mock('@material-ui/core/utils/unstable_useId', () =>
|
||||
jest.fn().mockReturnValue('mui-test-id'),
|
||||
);
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { CANFiltersProvider } from "../../Contexts/CANFiltersContext";
|
||||
import { StatusProvider } from "../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
|
||||
import MainForm from "./index"
|
||||
|
||||
const renderCANFiltersTable = async () => {
|
||||
const { container } = render(
|
||||
<CANFiltersProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<MainForm />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</CANFiltersProvider>
|
||||
);
|
||||
await waitFor(() => { });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("CANFiltersTable", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderCANFiltersTable();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CANFiltersUpdate Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-canfiltersprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-canfiltersprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3"
|
||||
>
|
||||
<form
|
||||
action="{onSubmit}"
|
||||
class="makeStyles-form-5"
|
||||
novalidate=""
|
||||
>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||
data-shrink="false"
|
||||
for="vin"
|
||||
id="vin-label"
|
||||
>
|
||||
VIN
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="vin"
|
||||
maxlength="255"
|
||||
name="vin"
|
||||
readonly=""
|
||||
required=""
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64"
|
||||
>
|
||||
<span>
|
||||
VIN
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||
data-shrink="false"
|
||||
for="canId"
|
||||
id="canId-label"
|
||||
>
|
||||
CAN ID
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="canId"
|
||||
maxlength="255"
|
||||
name="canId"
|
||||
readonly=""
|
||||
required=""
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64"
|
||||
>
|
||||
<span>
|
||||
CAN ID
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||
data-shrink="false"
|
||||
for="interval"
|
||||
id="interval-label"
|
||||
>
|
||||
Interval
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="interval"
|
||||
maxlength="255"
|
||||
name="interval"
|
||||
required=""
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64"
|
||||
>
|
||||
<span>
|
||||
Interval
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label"
|
||||
>
|
||||
Submit
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
131
src/components/CANFilter/Update/index.jsx
Normal file
131
src/components/CANFilter/Update/index.jsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Redirect } from "react-router";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { Button, TextField } from "@material-ui/core";
|
||||
|
||||
import {
|
||||
CANFiltersProvider,
|
||||
useCANFiltersContext,
|
||||
} from "../../Contexts/CANFiltersContext";
|
||||
import { useUserContext } from "../../Contexts/UserContext";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import useStyles from "../../useStyles";
|
||||
import { logger } from "../../../services/monitoring";
|
||||
|
||||
|
||||
const MainForm = () => {
|
||||
const { updateFilter, busy } = useCANFiltersContext();
|
||||
const { token: { idToken: { jwtToken: token } } } = useUserContext();
|
||||
const { setMessage, setTitle, setSitePath } = useStatusContext();
|
||||
const [redirect, setRedirect] = useState(null);
|
||||
const classes = useStyles();
|
||||
const intervalEl = useRef(null);
|
||||
const queries = new URLSearchParams(useLocation().search);
|
||||
const vin = queries.get("vin") ?? ""
|
||||
const canID = queries.get("can_id") ?? ""
|
||||
const interval = queries.get("interval") ?? ""
|
||||
|
||||
useEffect(() => {
|
||||
setTitle("Update CAN Filter");
|
||||
setSitePath([
|
||||
{
|
||||
label: `Vehicle ${vin}`,
|
||||
link: "/vehicles",
|
||||
},
|
||||
{
|
||||
label: "Update CAN Filter",
|
||||
},
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (event) => {
|
||||
try {
|
||||
event.preventDefault();
|
||||
const formData = {
|
||||
can_id: canID,
|
||||
interval: parseInt(intervalEl.current.value)
|
||||
};
|
||||
const result = await updateFilter(vin, canID, formData, token);
|
||||
if (!result || result.error) return;
|
||||
|
||||
setMessage(`Updated filter`);
|
||||
setRedirect(`/vehicle-status/${vin}#filters`);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
};
|
||||
|
||||
if (redirect && redirect.length > 0) {
|
||||
return <Redirect to={redirect} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.paper}>
|
||||
<form className={classes.form} noValidate action="{onSubmit}">
|
||||
<TextField
|
||||
id="vin"
|
||||
name="vin"
|
||||
label="VIN"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "255",
|
||||
readOnly: true,
|
||||
}}
|
||||
value={vin}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
id="canId"
|
||||
name="canId"
|
||||
label="CAN ID"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "255",
|
||||
readOnly: true,
|
||||
}}
|
||||
value={canID}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
id="interval"
|
||||
name="interval"
|
||||
label="Interval"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "255",
|
||||
}}
|
||||
defaultValue={interval}
|
||||
required
|
||||
fullWidth
|
||||
inputRef={intervalEl}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.submit}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CANFilterUpdate = (props) => (
|
||||
<CANFiltersProvider>
|
||||
<MainForm {...props} />
|
||||
</CANFiltersProvider>
|
||||
);
|
||||
|
||||
export default CANFilterUpdate;
|
||||
36
src/components/CANFilter/Update/index.test.jsx
Normal file
36
src/components/CANFilter/Update/index.test.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
jest.mock("../../Contexts/CANFiltersContext");
|
||||
jest.mock("../../Contexts/StatusContext");
|
||||
jest.mock("../../Contexts/UserContext");
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { CANFiltersProvider } from "../../Contexts/CANFiltersContext";
|
||||
import { StatusProvider } from "../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
|
||||
import MainForm from "./index"
|
||||
|
||||
const renderCANFiltersUpdate = async () => {
|
||||
const { container } = render(
|
||||
<CANFiltersProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<MainForm />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</CANFiltersProvider>
|
||||
);
|
||||
await waitFor(() => { });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("CANFiltersUpdate", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderCANFiltersUpdate();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
730
src/components/Cars/Add/__snapshots__/index.test.jsx.snap
Normal file
730
src/components/Cars/Add/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,730 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VehicleAddForm Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-vehicleprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-vehicleprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3"
|
||||
>
|
||||
<form
|
||||
action="{onSubmit}"
|
||||
class="makeStyles-form-5"
|
||||
novalidate=""
|
||||
>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||
data-shrink="false"
|
||||
for="vin"
|
||||
id="vin-label"
|
||||
>
|
||||
VIN
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="vin"
|
||||
maxlength="17"
|
||||
name="vin"
|
||||
required=""
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64"
|
||||
>
|
||||
<span>
|
||||
VIN
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
|
||||
data-shrink="true"
|
||||
for="model"
|
||||
id="model-label"
|
||||
>
|
||||
Model
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="model"
|
||||
maxlength="255"
|
||||
name="model"
|
||||
required=""
|
||||
type="text"
|
||||
value="Ocean"
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
|
||||
>
|
||||
<span>
|
||||
Model
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
|
||||
data-shrink="true"
|
||||
for="year"
|
||||
id="year-label"
|
||||
>
|
||||
Year
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="year"
|
||||
maxlength="4"
|
||||
minlength="4"
|
||||
name="year"
|
||||
required=""
|
||||
type="number"
|
||||
value="2022"
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
|
||||
>
|
||||
<span>
|
||||
Year
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
|
||||
data-shrink="true"
|
||||
for="trim"
|
||||
id="trim-label"
|
||||
>
|
||||
Trim
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="trim"
|
||||
maxlength="4"
|
||||
minlength="4"
|
||||
name="trim"
|
||||
required=""
|
||||
type="text"
|
||||
value="Base"
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
|
||||
>
|
||||
<span>
|
||||
Trim
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<label
|
||||
class="MuiFormLabel-root"
|
||||
id="demo-row-radio-buttons-group-label"
|
||||
>
|
||||
Log Level
|
||||
</label>
|
||||
<div
|
||||
aria-labelledby="demo-row-radio-buttons-group-label"
|
||||
class="MuiFormGroup-root MuiFormGroup-row"
|
||||
margin="normal"
|
||||
role="radiogroup"
|
||||
>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="trace"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Trace
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="debug"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Debug
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary PrivateSwitchBase-checked-67 Mui-checked MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="info"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70 PrivateRadioButtonIcon-checked-72"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Info
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="warn"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Warn
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="error"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Error
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="critical"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Critical
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<label
|
||||
class="MuiFormLabel-root"
|
||||
id="demo-row-radio-buttons-group-label"
|
||||
>
|
||||
CAN Bus
|
||||
</label>
|
||||
<div
|
||||
class="MuiFormGroup-root"
|
||||
>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiCheckbox-root MuiCheckbox-colorSecondary PrivateSwitchBase-checked-67 Mui-checked MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="PrivateSwitchBase-input-69"
|
||||
data-indeterminate="false"
|
||||
type="checkbox"
|
||||
value=""
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
CAN Bus Enabled
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
|
||||
data-shrink="true"
|
||||
for="max_mem_buffer_size"
|
||||
id="max_mem_buffer_size-label"
|
||||
>
|
||||
Max Memory Buffer Size (0 uses default size)
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="max_mem_buffer_size"
|
||||
maxlength="12"
|
||||
name="max_mem_buffer_size"
|
||||
required=""
|
||||
type="number"
|
||||
value="0"
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
|
||||
>
|
||||
<span>
|
||||
Max Memory Buffer Size (0 uses default size)
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiCheckbox-root MuiCheckbox-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
data-indeterminate="false"
|
||||
type="checkbox"
|
||||
value=""
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Data Logger Enabled
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined Mui-disabled Mui-disabled MuiFormLabel-filled Mui-required Mui-required"
|
||||
data-shrink="true"
|
||||
for="max_disk_buffer_size"
|
||||
id="max_disk_buffer_size-label"
|
||||
>
|
||||
Max Disk Buffer Size (0 uses default size)
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root Mui-disabled Mui-disabled MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input Mui-disabled Mui-disabled"
|
||||
disabled=""
|
||||
id="max_disk_buffer_size"
|
||||
maxlength="12"
|
||||
name="max_disk_buffer_size"
|
||||
required=""
|
||||
type="number"
|
||||
value="0"
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
|
||||
>
|
||||
<span>
|
||||
Max Disk Buffer Size (0 uses default size)
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label"
|
||||
>
|
||||
Submit
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,4 +1,15 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Redirect } from "react-router";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
FormGroup,
|
||||
FormLabel,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
TextField
|
||||
} from "@material-ui/core";
|
||||
|
||||
import useStyles from "../../useStyles";
|
||||
import {
|
||||
@@ -7,7 +18,6 @@ import {
|
||||
} from "../../Contexts/VehicleContext";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import { useUserContext } from "../../Contexts/UserContext";
|
||||
import { Button, TextField } from "@material-ui/core";
|
||||
import { logger } from "../../../services/monitoring";
|
||||
|
||||
const MainForm = () => {
|
||||
@@ -19,10 +29,17 @@ const MainForm = () => {
|
||||
},
|
||||
} = useUserContext();
|
||||
const classes = useStyles();
|
||||
const [redirect, setRedirect] = useState(null);
|
||||
|
||||
const vinEl = useRef(null);
|
||||
const modelEl = useRef(null);
|
||||
const yearEl = useRef(null);
|
||||
const trimEl = useRef(null);
|
||||
const [selectedLogLevel, setSelectedLogLevel] = useState("info");
|
||||
const [canbusEnabled, setCANBusEnabled] = useState(true);
|
||||
const [dataLoggerEnabled, setDataLoggerEnabled] = useState(false);
|
||||
const [maxMemBufferSize, setMaxMemBufferSize] = useState(0);
|
||||
const [maxDiskBufferSize, setMaxDiskBufferSize] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle("Add Vehicle");
|
||||
@@ -37,6 +54,27 @@ const MainForm = () => {
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onLogLevelChange = (event) => {
|
||||
setSelectedLogLevel(event.target.value);
|
||||
}
|
||||
|
||||
const onCANBusChange = (event) => {
|
||||
setCANBusEnabled(event.target.checked);
|
||||
}
|
||||
|
||||
const onDataLoggerChange = (event) => {
|
||||
setDataLoggerEnabled(event.target.checked);
|
||||
}
|
||||
|
||||
const onMaxMemBufferSizeChange = (event) => {
|
||||
setMaxMemBufferSize(event.target.value);
|
||||
}
|
||||
|
||||
const onMaxDiskBufferSizeChange = (event) => {
|
||||
setMaxDiskBufferSize(event.target.value);
|
||||
}
|
||||
|
||||
const onSubmit = async (event) => {
|
||||
try {
|
||||
event.preventDefault();
|
||||
@@ -46,18 +84,29 @@ const MainForm = () => {
|
||||
model: modelEl.current.value,
|
||||
year: parseInt(yearEl.current.value),
|
||||
trim: trimEl.current.value,
|
||||
log_level: selectedLogLevel,
|
||||
canbus: {
|
||||
enabled: canbusEnabled,
|
||||
data_logger_enabled: canbusEnabled ? dataLoggerEnabled : false,
|
||||
max_mem_buffer_size: canbusEnabled ? parseInt(maxMemBufferSize) : 0,
|
||||
max_disk_buffer_size: canbusEnabled && dataLoggerEnabled ? parseInt(maxDiskBufferSize) : 0
|
||||
}
|
||||
};
|
||||
|
||||
const result = await addVehicle(formData, token);
|
||||
|
||||
setMessage(`Added ${result.vin}`);
|
||||
vinEl.current.value = "";
|
||||
setRedirect(`/vehicles`);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
};
|
||||
|
||||
if (redirect && redirect.length > 0) {
|
||||
return <Redirect to={redirect} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.paper}>
|
||||
<form className={classes.form} noValidate action="{onSubmit}">
|
||||
@@ -119,6 +168,70 @@ const MainForm = () => {
|
||||
fullWidth
|
||||
inputRef={trimEl}
|
||||
/>
|
||||
<FormLabel id="demo-row-radio-buttons-group-label">Log Level</FormLabel>
|
||||
<RadioGroup
|
||||
row
|
||||
aria-labelledby="demo-row-radio-buttons-group-label"
|
||||
name="log-level-group"
|
||||
value={selectedLogLevel}
|
||||
onChange={onLogLevelChange}
|
||||
margin="normal"
|
||||
>
|
||||
<FormControlLabel value="trace" control={<Radio />} label="Trace" />
|
||||
<FormControlLabel value="debug" control={<Radio />} label="Debug" />
|
||||
<FormControlLabel value="info" control={<Radio />} label="Info" />
|
||||
<FormControlLabel value="warn" control={<Radio />} label="Warn" />
|
||||
<FormControlLabel value="error" control={<Radio />} label="Error" />
|
||||
<FormControlLabel value="critical" control={<Radio />} label="Critical" />
|
||||
</RadioGroup>
|
||||
<FormLabel id="demo-row-radio-buttons-group-label">CAN Bus</FormLabel>
|
||||
<FormGroup>
|
||||
<FormControlLabel control={
|
||||
<Checkbox
|
||||
checked={canbusEnabled}
|
||||
onChange={onCANBusChange}
|
||||
/>
|
||||
} label="CAN Bus Enabled" />
|
||||
<TextField
|
||||
id="max_mem_buffer_size"
|
||||
name="max_mem_buffer_size"
|
||||
label='Max Memory Buffer Size (0 uses default size)'
|
||||
value={maxMemBufferSize}
|
||||
onChange={onMaxMemBufferSizeChange}
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "12",
|
||||
}}
|
||||
type="number"
|
||||
disabled={!canbusEnabled}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<FormControlLabel control={
|
||||
<Checkbox
|
||||
checked={dataLoggerEnabled}
|
||||
onChange={onDataLoggerChange}
|
||||
disabled={!canbusEnabled}
|
||||
/>
|
||||
} label="Data Logger Enabled" />
|
||||
</FormGroup>
|
||||
<TextField
|
||||
id="max_disk_buffer_size"
|
||||
name="max_disk_buffer_size"
|
||||
label='Max Disk Buffer Size (0 uses default size)'
|
||||
value={maxDiskBufferSize}
|
||||
onChange={onMaxDiskBufferSizeChange}
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "12",
|
||||
}}
|
||||
type="number"
|
||||
disabled={!dataLoggerEnabled}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
|
||||
36
src/components/Cars/Add/index.test.jsx
Normal file
36
src/components/Cars/Add/index.test.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
jest.mock("../../Contexts/VehicleContext");
|
||||
jest.mock("../../Contexts/StatusContext");
|
||||
jest.mock("../../Contexts/UserContext");
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { VehicleProvider } from "../../Contexts/VehicleContext";
|
||||
import { StatusProvider } from "../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
|
||||
import MainForm from "./index"
|
||||
|
||||
const renderVehicleAdd = async () => {
|
||||
const { container } = render(
|
||||
<VehicleProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<MainForm />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</VehicleProvider>
|
||||
);
|
||||
await waitFor(() => { /* render */ });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("VehicleAddForm", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderVehicleAdd();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,174 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TablePagination,
|
||||
TableRow,
|
||||
} from "@material-ui/core";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { LocalDateTimeString } from "../../../utils/dates";
|
||||
import TableHeaderSortable from "../../Table/HeaderSortable";
|
||||
import { useVehicleContext } from "../../Contexts/VehicleContext";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import useStyles from "../../useStyles";
|
||||
import { logger } from "../../../services/monitoring";
|
||||
|
||||
const tableColumns = [
|
||||
{
|
||||
id: "ecu",
|
||||
label: "ECU",
|
||||
},
|
||||
{
|
||||
id: "sw_version",
|
||||
label: "SW Version",
|
||||
},
|
||||
{
|
||||
id: "boot_loader_version",
|
||||
label: "BL Version",
|
||||
},
|
||||
{
|
||||
id: "hw_version",
|
||||
label: "HW Version",
|
||||
},
|
||||
{
|
||||
id: "vendor",
|
||||
label: "Vendor",
|
||||
},
|
||||
{
|
||||
id: "config",
|
||||
label: "Config",
|
||||
},
|
||||
{
|
||||
id: "fingerprint",
|
||||
label: "Fingerprint",
|
||||
},
|
||||
{
|
||||
id: "serial_number",
|
||||
label: "Serial",
|
||||
},
|
||||
{
|
||||
id: "created_at",
|
||||
label: "Created",
|
||||
},
|
||||
{
|
||||
id: "updated_at",
|
||||
label: "Updated",
|
||||
},
|
||||
];
|
||||
|
||||
const CarECUs = ({ vin, token }) => {
|
||||
const [ecus, setECUs] = useState([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const classes = useStyles();
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [orderBy, setOrderBy] = useState("ecu");
|
||||
const [order, setOrder] = useState("desc");
|
||||
const { getECUs } = useVehicleContext();
|
||||
const { setMessage } = useStatusContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (!vin || !token) return;
|
||||
const result = await getECUs(
|
||||
{
|
||||
vin,
|
||||
limit: pageSize,
|
||||
offset: pageSize * pageIndex,
|
||||
order: `${orderBy} ${order}`,
|
||||
},
|
||||
token
|
||||
);
|
||||
setECUs(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 (
|
||||
<div className={clsx(classes.paper, classes.tableSize)}>
|
||||
<Table>
|
||||
<TableHeaderSortable
|
||||
classes={classes}
|
||||
orderBy={orderBy}
|
||||
order={order}
|
||||
columnData={tableColumns}
|
||||
onSortRequest={handleSort}
|
||||
/>
|
||||
<TableBody>
|
||||
{ecus.map((row) => (
|
||||
<TableRow key={row.ecu}>
|
||||
<TableCell align="center">{row.ecu}</TableCell>
|
||||
<TableCell align="center">{row.sw_version}</TableCell>
|
||||
<TableCell align="center">{row.boot_loader_version}</TableCell>
|
||||
<TableCell align="center">{row.hw_version}</TableCell>
|
||||
<TableCell align="center">{row.vendor}</TableCell>
|
||||
<TableCell align="center">{row.config}</TableCell>
|
||||
<TableCell align="center">{row.fingerprint}</TableCell>
|
||||
<TableCell align="center">{row.serial_number}</TableCell>
|
||||
<TableCell align="center">
|
||||
{LocalDateTimeString(row.created)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{LocalDateTimeString(row.updated)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25, 100]}
|
||||
colSpan={10}
|
||||
count={total}
|
||||
rowsPerPage={pageSize}
|
||||
page={pageIndex}
|
||||
SelectProps={{
|
||||
inputProps: { "aria-label": "rows per page" },
|
||||
native: true,
|
||||
}}
|
||||
onPageChange={handleChangePageIndex}
|
||||
onRowsPerPageChange={handleChangePageSize}
|
||||
/>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CarECUs;
|
||||
@@ -1,159 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TablePagination,
|
||||
TableRow,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import { LocalDateTimeString } from "../../../utils/dates";
|
||||
import TableHeaderSortable from "../../Table/HeaderSortable";
|
||||
import {
|
||||
UpdatesProvider,
|
||||
useUpdatesContext,
|
||||
} from "../../Contexts/UpdatesContext";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import useStyles from "../../useStyles";
|
||||
import { logger } from "../../../services/monitoring";
|
||||
|
||||
const tableColumns = [
|
||||
{
|
||||
id: "id",
|
||||
label: "ID",
|
||||
},
|
||||
{
|
||||
id: "update_package_id",
|
||||
label: "Name",
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
label: "Status",
|
||||
},
|
||||
{
|
||||
id: "created_at",
|
||||
label: "Created",
|
||||
},
|
||||
{
|
||||
id: "updated_at",
|
||||
label: "Updated",
|
||||
},
|
||||
];
|
||||
|
||||
const MainForm = ({ vin, token }) => {
|
||||
const classes = useStyles();
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [orderBy, setOrderBy] = useState("id");
|
||||
const [order, setOrder] = useState("desc");
|
||||
const { getCarUpdates, carUpdates, totalCarUpdates } = useUpdatesContext();
|
||||
const { setMessage } = useStatusContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (!vin || !token) return;
|
||||
await getCarUpdates(
|
||||
{
|
||||
vin,
|
||||
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
|
||||
}, [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);
|
||||
}
|
||||
};
|
||||
|
||||
const updateName = (row) => {
|
||||
if (row.updatepackage)
|
||||
return `${row.updatepackage.package_name} ${row.updatepackage.version}`;
|
||||
if (row.updatemanifest)
|
||||
return `${row.updatemanifest.name} ${row.updatemanifest.version}`;
|
||||
return "None";
|
||||
};
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeaderSortable
|
||||
classes={classes}
|
||||
orderBy={orderBy}
|
||||
order={order}
|
||||
columnData={tableColumns}
|
||||
onSortRequest={handleSort}
|
||||
/>
|
||||
<TableBody>
|
||||
{carUpdates.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
<TableCell align="center">{row.id}</TableCell>
|
||||
<TableCell align="center">{updateName(row)}</TableCell>
|
||||
<TableCell align="center">{row.status}</TableCell>
|
||||
<TableCell align="center">
|
||||
{LocalDateTimeString(row.created)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{LocalDateTimeString(row.updated)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25, 100]}
|
||||
colSpan={5}
|
||||
count={totalCarUpdates}
|
||||
rowsPerPage={pageSize}
|
||||
page={pageIndex}
|
||||
SelectProps={{
|
||||
inputProps: { "aria-label": "rows per page" },
|
||||
native: true,
|
||||
}}
|
||||
onPageChange={handleChangePageIndex}
|
||||
onRowsPerPageChange={handleChangePageSize}
|
||||
/>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
const CarUpdates = (props) => (
|
||||
<UpdatesProvider>
|
||||
<MainForm {...props} />
|
||||
</UpdatesProvider>
|
||||
);
|
||||
|
||||
export default CarUpdates;
|
||||
391
src/components/Cars/List/__snapshots__/index.test.jsx.snap
Normal file
391
src/components/Cars/List/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,391 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VehicleTable Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-vehicleprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-vehicleprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3 makeStyles-tableSize-53"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textJustifyAlign-47 MuiGrid-item MuiGrid-grid-md-4"
|
||||
>
|
||||
<a
|
||||
href="/vehicle-add"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-4"
|
||||
>
|
||||
<div
|
||||
class="MuiFormControl-root makeStyles-margin-28 makeStyles-fullWidth-50"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated"
|
||||
data-shrink="false"
|
||||
for="search"
|
||||
>
|
||||
Search
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedEnd"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedEnd"
|
||||
id="search"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
class="MuiInputAdornment-root MuiInputAdornment-positionEnd"
|
||||
>
|
||||
<button
|
||||
aria-label="search"
|
||||
class="MuiButtonBase-root MuiIconButton-root"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textRightAlign-49 MuiGrid-item MuiGrid-grid-md-4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="makeStyles-paper-3 makeStyles-tableSize-53"
|
||||
>
|
||||
<table
|
||||
class="MuiTable-root"
|
||||
>
|
||||
<thead
|
||||
class="MuiTableHead-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root MuiTableRow-head"
|
||||
>
|
||||
<th
|
||||
aria-sort="ascending"
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root MuiTableSortLabel-active"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
VIN
|
||||
<span
|
||||
class="makeStyles-hiddenSortSpan-27"
|
||||
>
|
||||
sorted ascending
|
||||
</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Model
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Year
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Trim
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Created
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Updated
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="MuiTableBody-root"
|
||||
/>
|
||||
<tfoot
|
||||
class="MuiTableFooter-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root MuiTableRow-footer"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
|
||||
colspan="7"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -8,14 +8,11 @@ import { VehicleProvider } from "../../Contexts/VehicleContext";
|
||||
import { useUserContext } from "../../Contexts/UserContext";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import useStyles from "../../useStyles";
|
||||
import SendCommand from "../../Controls/SendCommand";
|
||||
import SearchField from "../../Controls/SearchField";
|
||||
import CarSelectionTable from "../../Controls/CarSelectionTable";
|
||||
import { logger } from "../../../services/monitoring";
|
||||
|
||||
const MainForm = () => {
|
||||
const classes = useStyles();
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [search, setSearch] = useState("");
|
||||
const { setTitle, setSitePath } = useStatusContext();
|
||||
const {
|
||||
@@ -25,29 +22,9 @@ const MainForm = () => {
|
||||
} = useUserContext();
|
||||
|
||||
const handleSearch = (query) => {
|
||||
setSelected([]);
|
||||
setSearch(query);
|
||||
};
|
||||
|
||||
const handleSelectAll = (cars) => {
|
||||
setSelected(cars);
|
||||
};
|
||||
|
||||
const handleSelect = (event, key) => {
|
||||
try {
|
||||
let newSelected;
|
||||
if (event.target.checked) {
|
||||
newSelected = [...selected];
|
||||
newSelected.push(key);
|
||||
} else {
|
||||
newSelected = selected.filter((vin) => vin !== key);
|
||||
}
|
||||
setSelected(newSelected);
|
||||
} catch (e) {
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTitle("Vehicles");
|
||||
setSitePath([]);
|
||||
@@ -61,24 +38,17 @@ const MainForm = () => {
|
||||
<Link to="/vehicle-add">
|
||||
<AddCircleIcon fontSize="large" />
|
||||
</Link>
|
||||
<div
|
||||
className={classes.labelInline}
|
||||
>{`${selected.length} Selected`}</div>
|
||||
</Grid>
|
||||
<Grid item md={4} className={classes.textCenterAlign}>
|
||||
<SearchField classes={classes} onSearch={handleSearch} />
|
||||
</Grid>
|
||||
<Grid item md={4} className={classes.textRightAlign}>
|
||||
<SendCommand vins={selected} />
|
||||
</Grid>
|
||||
<Grid item md={4} className={classes.textRightAlign} />
|
||||
</Grid>
|
||||
<CarSelectionTable
|
||||
classes={classes}
|
||||
token={token}
|
||||
multiSelect={false}
|
||||
search={{ search }}
|
||||
selected={selected}
|
||||
onSelect={handleSelect}
|
||||
onSelectAll={handleSelectAll}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
39
src/components/Cars/List/index.test.jsx
Normal file
39
src/components/Cars/List/index.test.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
jest.mock("../../Contexts/VehicleContext");
|
||||
jest.mock("../../Contexts/StatusContext");
|
||||
jest.mock("../../Contexts/UserContext");
|
||||
jest.mock('@material-ui/core/utils/unstable_useId', () =>
|
||||
jest.fn().mockReturnValue('mui-test-id'),
|
||||
);
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { VehicleProvider } from "../../Contexts/VehicleContext";
|
||||
import { StatusProvider } from "../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
|
||||
import MainForm from "./index"
|
||||
|
||||
const renderVehicleTable = async () => {
|
||||
const { container } = render(
|
||||
<VehicleProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<MainForm />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</VehicleProvider>
|
||||
);
|
||||
await waitFor(() => { /* render */ });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("VehicleTable", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderVehicleTable();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
21
src/components/Cars/Status/CANFiltersTab.jsx
Normal file
21
src/components/Cars/Status/CANFiltersTab.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 CANFiltersTable from "../../CANFilter/Table";
|
||||
import useStyles from "../../useStyles";
|
||||
|
||||
const CANFiltersTab = () => {
|
||||
const { vin } = useParams();
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.paper, classes.tableSize)}>
|
||||
<Typography variant="h6">CAN Filters</Typography>
|
||||
<CANFiltersTable vin={vin} classes={classes} />
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
export default CANFiltersTab;
|
||||
31
src/components/Cars/Status/CANFiltersTab.test.jsx
Normal file
31
src/components/Cars/Status/CANFiltersTab.test.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
jest.mock("../../Contexts/CANFiltersContext");
|
||||
jest.mock("../../Contexts/StatusContext");
|
||||
jest.mock("../../Contexts/UserContext");
|
||||
jest.mock('@material-ui/core/utils/unstable_useId', () =>
|
||||
jest.fn().mockReturnValue('mui-test-id'),
|
||||
);
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { setToken } from "../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
|
||||
import CANFiltersTab from "./CANFiltersTab"
|
||||
|
||||
const renderCANFiltersTab = async () => {
|
||||
const { container } = render(
|
||||
<BrowserRouter>
|
||||
<CANFiltersTab vin="TESTVIN1234567890" />
|
||||
</BrowserRouter>
|
||||
);
|
||||
await waitFor(() => { /* render */ });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("CANFiltersTab", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderCANFiltersTab();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
73
src/components/Cars/Status/CarUpdatesTab.jsx
Normal file
73
src/components/Cars/Status/CarUpdatesTab.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from "react";
|
||||
import { useParams } from "react-router";
|
||||
import clsx from "clsx";
|
||||
import { Button, Grid, Typography } from "@material-ui/core";
|
||||
|
||||
import CarECUsTable from "../../Controls/CarECUsTable";
|
||||
import CarUpdatesTable from "../../Controls/CarUpdatesTable";
|
||||
import { logger } from "../../../services/monitoring";
|
||||
import {
|
||||
VehicleProvider,
|
||||
useVehicleContext,
|
||||
} from "../../Contexts/VehicleContext";
|
||||
import { useUserContext } from "../../Contexts/UserContext";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import useStyles from "../../useStyles";
|
||||
|
||||
|
||||
const MainForm = () => {
|
||||
const { vin } = useParams();
|
||||
const classes = useStyles();
|
||||
const { setMessage } = useStatusContext();
|
||||
const { busy, sendCommand } = useVehicleContext();
|
||||
const {
|
||||
token: {
|
||||
idToken: { jwtToken: token },
|
||||
},
|
||||
} = useUserContext();
|
||||
const updateHandler = async (e) => {
|
||||
try {
|
||||
await sendCommand([vin], "ecu", "", token);
|
||||
setMessage(`Sent command to ${vin}`);
|
||||
} catch (error) {
|
||||
setMessage(error.message);
|
||||
logger.error(error.stack);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.paper, classes.tableSize)}>
|
||||
<Typography variant="h6">Car Updates</Typography>
|
||||
<CarUpdatesTable vin={vin} token={token} classes={classes} />
|
||||
<Grid container className={classes.root} spacing={2}>
|
||||
<Grid item md={4} className={classes.textJustifyAlign}></Grid>
|
||||
<Grid item md={4} className={classes.textCenterAlign}>
|
||||
<Typography variant="h6" className={classes.labelInline}>
|
||||
Car ECUs
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item md={4} className={classes.textRightAlign}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={clsx(classes.formControl, classes.textField)}
|
||||
onClick={updateHandler}
|
||||
>
|
||||
{busy ? "Sending..." : "Refresh"}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<CarECUsTable vin={vin} token={token} classes={classes} />
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
const CarUpdatesTab = () => (
|
||||
<VehicleProvider>
|
||||
<MainForm />
|
||||
</VehicleProvider>
|
||||
);
|
||||
|
||||
export default CarUpdatesTab;
|
||||
39
src/components/Cars/Status/CarUpdatesTab.test.jsx
Normal file
39
src/components/Cars/Status/CarUpdatesTab.test.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
jest.mock("../../Contexts/CANFiltersContext");
|
||||
jest.mock("../../Contexts/StatusContext");
|
||||
jest.mock("../../Contexts/UserContext");
|
||||
jest.mock('@material-ui/core/utils/unstable_useId', () =>
|
||||
jest.fn().mockReturnValue('mui-test-id'),
|
||||
);
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { CANFiltersProvider } from "../../Contexts/CANFiltersContext";
|
||||
import { StatusProvider } from "../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
|
||||
import MainForm from "./CarUpdatesTab"
|
||||
|
||||
const renderCarUpdatesTab = async () => {
|
||||
const { container } = render(
|
||||
<CANFiltersProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<MainForm vin="TESTVIN1234567890" />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</CANFiltersProvider>
|
||||
);
|
||||
await waitFor(() => { /* render */ });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("CarUpdatesTab", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderCarUpdatesTab();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VehicleDetailsTab Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-vehicleprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-vehicleprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3 makeStyles-tableSize-53"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
|
||||
>
|
||||
<p>
|
||||
<b>
|
||||
VIN
|
||||
</b>
|
||||
:
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Log Level
|
||||
</b>
|
||||
:
|
||||
info
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
|
||||
>
|
||||
<b>
|
||||
CANBus
|
||||
</b>
|
||||
<p>
|
||||
<b>
|
||||
Enabled
|
||||
</b>
|
||||
:
|
||||
true
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Max Memory Buffer Size
|
||||
</b>
|
||||
:
|
||||
1
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Enabled
|
||||
</b>
|
||||
:
|
||||
true
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Max Disk Buffer Size
|
||||
</b>
|
||||
:
|
||||
2
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Filters
|
||||
</b>
|
||||
:
|
||||
3
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="/vehicle-update?vin=undefined"
|
||||
style="margin: 5px;"
|
||||
title="Update \\"undefined\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Update \\"undefined\\""
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class=""
|
||||
href="/"
|
||||
title="Delete \\"undefined\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Delete \\"undefined\\""
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
95
src/components/Cars/Status/Details/index.jsx
Normal file
95
src/components/Cars/Status/Details/index.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Redirect } from "react-router";
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Grid,
|
||||
Tooltip,
|
||||
} from "@material-ui/core";
|
||||
import EditIcon from "@material-ui/icons/Edit"
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { useUserContext } from "../../../Contexts/UserContext"
|
||||
import { useStatusContext } from "../../../Contexts/StatusContext";
|
||||
import useStyles from "../../../useStyles";
|
||||
import { logger } from "../../../../services/monitoring";
|
||||
import { useVehicleContext, VehicleProvider } from "../../../Contexts/VehicleContext";
|
||||
|
||||
const MainForm = ({ vin }) => {
|
||||
const classes = useStyles();
|
||||
const { setMessage } = useStatusContext();
|
||||
const { vehicle, getVehicle, deleteVehicle } = useVehicleContext();
|
||||
const [redirect, setRedirect] = useState(null);
|
||||
const { token: { idToken: { jwtToken: token } } } = useUserContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (!vin || !token) return;
|
||||
await getVehicle(vin, token);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
const onDelete = async () => {
|
||||
try {
|
||||
await deleteVehicle(vin, token);
|
||||
setMessage(`Deleted ${vin}`)
|
||||
setRedirect(`/vehicles`);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
};
|
||||
|
||||
if (redirect && redirect.length > 0) {
|
||||
return <Redirect to={redirect} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.paper, classes.tableSize)}>
|
||||
<Grid container className={classes.root} spacing={2}>
|
||||
<Grid item md={12} className={classes.textCenterAlign}>
|
||||
<p><b>VIN</b>: {vin}</p>
|
||||
{vehicle.log_level != null && (
|
||||
<p><b>Log Level</b>: {vehicle.log_level}</p>
|
||||
)}
|
||||
</Grid>
|
||||
{vehicle.canbus && (
|
||||
<Grid item md={12} className={classes.textCenterAlign}>
|
||||
<b>CANBus</b>
|
||||
<p><b>Enabled</b>: {vehicle.canbus.enabled.toString()}</p>
|
||||
<p><b>Max Memory Buffer Size</b>: {vehicle.canbus.max_mem_buffer_size ?? "Default"}</p>
|
||||
<p><b>Enabled</b>: {vehicle.canbus.data_logger_enabled.toString()}</p>
|
||||
<p><b>Max Disk Buffer Size</b>: {vehicle.canbus.max_disk_buffer_size ?? "Default"}</p>
|
||||
<p><b>Filters</b>: {vehicle.canbus.filters ? vehicle.canbus.filters.length : 0}</p>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item md={12} className={classes.textCenterAlign}>
|
||||
<Tooltip key={`update-${vin}`} title={`Update "${vin}"`}>
|
||||
<Link to={`/vehicle-update?vin=${vin}`} style={{ margin: 5 }}>
|
||||
<EditIcon aria-label={`Update "${vin}"`} />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Tooltip key={`delete-${vin}`} title={`Delete "${vin}"`}>
|
||||
<Link to="#" onClick={onDelete}>
|
||||
<DeleteIcon aria-label={`Delete "${vin}"`} />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
const CarDetails = (props) => (
|
||||
<VehicleProvider>
|
||||
<MainForm {...props} />
|
||||
</VehicleProvider>
|
||||
);
|
||||
|
||||
export default CarDetails;
|
||||
36
src/components/Cars/Status/Details/index.test.jsx
Normal file
36
src/components/Cars/Status/Details/index.test.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
jest.mock("../../../Contexts/VehicleContext");
|
||||
jest.mock("../../../Contexts/StatusContext");
|
||||
jest.mock("../../../Contexts/UserContext");
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { VehicleProvider } from "../../../Contexts/VehicleContext";
|
||||
import { StatusProvider } from "../../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../../utils/testing";
|
||||
import MainForm from "./index"
|
||||
|
||||
const renderVehicleDetailsTab = async () => {
|
||||
const { container } = render(
|
||||
<VehicleProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<MainForm />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</VehicleProvider>
|
||||
);
|
||||
await waitFor(() => { /* render */ });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("VehicleDetailsTab", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderVehicleDetailsTab();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
21
src/components/Cars/Status/DetailsTab.jsx
Normal file
21
src/components/Cars/Status/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 CarDetails from "./Details";
|
||||
import useStyles from "../../useStyles";
|
||||
|
||||
const CarDetailsTab = () => {
|
||||
const { vin } = useParams();
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.paper, classes.tableSize)}>
|
||||
<Typography variant="h6">Vehicle Details</Typography>
|
||||
<CarDetails vin={vin} classes={classes} />
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
export default CarDetailsTab;
|
||||
41
src/components/Cars/Status/DetailsTab.test.jsx
Normal file
41
src/components/Cars/Status/DetailsTab.test.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
jest.mock("../../Contexts/VehicleContext");
|
||||
jest.mock("../../Contexts/StatusContext");
|
||||
jest.mock("../../Contexts/UserContext");
|
||||
jest.mock('@material-ui/core/utils/unstable_useId', () =>
|
||||
jest.fn().mockReturnValue('mui-test-id'),
|
||||
);
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { MemoryRouter, Route } from "react-router-dom";
|
||||
|
||||
import { VehicleProvider } from "../../Contexts/VehicleContext";
|
||||
import { StatusProvider } from "../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
|
||||
import MainForm from "./DetailsTab"
|
||||
|
||||
const renderDetailsTab = async () => {
|
||||
const { container } = render(
|
||||
<VehicleProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<MemoryRouter initialEntries={['/testroute/TESTVIN1234567890']}>
|
||||
<Route path="/testroute/:vin">
|
||||
<MainForm vin="TESTVIN1234567890" />
|
||||
</Route>
|
||||
</MemoryRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</VehicleProvider >
|
||||
);
|
||||
await waitFor(() => { /* render */ });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("DetailsTab", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderDetailsTab();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,450 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CANFiltersTab Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="makeStyles-paper-3 makeStyles-tableSize-53"
|
||||
>
|
||||
<h6
|
||||
class="MuiTypography-root MuiTypography-h6"
|
||||
>
|
||||
CAN Filters
|
||||
</h6>
|
||||
<div
|
||||
data-testid="mocked-canfiltersprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3 makeStyles-tableSize-53"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textJustifyAlign-47 MuiGrid-item MuiGrid-grid-md-4"
|
||||
>
|
||||
<a
|
||||
class="makeStyles-labelInline-9"
|
||||
href="/filter-add?vin=undefined"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-8"
|
||||
>
|
||||
<div
|
||||
class="MuiFormControl-root makeStyles-margin-28 makeStyles-fullWidth-50"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated"
|
||||
data-shrink="false"
|
||||
for="search"
|
||||
>
|
||||
Search
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedEnd"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedEnd"
|
||||
id="search"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
class="MuiInputAdornment-root MuiInputAdornment-positionEnd"
|
||||
>
|
||||
<button
|
||||
aria-label="search"
|
||||
class="MuiButtonBase-root MuiIconButton-root"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table
|
||||
class="MuiTable-root"
|
||||
>
|
||||
<thead
|
||||
class="MuiTableHead-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root MuiTableRow-head"
|
||||
>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
CAN ID
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Interval (ms)
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="MuiTableBody-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
123
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
1000
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="/filter-update?vin=undefined&can_id=123&interval=1000"
|
||||
style="margin: 5px;"
|
||||
title="Update \\"123\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Update 123"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class=""
|
||||
href="/"
|
||||
title="Delete \\"123\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Delete 123"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="MuiTableRow-root"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
456-789
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
2000
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="/filter-update?vin=undefined&can_id=456-789&interval=2000"
|
||||
style="margin: 5px;"
|
||||
title="Update \\"456-789\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Update 456-789"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class=""
|
||||
href="/"
|
||||
title="Delete \\"456-789\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Delete 456-789"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="MuiTableRow-root"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
1
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
0
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="/filter-update?vin=undefined&can_id=1&interval=0"
|
||||
style="margin: 5px;"
|
||||
title="Update \\"1\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Update 1"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class=""
|
||||
href="/"
|
||||
title="Delete \\"1\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Delete 1"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot
|
||||
class="MuiTableFooter-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root MuiTableRow-footer"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
|
||||
colspan="8"
|
||||
>
|
||||
<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>
|
||||
<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"
|
||||
>
|
||||
1-3 of 3
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,606 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CarUpdatesTab Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-canfiltersprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3 makeStyles-tableSize-53"
|
||||
>
|
||||
<h6
|
||||
class="MuiTypography-root MuiTypography-h6"
|
||||
>
|
||||
Car Updates
|
||||
</h6>
|
||||
<table
|
||||
class="MuiTable-root"
|
||||
>
|
||||
<thead
|
||||
class="MuiTableHead-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root MuiTableRow-head"
|
||||
>
|
||||
<th
|
||||
aria-sort="descending"
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root MuiTableSortLabel-active"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
ID
|
||||
<span
|
||||
class="makeStyles-hiddenSortSpan-27"
|
||||
>
|
||||
sorted descending
|
||||
</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionDesc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Name
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Status
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Created
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Updated
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="MuiTableBody-root"
|
||||
/>
|
||||
<tfoot
|
||||
class="MuiTableFooter-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root MuiTableRow-footer"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
|
||||
colspan="5"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textJustifyAlign-47 MuiGrid-item MuiGrid-grid-md-4"
|
||||
/>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-4"
|
||||
>
|
||||
<h6
|
||||
class="MuiTypography-root makeStyles-labelInline-9 MuiTypography-h6"
|
||||
>
|
||||
Car ECUs
|
||||
</h6>
|
||||
</div>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textRightAlign-49 MuiGrid-item MuiGrid-grid-md-4"
|
||||
>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-formControl-7 makeStyles-textField-29 MuiButton-containedPrimary"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label"
|
||||
>
|
||||
Refresh
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="makeStyles-paper-3 makeStyles-tableSize-53"
|
||||
>
|
||||
<table
|
||||
class="MuiTable-root"
|
||||
>
|
||||
<thead
|
||||
class="MuiTableHead-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root MuiTableRow-head"
|
||||
>
|
||||
<th
|
||||
aria-sort="descending"
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root MuiTableSortLabel-active"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
ECU
|
||||
<span
|
||||
class="makeStyles-hiddenSortSpan-27"
|
||||
>
|
||||
sorted descending
|
||||
</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionDesc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
SW Version
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
HW Version
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Config
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Created
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Updated
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="MuiTableBody-root"
|
||||
/>
|
||||
<tfoot
|
||||
class="MuiTableFooter-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root MuiTableRow-footer"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
|
||||
colspan="10"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,138 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DetailsTab Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-vehicleprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3 makeStyles-tableSize-53"
|
||||
>
|
||||
<h6
|
||||
class="MuiTypography-root MuiTypography-h6"
|
||||
>
|
||||
Vehicle Details
|
||||
</h6>
|
||||
<div
|
||||
data-testid="mocked-vehicleprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3 makeStyles-tableSize-53"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
|
||||
>
|
||||
<p>
|
||||
<b>
|
||||
VIN
|
||||
</b>
|
||||
:
|
||||
TESTVIN1234567890
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Log Level
|
||||
</b>
|
||||
:
|
||||
info
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
|
||||
>
|
||||
<b>
|
||||
CANBus
|
||||
</b>
|
||||
<p>
|
||||
<b>
|
||||
Enabled
|
||||
</b>
|
||||
:
|
||||
true
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Max Memory Buffer Size
|
||||
</b>
|
||||
:
|
||||
1
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Enabled
|
||||
</b>
|
||||
:
|
||||
true
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Max Disk Buffer Size
|
||||
</b>
|
||||
:
|
||||
2
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Filters
|
||||
</b>
|
||||
:
|
||||
3
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="/vehicle-update?vin=TESTVIN1234567890"
|
||||
style="margin: 5px;"
|
||||
title="Update \\"TESTVIN1234567890\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Update \\"TESTVIN1234567890\\""
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class=""
|
||||
href="/testroute/TESTVIN1234567890"
|
||||
title="Delete \\"TESTVIN1234567890\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Delete \\"TESTVIN1234567890\\""
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
187
src/components/Cars/Status/__snapshots__/index.test.jsx.snap
Normal file
187
src/components/Cars/Status/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,187 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CarStatus Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-canfiltersprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3 makeStyles-tableSize-53"
|
||||
>
|
||||
<div
|
||||
class="MuiBox-root MuiBox-root-62 makeStyles-tableToolbar-30"
|
||||
>
|
||||
<div
|
||||
class="MuiTabs-root"
|
||||
>
|
||||
<div
|
||||
class="MuiTabs-scroller MuiTabs-fixed"
|
||||
style="overflow: hidden;"
|
||||
>
|
||||
<div
|
||||
aria-label="car tabs"
|
||||
class="MuiTabs-flexContainer"
|
||||
role="tablist"
|
||||
>
|
||||
<button
|
||||
aria-controls="tabpanel-0"
|
||||
aria-selected="true"
|
||||
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit Mui-selected"
|
||||
id="tab-0"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiTab-wrapper"
|
||||
>
|
||||
Details
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="tabpanel-1"
|
||||
aria-selected="false"
|
||||
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit"
|
||||
id="tab-1"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiTab-wrapper"
|
||||
>
|
||||
Car Updates
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="tabpanel-2"
|
||||
aria-selected="false"
|
||||
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit"
|
||||
id="tab-2"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiTab-wrapper"
|
||||
>
|
||||
CAN Filters
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<span
|
||||
class="PrivateTabIndicator-root-63 PrivateTabIndicator-colorSecondary-65 MuiTabs-indicator"
|
||||
style="left: 0px; width: 0px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-labelledby="tab-0"
|
||||
id="tabpanel-0"
|
||||
role="tabpanel"
|
||||
>
|
||||
<div
|
||||
class="MuiBox-root MuiBox-root-67"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3 makeStyles-tableSize-53"
|
||||
>
|
||||
<h6
|
||||
class="MuiTypography-root MuiTypography-h6"
|
||||
>
|
||||
Vehicle Details
|
||||
</h6>
|
||||
<div
|
||||
class="makeStyles-paper-3 makeStyles-tableSize-53"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
|
||||
>
|
||||
<p>
|
||||
<b>
|
||||
VIN
|
||||
</b>
|
||||
:
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="/vehicle-update?vin=undefined"
|
||||
style="margin: 5px;"
|
||||
title="Update \\"undefined\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Update \\"undefined\\""
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class=""
|
||||
href="/"
|
||||
title="Delete \\"undefined\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Delete \\"undefined\\""
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-labelledby="tab-1"
|
||||
hidden=""
|
||||
id="tabpanel-1"
|
||||
role="tabpanel"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="tab-2"
|
||||
hidden=""
|
||||
id="tabpanel-2"
|
||||
role="tabpanel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,38 +1,30 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import clsx from "clsx";
|
||||
import { Button, Grid, Typography } from "@material-ui/core";
|
||||
import { Box, Tab, Tabs } from "@material-ui/core";
|
||||
|
||||
import CarECUsTable from "../../Controls/CarECUsTable";
|
||||
import CarUpdatesTable from "../../Controls/CarUpdatesTable";
|
||||
import { logger } from "../../../services/monitoring";
|
||||
import {
|
||||
VehicleProvider,
|
||||
useVehicleContext,
|
||||
} from "../../Contexts/VehicleContext";
|
||||
import { useUserContext } from "../../Contexts/UserContext";
|
||||
import CarDetailsTab from "./DetailsTab";
|
||||
import CarUpdatesTab from "./CarUpdatesTab";
|
||||
import CANFiltersTab from "./CANFiltersTab";
|
||||
import TabPanel from "../../Controls/TabPanel";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import useStyles from "../../useStyles";
|
||||
|
||||
const MainForm = () => {
|
||||
const tabHashes = ["details", "updates", "filters"];
|
||||
|
||||
const CarStatus = () => {
|
||||
const { vin } = useParams();
|
||||
const classes = useStyles();
|
||||
const { setTitle, setSitePath, setMessage } = useStatusContext();
|
||||
const { busy, sendCommand } = useVehicleContext();
|
||||
const {
|
||||
token: {
|
||||
idToken: { jwtToken: token },
|
||||
},
|
||||
} = useUserContext();
|
||||
const updateHandler = async (e) => {
|
||||
try {
|
||||
await sendCommand([vin], "ecu", "", token);
|
||||
setMessage(`Sent command to ${vin}`);
|
||||
} catch (error) {
|
||||
setMessage(error.message);
|
||||
logger.error(error.stack);
|
||||
}
|
||||
};
|
||||
const { setTitle, setSitePath } = useStatusContext();
|
||||
const { hash } = useLocation();
|
||||
const [tabIndex, setTabIndex] = React.useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const key = hash.replace("#", "");
|
||||
const index = tabHashes.findIndex((element) => element === key);
|
||||
if (index >= 0) setTabIndex(index);
|
||||
}, [hash]);
|
||||
|
||||
useEffect(() => {
|
||||
const title = `Vehicle ${vin} Details`;
|
||||
@@ -49,39 +41,48 @@ const MainForm = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [vin]);
|
||||
|
||||
const handleTabChange = (_event, newIndex) => {
|
||||
setTabIndex(newIndex);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.paper, classes.tableSize)}>
|
||||
<Typography variant="h6">Car Updates</Typography>
|
||||
<CarUpdatesTable vin={vin} token={token} classes={classes} />
|
||||
<Grid container className={classes.root} spacing={2}>
|
||||
<Grid item md={4} className={classes.textJustifyAlign}></Grid>
|
||||
<Grid item md={4} className={classes.textCenterAlign}>
|
||||
<Typography variant="h6" className={classes.labelInline}>
|
||||
Car ECUs
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item md={4} className={classes.textRightAlign}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={clsx(classes.formControl, classes.textField)}
|
||||
onClick={updateHandler}
|
||||
>
|
||||
{busy ? "Sending..." : "Refresh"}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<CarECUsTable vin={vin} token={token} classes={classes} />
|
||||
<Box
|
||||
className={classes.tableToolbar}
|
||||
sx={{ borderBottom: 1, borderColor: "divider" }}
|
||||
>
|
||||
<Tabs
|
||||
value={tabIndex}
|
||||
onChange={handleTabChange}
|
||||
aria-label="car tabs"
|
||||
indicatorColor="secondary"
|
||||
>
|
||||
<Tab label="Details" {...tabProps(0)} />
|
||||
<Tab label="Car Updates" {...tabProps(1)} />
|
||||
<Tab label="CAN Filters" {...tabProps(2)} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<TabPanel value={tabIndex} index={0}>
|
||||
<CarDetailsTab />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabIndex} index={1}>
|
||||
<CarUpdatesTab />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabIndex} index={2}>
|
||||
<CANFiltersTab />
|
||||
</TabPanel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CarStatus = () => (
|
||||
<VehicleProvider>
|
||||
<MainForm />
|
||||
</VehicleProvider>
|
||||
);
|
||||
function tabProps(index) {
|
||||
return {
|
||||
id: `tab-${index}`,
|
||||
"aria-controls": `tabpanel-${index}`,
|
||||
};
|
||||
}
|
||||
|
||||
export default CarStatus;
|
||||
|
||||
39
src/components/Cars/Status/index.test.jsx
Normal file
39
src/components/Cars/Status/index.test.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
jest.mock("../../Contexts/CANFiltersContext");
|
||||
jest.mock("../../Contexts/StatusContext");
|
||||
jest.mock("../../Contexts/UserContext");
|
||||
jest.mock('@material-ui/core/utils/unstable_useId', () =>
|
||||
jest.fn().mockReturnValue('mui-test-id'),
|
||||
);
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { CANFiltersProvider } from "../../Contexts/CANFiltersContext";
|
||||
import { StatusProvider } from "../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
|
||||
import CarStatus from "./index"
|
||||
|
||||
const renderCarStatus = async () => {
|
||||
const { container } = render(
|
||||
<CANFiltersProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<CarStatus vin="TESTVIN1234567890" />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</CANFiltersProvider>
|
||||
);
|
||||
await waitFor(() => { /* render */ });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("CarStatus", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderCarStatus();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,91 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Backdrop,
|
||||
Modal,
|
||||
Fade,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import useStyles from "../../useStyles";
|
||||
import { useUpdatesContext } from "../../Contexts/UpdatesContext";
|
||||
import { useUserContext } from "../../Contexts/UserContext";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import { LocalDateTimeString } from "../../../utils/dates";
|
||||
|
||||
export default function CarStatusModal(props) {
|
||||
const classes = useStyles();
|
||||
const [updates, setUpdates] = useState([]);
|
||||
const { setMessage } = useStatusContext();
|
||||
const { getVINUpdates } = useUpdatesContext();
|
||||
const {
|
||||
token: {
|
||||
idToken: { jwtToken: token },
|
||||
},
|
||||
} = useUserContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (!props.vin) return;
|
||||
const result = await getVINUpdates(props.vin, token);
|
||||
if (result.error) {
|
||||
throw new Error(`Get VIN updates error. ${result.message}`);
|
||||
} else {
|
||||
setUpdates(result.data);
|
||||
}
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.vin]);
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
aria-labelledby="transition-modal-title"
|
||||
aria-describedby="transition-modal-description"
|
||||
className={classes.modal}
|
||||
open={props.vin !== null && props.vin !== undefined}
|
||||
onClose={props.handleClose}
|
||||
BackdropComponent={Backdrop}
|
||||
BackdropProps={{
|
||||
timeout: 500,
|
||||
}}
|
||||
>
|
||||
<Fade in={props.vin}>
|
||||
<div className={classes.modaldialog}>
|
||||
<h2 id="transition-modal-title">{props.vin} Updates</h2>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell align="center">Date</TableCell>
|
||||
<TableCell align="center">Update</TableCell>
|
||||
<TableCell align="center">Status</TableCell>
|
||||
<TableCell align="center">Updated</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{updates.map((update) => (
|
||||
<TableRow key={update.id}>
|
||||
<TableCell align="center">
|
||||
{LocalDateTimeString(update.created)}
|
||||
</TableCell>
|
||||
<TableCell align="center">{`${update.updatepackage.package_name} ${update.updatepackage.version}`}</TableCell>
|
||||
<TableCell align="center">{update.status}</TableCell>
|
||||
<TableCell align="center">
|
||||
{LocalDateTimeString(update.updated)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</Fade>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
731
src/components/Cars/Update/__snapshots__/index.test.jsx.snap
Normal file
731
src/components/Cars/Update/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,731 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VehicleUpdate Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-vehicleprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-vehicleprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3"
|
||||
>
|
||||
<form
|
||||
action="{onSubmit}"
|
||||
class="makeStyles-form-5"
|
||||
novalidate=""
|
||||
>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-disabled Mui-disabled Mui-required Mui-required"
|
||||
data-shrink="false"
|
||||
for="vin"
|
||||
id="vin-label"
|
||||
>
|
||||
VIN
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root Mui-disabled Mui-disabled MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input Mui-disabled Mui-disabled"
|
||||
disabled=""
|
||||
id="vin"
|
||||
maxlength="17"
|
||||
name="vin"
|
||||
readonly=""
|
||||
required=""
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64"
|
||||
>
|
||||
<span>
|
||||
VIN
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
|
||||
data-shrink="true"
|
||||
for="model"
|
||||
id="model-label"
|
||||
>
|
||||
Model
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="model"
|
||||
maxlength="255"
|
||||
name="model"
|
||||
required=""
|
||||
type="text"
|
||||
value="Ocean"
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
|
||||
>
|
||||
<span>
|
||||
Model
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
|
||||
data-shrink="true"
|
||||
for="year"
|
||||
id="year-label"
|
||||
>
|
||||
Year
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="year"
|
||||
maxlength="4"
|
||||
minlength="4"
|
||||
name="year"
|
||||
required=""
|
||||
type="number"
|
||||
value="2022"
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
|
||||
>
|
||||
<span>
|
||||
Year
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
|
||||
data-shrink="true"
|
||||
for="trim"
|
||||
id="trim-label"
|
||||
>
|
||||
Trim
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="trim"
|
||||
maxlength="4"
|
||||
minlength="4"
|
||||
name="trim"
|
||||
required=""
|
||||
type="text"
|
||||
value="Base"
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
|
||||
>
|
||||
<span>
|
||||
Trim
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<label
|
||||
class="MuiFormLabel-root"
|
||||
id="demo-row-radio-buttons-group-label"
|
||||
>
|
||||
Log Level
|
||||
</label>
|
||||
<div
|
||||
aria-labelledby="demo-row-radio-buttons-group-label"
|
||||
class="MuiFormGroup-root MuiFormGroup-row"
|
||||
margin="normal"
|
||||
role="radiogroup"
|
||||
>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="trace"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Trace
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="debug"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Debug
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary PrivateSwitchBase-checked-67 Mui-checked MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="info"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70 PrivateRadioButtonIcon-checked-72"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Info
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="warn"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Warn
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="error"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Error
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="critical"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Critical
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<label
|
||||
class="MuiFormLabel-root"
|
||||
id="demo-row-radio-buttons-group-label"
|
||||
>
|
||||
CAN Bus
|
||||
</label>
|
||||
<div
|
||||
class="MuiFormGroup-root"
|
||||
>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiCheckbox-root MuiCheckbox-colorSecondary PrivateSwitchBase-checked-67 Mui-checked MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="PrivateSwitchBase-input-69"
|
||||
data-indeterminate="false"
|
||||
type="checkbox"
|
||||
value=""
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
CAN Bus Enabled
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
|
||||
data-shrink="true"
|
||||
for="max_mem_buffer_size"
|
||||
id="max_mem_buffer_size-label"
|
||||
>
|
||||
Max Memory Buffer Size (0 uses default size)
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="max_mem_buffer_size"
|
||||
maxlength="12"
|
||||
name="max_mem_buffer_size"
|
||||
required=""
|
||||
type="number"
|
||||
value="1"
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
|
||||
>
|
||||
<span>
|
||||
Max Memory Buffer Size (0 uses default size)
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiCheckbox-root MuiCheckbox-colorSecondary PrivateSwitchBase-checked-67 Mui-checked MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
data-indeterminate="false"
|
||||
type="checkbox"
|
||||
value=""
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Data Logger Enabled
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
|
||||
data-shrink="true"
|
||||
for="max_disk_buffer_size"
|
||||
id="max_disk_buffer_size-label"
|
||||
>
|
||||
Max Disk Buffer Size (0 uses default size)
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="max_disk_buffer_size"
|
||||
maxlength="12"
|
||||
name="max_disk_buffer_size"
|
||||
required=""
|
||||
type="number"
|
||||
value="2"
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
|
||||
>
|
||||
<span>
|
||||
Max Disk Buffer Size (0 uses default size)
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label"
|
||||
>
|
||||
Submit
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
285
src/components/Cars/Update/index.jsx
Normal file
285
src/components/Cars/Update/index.jsx
Normal file
@@ -0,0 +1,285 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Redirect } from "react-router";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
FormGroup,
|
||||
FormLabel,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
TextField
|
||||
} from "@material-ui/core";
|
||||
|
||||
import useStyles from "../../useStyles";
|
||||
import {
|
||||
useVehicleContext,
|
||||
VehicleProvider
|
||||
} from "../../Contexts/VehicleContext";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import { useUserContext } from "../../Contexts/UserContext";
|
||||
import { logger } from "../../../services/monitoring";
|
||||
|
||||
|
||||
const MainForm = () => {
|
||||
const queries = new URLSearchParams(useLocation().search);
|
||||
const vin = queries.get("vin") ?? "";
|
||||
|
||||
const { vehicle, getVehicle, updateVehicle, busy } = useVehicleContext();
|
||||
const { token: { idToken: { jwtToken: token } } } = useUserContext();
|
||||
const { setMessage, setTitle, setSitePath } = useStatusContext();
|
||||
const [redirect, setRedirect] = useState(null);
|
||||
const classes = useStyles();
|
||||
|
||||
const modelEl = useRef(null);
|
||||
const yearEl = useRef(null);
|
||||
const trimEl = useRef(null);
|
||||
const [selectedLogLevel, setSelectedLogLevel] = useState("info");
|
||||
const [canbusEnabled, setCANBusEnabled] = useState(true);
|
||||
const [dataLoggerEnabled, setDataLoggerEnabled] = useState(false);
|
||||
const [maxMemBufferSize, setMaxMemBufferSize] = useState(0);
|
||||
const [maxDiskBufferSize, setMaxDiskBufferSize] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle("Update Vehicle");
|
||||
setSitePath([
|
||||
{
|
||||
label: `Vehicle ${vin}`,
|
||||
link: "/vehicles",
|
||||
},
|
||||
{
|
||||
label: "Update Vehicle",
|
||||
},
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (!vin || !token) return;
|
||||
await getVehicle(vin, token);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedLogLevel(vehicle.log_level ?? selectedLogLevel);
|
||||
|
||||
if (vehicle.canbus) {
|
||||
setCANBusEnabled(vehicle.canbus.enabled ?? canbusEnabled);
|
||||
setDataLoggerEnabled(vehicle.canbus.data_logger_enabled ?? dataLoggerEnabled);
|
||||
setMaxMemBufferSize(vehicle.canbus.max_mem_buffer_size ?? maxMemBufferSize);
|
||||
setMaxDiskBufferSize(vehicle.canbus.max_disk_buffer_size ?? maxDiskBufferSize);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [vehicle]);
|
||||
|
||||
const onLogLevelChange = (event) => {
|
||||
setSelectedLogLevel(event.target.value);
|
||||
}
|
||||
|
||||
const onCANBusChange = (event) => {
|
||||
setCANBusEnabled(event.target.checked);
|
||||
}
|
||||
|
||||
const onDataLoggerChange = (event) => {
|
||||
setDataLoggerEnabled(event.target.checked);
|
||||
}
|
||||
|
||||
const onMaxMemBufferSizeChange = (event) => {
|
||||
setMaxMemBufferSize(event.target.value);
|
||||
}
|
||||
|
||||
const onMaxDiskBufferSizeChange = (event) => {
|
||||
setMaxDiskBufferSize(event.target.value);
|
||||
}
|
||||
|
||||
const onSubmit = async (event) => {
|
||||
try {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = {
|
||||
vin: vin,
|
||||
model: modelEl.current.value,
|
||||
year: parseInt(yearEl.current.value),
|
||||
trim: trimEl.current.value,
|
||||
log_level: selectedLogLevel,
|
||||
canbus: {
|
||||
enabled: canbusEnabled,
|
||||
data_logger_enabled: canbusEnabled ? dataLoggerEnabled : false,
|
||||
max_mem_buffer_size: canbusEnabled ? parseInt(maxMemBufferSize) : 0,
|
||||
max_disk_buffer_size: canbusEnabled && dataLoggerEnabled ? parseInt(maxDiskBufferSize) : 0
|
||||
}
|
||||
};
|
||||
|
||||
const result = await updateVehicle(vin, formData, token);
|
||||
if (!result || result.error) return;
|
||||
|
||||
setMessage(`Updated ${result.vin}`);
|
||||
setRedirect(`/vehicle-status/${result.vin}`);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
};
|
||||
|
||||
if (redirect && redirect.length > 0) {
|
||||
return <Redirect to={redirect} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.paper}>
|
||||
<form className={classes.form} noValidate action="{onSubmit}">
|
||||
<TextField
|
||||
id="vin"
|
||||
name="vin"
|
||||
label="VIN"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "17",
|
||||
readOnly: true,
|
||||
}}
|
||||
disabled
|
||||
value={vin}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
id="model"
|
||||
name="model"
|
||||
label="Model"
|
||||
defaultValue="Ocean"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "255",
|
||||
}}
|
||||
required
|
||||
fullWidth
|
||||
inputRef={modelEl}
|
||||
/>
|
||||
<TextField
|
||||
id="year"
|
||||
name="year"
|
||||
label="Year"
|
||||
type="number"
|
||||
defaultValue="2022"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "4",
|
||||
minLength: "4",
|
||||
}}
|
||||
required
|
||||
fullWidth
|
||||
inputRef={yearEl}
|
||||
/>
|
||||
<TextField
|
||||
id="trim"
|
||||
name="trim"
|
||||
label="Trim"
|
||||
defaultValue="Base"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "4",
|
||||
minLength: "4",
|
||||
}}
|
||||
required
|
||||
fullWidth
|
||||
inputRef={trimEl}
|
||||
/>
|
||||
<FormLabel id="demo-row-radio-buttons-group-label">Log Level</FormLabel>
|
||||
<RadioGroup
|
||||
row
|
||||
aria-labelledby="demo-row-radio-buttons-group-label"
|
||||
name="log-level-group"
|
||||
value={selectedLogLevel}
|
||||
onChange={onLogLevelChange}
|
||||
margin="normal"
|
||||
>
|
||||
<FormControlLabel value="trace" control={<Radio />} label="Trace" />
|
||||
<FormControlLabel value="debug" control={<Radio />} label="Debug" />
|
||||
<FormControlLabel value="info" control={<Radio />} label="Info" />
|
||||
<FormControlLabel value="warn" control={<Radio />} label="Warn" />
|
||||
<FormControlLabel value="error" control={<Radio />} label="Error" />
|
||||
<FormControlLabel value="critical" control={<Radio />} label="Critical" />
|
||||
</RadioGroup>
|
||||
<FormLabel id="demo-row-radio-buttons-group-label">CAN Bus</FormLabel>
|
||||
<FormGroup>
|
||||
<FormControlLabel control={
|
||||
<Checkbox
|
||||
checked={canbusEnabled}
|
||||
onChange={onCANBusChange}
|
||||
/>
|
||||
} label="CAN Bus Enabled" />
|
||||
<TextField
|
||||
id="max_mem_buffer_size"
|
||||
name="max_mem_buffer_size"
|
||||
label='Max Memory Buffer Size (0 uses default size)'
|
||||
value={maxMemBufferSize}
|
||||
onChange={onMaxMemBufferSizeChange}
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "12",
|
||||
}}
|
||||
type="number"
|
||||
disabled={!canbusEnabled}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<FormControlLabel control={
|
||||
<Checkbox
|
||||
checked={dataLoggerEnabled}
|
||||
onChange={onDataLoggerChange}
|
||||
disabled={!canbusEnabled}
|
||||
/>
|
||||
} label="Data Logger Enabled" />
|
||||
</FormGroup>
|
||||
<TextField
|
||||
id="max_disk_buffer_size"
|
||||
name="max_disk_buffer_size"
|
||||
label='Max Disk Buffer Size (0 uses default size)'
|
||||
value={maxDiskBufferSize}
|
||||
onChange={onMaxDiskBufferSizeChange}
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "12",
|
||||
}}
|
||||
type="number"
|
||||
disabled={!dataLoggerEnabled}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.submit}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VehicleUpdateForm = (props) => (
|
||||
<VehicleProvider>
|
||||
<MainForm {...props} />
|
||||
</VehicleProvider>
|
||||
);
|
||||
|
||||
export default VehicleUpdateForm;
|
||||
36
src/components/Cars/Update/index.test.jsx
Normal file
36
src/components/Cars/Update/index.test.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
jest.mock("../../Contexts/VehicleContext");
|
||||
jest.mock("../../Contexts/StatusContext");
|
||||
jest.mock("../../Contexts/UserContext");
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { VehicleProvider } from "../../Contexts/VehicleContext";
|
||||
import { StatusProvider } from "../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
|
||||
import MainForm from "./index"
|
||||
|
||||
const renderVehicleUpdate = async () => {
|
||||
const { container } = render(
|
||||
<VehicleProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<MainForm />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</VehicleProvider>
|
||||
);
|
||||
await waitFor(() => { /* render */ });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("VehicleUpdate", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderVehicleUpdate();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
90
src/components/Certificates/Add/CreateForm.jsx
Normal file
90
src/components/Certificates/Add/CreateForm.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
FormControlLabel,
|
||||
FormLabel,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
TextField,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import useStyles from "../../useStyles";
|
||||
import { CertTypes } from "../../Contexts/CertificateContext";
|
||||
|
||||
const CreateForm = ({ onCreate, busy }) => {
|
||||
const classes = useStyles();
|
||||
const vinEl = useRef(null);
|
||||
const [certType, setCertType] = useState(CertTypes.TBOX);
|
||||
|
||||
const onSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (onCreate)
|
||||
onCreate({
|
||||
vin: vinEl.current.value,
|
||||
type: certType,
|
||||
});
|
||||
};
|
||||
|
||||
const onCertTypeChange = (event) => {
|
||||
setCertType(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.paper}>
|
||||
<form className={classes.form} noValidate action="{onSubmit}">
|
||||
<TextField
|
||||
id="vin"
|
||||
name="vin"
|
||||
label="VIN"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "17",
|
||||
}}
|
||||
required
|
||||
fullWidth
|
||||
inputRef={vinEl}
|
||||
/>
|
||||
<FormLabel id="cert-type-group-label">Type</FormLabel>
|
||||
<RadioGroup
|
||||
row
|
||||
aria-labelledby="cert-type-group-label"
|
||||
name="cert-type"
|
||||
value={certType}
|
||||
onChange={onCertTypeChange}
|
||||
margin="normal"
|
||||
>
|
||||
<FormControlLabel
|
||||
value={CertTypes.TBOX}
|
||||
control={<Radio />}
|
||||
label="TBOX"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={CertTypes.ICC}
|
||||
control={<Radio />}
|
||||
label="ICC"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={CertTypes.Charging}
|
||||
control={<Radio />}
|
||||
label="Charging"
|
||||
/>
|
||||
</RadioGroup>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.submit}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{busy ? "Submitting..." : "Submit"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateForm;
|
||||
50
src/components/Certificates/Add/DownloadCerts.jsx
Normal file
50
src/components/Certificates/Add/DownloadCerts.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Button } from "@material-ui/core";
|
||||
import React from "react";
|
||||
|
||||
import DownloadFileLink from "../../Controls/DownloadFileLink";
|
||||
import useStyles from "../../useStyles";
|
||||
|
||||
const CertMimeType = "application/x-pem-file";
|
||||
|
||||
const DownloadCerts = ({ vin, publicCert, privateCert, onChangeView }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const onNewCert = (event) => {
|
||||
event.preventDefault();
|
||||
if (!onChangeView) return;
|
||||
onChangeView();
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<h2>Download Certificates</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<DownloadFileLink
|
||||
data={publicCert}
|
||||
filename={`${vin}_cert.pem`}
|
||||
mimetype={CertMimeType}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<DownloadFileLink
|
||||
data={privateCert}
|
||||
filename={`${vin}_key.pem`}
|
||||
mimetype={CertMimeType}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.submit}
|
||||
onClick={onNewCert}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadCerts;
|
||||
25
src/components/Certificates/Add/DownloadCerts.test.jsx
Normal file
25
src/components/Certificates/Add/DownloadCerts.test.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
|
||||
import DownloadCerts from "./DownloadCerts";
|
||||
|
||||
describe("DownloadCerts", () => {
|
||||
beforeAll(() => {
|
||||
global.URL.createObjectURL = jest.fn();
|
||||
global.URL.revokeObjectURL = jest.fn();
|
||||
});
|
||||
|
||||
it("Render", async () => {
|
||||
const { container } = render(
|
||||
<DownloadCerts
|
||||
vin={"TESTVIN"}
|
||||
publicCert={"PUBLIC"}
|
||||
privateCert={"PRIVATE"}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => {
|
||||
/* render */
|
||||
});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DownloadCerts Render 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<h2>
|
||||
Download Certificates
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
download="TESTVIN_cert.pem"
|
||||
>
|
||||
TESTVIN_cert.pem
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
download="TESTVIN_key.pem"
|
||||
>
|
||||
TESTVIN_key.pem
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label"
|
||||
>
|
||||
Done
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
85
src/components/Certificates/Add/index.jsx
Normal file
85
src/components/Certificates/Add/index.jsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
useCertificateContext,
|
||||
CertificateProvider,
|
||||
} from "../../Contexts/CertificateContext";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import { useUserContext } from "../../Contexts/UserContext";
|
||||
import { logger } from "../../../services/monitoring";
|
||||
import CreateForm from "./CreateForm";
|
||||
import DownloadCerts from "./DownloadCerts";
|
||||
|
||||
const VIEW_FORM = 0;
|
||||
const VIEW_DOWNLOAD = 1;
|
||||
|
||||
const MainForm = () => {
|
||||
const { busy, createCert } = useCertificateContext();
|
||||
const { setMessage, setTitle, setSitePath } = useStatusContext();
|
||||
const {
|
||||
token: {
|
||||
idToken: { jwtToken: token },
|
||||
},
|
||||
} = useUserContext();
|
||||
const [view, setView] = useState(VIEW_FORM);
|
||||
const [pubCert, setPubCert] = useState(null);
|
||||
const [privCert, setPrivCert] = useState(null);
|
||||
const [vin, setVIN] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle("Create Certificate");
|
||||
setSitePath([
|
||||
{
|
||||
label: "Tools",
|
||||
link: "/tools/certificates/add",
|
||||
},
|
||||
{
|
||||
label: "Create Certificate",
|
||||
},
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onCreate = async (data) => {
|
||||
try {
|
||||
const result = await createCert(data, token);
|
||||
|
||||
setPubCert(result.public_key);
|
||||
setPrivCert(result.private_key);
|
||||
setVIN(data.vin);
|
||||
setMessage(`Created ${data.vin} certificate`);
|
||||
setView(VIEW_DOWNLOAD);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeView = () => {
|
||||
setPubCert(null);
|
||||
setPrivCert(null);
|
||||
setVIN(null);
|
||||
|
||||
setView(VIEW_FORM);
|
||||
};
|
||||
|
||||
if (view === VIEW_DOWNLOAD)
|
||||
return (
|
||||
<DownloadCerts
|
||||
vin={vin}
|
||||
publicCert={pubCert}
|
||||
privateCert={privCert}
|
||||
onChangeView={onChangeView}
|
||||
/>
|
||||
);
|
||||
|
||||
return <CreateForm onCreate={onCreate} busy={busy} />;
|
||||
};
|
||||
|
||||
const CertificateCreate = () => (
|
||||
<CertificateProvider>
|
||||
<MainForm />
|
||||
</CertificateProvider>
|
||||
);
|
||||
|
||||
export default CertificateCreate;
|
||||
127
src/components/Contexts/CANFiltersContext.jsx
Normal file
127
src/components/Contexts/CANFiltersContext.jsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import api from "../../services/CANFiltersAPI";
|
||||
|
||||
const CANFiltersContext = React.createContext();
|
||||
|
||||
export const CANFiltersProvider = ({ children }) => {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [filters, setFilters] = useState([]);
|
||||
const [totalFilters, setTotalFilters] = useState(0);
|
||||
|
||||
const addFilter = async (vin, filter, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
|
||||
validateVIN(vin);
|
||||
validateFilter(filter);
|
||||
|
||||
const result = await api.addFilter(vin, filter, token);
|
||||
if (result.error) throw new Error(`Add filter error. ${result.message}`);
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getFilters = async (vin, search, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
|
||||
validateVIN(vin);
|
||||
|
||||
const result = await api.getFilters(vin, search, token);
|
||||
if (result.error) {
|
||||
setFilters([])
|
||||
throw new Error(`Get filters error. ${result.message}`);
|
||||
}
|
||||
|
||||
setFilters(result.data)
|
||||
if (result.total) {
|
||||
setTotalFilters(result.total);
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateFilter = async (vin, canID, filter, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
|
||||
validateVIN(vin);
|
||||
validateID(canID);
|
||||
validateFilter(filter);
|
||||
|
||||
const result = await api.updateFilter(vin, canID, filter, token);
|
||||
if (result.error) {
|
||||
throw new Error(`Update filters error. ${result.message}`);
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const deleteFilter = async (vin, canID, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
|
||||
validateVIN(vin);
|
||||
validateID(canID);
|
||||
|
||||
const result = await api.deleteFilter(vin, canID, token);
|
||||
if (result.error) {
|
||||
throw new Error(`Delete filter error. ${result.message}`);
|
||||
}
|
||||
|
||||
const index = filters.findIndex(element => element.can_id === canID);
|
||||
if (index >= 0) filters.splice(index, 1);
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<CANFiltersContext.Provider
|
||||
value={{
|
||||
busy,
|
||||
filters,
|
||||
totalFilters,
|
||||
addFilter,
|
||||
getFilters,
|
||||
updateFilter,
|
||||
deleteFilter
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CANFiltersContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const validateVIN = (vin) => {
|
||||
if (vin == null || vin.length !== 17) {
|
||||
throw new Error("Invalid VIN");
|
||||
}
|
||||
}
|
||||
|
||||
const validateID = (can_id) => {
|
||||
if (can_id == null || can_id === "") {
|
||||
throw new Error("CAN ID required");
|
||||
}
|
||||
}
|
||||
|
||||
const validateFilter = (filter) => {
|
||||
if (filter == null) {
|
||||
throw new Error("No filter data");
|
||||
}
|
||||
|
||||
validateID(filter.can_id)
|
||||
|
||||
if (filter.interval == null) {
|
||||
throw new Error("Interval required");
|
||||
}
|
||||
};
|
||||
|
||||
export const useCANFiltersContext = () => useContext(CANFiltersContext);
|
||||
289
src/components/Contexts/CANFiltersContext.test.jsx
Normal file
289
src/components/Contexts/CANFiltersContext.test.jsx
Normal file
@@ -0,0 +1,289 @@
|
||||
jest.mock("../../services/CANFiltersAPI")
|
||||
|
||||
import {
|
||||
render,
|
||||
cleanup,
|
||||
screen,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import { CANFiltersProvider, useCANFiltersContext } from "./CANFiltersContext";
|
||||
import { StatusProvider, useStatusContext } from "./StatusContext";
|
||||
|
||||
const checkFiltersResults = (error, busy, filters) => {
|
||||
checkBaseResults(error, busy);
|
||||
expect(screen.getByTestId("filters").innerHTML).toEqual(filters);
|
||||
};
|
||||
|
||||
const checkBaseResults = (error, busy) => {
|
||||
expect(screen.getByTestId("error").innerHTML).toEqual(error);
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual(busy);
|
||||
};
|
||||
|
||||
describe("CANFiltersContext", () => {
|
||||
describe("getFilters", () => {
|
||||
beforeEach(() => {
|
||||
const TestComp = () => {
|
||||
const { busy, error, filters, getFilters } = useCANFiltersContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{error}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<div data-testid="filters">{JSON.stringify(filters)}</div>
|
||||
<button
|
||||
data-testid="getFilters"
|
||||
onClick={() => getFilters("TESTVIN1234567890")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<CANFiltersProvider>
|
||||
<TestComp />
|
||||
</CANFiltersProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkFiltersResults("", "false", "[]");
|
||||
});
|
||||
|
||||
it("getFilters", async () => {
|
||||
fireEvent.click(screen.getByTestId("getFilters"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("filters").innerHTML).not.toBe("[]")
|
||||
);
|
||||
checkFiltersResults("", "false", JSON.stringify(expectedFiltersData));
|
||||
});
|
||||
});
|
||||
|
||||
describe("addFilter", () => {
|
||||
beforeEach(async () => {
|
||||
const TestComp = () => {
|
||||
const { busy, addFilter } = useCANFiltersContext();
|
||||
const { message, setMessage } = useStatusContext();
|
||||
const add = async (data) => {
|
||||
try {
|
||||
await addFilter("TESTVIN1234567890", data);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{message}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<button data-testid="addFilterNull" onClick={() => add(null)} />
|
||||
<button data-testid="addFilterNoCANID" onClick={() => add({})} />
|
||||
<button
|
||||
data-testid="addFilter"
|
||||
onClick={() =>
|
||||
add({ can_id: "123", interval: 1000 })
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<StatusProvider>
|
||||
<CANFiltersProvider>
|
||||
<TestComp />
|
||||
</CANFiltersProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
|
||||
it("addFilterNull", async () => {
|
||||
fireEvent.click(screen.getByTestId("addFilterNull"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("No filter data", "false");
|
||||
});
|
||||
|
||||
it("addFilterNoCANID", async () => {
|
||||
fireEvent.click(screen.getByTestId("addFilterNoCANID"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("CAN ID required", "false");
|
||||
});
|
||||
|
||||
it("addFilter", async () => {
|
||||
fireEvent.click(screen.getByTestId("addFilter"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateFilter", () => {
|
||||
beforeEach(async () => {
|
||||
const TestComp = () => {
|
||||
const { busy, updateFilter } = useCANFiltersContext();
|
||||
const { message, setMessage } = useStatusContext();
|
||||
const update = async (data) => {
|
||||
try {
|
||||
await updateFilter("TESTVIN1234567890", data.can_id, data);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{message}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<button data-testid="updateFilterNull" onClick={() => update(null)} />
|
||||
<button data-testid="updateFilterNoCANID" onClick={() => update({})} />
|
||||
<button
|
||||
data-testid="updateFilter"
|
||||
onClick={() =>
|
||||
update({ can_id: "123", interval: 1000 })
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<StatusProvider>
|
||||
<CANFiltersProvider>
|
||||
<TestComp />
|
||||
</CANFiltersProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
|
||||
it("updateFilterNull", async () => {
|
||||
fireEvent.click(screen.getByTestId("updateFilterNull"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("Cannot read property 'can_id' of null", "false");
|
||||
});
|
||||
|
||||
it("updateFilterNoCANID", async () => {
|
||||
fireEvent.click(screen.getByTestId("updateFilterNoCANID"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("CAN ID required", "false");
|
||||
});
|
||||
|
||||
it("updateFilter", async () => {
|
||||
fireEvent.click(screen.getByTestId("updateFilter"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteFilter", () => {
|
||||
beforeEach(async () => {
|
||||
const TestComp = () => {
|
||||
const { busy, deleteFilter } = useCANFiltersContext();
|
||||
const { message, setMessage } = useStatusContext();
|
||||
const deleteF = async (can_id) => {
|
||||
try {
|
||||
await deleteFilter("TESTVIN1234567890", can_id);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{message}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<button data-testid="deleteFilterNull" onClick={() => deleteF(null)} />
|
||||
<button data-testid="deleteFilterNonexistent" onClick={() => deleteF(-1)} />
|
||||
<button
|
||||
data-testid="deleteFilter"
|
||||
onClick={() =>
|
||||
deleteF(123)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<StatusProvider>
|
||||
<CANFiltersProvider>
|
||||
<TestComp />
|
||||
</CANFiltersProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
|
||||
it("deleteFilterNull", async () => {
|
||||
fireEvent.click(screen.getByTestId("deleteFilterNull"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("CAN ID required", "false");
|
||||
});
|
||||
|
||||
it("deleteFilterNonexistent", async () => {
|
||||
fireEvent.click(screen.getByTestId("deleteFilterNonexistent"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
|
||||
it("deleteFilter", async () => {
|
||||
fireEvent.click(screen.getByTestId("deleteFilter"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const expectedFiltersData = [
|
||||
{
|
||||
can_id: "123",
|
||||
interval: 1000
|
||||
},
|
||||
{
|
||||
can_id: "456",
|
||||
interval: 0
|
||||
},
|
||||
{
|
||||
can_id: "789-1000",
|
||||
interval: 5
|
||||
},
|
||||
];
|
||||
50
src/components/Contexts/CertificateContext.jsx
Normal file
50
src/components/Contexts/CertificateContext.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
|
||||
import api from "../../services/certificatesAPI";
|
||||
|
||||
const CertificateContext = React.createContext();
|
||||
|
||||
export const CertTypes = {
|
||||
TBOX: "TBOX",
|
||||
ICC: "ICC",
|
||||
Charging: "Charging",
|
||||
};
|
||||
|
||||
const validateCreate = (data) => {
|
||||
if (!data.type) throw new Error("type is required");
|
||||
if (!data.vin) throw new Error("vin is required");
|
||||
};
|
||||
|
||||
export const CertificateProvider = ({ children }) => {
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const createCert = async (data, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
|
||||
validateCreate(data);
|
||||
|
||||
const result = await api.create(data, token);
|
||||
if (result.error) {
|
||||
throw new Error(`Create certificate error. ${result.message}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CertificateContext.Provider
|
||||
value={{
|
||||
busy,
|
||||
createCert,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CertificateContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useCertificateContext = () => useContext(CertificateContext);
|
||||
307
src/components/Contexts/FleetContext.jsx
Normal file
307
src/components/Contexts/FleetContext.jsx
Normal file
@@ -0,0 +1,307 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import api from "../../services/fleetsAPI";
|
||||
|
||||
const FleetContext = React.createContext();
|
||||
|
||||
export const FleetProvider = ({ children }) => {
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const [fleet, setFleet] = useState({});
|
||||
|
||||
const [fleets, setFleets] = useState([]);
|
||||
const [totalFleets, setTotalFleets] = useState(0);
|
||||
|
||||
const [fleetVehicles, setFleetVehicles] = useState([]);
|
||||
const [totalFleetVehicles, setTotalFleetVehicles] = useState(0);
|
||||
|
||||
const [fleetCANFilters, setFleetCANFilters] = useState([]);
|
||||
const [totalFleetCANFilters, setTotalFleetCANFilters] = useState(0);
|
||||
|
||||
const addFleet = async (f, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
validateFleet(f);
|
||||
|
||||
const result = await api.addFleet(f, token);
|
||||
if (result.error) throw new Error(`Add fleet error. ${result.message}`);
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getFleet = async (name, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
validateFleetName(name);
|
||||
|
||||
const result = await api.getFleet(name, token);
|
||||
if (result.error) {
|
||||
setFleet({});
|
||||
throw new Error(`Get fleet error. ${result.message}`);
|
||||
}
|
||||
|
||||
setFleet(result);
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getFleets = async (search, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
|
||||
const result = await api.getFleets(search, token);
|
||||
if (result.error) {
|
||||
setFleets([])
|
||||
throw new Error(`Get fleets error. ${result.message}`);
|
||||
}
|
||||
setFleets(result.data)
|
||||
if (result.total) {
|
||||
setTotalFleets(result.total);
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateFleet = async (name, f, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
|
||||
validateFleetName(name);
|
||||
validateFleet(f);
|
||||
|
||||
const result = await api.updateFleet(name, f, token);
|
||||
if (result.error) {
|
||||
throw new Error(`Update fleet error. ${result.message}`);
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteFleet = async (name, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
|
||||
validateFleetName(name);
|
||||
|
||||
const result = await api.deleteFleet(name, token);
|
||||
if (result.error) {
|
||||
throw new Error(`Delete filter error. ${result.message}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getFleetVehicles = async (name, search, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
|
||||
const result = await api.getFleetVehicles(name, search, token);
|
||||
if (result.error) {
|
||||
setFleetVehicles([])
|
||||
throw new Error(`Get fleet vehicles error. ${result.message}`);
|
||||
}
|
||||
|
||||
setFleetVehicles(result.data)
|
||||
if (result.total) {
|
||||
setTotalFleetVehicles(result.total);
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addFleetVehicle = async (name, vehicle, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
|
||||
validateFleetName(name);
|
||||
validateVIN(vehicle.vin);
|
||||
|
||||
const result = await api.addFleetVehicle(name, vehicle, token);
|
||||
if (result.error) {
|
||||
throw new Error(`Add fleet vehicle error. ${result.message}`);
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteFleetVehicle = async (name, vehicle, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
|
||||
validateFleetName(name);
|
||||
validateVIN(vehicle.vin);
|
||||
|
||||
const result = await api.deleteFleetVehicle(name, vehicle, token);
|
||||
if (result.error) {
|
||||
throw new Error(`Delete fleet vehicle error. ${result.message}`);
|
||||
}
|
||||
|
||||
const index = fleetVehicles.findIndex(element => element === vehicle.vin);
|
||||
if (index >= 0) fleetVehicles.splice(index, 1);
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getFleetCANFilters = async (name, search, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
|
||||
const result = await api.getFleetCANFilters(name, search, token);
|
||||
if (result.error) {
|
||||
setFleetCANFilters([])
|
||||
throw new Error(`Get fleet filters error. ${result.message}`);
|
||||
}
|
||||
|
||||
setFleetCANFilters(result.data)
|
||||
if (result.total) {
|
||||
setTotalFleetCANFilters(result.total);
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addFleetCANFilter = async (name, filter, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
|
||||
validateFleetName(name);
|
||||
validateFilter(filter);
|
||||
|
||||
const result = await api.addFleetCANFilter(name, filter, token);
|
||||
if (result.error) {
|
||||
throw new Error(`Add fleet CAN filter error. ${result.message}`);
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const updateFleetCANFilter = async (name, can_id, filter, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
|
||||
validateFleetName(name);
|
||||
validateCANID(can_id);
|
||||
validateFilter(filter);
|
||||
|
||||
const result = await api.updateFleetCANFilter(name, can_id, filter, token);
|
||||
if (result.error) {
|
||||
throw new Error(`Update fleet CAN filter error. ${result.message}`);
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const deleteFleetCANFilter = async (name, can_id, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
|
||||
validateFleetName(name);
|
||||
validateCANID(can_id);
|
||||
|
||||
const result = await api.deleteFleetCANFilter(name, can_id, token);
|
||||
if (result.error) {
|
||||
throw new Error(`Delete fleet vehicle error. ${result.message}`);
|
||||
}
|
||||
|
||||
const index = fleetCANFilters.findIndex(element => element.can_id === can_id);
|
||||
if (index >= 0) fleetCANFilters.splice(index, 1);
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FleetContext.Provider
|
||||
value={{
|
||||
busy,
|
||||
|
||||
fleet,
|
||||
fleets,
|
||||
totalFleets,
|
||||
addFleet,
|
||||
getFleet,
|
||||
getFleets,
|
||||
updateFleet,
|
||||
deleteFleet,
|
||||
|
||||
fleetVehicles,
|
||||
totalFleetVehicles,
|
||||
getFleetVehicles,
|
||||
addFleetVehicle,
|
||||
deleteFleetVehicle,
|
||||
|
||||
fleetCANFilters,
|
||||
totalFleetCANFilters,
|
||||
getFleetCANFilters,
|
||||
addFleetCANFilter,
|
||||
updateFleetCANFilter,
|
||||
deleteFleetCANFilter
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FleetContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const validateFleet = (f) => {
|
||||
if (f == null) {
|
||||
throw new Error("No fleet data");
|
||||
}
|
||||
|
||||
validateFleetName(f.name);
|
||||
}
|
||||
|
||||
const validateFleetName = (name) => {
|
||||
if (name == null || !/^[\w-]+$/.test(name)) {
|
||||
throw new Error("Invalid name");
|
||||
}
|
||||
}
|
||||
|
||||
const validateVIN = (vin) => {
|
||||
if (vin == null || vin.length !== 17) {
|
||||
throw new Error("Invalid VIN");
|
||||
}
|
||||
}
|
||||
|
||||
const validateFilter = (filter) => {
|
||||
if (filter == null) {
|
||||
throw new Error("No CAN filter data");
|
||||
}
|
||||
|
||||
validateCANID(filter.can_id)
|
||||
|
||||
if (filter.interval == null || !/^\d+$/.test(filter.interval)) {
|
||||
throw new Error("Invalid interval");
|
||||
}
|
||||
}
|
||||
|
||||
const validateCANID = (can_id) => {
|
||||
if (can_id == null || !/^\d+(-\d+)?$/.test(can_id)) {
|
||||
throw new Error("Invalid CAN ID");
|
||||
}
|
||||
}
|
||||
|
||||
export const useFleetContext = () => useContext(FleetContext);
|
||||
752
src/components/Contexts/FleetContext.test.jsx
Normal file
752
src/components/Contexts/FleetContext.test.jsx
Normal file
@@ -0,0 +1,752 @@
|
||||
jest.mock("../../services/fleetsAPI")
|
||||
|
||||
import {
|
||||
render,
|
||||
cleanup,
|
||||
screen,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import { FleetProvider, useFleetContext } from "./FleetContext";
|
||||
import { StatusProvider, useStatusContext } from "./StatusContext";
|
||||
|
||||
const checkFleetResults = (error, busy, fleet) => {
|
||||
checkBaseResults(error, busy);
|
||||
expect(screen.getByTestId("fleet").innerHTML).toEqual(fleet);
|
||||
};
|
||||
|
||||
const checkFleetsResults = (error, busy, fleets) => {
|
||||
checkBaseResults(error, busy);
|
||||
expect(screen.getByTestId("fleets").innerHTML).toEqual(fleets);
|
||||
};
|
||||
|
||||
const checkFleetVehicleResults = (error, busy, vehicles) => {
|
||||
checkBaseResults(error, busy);
|
||||
expect(screen.getByTestId("fleet-vehicles").innerHTML).toEqual(vehicles);
|
||||
}
|
||||
|
||||
const checkFleetCANFilterResults = (error, busy, filters) => {
|
||||
checkBaseResults(error, busy);
|
||||
expect(screen.getByTestId("fleet-filters").innerHTML).toEqual(filters);
|
||||
}
|
||||
|
||||
const checkBaseResults = (error, busy) => {
|
||||
expect(screen.getByTestId("error").innerHTML).toEqual(error);
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual(busy);
|
||||
};
|
||||
|
||||
describe("FleetContext", () => {
|
||||
describe("getFleets", () => {
|
||||
beforeEach(() => {
|
||||
const TestComp = () => {
|
||||
const { busy, error, fleets, getFleets } = useFleetContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{error}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<div data-testid="fleets">{JSON.stringify(fleets)}</div>
|
||||
<button
|
||||
data-testid="getFleets"
|
||||
onClick={() => getFleets()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<FleetProvider>
|
||||
<TestComp />
|
||||
</FleetProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkFleetsResults("", "false", "[]");
|
||||
});
|
||||
|
||||
it("getFleets", async () => {
|
||||
fireEvent.click(screen.getByTestId("getFleets"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("fleets").innerHTML).not.toBe("[]")
|
||||
);
|
||||
checkFleetsResults("", "false", JSON.stringify(expectedFleetsData));
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFleet", () => {
|
||||
beforeEach(() => {
|
||||
const TestComp = () => {
|
||||
const { busy, error, fleet, getFleet } = useFleetContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{error}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<div data-testid="fleet">{JSON.stringify(fleet)}</div>
|
||||
<button
|
||||
data-testid="getFleet"
|
||||
onClick={() => getFleet("US-WEST")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<FleetProvider>
|
||||
<TestComp />
|
||||
</FleetProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkFleetResults("", "false", "{}");
|
||||
});
|
||||
|
||||
it("getFleet", async () => {
|
||||
fireEvent.click(screen.getByTestId("getFleet"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("fleet").innerHTML).not.toBe("{}")
|
||||
);
|
||||
checkFleetResults("", "false", JSON.stringify(expectedFleetData));
|
||||
});
|
||||
});
|
||||
|
||||
describe("addFleet", () => {
|
||||
beforeEach(async () => {
|
||||
const TestComp = () => {
|
||||
const { busy, addFleet } = useFleetContext();
|
||||
const { message, setMessage } = useStatusContext();
|
||||
const add = async (fleet) => {
|
||||
try {
|
||||
await addFleet(fleet);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{message}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<button data-testid="addFleetNull" onClick={() => add(null)} />
|
||||
<button data-testid="addFleetNoName" onClick={() => add({})} />
|
||||
<button
|
||||
data-testid="addFleet"
|
||||
onClick={() =>
|
||||
add({ name: "EU-WEST", log_level: "warn", canbus: { enabled: false } })
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<StatusProvider>
|
||||
<FleetProvider>
|
||||
<TestComp />
|
||||
</FleetProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
|
||||
it("addFleetNull", async () => {
|
||||
fireEvent.click(screen.getByTestId("addFleetNull"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("No fleet data", "false");
|
||||
});
|
||||
|
||||
it("addFleetNoName", async () => {
|
||||
fireEvent.click(screen.getByTestId("addFleetNoName"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("Invalid name", "false");
|
||||
});
|
||||
|
||||
it("addFleet", async () => {
|
||||
fireEvent.click(screen.getByTestId("addFleet"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateFleet", () => {
|
||||
beforeEach(async () => {
|
||||
const TestComp = () => {
|
||||
const { busy, updateFleet } = useFleetContext();
|
||||
const { message, setMessage } = useStatusContext();
|
||||
const update = async (data) => {
|
||||
try {
|
||||
await updateFleet("EU-WEST", data);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{message}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<button data-testid="updateFleetNull" onClick={() => update(null)} />
|
||||
<button data-testid="updateFleetNoName" onClick={() => update({})} />
|
||||
<button
|
||||
data-testid="updateFleet"
|
||||
onClick={() =>
|
||||
update({ name: "EU-WEST", log_level: "warn", canbus: { enabled: false } })
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<StatusProvider>
|
||||
<FleetProvider>
|
||||
<TestComp />
|
||||
</FleetProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
|
||||
it("updateFleetNull", async () => {
|
||||
fireEvent.click(screen.getByTestId("updateFleetNull"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("No fleet data", "false");
|
||||
});
|
||||
|
||||
it("updateFleetNoName", async () => {
|
||||
fireEvent.click(screen.getByTestId("updateFleetNoName"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("Invalid name", "false");
|
||||
});
|
||||
|
||||
it("updateFleet", async () => {
|
||||
fireEvent.click(screen.getByTestId("updateFleet"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteFleet", () => {
|
||||
beforeEach(async () => {
|
||||
const TestComp = () => {
|
||||
const { busy, deleteFleet } = useFleetContext();
|
||||
const { message, setMessage } = useStatusContext();
|
||||
const deleteF = async (name) => {
|
||||
try {
|
||||
await deleteFleet(name);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{message}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<button data-testid="deleteFleetNull" onClick={() => deleteF(null)} />
|
||||
<button data-testid="deleteFleetNonexistent" onClick={() => deleteF("INVALID")} />
|
||||
<button
|
||||
data-testid="deleteFleet"
|
||||
onClick={() =>
|
||||
deleteF("US-WEST")
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<StatusProvider>
|
||||
<FleetProvider>
|
||||
<TestComp />
|
||||
</FleetProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
|
||||
it("deleteFleetNull", async () => {
|
||||
fireEvent.click(screen.getByTestId("deleteFleetNull"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("Invalid name", "false");
|
||||
});
|
||||
|
||||
it("deleteFleetNonexistent", async () => {
|
||||
fireEvent.click(screen.getByTestId("deleteFleetNonexistent"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
|
||||
it("deleteFleet", async () => {
|
||||
fireEvent.click(screen.getByTestId("deleteFleet"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFleetVehicles", () => {
|
||||
beforeEach(() => {
|
||||
const TestComp = () => {
|
||||
const { busy, error, fleetVehicles, getFleetVehicles } = useFleetContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{error}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<div data-testid="fleet-vehicles">{JSON.stringify(fleetVehicles)}</div>
|
||||
<button
|
||||
data-testid="getFleetVehicles"
|
||||
onClick={() => getFleetVehicles("US-WEST")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<FleetProvider>
|
||||
<TestComp />
|
||||
</FleetProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkFleetVehicleResults("", "false", "[]");
|
||||
});
|
||||
|
||||
it("getFleetVehicles", async () => {
|
||||
fireEvent.click(screen.getByTestId("getFleetVehicles"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("fleet-vehicles").innerHTML).not.toBe("[]")
|
||||
);
|
||||
checkFleetVehicleResults("", "false", JSON.stringify(expectedFleetVehiclesData));
|
||||
});
|
||||
});
|
||||
|
||||
describe("addFleetVehicle", () => {
|
||||
beforeEach(async () => {
|
||||
const TestComp = () => {
|
||||
const { busy, addFleetVehicle } = useFleetContext();
|
||||
const { message, setMessage } = useStatusContext();
|
||||
const add = async (name, vehicle) => {
|
||||
try {
|
||||
await addFleetVehicle(name, vehicle);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{message}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<button data-testid="addFleetVehicleNull" onClick={() => add(null)} />
|
||||
<button data-testid="addFleetVehicleNoName" onClick={() => add({})} />
|
||||
<button
|
||||
data-testid="addFleetVehicle"
|
||||
onClick={() =>
|
||||
add("US-TEST", { vin: "TESTVIN1234567890" })
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<StatusProvider>
|
||||
<FleetProvider>
|
||||
<TestComp />
|
||||
</FleetProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
|
||||
it("addFleetVehicleNull", async () => {
|
||||
fireEvent.click(screen.getByTestId("addFleetVehicleNull"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("Invalid name", "false");
|
||||
});
|
||||
|
||||
it("addFleetVehicleNoName", async () => {
|
||||
fireEvent.click(screen.getByTestId("addFleetVehicleNoName"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("Invalid name", "false");
|
||||
});
|
||||
|
||||
it("addFleetVehicle", async () => {
|
||||
fireEvent.click(screen.getByTestId("addFleetVehicle"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteFleetVehicle", () => {
|
||||
beforeEach(async () => {
|
||||
const TestComp = () => {
|
||||
const { busy, deleteFleetVehicle } = useFleetContext();
|
||||
const { message, setMessage } = useStatusContext();
|
||||
const deleteFV = async (name, vehicle) => {
|
||||
try {
|
||||
await deleteFleetVehicle(name, vehicle);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{message}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<button data-testid="deleteFleetVehicleNull" onClick={() => deleteFV("US-WEST", null)} />
|
||||
<button data-testid="deleteFleetVehicleInvalid" onClick={() => deleteFV("US-WEST", "INVALID")} />
|
||||
<button
|
||||
data-testid="deleteFleetVehicle"
|
||||
onClick={() =>
|
||||
deleteFV("US-WEST", { vin: "USWESTVIN12345678" })
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<StatusProvider>
|
||||
<FleetProvider>
|
||||
<TestComp />
|
||||
</FleetProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
|
||||
it("deleteFleetVehicleNull", async () => {
|
||||
fireEvent.click(screen.getByTestId("deleteFleetVehicleNull"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("Cannot read property 'vin' of null", "false");
|
||||
});
|
||||
|
||||
it("deleteFleetVehicleNonexistent", async () => {
|
||||
fireEvent.click(screen.getByTestId("deleteFleetVehicleInvalid"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("Invalid VIN", "false");
|
||||
});
|
||||
|
||||
it("deleteFleetVehicle", async () => {
|
||||
fireEvent.click(screen.getByTestId("deleteFleetVehicle"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFleetCANFilters", () => {
|
||||
beforeEach(() => {
|
||||
const TestComp = () => {
|
||||
const { busy, error, fleetCANFilters, getFleetCANFilters } = useFleetContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{error}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<div data-testid="fleet-filters">{JSON.stringify(fleetCANFilters)}</div>
|
||||
<button
|
||||
data-testid="getFleetCANFilters"
|
||||
onClick={() => getFleetCANFilters("US-TEST")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<FleetProvider>
|
||||
<TestComp />
|
||||
</FleetProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkFleetCANFilterResults("", "false", "[]");
|
||||
});
|
||||
|
||||
it("getFleetCANFilters", async () => {
|
||||
fireEvent.click(screen.getByTestId("getFleetCANFilters"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("fleet-filters").innerHTML).not.toBe("[]")
|
||||
);
|
||||
checkFleetCANFilterResults("", "false", JSON.stringify(expectedFleetCANFiltersData));
|
||||
});
|
||||
});
|
||||
|
||||
describe("addFleetCANFilter", () => {
|
||||
beforeEach(async () => {
|
||||
const TestComp = () => {
|
||||
const { busy, addFleetCANFilter } = useFleetContext();
|
||||
const { message, setMessage } = useStatusContext();
|
||||
const add = async (name, filter) => {
|
||||
try {
|
||||
await addFleetCANFilter(name, filter);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{message}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<button data-testid="addFleetCANFilterNull" onClick={() => add(null)} />
|
||||
<button data-testid="addFleetCANFilterNoName" onClick={() => add({})} />
|
||||
<button
|
||||
data-testid="addFleetCANFilter"
|
||||
onClick={() =>
|
||||
add("US-TEST", { can_id: "111", interval: 222 })
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<StatusProvider>
|
||||
<FleetProvider>
|
||||
<TestComp />
|
||||
</FleetProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
|
||||
it("addFleetCANFilterNull", async () => {
|
||||
fireEvent.click(screen.getByTestId("addFleetCANFilterNull"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("Invalid name", "false");
|
||||
});
|
||||
|
||||
it("addFleetCANFilterNoName", async () => {
|
||||
fireEvent.click(screen.getByTestId("addFleetCANFilterNoName"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("Invalid name", "false");
|
||||
});
|
||||
|
||||
it("addFleetCANFilter", async () => {
|
||||
fireEvent.click(screen.getByTestId("addFleetCANFilter"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteFleetCANFilter", () => {
|
||||
beforeEach(async () => {
|
||||
const TestComp = () => {
|
||||
const { busy, deleteFleetCANFilter } = useFleetContext();
|
||||
const { message, setMessage } = useStatusContext();
|
||||
const deleteFF = async (name, can_id) => {
|
||||
try {
|
||||
await deleteFleetCANFilter(name, can_id);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{message}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<button data-testid="deleteFleetCANFilterNull" onClick={() => deleteFF("US-WEST", null)} />
|
||||
<button data-testid="deleteFleetCANFilterInvalid" onClick={() => deleteFF("US-WEST", "INVALID")} />
|
||||
<button
|
||||
data-testid="deleteFleetCANFilter"
|
||||
onClick={() =>
|
||||
deleteFF("US-WEST", "123-456")
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<StatusProvider>
|
||||
<FleetProvider>
|
||||
<TestComp />
|
||||
</FleetProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
|
||||
it("deleteFleetCANFilterNull", async () => {
|
||||
fireEvent.click(screen.getByTestId("deleteFleetCANFilterNull"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("Invalid CAN ID", "false");
|
||||
});
|
||||
|
||||
it("deleteFleetCANFilterNonexistent", async () => {
|
||||
fireEvent.click(screen.getByTestId("deleteFleetCANFilterInvalid"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("Invalid CAN ID", "false");
|
||||
});
|
||||
|
||||
it("deleteFleetCANFilter", async () => {
|
||||
fireEvent.click(screen.getByTestId("deleteFleetCANFilter"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const expectedFilters = [
|
||||
{
|
||||
can_id: "123-456",
|
||||
interval: 789
|
||||
},
|
||||
{
|
||||
can_id: "1",
|
||||
interval: 1000
|
||||
},
|
||||
{
|
||||
can_id: "1000",
|
||||
interval: 1
|
||||
}
|
||||
]
|
||||
|
||||
const expectedFleetData = {
|
||||
name: "US-WEST",
|
||||
log_level: "info",
|
||||
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: expectedFilters },
|
||||
vehicles: ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"]
|
||||
}
|
||||
|
||||
const expectedFleetsData = [
|
||||
{
|
||||
name: "US-WEST",
|
||||
log_level: "info",
|
||||
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: expectedFilters },
|
||||
vehicles: ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"]
|
||||
},
|
||||
{
|
||||
name: "US-CENTRAL",
|
||||
log_level: "warn",
|
||||
canbus: { enabled: false, data_logger_enabled: false, max_mem_buffer_size: 0, max_disk_buffer_size: 0 },
|
||||
vehicles: ["USCENTVIN12345678", "USCENTVIN12345679", "USCENTVIN12345670"]
|
||||
},
|
||||
{
|
||||
name: "US-EAST",
|
||||
log_level: "error",
|
||||
canbus: { enabled: true },
|
||||
vehicles: ["USEASTVIN12345678", "USEASTVIN12345679", "USEASTVIN12345670"]
|
||||
},
|
||||
];
|
||||
|
||||
const expectedFleetVehiclesData = ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"];
|
||||
|
||||
const expectedFleetCANFiltersData = [
|
||||
{
|
||||
can_id: "123-456",
|
||||
interval: 789
|
||||
},
|
||||
{
|
||||
can_id: "1",
|
||||
interval: 1000
|
||||
},
|
||||
{
|
||||
can_id: "1000",
|
||||
interval: 1
|
||||
}
|
||||
];
|
||||
@@ -22,13 +22,10 @@ const checkExistingManifest = async (data, token) => {
|
||||
|
||||
const ECUTemplate = {
|
||||
name: "AGS",
|
||||
part_number: "",
|
||||
version: "",
|
||||
serial_number: "",
|
||||
hw_version: "",
|
||||
vendor: "",
|
||||
configuration_mask: "",
|
||||
configuration: "",
|
||||
fingerprint: "",
|
||||
files: [],
|
||||
manifest_id: 0,
|
||||
};
|
||||
|
||||
@@ -46,16 +46,16 @@ export const ManifestsProvider = ({ children }) => {
|
||||
const deleteManifest = async (package_id, token) => {
|
||||
let result;
|
||||
|
||||
const index = manifests.findIndex((element) => {
|
||||
return element.id === package_id;
|
||||
});
|
||||
manifests.splice(index, 1);
|
||||
|
||||
try {
|
||||
setBusy(true);
|
||||
result = await api.deleteManifest(package_id, token);
|
||||
if (result.error)
|
||||
throw new Error(`Delete manifest error. ${result.message}`);
|
||||
|
||||
const index = manifests.findIndex((element) => {
|
||||
return element.id === package_id;
|
||||
});
|
||||
manifests.splice(index, 1);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import api from "../../services/vehiclesAPI";
|
||||
const VehicleContext = React.createContext();
|
||||
|
||||
const validateAdd = (vehicle) => {
|
||||
if (vehicle === null) {
|
||||
if (vehicle == null) {
|
||||
throw new Error("No vehicle data");
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ const validateAdd = (vehicle) => {
|
||||
|
||||
export const VehicleProvider = ({ children }) => {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [vehicle, setVehicle] = useState({});
|
||||
const [vehicles, setVehicles] = useState([]);
|
||||
const [totalVehicles, setTotalVehicles] = useState(0);
|
||||
const [models, setModels] = useState([]);
|
||||
@@ -49,11 +50,11 @@ export const VehicleProvider = ({ children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const addVehicle = async (vehicle, token) => {
|
||||
const addVehicle = async (v, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
validateAdd(vehicle);
|
||||
const result = await api.addVehicle(vehicle, token);
|
||||
validateAdd(v);
|
||||
const result = await api.addVehicle(v, token);
|
||||
if (result.error) throw new Error(`Add vehicle error. ${result.message}`);
|
||||
return result;
|
||||
} finally {
|
||||
@@ -118,6 +119,21 @@ export const VehicleProvider = ({ children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getVehicle = async (vin, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
validateVIN(vin);
|
||||
|
||||
const result = await api.getVehicle(vin, token);
|
||||
if (result.error) throw new Error(`Get vehicle error. ${result.message}`);
|
||||
|
||||
setVehicle(result);
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const getVehicles = async (search, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
@@ -159,23 +175,56 @@ export const VehicleProvider = ({ children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const updateVehicle = async (vin, v, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
validateVIN(vin);
|
||||
validateVehicle(v);
|
||||
|
||||
const result = await api.updateVehicle(vin, v, token);
|
||||
if (result.error)
|
||||
throw new Error(`Update vehicle error. ${result.message}`);
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const deleteVehicle = async (vin, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
validateVIN(vin);
|
||||
|
||||
const result = await api.deleteVehicle(vin, token);
|
||||
if (result.error)
|
||||
throw new Error(`Delete vehicle error. ${result.message}`);
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<VehicleContext.Provider
|
||||
value={{
|
||||
busy,
|
||||
models,
|
||||
totalVehicles,
|
||||
vehicle,
|
||||
vehicles,
|
||||
years,
|
||||
addVehicle,
|
||||
deleteVehicle,
|
||||
getConnections,
|
||||
getECUs,
|
||||
getLocations,
|
||||
getModels,
|
||||
getState,
|
||||
getYears,
|
||||
getVehicle,
|
||||
getVehicles,
|
||||
sendCommand,
|
||||
updateVehicle
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -183,4 +232,19 @@ export const VehicleProvider = ({ children }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const validateVehicle = (v) => {
|
||||
if (v == null) {
|
||||
throw new Error("No vehicle data");
|
||||
}
|
||||
|
||||
validateVIN(v.vin);
|
||||
};
|
||||
|
||||
const validateVIN = (vin) => {
|
||||
if (vin == null || vin.length !== 17) {
|
||||
throw new Error("Invalid VIN");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const useVehicleContext = () => useContext(VehicleContext);
|
||||
|
||||
@@ -10,7 +10,12 @@ import {
|
||||
import { VehicleProvider, useVehicleContext } from "./VehicleContext";
|
||||
import { StatusProvider, useStatusContext } from "./StatusContext";
|
||||
|
||||
const checkVehicleResults = (error, busy, vehicles) => {
|
||||
const checkVehicleResult = (error, busy, vehicle) => {
|
||||
checkBaseResults(error, busy);
|
||||
expect(screen.getByTestId("vehicle").innerHTML).toEqual(vehicle);
|
||||
}
|
||||
|
||||
const checkVehiclesResult = (error, busy, vehicles) => {
|
||||
checkBaseResults(error, busy);
|
||||
expect(screen.getByTestId("vehicles").innerHTML).toEqual(vehicles);
|
||||
};
|
||||
@@ -50,7 +55,7 @@ describe("VehicleContext", () => {
|
||||
});
|
||||
|
||||
it("Initial state", () => {
|
||||
checkVehicleResults("", "false", "[]");
|
||||
checkVehiclesResult("", "false", "[]");
|
||||
});
|
||||
|
||||
it("getVehicles", async () => {
|
||||
@@ -58,7 +63,48 @@ describe("VehicleContext", () => {
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("vehicles").innerHTML).not.toBe("[]")
|
||||
);
|
||||
checkVehicleResults("", "false", JSON.stringify(expectedVehicleData));
|
||||
checkVehiclesResult("", "false", JSON.stringify(expectedVehiclesData));
|
||||
});
|
||||
});
|
||||
|
||||
describe("getVehicle", () => {
|
||||
beforeEach(() => {
|
||||
const TestComp = () => {
|
||||
const { busy, error, vehicle, getVehicle } = useVehicleContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{error}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<div data-testid="vehicle">{JSON.stringify(vehicle)}</div>
|
||||
<button
|
||||
data-testid="getVehicle"
|
||||
onClick={() => getVehicle("3C4PDCBG0ET127145")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<VehicleProvider>
|
||||
<TestComp />
|
||||
</VehicleProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("Initial state", () => {
|
||||
checkVehicleResult("", "false", "{}");
|
||||
});
|
||||
|
||||
it("getVehicle", async () => {
|
||||
fireEvent.click(screen.getByTestId("getVehicle"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("vehicle").innerHTML).not.toBe("{}")
|
||||
);
|
||||
checkVehicleResult("", "false", JSON.stringify(expectedVehicleData));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -131,15 +177,183 @@ describe("VehicleContext", () => {
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateVehicle", () => {
|
||||
beforeEach(async () => {
|
||||
const TestComp = () => {
|
||||
const { busy, updateVehicle } = useVehicleContext();
|
||||
const { message, setMessage } = useStatusContext();
|
||||
const update = async (data) => {
|
||||
try {
|
||||
await updateVehicle("3C4PDCBG0ET127145", data);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{message}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<button data-testid="updateVehicleNull" onClick={() => update(null)} />
|
||||
<button data-testid="updateVehicleNoVIN" onClick={() => update({})} />
|
||||
<button
|
||||
data-testid="updateVehicle"
|
||||
onClick={() =>
|
||||
update({ vin: "3C4PDCBG0ET127145", log_level: "warn", canbus: { enabled: false } })
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<StatusProvider>
|
||||
<VehicleProvider>
|
||||
<TestComp />
|
||||
</VehicleProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
|
||||
it("updateVehicleNull", async () => {
|
||||
fireEvent.click(screen.getByTestId("updateVehicleNull"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("No vehicle data", "false");
|
||||
});
|
||||
|
||||
it("updateVehicleNoVIN", async () => {
|
||||
fireEvent.click(screen.getByTestId("updateVehicleNoVIN"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("Invalid VIN", "false");
|
||||
});
|
||||
|
||||
it("updateVehicle", async () => {
|
||||
fireEvent.click(screen.getByTestId("updateVehicle"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteVehicle", () => {
|
||||
beforeEach(async () => {
|
||||
const TestComp = () => {
|
||||
const { busy, deleteVehicle } = useVehicleContext();
|
||||
const { message, setMessage } = useStatusContext();
|
||||
const deleteV = async (name) => {
|
||||
try {
|
||||
await deleteVehicle(name);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{message}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<button data-testid="deleteVehicleNull" onClick={() => deleteV(null)} />
|
||||
<button data-testid="deleteVehicleNonexistent" onClick={() => deleteV("11111111111111111")} />
|
||||
<button
|
||||
data-testid="deleteVehicle"
|
||||
onClick={() =>
|
||||
deleteV("3C4PDCBG0ET127145")
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<StatusProvider>
|
||||
<VehicleProvider>
|
||||
<TestComp />
|
||||
</VehicleProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("initial state", () => {
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
|
||||
it("deleteVehicleNull", async () => {
|
||||
fireEvent.click(screen.getByTestId("deleteVehicleNull"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("Invalid VIN", "false");
|
||||
});
|
||||
|
||||
it("deleteVehicleNonexistent", async () => {
|
||||
fireEvent.click(screen.getByTestId("deleteVehicleNonexistent"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
|
||||
it("deleteVehicle", async () => {
|
||||
fireEvent.click(screen.getByTestId("deleteVehicle"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const expectedVehicleData = [
|
||||
const expectedFilters = [
|
||||
{
|
||||
can_id: "123-456",
|
||||
interval: 789
|
||||
},
|
||||
{
|
||||
can_id: "1",
|
||||
interval: 1000
|
||||
},
|
||||
{
|
||||
can_id: "1000",
|
||||
interval: 1
|
||||
}
|
||||
]
|
||||
|
||||
const expectedVehicleData = {
|
||||
vin: "3C4PDCBG0ET127145",
|
||||
year: 2021,
|
||||
model: "Ocean",
|
||||
trim: "Basic",
|
||||
ecu_list: "ECUA 2.0.0, ECUB 2.1.1",
|
||||
log_level: "info",
|
||||
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: expectedFilters },
|
||||
connected: true,
|
||||
}
|
||||
|
||||
const expectedVehiclesData = [
|
||||
{
|
||||
vin: "3C4PDCBG0ET127145",
|
||||
year: 2021,
|
||||
model: "Ocean",
|
||||
trim: "Basic",
|
||||
ecu_list: "ECUA 2.0.0, ECUB 2.1.1",
|
||||
log_level: "info",
|
||||
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: expectedFilters },
|
||||
connected: true,
|
||||
},
|
||||
{ vin: "1G1FP87S3GN100062", connected: true },
|
||||
|
||||
30
src/components/Contexts/__mocks__/CANFiltersContext.jsx
Normal file
30
src/components/Contexts/__mocks__/CANFiltersContext.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
let busy = false;
|
||||
let filters = [
|
||||
{
|
||||
can_id: "123",
|
||||
interval: 1000
|
||||
},
|
||||
{
|
||||
can_id: "456-789",
|
||||
interval: 2000
|
||||
},
|
||||
{
|
||||
can_id: "1",
|
||||
interval: 0
|
||||
},
|
||||
];
|
||||
let totalFilters = 3;
|
||||
|
||||
export const CANFiltersProvider = ({ children }) => {
|
||||
return <div data-testid="mocked-canfiltersprovider">{children}</div>;
|
||||
};
|
||||
|
||||
export const useCANFiltersContext = () => ({
|
||||
busy,
|
||||
filters,
|
||||
totalFilters,
|
||||
addFilter: jest.fn(),
|
||||
getFilters: jest.fn(),
|
||||
updateFilter: jest.fn(),
|
||||
deleteFilter: jest.fn(),
|
||||
});
|
||||
@@ -44,8 +44,17 @@ let carUpdateLog = {
|
||||
created: "2021-08-23T17:06:38.030052Z",
|
||||
updated: "2021-08-23T17:06:38.030052Z",
|
||||
},
|
||||
{
|
||||
id: 88,
|
||||
carupdate_id: 284,
|
||||
status: "install_approval_await",
|
||||
error_code: 0,
|
||||
info: "TEST",
|
||||
created: "2021-08-23T17:06:38.030052Z",
|
||||
updated: "2021-08-23T17:06:38.030052Z",
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
total: 3,
|
||||
};
|
||||
|
||||
export const CarUpdatesProvider = ({ children }) => {
|
||||
@@ -64,4 +73,5 @@ export const useCarUpdatesContext = () => ({
|
||||
getVINUpdates: jest.fn(() => carUpdates),
|
||||
startMonitor: jest.fn(),
|
||||
stopMonitor: jest.fn(),
|
||||
approveUpdate: jest.fn(),
|
||||
});
|
||||
|
||||
75
src/components/Contexts/__mocks__/FleetContext.jsx
Normal file
75
src/components/Contexts/__mocks__/FleetContext.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
let busy = false;
|
||||
let fleetCANFilters = [
|
||||
{
|
||||
can_id: "123-456",
|
||||
interval: 789
|
||||
},
|
||||
{
|
||||
can_id: "1",
|
||||
interval: 1000
|
||||
},
|
||||
{
|
||||
can_id: "1000",
|
||||
interval: 1
|
||||
}
|
||||
]
|
||||
let fleet = {
|
||||
name: "US-WEST",
|
||||
log_level: "info",
|
||||
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: fleetCANFilters },
|
||||
vehicles: ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"],
|
||||
}
|
||||
let fleets = [
|
||||
{
|
||||
name: "US-WEST",
|
||||
log_level: "info",
|
||||
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: fleetCANFilters },
|
||||
vehicles: ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"]
|
||||
},
|
||||
{
|
||||
name: "US-CENTRAL",
|
||||
log_level: "warn",
|
||||
canbus: { enabled: false, data_logger_enabled: false, max_mem_buffer_size: 0, max_disk_buffer_size: 0 },
|
||||
vehicles: ["USCENTVIN12345678", "USCENTVIN12345679", "USCENTVIN12345670"]
|
||||
},
|
||||
{
|
||||
name: "US-EAST",
|
||||
log_level: "error",
|
||||
canbus: { enabled: true },
|
||||
vehicles: ["USEASTVIN12345678", "USEASTVIN12345679", "USEASTVIN12345670"]
|
||||
},
|
||||
];
|
||||
let totalFleets = 3;
|
||||
let fleetVehicles = ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"];
|
||||
let totalFleetVehicles = 3;
|
||||
let totalFleetCANFilters = 3;
|
||||
|
||||
export const FleetProvider = ({ children }) => {
|
||||
return <div data-testid="mocked-fleetprovider">{children}</div>;
|
||||
};
|
||||
|
||||
export const useFleetContext = () => ({
|
||||
busy,
|
||||
|
||||
fleet,
|
||||
fleets,
|
||||
totalFleets,
|
||||
addFleet: jest.fn(),
|
||||
getFleet: jest.fn(),
|
||||
getFleets: jest.fn(),
|
||||
updateFleet: jest.fn(),
|
||||
deleteFleet: jest.fn(),
|
||||
|
||||
fleetVehicles,
|
||||
totalFleetVehicles,
|
||||
getFleetVehicles: jest.fn(),
|
||||
addFleetVehicle: jest.fn(),
|
||||
deleteFleetVehicle: jest.fn(),
|
||||
|
||||
fleetCANFilters,
|
||||
totalFleetCANFilters,
|
||||
getFleetCANFilters: jest.fn(),
|
||||
addFleetCANFilter: jest.fn(),
|
||||
updateFleetCANFilter: jest.fn(),
|
||||
deleteFleetCANFilter: jest.fn(),
|
||||
});
|
||||
@@ -7,13 +7,8 @@ let ecus = [
|
||||
{
|
||||
data_id: 0,
|
||||
name: "AGS",
|
||||
part_number: "",
|
||||
version: "",
|
||||
serial_number: "",
|
||||
hw_version: "",
|
||||
vendor: "",
|
||||
configuration: "",
|
||||
fingerprint: "",
|
||||
manifest_id: 0,
|
||||
files: [
|
||||
{
|
||||
@@ -21,6 +16,8 @@ let ecus = [
|
||||
order: 0,
|
||||
offset: "0",
|
||||
checksum: "",
|
||||
self_download: false,
|
||||
mode: "D",
|
||||
type: 1,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -20,8 +20,29 @@ let manifest = {
|
||||
file_id: "b0cda514c94080b4",
|
||||
filename: "LARGE.jpg",
|
||||
url: "https://upload-dev.fiskerdps.com/92bbc448-99c8-4851-91ad-f8042e4deb49/LARGE.jpg",
|
||||
write_region: {
|
||||
offset: 100,
|
||||
length: 14488498,
|
||||
},
|
||||
erase_region: {
|
||||
offset: 0,
|
||||
length: 120559274,
|
||||
},
|
||||
file_size: 14559274,
|
||||
size: 14488498,
|
||||
type: "ODX Data",
|
||||
created: "2021-12-09T22:38:29.102813Z",
|
||||
updated: "2021-12-09T22:38:29.102813Z",
|
||||
},
|
||||
{
|
||||
file_id: "4B897b1DcbeCds8e9",
|
||||
filename: "SMALL.jpg",
|
||||
url: "https://upload-dev.fiskerdps.com/92bbc448-99c8-4851-91ad-f8042e4deb49/SMALL.jpg",
|
||||
write_region: {
|
||||
offset: 120559274,
|
||||
length: 559274,
|
||||
},
|
||||
checksum: "0a06d87c",
|
||||
file_size: 488498,
|
||||
type: "ODX Data",
|
||||
created: "2021-12-09T22:38:29.102813Z",
|
||||
updated: "2021-12-09T22:38:29.102813Z",
|
||||
|
||||
16
src/components/Contexts/__mocks__/StatusContext.jsx
Normal file
16
src/components/Contexts/__mocks__/StatusContext.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
let message = ""
|
||||
let title = ""
|
||||
let sitePath = {}
|
||||
|
||||
export const StatusProvider = ({ children }) => {
|
||||
return <div data-testid="mocked-statusprovider">{children}</div>;
|
||||
};
|
||||
|
||||
export const useStatusContext = () => ({
|
||||
message,
|
||||
title,
|
||||
sitePath,
|
||||
setMessage: jest.fn(m => message = m),
|
||||
setTitle: jest.fn(t => title = t),
|
||||
setSitePath: jest.fn(s => sitePath = s),
|
||||
});
|
||||
@@ -1,6 +1,31 @@
|
||||
import React from "react";
|
||||
|
||||
let busy = false;
|
||||
|
||||
const filters = [
|
||||
{
|
||||
can_id: "123-456",
|
||||
interval: 789
|
||||
},
|
||||
{
|
||||
can_id: "1",
|
||||
interval: 1000
|
||||
},
|
||||
{
|
||||
can_id: "1000",
|
||||
interval: 1
|
||||
}
|
||||
]
|
||||
|
||||
let vehicle = {
|
||||
vin: "3C4PDCBG0ET127145",
|
||||
year: 2021,
|
||||
model: "Ocean",
|
||||
trim: "Basic",
|
||||
ecu_list: "ECUA 2.0.0, ECUB 2.1.1",
|
||||
log_level: "info",
|
||||
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: filters },
|
||||
};
|
||||
let vehicles = [];
|
||||
let models = ["Ocean", "PEAR"];
|
||||
let years = [2023, 2024];
|
||||
@@ -15,10 +40,11 @@ export const useVehicleContext = () => ({
|
||||
busy,
|
||||
models,
|
||||
totalVehicles,
|
||||
vehicle,
|
||||
vehicles,
|
||||
years,
|
||||
addVehicle: jest.fn(),
|
||||
getConnections: jest.fn((vins, token) => {
|
||||
getConnections: jest.fn((vins, _token) => {
|
||||
const result = {};
|
||||
|
||||
vins.forEach((vin) => {
|
||||
@@ -31,28 +57,19 @@ export const useVehicleContext = () => ({
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
boot_loader_version: "BLVERSION",
|
||||
config: "CONFIG",
|
||||
created: "2021-07-14T20:09:40.98187Z",
|
||||
ecu: "ECUA",
|
||||
fingerprint: "FINGERPRINT",
|
||||
hw_version: "HWVERSION",
|
||||
serial_number: "SERIAL",
|
||||
sw_version: "SWVERSION",
|
||||
updated: "2021-07-14T20:09:40.98187Z",
|
||||
vendor: "VENDOR",
|
||||
},
|
||||
{
|
||||
boot_loader_version: "BLVERSION",
|
||||
config: "CONFIG",
|
||||
created: "2021-07-14T20:09:40.98187Z",
|
||||
ecu: "ECUB",
|
||||
fingerprint: "FINGERPRINT",
|
||||
hw_version: "HWVERSION",
|
||||
serial_number: "SERIAL",
|
||||
sw_version: "SWVERSION",
|
||||
updated: "2021-07-14T20:09:40.98187Z",
|
||||
vendor: "VENDOR",
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
@@ -70,8 +87,9 @@ export const useVehicleContext = () => ({
|
||||
getYears: jest.fn(() => {
|
||||
years = [2023, 2024];
|
||||
}),
|
||||
getVehicle: jest.fn(),
|
||||
getVehicles: jest.fn(() => vehicles),
|
||||
sendCommand: jest.fn((vins, command, parameters, token) => ({
|
||||
sendCommand: jest.fn((vins, command, parameters, _token) => ({
|
||||
vins,
|
||||
command,
|
||||
parameters,
|
||||
|
||||
@@ -24,30 +24,14 @@ const tableColumns = [
|
||||
id: "sw_version",
|
||||
label: "SW Version",
|
||||
},
|
||||
{
|
||||
id: "boot_loader_version",
|
||||
label: "BL Version",
|
||||
},
|
||||
{
|
||||
id: "hw_version",
|
||||
label: "HW Version",
|
||||
},
|
||||
{
|
||||
id: "vendor",
|
||||
label: "Vendor",
|
||||
},
|
||||
{
|
||||
id: "config",
|
||||
label: "Config",
|
||||
},
|
||||
{
|
||||
id: "fingerprint",
|
||||
label: "Fingerprint",
|
||||
},
|
||||
{
|
||||
id: "serial_number",
|
||||
label: "Serial",
|
||||
},
|
||||
{
|
||||
id: "created_at",
|
||||
label: "Created",
|
||||
@@ -128,16 +112,12 @@ const CarECUsTable = ({ vin, token, classes }) => {
|
||||
onSortRequest={handleSort}
|
||||
/>
|
||||
<TableBody>
|
||||
{ecus.map((row) => (
|
||||
<TableRow key={row.ecu}>
|
||||
{ecus.map((row, i) => (
|
||||
<TableRow key={row.ecu + i}>
|
||||
<TableCell align="center">{row.ecu}</TableCell>
|
||||
<TableCell align="center">{row.sw_version}</TableCell>
|
||||
<TableCell align="center">{row.boot_loader_version}</TableCell>
|
||||
<TableCell align="center">{row.hw_version}</TableCell>
|
||||
<TableCell align="center">{row.vendor}</TableCell>
|
||||
<TableCell align="center">{row.config}</TableCell>
|
||||
<TableCell align="center">{row.fingerprint}</TableCell>
|
||||
<TableCell align="center">{row.serial_number}</TableCell>
|
||||
<TableCell align="center">
|
||||
{LocalDateTimeString(row.created)}
|
||||
</TableCell>
|
||||
|
||||
@@ -48,7 +48,7 @@ const tableColumns = [
|
||||
];
|
||||
|
||||
const CarSelectionTable = (props) => {
|
||||
const { token, classes, search, selected, onSelect, onSelectAll } = props;
|
||||
const { token, classes, search, multiSelect, selected, onSelect, onSelectAll } = props;
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [orderBy, setOrderBy] = useState("vin");
|
||||
@@ -56,7 +56,7 @@ const CarSelectionTable = (props) => {
|
||||
const { getVehicles, vehicles, totalVehicles } = useVehicleContext();
|
||||
const { setMessage } = useStatusContext();
|
||||
const { search: searchTerm } = search;
|
||||
const sortHandler = (event, property) => {
|
||||
const sortHandler = (_event, property) => {
|
||||
if (property === orderBy) {
|
||||
if (order === "asc") {
|
||||
setOrder("desc");
|
||||
@@ -69,7 +69,7 @@ const CarSelectionTable = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangePageIndex = (event, newIndex) => {
|
||||
const handleChangePageIndex = (_event, newIndex) => {
|
||||
setPageIndex(newIndex);
|
||||
};
|
||||
|
||||
@@ -123,22 +123,22 @@ const CarSelectionTable = (props) => {
|
||||
order={order}
|
||||
columnData={tableColumns}
|
||||
onSortRequest={sortHandler}
|
||||
multiSelect={true}
|
||||
multiSelect={multiSelect}
|
||||
onSelectAll={handleSelectAll}
|
||||
selectCount={selected.length}
|
||||
selectCount={selected ? selected.length : 0}
|
||||
rowCount={vehicles.length}
|
||||
/>
|
||||
<TableBody>
|
||||
{vehicles.map((row) => {
|
||||
const isSelected = selected.indexOf(row.vin) !== -1;
|
||||
const isSelected = selected ? selected.indexOf(row.vin) !== -1 : false;
|
||||
return (
|
||||
<TableRow key={row.vin}>
|
||||
<TableCell padding="checkbox">
|
||||
{multiSelect && (<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={(event) => handleSelect(event, row.vin)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableCell>)}
|
||||
<TableCell align="center">
|
||||
<ConnectedIcon
|
||||
connected={row.connected}
|
||||
@@ -195,9 +195,10 @@ CarSelectionTable.propTypes = {
|
||||
token: PropTypes.string.isRequired,
|
||||
classes: PropTypes.object.isRequired,
|
||||
search: PropTypes.object.isRequired,
|
||||
selected: PropTypes.array.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onSelectAll: PropTypes.func.isRequired,
|
||||
multiSelect: PropTypes.bool.isRequired,
|
||||
selected: PropTypes.array,
|
||||
onSelect: PropTypes.func,
|
||||
onSelectAll: PropTypes.func,
|
||||
connectionStatus: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { render, waitFor } from "@testing-library/react";
|
||||
|
||||
import CarUpdateStatusProgress from "../CarUpdateStatusProgress";
|
||||
import useStyles from "../../useStyles";
|
||||
import s from "./Statuses";
|
||||
|
||||
const TestWrapper = ({ status }) => {
|
||||
const classes = useStyles();
|
||||
@@ -21,7 +22,7 @@ describe("CarUpdateStatusProgress", () => {
|
||||
name: "manifest_received",
|
||||
status: {
|
||||
car_update_id: 297,
|
||||
msg: "manifest_received",
|
||||
msg: s.ManifestReceived,
|
||||
err: -6,
|
||||
extra_info: "",
|
||||
},
|
||||
@@ -30,7 +31,7 @@ describe("CarUpdateStatusProgress", () => {
|
||||
name: "manifest_accepted",
|
||||
status: {
|
||||
car_update_id: 297,
|
||||
msg: "manifest_accepted",
|
||||
msg: s.ManifestAccepted,
|
||||
err: -7,
|
||||
extra_info: "",
|
||||
},
|
||||
@@ -39,7 +40,7 @@ describe("CarUpdateStatusProgress", () => {
|
||||
name: "download_started",
|
||||
status: {
|
||||
car_update_id: 297,
|
||||
msg: "download_started",
|
||||
msg: s.DownloadStarted,
|
||||
err: -14,
|
||||
extra_info: "",
|
||||
},
|
||||
@@ -53,7 +54,7 @@ describe("CarUpdateStatusProgress", () => {
|
||||
file_total: 1264672,
|
||||
package_current: 0,
|
||||
package_total: 2529856,
|
||||
msg: "download_start",
|
||||
msg: s.DownloadStarted,
|
||||
err: 0,
|
||||
},
|
||||
},
|
||||
@@ -66,7 +67,7 @@ describe("CarUpdateStatusProgress", () => {
|
||||
file_total: 1264672,
|
||||
package_current: 1048576,
|
||||
package_total: 2529856,
|
||||
msg: "downloading",
|
||||
msg: s.Downloading,
|
||||
err: 0,
|
||||
},
|
||||
},
|
||||
@@ -79,7 +80,7 @@ describe("CarUpdateStatusProgress", () => {
|
||||
file_total: 1264672,
|
||||
package_current: 1264672,
|
||||
package_total: 2529856,
|
||||
msg: "download_complete",
|
||||
msg: s.DownloadCompleted,
|
||||
err: 0,
|
||||
},
|
||||
},
|
||||
@@ -87,7 +88,7 @@ describe("CarUpdateStatusProgress", () => {
|
||||
name: "package_download_complete",
|
||||
status: {
|
||||
car_update_id: 297,
|
||||
msg: "package_download_complete",
|
||||
msg: s.PackageDownloadCompleted,
|
||||
err: -15,
|
||||
extra_info: "",
|
||||
},
|
||||
@@ -101,7 +102,7 @@ describe("CarUpdateStatusProgress", () => {
|
||||
file_total: 100,
|
||||
package_current: 0,
|
||||
package_total: 1000,
|
||||
msg: "download_error",
|
||||
msg: s.DownloadFailed,
|
||||
err: 0,
|
||||
},
|
||||
},
|
||||
@@ -112,7 +113,7 @@ describe("CarUpdateStatusProgress", () => {
|
||||
ecu: "TEST",
|
||||
installed: 5,
|
||||
total_files: 10,
|
||||
msg: "installing",
|
||||
msg: s.Installing,
|
||||
err: 0,
|
||||
},
|
||||
},
|
||||
@@ -123,7 +124,7 @@ describe("CarUpdateStatusProgress", () => {
|
||||
ecu: "TEST",
|
||||
installed: 10,
|
||||
total_files: 10,
|
||||
msg: "install_complete",
|
||||
msg: s.InstallSucceeded,
|
||||
err: 0,
|
||||
},
|
||||
},
|
||||
@@ -134,7 +135,7 @@ describe("CarUpdateStatusProgress", () => {
|
||||
ecu: "TEST",
|
||||
installed: 5,
|
||||
total_files: 10,
|
||||
msg: "install_error",
|
||||
msg: s.InstallFailed,
|
||||
err: 0,
|
||||
},
|
||||
},
|
||||
|
||||
38
src/components/Controls/CarUpdateStatusProgress/Statuses.js
Normal file
38
src/components/Controls/CarUpdateStatusProgress/Statuses.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const Statuses = {
|
||||
Pending: "pending",
|
||||
ManifestReceived: "manifest_received",
|
||||
ManifestAccepted: "manifest_accepted",
|
||||
ManifestRejected: "manifest_rejected",
|
||||
PreconditionAwait: "requirements_await",
|
||||
PreconditionSuceeded: "requirements_succeeded",
|
||||
ManifestCancelReceived: "manifest_cancel_received",
|
||||
ManifestCancelAccepted: "manifest_cancel_accepted",
|
||||
ManifestCancelRejected: "manifest_cancel_rejected",
|
||||
ManifestValidationSucceeded: "manifest_validation_succeeded",
|
||||
ManifestValidationFailed: "manifest_validation_failed",
|
||||
DownloadStarted: "download_started",
|
||||
Downloading: "downloading",
|
||||
DownloadCompleted: "download_completed",
|
||||
DownloadFailed: "download_failed",
|
||||
InstallApprovalAwait: "install_approval_await",
|
||||
InstallApprovalReceived: "install_approval_received",
|
||||
InstallStarted: "install_started",
|
||||
Installing: "installing",
|
||||
InstallSucceeded: "install_succeeded",
|
||||
InstallFailed: "install_failed",
|
||||
RollbackStarted: "rollback_started",
|
||||
RollbackSucceeded: "rollback_succeeded",
|
||||
RollbackFailed: "rollback_failed",
|
||||
CleanupSucceeded: "cleanup_succeeded",
|
||||
CleanupFailed: "cleanup_failed",
|
||||
ManifestError: "manifest_error",
|
||||
ManifestRollback: "manifest_rollback",
|
||||
ManifestSucceeded: "manifest_succeeded",
|
||||
ManifesCanceled: "manifest_canceled",
|
||||
PackageDownloadStarted: "package_download_start",
|
||||
PackageDownloadCompleted: "package_download_complete",
|
||||
PackageInstallStarted: "package_install_start",
|
||||
PackageInstallCompleted: "package_install_complete",
|
||||
};
|
||||
|
||||
export default Statuses;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,78 +4,65 @@ import Typography from "@material-ui/core/Typography";
|
||||
import clsx from "clsx";
|
||||
|
||||
import CircularProgress from "../CircularProgress";
|
||||
import s from "./Statuses";
|
||||
|
||||
const AwaitStatus = -1;
|
||||
const ErrorStatus = -100;
|
||||
|
||||
const PHASES = [
|
||||
{
|
||||
label: "Pending",
|
||||
events: ["pending"],
|
||||
events: [s.Pending],
|
||||
progress: () => 100,
|
||||
},
|
||||
{
|
||||
label: "Recieved",
|
||||
events: ["manifest_accepted", "manifest_received"],
|
||||
label: "Received",
|
||||
events: [s.ManifestAccepted, s.ManifestReceived, s.ManifestRejected],
|
||||
progress: () => 100,
|
||||
},
|
||||
{
|
||||
label: "Precondition",
|
||||
events: ["requirements_succeeded"],
|
||||
events: [s.PreconditionAwait, s.PreconditionSuceeded],
|
||||
progress: () => 100,
|
||||
},
|
||||
{
|
||||
label: "Download",
|
||||
events: [
|
||||
"downloading",
|
||||
"download_start",
|
||||
"download_complete",
|
||||
"download_error",
|
||||
"package_download_start",
|
||||
s.Downloading,
|
||||
s.DownloadStarted,
|
||||
s.DownloadCompleted,
|
||||
s.DownloadFailed,
|
||||
s.PackageDownloadStarted,
|
||||
],
|
||||
progress: (msg, progress) =>
|
||||
[
|
||||
"package_download_start",
|
||||
"downloading",
|
||||
"download_start",
|
||||
"download_complete",
|
||||
].indexOf(msg) > -1
|
||||
? progress
|
||||
: -100,
|
||||
progress: (msg, progress) => {
|
||||
if (msg === s.DownloadFailed) return ErrorStatus;
|
||||
return progress;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Approved",
|
||||
events: ["package_download_complete", "install_approval_await"],
|
||||
progress: () => -1,
|
||||
events: [s.PackageDownloadCompleted, s.InstallApprovalAwait],
|
||||
progress: () => AwaitStatus,
|
||||
},
|
||||
{
|
||||
label: "Install",
|
||||
events: [
|
||||
"install_approval_received",
|
||||
"install_start",
|
||||
"installing",
|
||||
"install_complete",
|
||||
"install_error",
|
||||
s.InstallApprovalReceived,
|
||||
s.InstallStarted,
|
||||
s.Installing,
|
||||
s.InstallSucceeded,
|
||||
s.InstallFailed,
|
||||
],
|
||||
progress: (msg, progress) =>
|
||||
[
|
||||
"install_approval_received",
|
||||
"install_start",
|
||||
"installing",
|
||||
"install_complete",
|
||||
].indexOf(msg) > -1
|
||||
? progress
|
||||
: -100,
|
||||
},
|
||||
{
|
||||
label: "Clean up",
|
||||
events: ["package_install_complete", "cleanup_failed"],
|
||||
progress: (msg, progress) => {
|
||||
if (msg === "package_install_complete") return -1;
|
||||
return -100;
|
||||
if (msg === s.InstallFailed) return ErrorStatus;
|
||||
if (msg === s.PackageInstallCompleted) return 100;
|
||||
return progress;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Updated",
|
||||
events: ["cleanup_success", "manifest_succeeded"],
|
||||
progress: (msg, progress) => 100,
|
||||
events: [s.ManifestSucceeded],
|
||||
progress: (_msg, _progress) => 100,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -153,6 +153,28 @@ exports[`CarUpdateStatusTable Render 1`] = `
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
class="MuiTableRow-root"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
8/23/2021 5:06:38 PM
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
install_approval_await
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
TEST
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot
|
||||
class="MuiTableFooter-root"
|
||||
@@ -223,7 +245,7 @@ exports[`CarUpdateStatusTable Render 1`] = `
|
||||
<p
|
||||
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
|
||||
>
|
||||
1-2 of 2
|
||||
1-3 of 3
|
||||
</p>
|
||||
<div
|
||||
class="MuiTablePagination-actions"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DownloadFileLink Render 1`] = `
|
||||
<div>
|
||||
<a
|
||||
download="test.txt"
|
||||
>
|
||||
test.txt
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
32
src/components/Controls/DownloadFileLink/index.jsx
Normal file
32
src/components/Controls/DownloadFileLink/index.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
const DownloadFileLink = ({ data, filename, mimetype }) => {
|
||||
const [link, setLink] = useState("");
|
||||
|
||||
const releaseLink = () => {
|
||||
if (link === "") return;
|
||||
URL.revokeObjectURL(link);
|
||||
};
|
||||
|
||||
const makeFile = () => {
|
||||
const file = new Blob([data], { type: mimetype ?? "text/plain" });
|
||||
|
||||
releaseLink();
|
||||
setLink(URL.createObjectURL(file));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
makeFile();
|
||||
return releaseLink;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data, filename, mimetype]);
|
||||
|
||||
return (
|
||||
<a download={filename ?? "file.txt"} href={link}>
|
||||
{filename}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadFileLink;
|
||||
21
src/components/Controls/DownloadFileLink/index.test.jsx
Normal file
21
src/components/Controls/DownloadFileLink/index.test.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
|
||||
import DownloadFileLink from ".";
|
||||
|
||||
describe("DownloadFileLink", () => {
|
||||
beforeAll(() => {
|
||||
global.URL.createObjectURL = jest.fn();
|
||||
global.URL.revokeObjectURL = jest.fn();
|
||||
});
|
||||
|
||||
it("Render", async () => {
|
||||
const { container } = render(
|
||||
<DownloadFileLink data={"ABCDEFGHIJK"} filename="test.txt" />
|
||||
);
|
||||
await waitFor(() => {
|
||||
/* render */
|
||||
});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -31,22 +31,10 @@ const options = [
|
||||
field: "version",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: "Part Number",
|
||||
field: "part_number",
|
||||
},
|
||||
{
|
||||
label: "Serial",
|
||||
field: "serial_number",
|
||||
},
|
||||
{
|
||||
label: "Hardware",
|
||||
field: "hw_version",
|
||||
},
|
||||
{
|
||||
label: "Vendor",
|
||||
field: "vendor",
|
||||
},
|
||||
{
|
||||
label: "",
|
||||
delete: true,
|
||||
@@ -74,7 +62,7 @@ const ManifestECUList = () => {
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} align="center">
|
||||
<TableCell colSpan={5} align="center">
|
||||
<Button onClick={addECU}>Add ECU</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TabPanel Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-labelledby="tab-0"
|
||||
id="tabpanel-0"
|
||||
role="tabpanel"
|
||||
>
|
||||
<div
|
||||
class="MuiBox-root MuiBox-root-1"
|
||||
>
|
||||
<div>
|
||||
Test
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
24
src/components/Controls/TabPanel/index.jsx
Normal file
24
src/components/Controls/TabPanel/index.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import { Box } from "@material-ui/core";
|
||||
|
||||
function TabPanel(props) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`tabpanel-${index}`}
|
||||
aria-labelledby={`tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && (
|
||||
<Box sx={{ p: 3 }}>
|
||||
{children}
|
||||
</Box>
|
||||
)}
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
export default TabPanel;
|
||||
21
src/components/Controls/TabPanel/index.test.jsx
Normal file
21
src/components/Controls/TabPanel/index.test.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
|
||||
import TabPanel from "./index"
|
||||
|
||||
|
||||
const renderTabPanel = async () => {
|
||||
const { container } = render(
|
||||
<TabPanel value={0} index={0}>
|
||||
<div>Test</div>
|
||||
</TabPanel>
|
||||
);
|
||||
await waitFor(() => { });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("TabPanel", () => {
|
||||
it("Render", async () => {
|
||||
const container = await renderTabPanel();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,160 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
FormControl,
|
||||
Grid,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
TextField,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import useStyles from "../../useStyles";
|
||||
import ResponsiveIFrame from "../../Controls/ResponsiveIFrame";
|
||||
import { grafanaCharts } from "../../../services/grafanaCharts";
|
||||
|
||||
const Battery = () => {
|
||||
const classes = useStyles();
|
||||
const { setTitle, setSitePath } = useStatusContext();
|
||||
|
||||
useEffect(() => {
|
||||
setTitle("Battery");
|
||||
setSitePath([
|
||||
{
|
||||
label: "Datascope",
|
||||
link: "/datascope",
|
||||
},
|
||||
{
|
||||
label: "Battery",
|
||||
},
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const [vin, setVIN] = useState("1F15K3R45N1234567");
|
||||
const [cellNum, setCellNum] = useState(1);
|
||||
|
||||
const handleVINForm = (e) => {
|
||||
if (e.target.value.length === 17) {
|
||||
setVIN(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.paper}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid container item md={4} space={2}>
|
||||
<Grid item md={12}>
|
||||
<Paper className={classes.grafanaContainer}>
|
||||
<form className={classes.formControl}>
|
||||
<TextField
|
||||
id="vin"
|
||||
label="VIN"
|
||||
defaultValue="1F15K3R45N1234567"
|
||||
variant="outlined"
|
||||
onChange={handleVINForm}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</form>
|
||||
<FormControl variant="outlined" className={classes.formControl}>
|
||||
<InputLabel id="demo-simple-select-outlined-label">
|
||||
Cell
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="demo-simple-select-outlined-label"
|
||||
id="demo-simple-select-outlined"
|
||||
value={cellNum}
|
||||
onChange={(e) => setCellNum(e.target.value)}
|
||||
label="Cell"
|
||||
>
|
||||
{[...Array(112)].map((_, i) => (
|
||||
<MenuItem key={i + 1} value={i + 1}>
|
||||
{i + 1}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item md={12}>
|
||||
<Paper className={classes.grafanaContainer}>
|
||||
Cell Voltage {cellNum}
|
||||
<ResponsiveIFrame
|
||||
classes={classes}
|
||||
src={grafanaCharts.CELLVOLTAGE_CHART({ vin, cellNum })}
|
||||
title="Cell Voltage"
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item md={12}>
|
||||
<Paper className={classes.grafanaContainer}>
|
||||
Cell Temperature {cellNum}
|
||||
<ResponsiveIFrame
|
||||
classes={classes}
|
||||
src={grafanaCharts.CELLTEMP_CHART({ vin, cellNum })}
|
||||
title="Cell Temperature"
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item md={12}>
|
||||
<Paper className={classes.grafanaContainer}>
|
||||
<ResponsiveIFrame
|
||||
classes={classes}
|
||||
src={grafanaCharts.BATTERYTEMP_CHART({ vin })}
|
||||
title="Battery Temperature Time Series"
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container item md={8} space={2}>
|
||||
<Grid item md={12}>
|
||||
<Paper className={classes.grafanaContainer}>
|
||||
<ResponsiveIFrame
|
||||
classes={classes}
|
||||
src={grafanaCharts.BATTERYCAP_CHART({ vin })}
|
||||
title="Battery Capacity Time Series"
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item md={12}>
|
||||
<Paper className={classes.grafanaContainer}>
|
||||
<ResponsiveIFrame
|
||||
classes={classes}
|
||||
src={grafanaCharts.BATTERYPERCENT_CHART({ vin })}
|
||||
title="Battery Percent Time Series"
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item md={6}>
|
||||
<Paper className={classes.grafanaContainer}>
|
||||
<ResponsiveIFrame
|
||||
classes={classes}
|
||||
src={grafanaCharts.BATTERY12VPERCENT_CHART({ vin })}
|
||||
title="12V Battery Percentage Time Series"
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item md={6}>
|
||||
<Paper className={classes.grafanaContainer}>
|
||||
<ResponsiveIFrame
|
||||
classes={classes}
|
||||
src={grafanaCharts.BATTERY12VVOLTAGE_CHART({ vin })}
|
||||
title="12V Battery Voltage Time Series"
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Battery;
|
||||
@@ -1,98 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Grid, Link, Paper } from "@material-ui/core";
|
||||
import CreateIcon from "@material-ui/icons/Create";
|
||||
|
||||
import api from "../../../services/grafanaAPI";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import useStyles from "../../useStyles";
|
||||
import ResponsiveIFrame from "../../Controls/ResponsiveIFrame";
|
||||
import { logger } from "../../../services/monitoring";
|
||||
import { grafanaCharts } from "../../../services/grafanaCharts";
|
||||
|
||||
const Datascope = () => {
|
||||
const classes = useStyles();
|
||||
const { setTitle, setSitePath } = useStatusContext();
|
||||
const REQUEST_INTERVAL = 10000;
|
||||
|
||||
useEffect(() => {
|
||||
setTitle("Datascope");
|
||||
setSitePath([]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const [carsCount, setCarsCount] = useState(0);
|
||||
useEffect(() => {
|
||||
api
|
||||
.getCarsCount()
|
||||
.then((result) => setCarsCount(result))
|
||||
.catch((error) => logger.warn(error.stack));
|
||||
}, []);
|
||||
|
||||
const [signalsCount, setSignalsCount] = useState("0");
|
||||
useEffect(() => {
|
||||
storeSignals();
|
||||
|
||||
const id = setInterval(function () {
|
||||
storeSignals();
|
||||
}, REQUEST_INTERVAL);
|
||||
return () => {
|
||||
clearInterval(id);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const storeSignals = () => {
|
||||
api
|
||||
.getSignalsCount()
|
||||
.then((result) => {
|
||||
let num = result.toLocaleString();
|
||||
setSignalsCount(num);
|
||||
})
|
||||
.catch((error) => logger.warn(error.stack));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.paper}>
|
||||
<Grid container className={classes.root} spacing={2}>
|
||||
<Grid item md={6}>
|
||||
<Paper className={classes.grafanaContainer} style={{ height: 150 }}>
|
||||
<h1 className={classes.datascopeContainerValue}>{carsCount}</h1>
|
||||
<h2 className={classes.datascopeContainerText}>Cars</h2>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item md={6}>
|
||||
<Paper className={classes.grafanaContainer} style={{ height: 150 }}>
|
||||
<h1 className={classes.datascopeContainerValue}>{signalsCount}</h1>
|
||||
<h2 className={classes.datascopeContainerText}>
|
||||
Signals Collected
|
||||
</h2>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item md={12}>
|
||||
<Paper className={classes.grafanaContainer}>
|
||||
<ResponsiveIFrame
|
||||
classes={classes}
|
||||
src={grafanaCharts.HOME_CHART}
|
||||
title="Signals Time Series"
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Button
|
||||
style={{ marginTop: 10 }}
|
||||
aria-label="create"
|
||||
color="primary"
|
||||
component={Link}
|
||||
href={grafanaCharts.BASE}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<CreateIcon fontSize="large" />
|
||||
Go to Grafana
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Datascope;
|
||||
590
src/components/Fleets/Add/__snapshots__/index.test.jsx.snap
Normal file
590
src/components/Fleets/Add/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,590 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FleetAddForm Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-fleetprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-fleetprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3"
|
||||
>
|
||||
<form
|
||||
action="{onSubmit}"
|
||||
class="makeStyles-form-5"
|
||||
novalidate=""
|
||||
>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||
data-shrink="false"
|
||||
for="name"
|
||||
id="name-label"
|
||||
>
|
||||
Name
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="name"
|
||||
maxlength="17"
|
||||
name="name"
|
||||
required=""
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64"
|
||||
>
|
||||
<span>
|
||||
Name
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<label
|
||||
class="MuiFormLabel-root"
|
||||
id="demo-row-radio-buttons-group-label"
|
||||
>
|
||||
Log Level
|
||||
</label>
|
||||
<div
|
||||
aria-labelledby="demo-row-radio-buttons-group-label"
|
||||
class="MuiFormGroup-root MuiFormGroup-row"
|
||||
margin="normal"
|
||||
role="radiogroup"
|
||||
>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="trace"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Trace
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="debug"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Debug
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary PrivateSwitchBase-checked-67 Mui-checked MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="info"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70 PrivateRadioButtonIcon-checked-72"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Info
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="warn"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Warn
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="error"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Error
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
name="log-level-group"
|
||||
type="radio"
|
||||
value="critical"
|
||||
/>
|
||||
<div
|
||||
class="PrivateRadioButtonIcon-root-70"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-71"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Critical
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<label
|
||||
class="MuiFormLabel-root"
|
||||
id="demo-row-radio-buttons-group-label"
|
||||
>
|
||||
CAN Bus
|
||||
</label>
|
||||
<div
|
||||
class="MuiFormGroup-root"
|
||||
>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiCheckbox-root MuiCheckbox-colorSecondary PrivateSwitchBase-checked-67 Mui-checked MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="PrivateSwitchBase-input-69"
|
||||
data-indeterminate="false"
|
||||
type="checkbox"
|
||||
value=""
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
CAN Bus Enabled
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required"
|
||||
data-shrink="true"
|
||||
for="max_mem_buffer_size"
|
||||
id="max_mem_buffer_size-label"
|
||||
>
|
||||
Max Memory Buffer Size (0 uses default size)
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="max_mem_buffer_size"
|
||||
maxlength="12"
|
||||
name="max_mem_buffer_size"
|
||||
required=""
|
||||
type="number"
|
||||
value="0"
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
|
||||
>
|
||||
<span>
|
||||
Max Memory Buffer Size (0 uses default size)
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<label
|
||||
class="MuiFormControlLabel-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-66 MuiCheckbox-root MuiCheckbox-colorSecondary MuiIconButton-colorSecondary"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
class="PrivateSwitchBase-input-69"
|
||||
data-indeterminate="false"
|
||||
type="checkbox"
|
||||
value=""
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||
>
|
||||
Data Logger Enabled
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined Mui-disabled Mui-disabled MuiFormLabel-filled Mui-required Mui-required"
|
||||
data-shrink="true"
|
||||
for="max_disk_buffer_size"
|
||||
id="max_disk_buffer_size-label"
|
||||
>
|
||||
Max Disk Buffer Size (0 uses default size)
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root Mui-disabled Mui-disabled MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input Mui-disabled Mui-disabled"
|
||||
disabled=""
|
||||
id="max_disk_buffer_size"
|
||||
maxlength="12"
|
||||
name="max_disk_buffer_size"
|
||||
required=""
|
||||
type="number"
|
||||
value="0"
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64 PrivateNotchedOutline-legendNotched-65"
|
||||
>
|
||||
<span>
|
||||
Max Disk Buffer Size (0 uses default size)
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label"
|
||||
>
|
||||
Submit
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
206
src/components/Fleets/Add/index.jsx
Normal file
206
src/components/Fleets/Add/index.jsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Redirect } from "react-router";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
FormGroup,
|
||||
FormLabel,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
TextField
|
||||
} from "@material-ui/core";
|
||||
|
||||
import useStyles from "../../useStyles";
|
||||
import {
|
||||
useFleetContext,
|
||||
FleetProvider
|
||||
} from "../../Contexts/FleetContext";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import { useUserContext } from "../../Contexts/UserContext";
|
||||
import { logger } from "../../../services/monitoring";
|
||||
|
||||
const MainForm = () => {
|
||||
const { setMessage, setTitle, setSitePath } = useStatusContext();
|
||||
const { addFleet, busy } = useFleetContext();
|
||||
const {
|
||||
token: {
|
||||
idToken: { jwtToken: token },
|
||||
},
|
||||
} = useUserContext();
|
||||
const classes = useStyles();
|
||||
const [redirect, setRedirect] = useState(null);
|
||||
|
||||
const nameEl = useRef(null);
|
||||
const [selectedLogLevel, setSelectedLogLevel] = useState("info");
|
||||
const [canbusEnabled, setCANBusEnabled] = useState(true);
|
||||
const [dataLoggerEnabled, setDataLoggerEnabled] = useState(false);
|
||||
const [maxMemBufferSize, setMaxMemBufferSize] = useState(0);
|
||||
const [maxDiskBufferSize, setMaxDiskBufferSize] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle("Add Fleet");
|
||||
setSitePath([
|
||||
{
|
||||
label: "Fleets",
|
||||
link: "/fleets",
|
||||
},
|
||||
{
|
||||
label: "Add Fleet",
|
||||
},
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onLogLevelChange = (event) => {
|
||||
setSelectedLogLevel(event.target.value);
|
||||
}
|
||||
|
||||
const onCANBusChange = (event) => {
|
||||
setCANBusEnabled(event.target.checked);
|
||||
}
|
||||
|
||||
const onDataLoggerChange = (event) => {
|
||||
setDataLoggerEnabled(event.target.checked);
|
||||
}
|
||||
|
||||
const onMaxMemBufferSizeChange = (event) => {
|
||||
setMaxMemBufferSize(event.target.value);
|
||||
}
|
||||
|
||||
const onMaxDiskBufferSizeChange = (event) => {
|
||||
setMaxDiskBufferSize(event.target.value);
|
||||
}
|
||||
|
||||
const onSubmit = async (event) => {
|
||||
try {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = {
|
||||
name: nameEl.current.value,
|
||||
log_level: selectedLogLevel,
|
||||
canbus: {
|
||||
enabled: canbusEnabled,
|
||||
data_logger_enabled: canbusEnabled ? dataLoggerEnabled : false,
|
||||
max_mem_buffer_size: canbusEnabled ? parseInt(maxMemBufferSize) : 0,
|
||||
max_disk_buffer_size: canbusEnabled && dataLoggerEnabled ? parseInt(maxDiskBufferSize) : 0
|
||||
}
|
||||
};
|
||||
const result = await addFleet(formData, token);
|
||||
if (!result || result.error) return;
|
||||
|
||||
setMessage(`Added ${result.name}`);
|
||||
setRedirect(`/fleets`);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
};
|
||||
|
||||
if (redirect && redirect.length > 0) {
|
||||
return <Redirect to={redirect} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.paper}>
|
||||
<form className={classes.form} noValidate action="{onSubmit}">
|
||||
<TextField
|
||||
id="name"
|
||||
name="name"
|
||||
label="Name"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "17",
|
||||
}}
|
||||
required
|
||||
fullWidth
|
||||
inputRef={nameEl}
|
||||
/>
|
||||
<FormLabel id="demo-row-radio-buttons-group-label">Log Level</FormLabel>
|
||||
<RadioGroup
|
||||
row
|
||||
aria-labelledby="demo-row-radio-buttons-group-label"
|
||||
name="log-level-group"
|
||||
value={selectedLogLevel}
|
||||
onChange={onLogLevelChange}
|
||||
margin="normal"
|
||||
>
|
||||
<FormControlLabel value="trace" control={<Radio />} label="Trace" />
|
||||
<FormControlLabel value="debug" control={<Radio />} label="Debug" />
|
||||
<FormControlLabel value="info" control={<Radio />} label="Info" />
|
||||
<FormControlLabel value="warn" control={<Radio />} label="Warn" />
|
||||
<FormControlLabel value="error" control={<Radio />} label="Error" />
|
||||
<FormControlLabel value="critical" control={<Radio />} label="Critical" />
|
||||
</RadioGroup>
|
||||
<FormLabel id="demo-row-radio-buttons-group-label">CAN Bus</FormLabel>
|
||||
<FormGroup>
|
||||
<FormControlLabel control={
|
||||
<Checkbox
|
||||
checked={canbusEnabled}
|
||||
onChange={onCANBusChange}
|
||||
/>
|
||||
} label="CAN Bus Enabled" />
|
||||
<TextField
|
||||
id="max_mem_buffer_size"
|
||||
name="max_mem_buffer_size"
|
||||
label='Max Memory Buffer Size (0 uses default size)'
|
||||
value={maxMemBufferSize}
|
||||
onChange={onMaxMemBufferSizeChange}
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "12",
|
||||
}}
|
||||
type="number"
|
||||
disabled={!canbusEnabled}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<FormControlLabel control={
|
||||
<Checkbox
|
||||
checked={dataLoggerEnabled}
|
||||
onChange={onDataLoggerChange}
|
||||
disabled={!canbusEnabled}
|
||||
/>
|
||||
} label="Data Logger Enabled" />
|
||||
</FormGroup>
|
||||
<TextField
|
||||
id="max_disk_buffer_size"
|
||||
name="max_disk_buffer_size"
|
||||
label='Max Disk Buffer Size (0 uses default size)'
|
||||
value={maxDiskBufferSize}
|
||||
onChange={onMaxDiskBufferSizeChange}
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "12",
|
||||
}}
|
||||
type="number"
|
||||
disabled={!dataLoggerEnabled}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.submit}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{busy ? "Submitting..." : "Submit"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FleetAddForm = () => (
|
||||
<FleetProvider>
|
||||
<MainForm />
|
||||
</FleetProvider>
|
||||
);
|
||||
|
||||
export default FleetAddForm;
|
||||
36
src/components/Fleets/Add/index.test.jsx
Normal file
36
src/components/Fleets/Add/index.test.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
jest.mock("../../Contexts/FleetContext");
|
||||
jest.mock("../../Contexts/StatusContext");
|
||||
jest.mock("../../Contexts/UserContext");
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { FleetProvider } from "../../Contexts/FleetContext";
|
||||
import { StatusProvider } from "../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
|
||||
import MainForm from "./index"
|
||||
|
||||
const renderFleetAdd = async () => {
|
||||
const { container } = render(
|
||||
<FleetProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<MainForm />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</FleetProvider>
|
||||
);
|
||||
await waitFor(() => { /* render */ });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("FleetAddForm", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderFleetAdd();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FleetCANFilterAdd Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-fleetprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-fleetprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3"
|
||||
>
|
||||
<form
|
||||
action="{onSubmit}"
|
||||
class="makeStyles-form-5"
|
||||
novalidate=""
|
||||
>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||
data-shrink="false"
|
||||
for="name"
|
||||
id="name-label"
|
||||
>
|
||||
Fleet Name
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="name"
|
||||
maxlength="255"
|
||||
name="name"
|
||||
readonly=""
|
||||
required=""
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64"
|
||||
>
|
||||
<span>
|
||||
Fleet Name
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||
data-shrink="false"
|
||||
for="canId"
|
||||
id="canId-label"
|
||||
>
|
||||
CAN ID
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="canId"
|
||||
maxlength="255"
|
||||
name="canId"
|
||||
required=""
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64"
|
||||
>
|
||||
<span>
|
||||
CAN ID
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined"
|
||||
data-shrink="false"
|
||||
for="interval"
|
||||
id="interval-label"
|
||||
>
|
||||
Interval
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="interval"
|
||||
maxlength="255"
|
||||
name="interval"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64"
|
||||
>
|
||||
<span>
|
||||
Interval
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label"
|
||||
>
|
||||
Submit
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
127
src/components/Fleets/Status/CANFilters/Add/index.jsx
Normal file
127
src/components/Fleets/Status/CANFilters/Add/index.jsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Redirect, useParams } from "react-router";
|
||||
import { Button, TextField } from "@material-ui/core";
|
||||
|
||||
import { useUserContext } from "../../../../Contexts/UserContext";
|
||||
import { useStatusContext } from "../../../../Contexts/StatusContext";
|
||||
import { useFleetContext, FleetProvider } from "../../../../Contexts/FleetContext";
|
||||
import useStyles from "../../../../useStyles";
|
||||
import { logger } from "../../../../../services/monitoring";
|
||||
|
||||
|
||||
const MainForm = () => {
|
||||
const { name } = useParams();
|
||||
const { setMessage, setTitle, setSitePath } = useStatusContext();
|
||||
const { addFleetCANFilter, busy } = useFleetContext();
|
||||
const { token: { idToken: { jwtToken: token } } } = useUserContext();
|
||||
const classes = useStyles();
|
||||
const canIdEl = useRef(null);
|
||||
const intervalEl = useRef(null);
|
||||
const [redirect, setRedirect] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const title = "Add CAN Filter"
|
||||
setTitle(title);
|
||||
setSitePath([
|
||||
{
|
||||
label: `Fleets`,
|
||||
link: "/fleets",
|
||||
},
|
||||
{
|
||||
label: `${name}`,
|
||||
link: `/fleet/${name}`
|
||||
},
|
||||
{
|
||||
label: title
|
||||
},
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (event) => {
|
||||
try {
|
||||
event.preventDefault();
|
||||
const formData = {
|
||||
can_id: canIdEl.current.value,
|
||||
interval: parseInt(intervalEl.current.value)
|
||||
};
|
||||
const result = await addFleetCANFilter(name, formData, token);
|
||||
if (!result || result.error) return;
|
||||
|
||||
setMessage(`Added CAN filter ${result.can_id}`);
|
||||
setRedirect(`/fleet/${name}#filters`);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
};
|
||||
|
||||
if (redirect && redirect.length > 0) {
|
||||
return <Redirect to={redirect} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.paper}>
|
||||
<form className={classes.form} noValidate action="{onSubmit}">
|
||||
<TextField
|
||||
id="name"
|
||||
name="name"
|
||||
label="Fleet Name"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "255",
|
||||
readOnly: true,
|
||||
}}
|
||||
value={name}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
id="canId"
|
||||
name="canId"
|
||||
label="CAN ID"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "255",
|
||||
}}
|
||||
required
|
||||
fullWidth
|
||||
inputRef={canIdEl}
|
||||
/>
|
||||
<TextField
|
||||
id="interval"
|
||||
name="interval"
|
||||
label="Interval"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "255",
|
||||
}}
|
||||
fullWidth
|
||||
inputRef={intervalEl}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.submit}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{busy ? "Submitting..." : "Submit"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FleetAddCANFilterForm = (props) => (
|
||||
<FleetProvider>
|
||||
<MainForm {...props} />
|
||||
</FleetProvider>
|
||||
);
|
||||
|
||||
export default FleetAddCANFilterForm;
|
||||
36
src/components/Fleets/Status/CANFilters/Add/index.test.jsx
Normal file
36
src/components/Fleets/Status/CANFilters/Add/index.test.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
jest.mock("../../../../Contexts/FleetContext");
|
||||
jest.mock("../../../../Contexts/StatusContext");
|
||||
jest.mock("../../../../Contexts/UserContext");
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { FleetProvider } from "../../../../Contexts/FleetContext";
|
||||
import { StatusProvider } from "../../../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../../../utils/testing";
|
||||
import MainForm from "./index"
|
||||
|
||||
const renderFleetCANFilterAdd = async () => {
|
||||
const { container } = render(
|
||||
<FleetProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<MainForm />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</FleetProvider>
|
||||
);
|
||||
await waitFor(() => { });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("FleetCANFilterAdd", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderFleetCANFilterAdd();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,454 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FleetCANFiltersTable Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-fleetprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-fleetprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3 makeStyles-tableSize-53"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textJustifyAlign-47 MuiGrid-item MuiGrid-grid-md-4"
|
||||
>
|
||||
<a
|
||||
class="makeStyles-labelInline-9"
|
||||
href="/fleet/undefined/filter-add"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
align="right"
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-8"
|
||||
>
|
||||
<div
|
||||
class="MuiFormControl-root makeStyles-margin-28 makeStyles-fullWidth-50"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated"
|
||||
data-shrink="false"
|
||||
for="search"
|
||||
>
|
||||
Search
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedEnd"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedEnd"
|
||||
id="search"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
class="MuiInputAdornment-root MuiInputAdornment-positionEnd"
|
||||
>
|
||||
<button
|
||||
aria-label="search"
|
||||
class="MuiButtonBase-root MuiIconButton-root"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table
|
||||
class="MuiTable-root"
|
||||
>
|
||||
<thead
|
||||
class="MuiTableHead-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root MuiTableRow-head"
|
||||
>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
CAN ID
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Interval (ms)
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="MuiTableBody-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
123-456
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
789
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="/fleet/undefined/filter-update?name=undefined&can_id=123-456&interval=789"
|
||||
style="margin: 5px;"
|
||||
title="Update \\"123-456\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Update 123-456"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class=""
|
||||
href="/"
|
||||
title="Delete \\"123-456\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Delete 123-456"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="MuiTableRow-root"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
1
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
1000
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="/fleet/undefined/filter-update?name=undefined&can_id=1&interval=1000"
|
||||
style="margin: 5px;"
|
||||
title="Update \\"1\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Update 1"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class=""
|
||||
href="/"
|
||||
title="Delete \\"1\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Delete 1"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="MuiTableRow-root"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
1000
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
1
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="/fleet/undefined/filter-update?name=undefined&can_id=1000&interval=1"
|
||||
style="margin: 5px;"
|
||||
title="Update \\"1000\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Update 1000"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class=""
|
||||
href="/"
|
||||
title="Delete \\"1000\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Delete 1000"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot
|
||||
class="MuiTableFooter-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root MuiTableRow-footer"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
|
||||
colspan="8"
|
||||
>
|
||||
<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>
|
||||
<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"
|
||||
>
|
||||
1-3 of 3
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
205
src/components/Fleets/Status/CANFilters/Table/index.jsx
Normal file
205
src/components/Fleets/Status/CANFilters/Table/index.jsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Grid,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TablePagination,
|
||||
TableRow,
|
||||
Tooltip,
|
||||
} from "@material-ui/core";
|
||||
import AddCircleIcon from "@material-ui/icons/AddCircle";
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import clsx from "clsx";
|
||||
|
||||
import TableHeaderSortable from "../../../../Table/HeaderSortable";
|
||||
import { useUserContext } from "../../../../Contexts/UserContext"
|
||||
import { useStatusContext } from "../../../../Contexts/StatusContext";
|
||||
import { FleetProvider, useFleetContext } from "../../../../Contexts/FleetContext"
|
||||
import useStyles from "../../../../useStyles";
|
||||
import SearchField from "../../../../Controls/SearchField";
|
||||
import { logger } from "../../../../../services/monitoring";
|
||||
import { Roles, hasRole } from "../../../../../utils/roles";
|
||||
|
||||
const tableColumns = [
|
||||
{
|
||||
id: "can_id",
|
||||
label: "CAN ID"
|
||||
},
|
||||
{
|
||||
id: "interval",
|
||||
label: "Interval (ms)"
|
||||
},
|
||||
{
|
||||
id: "",
|
||||
label: "Actions"
|
||||
}
|
||||
];
|
||||
|
||||
const MainForm = ({ name }) => {
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [orderBy, setOrderBy] = useState("id");
|
||||
const [order, setOrder] = useState("desc");
|
||||
const classes = useStyles();
|
||||
const { setMessage } = useStatusContext();
|
||||
const { fleetCANFilters, totalFleetCANFilters, getFleetCANFilters, deleteFleetCANFilter } = useFleetContext();
|
||||
const { token: { idToken: { jwtToken: token } }, groups } = useUserContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (!name || !token) return;
|
||||
await getFleetCANFilters(
|
||||
name,
|
||||
{
|
||||
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
|
||||
}, [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);
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async (can_id) => {
|
||||
try {
|
||||
await deleteFleetCANFilter(name, can_id, token);
|
||||
setMessage(`Deleted ${can_id}`)
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
};
|
||||
|
||||
const Actions = (row) => {
|
||||
let actions = [];
|
||||
if (hasRole([Roles.CREATE], groups)) {
|
||||
actions.push({
|
||||
tip: `Update "${row.can_id}"`,
|
||||
link: `/fleet/${name}/filter-update?name=${name}&can_id=${row.can_id}&interval=${row.interval}`,
|
||||
icon: <EditIcon aria-label={`Update ${row.can_id}`} />
|
||||
});
|
||||
}
|
||||
if (hasRole([Roles.DELETE], groups)) {
|
||||
actions.push({
|
||||
tip: `Delete "${row.can_id}"`,
|
||||
id: row.can_id,
|
||||
icon: <DeleteIcon aria-label={`Delete ${row.can_id}`} />
|
||||
})
|
||||
}
|
||||
if (actions.length === 0) return ["No actions"];
|
||||
|
||||
return actions.map((action) => {
|
||||
if (action.link != null) {
|
||||
return (
|
||||
<Tooltip key={action.link} title={action.tip}>
|
||||
<Link to={action.link} style={{ margin: 5 }}>
|
||||
{action.icon}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tooltip key={`delete-${action.id}`} title={action.tip}>
|
||||
<Link to="#" onClick={() => onDelete(action.id)}>
|
||||
{action.icon}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.paper, classes.tableSize)}>
|
||||
<Grid container className={classes.root} spacing={2}>
|
||||
<Grid item md={4} className={classes.textJustifyAlign}>
|
||||
<Link to={`/fleet/${name}/filter-add`} className={classes.labelInline}>
|
||||
<AddCircleIcon fontSize="large" />
|
||||
</Link>
|
||||
</Grid>
|
||||
<Grid item md={8} align="right" className={classes.textCenterAlign}>
|
||||
<SearchField classes={classes} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Table>
|
||||
<TableHeaderSortable
|
||||
classes={classes}
|
||||
orderBy={orderBy}
|
||||
order={order}
|
||||
columnData={tableColumns}
|
||||
onSortRequest={handleSort}
|
||||
/>
|
||||
<TableBody>
|
||||
{fleetCANFilters.map(row => (
|
||||
<TableRow key={row.can_id}>
|
||||
<TableCell align="center">{row.can_id}</TableCell>
|
||||
<TableCell align="center">{row.interval}</TableCell>
|
||||
<TableCell align="center">{Actions(row)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25, 100]}
|
||||
colSpan={8}
|
||||
count={totalFleetCANFilters}
|
||||
rowsPerPage={pageSize}
|
||||
page={pageIndex}
|
||||
SelectProps={{
|
||||
inputProps: { "aria-label": "rows per page" },
|
||||
native: true,
|
||||
}}
|
||||
onPageChange={handleChangePageIndex}
|
||||
onRowsPerPageChange={handleChangePageSize}
|
||||
/>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
const FleetCANFiltersTable = (props) => (
|
||||
<FleetProvider>
|
||||
<MainForm {...props} />
|
||||
</FleetProvider>
|
||||
);
|
||||
|
||||
export default FleetCANFiltersTable;
|
||||
39
src/components/Fleets/Status/CANFilters/Table/index.test.jsx
Normal file
39
src/components/Fleets/Status/CANFilters/Table/index.test.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
jest.mock("../../../../Contexts/FleetContext");
|
||||
jest.mock("../../../../Contexts/StatusContext");
|
||||
jest.mock("../../../../Contexts/UserContext");
|
||||
jest.mock('@material-ui/core/utils/unstable_useId', () =>
|
||||
jest.fn().mockReturnValue('mui-test-id'),
|
||||
);
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { FleetProvider } from "../../../../Contexts/FleetContext";
|
||||
import { StatusProvider } from "../../../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../../../utils/testing";
|
||||
import MainForm from "./index"
|
||||
|
||||
const renderFleetCANFiltersTable = async () => {
|
||||
const { container } = render(
|
||||
<FleetProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<MainForm />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</FleetProvider>
|
||||
);
|
||||
await waitFor(() => { });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("FleetCANFiltersTable", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderFleetCANFiltersTable();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FleetCANFilterUpdate Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-fleetprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-fleetprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3"
|
||||
>
|
||||
<form
|
||||
action="{onSubmit}"
|
||||
class="makeStyles-form-5"
|
||||
novalidate=""
|
||||
>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||
data-shrink="false"
|
||||
for="name"
|
||||
id="name-label"
|
||||
>
|
||||
Fleet Name
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="name"
|
||||
maxlength="255"
|
||||
name="name"
|
||||
readonly=""
|
||||
required=""
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64"
|
||||
>
|
||||
<span>
|
||||
Fleet Name
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||
data-shrink="false"
|
||||
for="canId"
|
||||
id="canId-label"
|
||||
>
|
||||
CAN ID
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="canId"
|
||||
maxlength="255"
|
||||
name="canId"
|
||||
readonly=""
|
||||
required=""
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64"
|
||||
>
|
||||
<span>
|
||||
CAN ID
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||
data-shrink="false"
|
||||
for="interval"
|
||||
id="interval-label"
|
||||
>
|
||||
Interval
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="interval"
|
||||
maxlength="255"
|
||||
name="interval"
|
||||
required=""
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64"
|
||||
>
|
||||
<span>
|
||||
Interval
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label"
|
||||
>
|
||||
Submit
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
133
src/components/Fleets/Status/CANFilters/Update/index.jsx
Normal file
133
src/components/Fleets/Status/CANFilters/Update/index.jsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Redirect, useParams } from "react-router";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { Button, TextField } from "@material-ui/core";
|
||||
|
||||
import { useUserContext } from "../../../../Contexts/UserContext";
|
||||
import { useStatusContext } from "../../../../Contexts/StatusContext";
|
||||
import { useFleetContext, FleetProvider } from "../../../../Contexts/FleetContext";
|
||||
import useStyles from "../../../../useStyles";
|
||||
import { logger } from "../../../../../services/monitoring";
|
||||
|
||||
|
||||
const MainForm = () => {
|
||||
const { name } = useParams();
|
||||
const { setMessage, setTitle, setSitePath } = useStatusContext();
|
||||
const { updateFleetCANFilter, busy } = useFleetContext();
|
||||
const { token: { idToken: { jwtToken: token } } } = useUserContext();
|
||||
const classes = useStyles();
|
||||
const intervalEl = useRef(null);
|
||||
const [redirect, setRedirect] = useState(null);
|
||||
const queries = new URLSearchParams(useLocation().search);
|
||||
const canID = queries.get("can_id") ?? "";
|
||||
const interval = queries.get("interval") ?? "";
|
||||
|
||||
useEffect(() => {
|
||||
const title = "Update CAN Filter"
|
||||
setTitle(title);
|
||||
setSitePath([
|
||||
{
|
||||
label: `Fleets`,
|
||||
link: "/fleets",
|
||||
},
|
||||
{
|
||||
label: `${name}`,
|
||||
link: `/fleet/${name}`,
|
||||
},
|
||||
{
|
||||
label: title
|
||||
},
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (event) => {
|
||||
try {
|
||||
event.preventDefault();
|
||||
const formData = {
|
||||
can_id: canID,
|
||||
interval: parseInt(intervalEl.current.value)
|
||||
};
|
||||
const result = await updateFleetCANFilter(name, canID, formData, token);
|
||||
if (!result || result.error) return;
|
||||
|
||||
setMessage(`Updated CAN filter ${result.can_id}`);
|
||||
setRedirect(`/fleet/${name}#filters`);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
};
|
||||
|
||||
if (redirect && redirect.length > 0) {
|
||||
return <Redirect to={redirect} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.paper}>
|
||||
<form className={classes.form} noValidate action="{onSubmit}">
|
||||
<TextField
|
||||
id="name"
|
||||
name="name"
|
||||
label="Fleet Name"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "255",
|
||||
readOnly: true,
|
||||
}}
|
||||
value={name}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
id="canId"
|
||||
name="canId"
|
||||
label="CAN ID"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "255",
|
||||
readOnly: true,
|
||||
}}
|
||||
value={canID}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
id="interval"
|
||||
name="interval"
|
||||
label="Interval"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "255",
|
||||
}}
|
||||
defaultValue={interval}
|
||||
required
|
||||
fullWidth
|
||||
inputRef={intervalEl}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.submit}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{busy ? "Submitting..." : "Submit"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FleetCANFilterUpdateForm = (props) => (
|
||||
<FleetProvider>
|
||||
<MainForm {...props} />
|
||||
</FleetProvider>
|
||||
);
|
||||
|
||||
export default FleetCANFilterUpdateForm;
|
||||
@@ -0,0 +1,36 @@
|
||||
jest.mock("../../../../Contexts/FleetContext");
|
||||
jest.mock("../../../../Contexts/StatusContext");
|
||||
jest.mock("../../../../Contexts/UserContext");
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { FleetProvider } from "../../../../Contexts/FleetContext";
|
||||
import { StatusProvider } from "../../../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../../../utils/testing";
|
||||
import MainForm from "./index"
|
||||
|
||||
const renderFleetCANFilterUpdate = async () => {
|
||||
const { container } = render(
|
||||
<FleetProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<MainForm />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</FleetProvider>
|
||||
);
|
||||
await waitFor(() => { });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("FleetCANFilterUpdate", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderFleetCANFilterUpdate();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
21
src/components/Fleets/Status/CANFiltersTab.jsx
Normal file
21
src/components/Fleets/Status/CANFiltersTab.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 FleetCANFiltersTable from "./CANFilters/Table";
|
||||
import useStyles from "../../useStyles";
|
||||
|
||||
const FleetCANFiltersTab = () => {
|
||||
const { name } = useParams();
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.paper, classes.tableSize)}>
|
||||
<Typography variant="h6">CAN Filters</Typography>
|
||||
<FleetCANFiltersTable name={name} classes={classes} />
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
export default FleetCANFiltersTab;
|
||||
31
src/components/Fleets/Status/CANFiltersTab.test.jsx
Normal file
31
src/components/Fleets/Status/CANFiltersTab.test.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
jest.mock("../../Contexts/FleetContext");
|
||||
jest.mock("../../Contexts/StatusContext");
|
||||
jest.mock("../../Contexts/UserContext");
|
||||
jest.mock('@material-ui/core/utils/unstable_useId', () =>
|
||||
jest.fn().mockReturnValue('mui-test-id'),
|
||||
);
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { setToken } from "../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
|
||||
import CANFiltersTab from "./CANFiltersTab"
|
||||
|
||||
const renderCANFitlersTab = async () => {
|
||||
const { container } = render(
|
||||
<BrowserRouter>
|
||||
<CANFiltersTab name="US-TEST" />
|
||||
</BrowserRouter>
|
||||
);
|
||||
await waitFor(() => { /* render */ });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("CANFiltersTab", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderCANFitlersTab();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FleetDetailsTab Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-fleetprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-fleetprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3 makeStyles-tableSize-53"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
|
||||
>
|
||||
<p>
|
||||
<b>
|
||||
Name
|
||||
</b>
|
||||
:
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Vehicles
|
||||
</b>
|
||||
:
|
||||
3
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Log Level
|
||||
</b>
|
||||
:
|
||||
info
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
|
||||
>
|
||||
<b>
|
||||
CANBus
|
||||
</b>
|
||||
<p>
|
||||
<b>
|
||||
Enabled
|
||||
</b>
|
||||
:
|
||||
true
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Max Memory Buffer Size
|
||||
</b>
|
||||
:
|
||||
1
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Enabled
|
||||
</b>
|
||||
:
|
||||
true
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Max Disk Buffer Size
|
||||
</b>
|
||||
:
|
||||
2
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Filters
|
||||
</b>
|
||||
:
|
||||
3
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-12"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="/fleet-update?name=undefined"
|
||||
style="margin: 5px;"
|
||||
title="Update \\"undefined\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Update \\"undefined\\""
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class=""
|
||||
href="/"
|
||||
title="Delete \\"undefined\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Delete \\"undefined\\""
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
96
src/components/Fleets/Status/Details/index.jsx
Normal file
96
src/components/Fleets/Status/Details/index.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Redirect } from "react-router";
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Grid,
|
||||
Tooltip,
|
||||
} from "@material-ui/core";
|
||||
import EditIcon from "@material-ui/icons/Edit"
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { useUserContext } from "../../../Contexts/UserContext"
|
||||
import { useStatusContext } from "../../../Contexts/StatusContext";
|
||||
import { FleetProvider, useFleetContext } from "../../../Contexts/FleetContext"
|
||||
import useStyles from "../../../useStyles";
|
||||
import { logger } from "../../../../services/monitoring";
|
||||
|
||||
const MainForm = ({ name }) => {
|
||||
const classes = useStyles();
|
||||
const { setMessage } = useStatusContext();
|
||||
const { fleet, getFleet, deleteFleet } = useFleetContext();
|
||||
const [redirect, setRedirect] = useState(null);
|
||||
const { token: { idToken: { jwtToken: token } } } = useUserContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (!name || !token) return;
|
||||
await getFleet(name, token);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
const onDelete = async () => {
|
||||
try {
|
||||
await deleteFleet(name, token);
|
||||
setMessage(`Deleted ${name}`)
|
||||
setRedirect(`/fleets`);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
};
|
||||
|
||||
if (redirect && redirect.length > 0) {
|
||||
return <Redirect to={redirect} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.paper, classes.tableSize)}>
|
||||
<Grid container className={classes.root} spacing={2}>
|
||||
<Grid item md={12} className={classes.textCenterAlign}>
|
||||
<p><b>Name</b>: {name}</p>
|
||||
<p><b>Vehicles</b>: {fleet.vehicles ? fleet.vehicles.length : 0}</p>
|
||||
{fleet.log_level != null && (
|
||||
<p><b>Log Level</b>: {fleet.log_level}</p>
|
||||
)}
|
||||
</Grid>
|
||||
{fleet.canbus && (
|
||||
<Grid item md={12} className={classes.textCenterAlign}>
|
||||
<b>CANBus</b>
|
||||
<p><b>Enabled</b>: {fleet.canbus.enabled.toString()}</p>
|
||||
<p><b>Max Memory Buffer Size</b>: {fleet.canbus.max_mem_buffer_size ?? "Default"}</p>
|
||||
<p><b>Enabled</b>: {fleet.canbus.data_logger_enabled.toString()}</p>
|
||||
<p><b>Max Disk Buffer Size</b>: {fleet.canbus.max_disk_buffer_size ?? "Default"}</p>
|
||||
<p><b>Filters</b>: {fleet.canbus.filters ? fleet.canbus.filters.length : 0}</p>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item md={12} className={classes.textCenterAlign}>
|
||||
<Tooltip key={`update-${name}`} title={`Update "${name}"`}>
|
||||
<Link to={`/fleet-update?name=${name}`} style={{ margin: 5 }}>
|
||||
<EditIcon aria-label={`Update "${name}"`} />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Tooltip key={`delete-${name}`} title={`Delete "${name}"`}>
|
||||
<Link to="#" onClick={onDelete}>
|
||||
<DeleteIcon aria-label={`Delete "${name}"`} />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
const FleetDetails = (props) => (
|
||||
<FleetProvider>
|
||||
<MainForm {...props} />
|
||||
</FleetProvider>
|
||||
);
|
||||
|
||||
export default FleetDetails;
|
||||
36
src/components/Fleets/Status/Details/index.test.jsx
Normal file
36
src/components/Fleets/Status/Details/index.test.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
jest.mock("../../../Contexts/FleetContext");
|
||||
jest.mock("../../../Contexts/StatusContext");
|
||||
jest.mock("../../../Contexts/UserContext");
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { FleetProvider } from "../../../Contexts/FleetContext";
|
||||
import { StatusProvider } from "../../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../../utils/testing";
|
||||
import MainForm from "./index"
|
||||
|
||||
const renderFleetDetailsTab = async () => {
|
||||
const { container } = render(
|
||||
<FleetProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<MainForm />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</FleetProvider>
|
||||
);
|
||||
await waitFor(() => { /* render */ });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("FleetDetailsTab", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderFleetDetailsTab();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
21
src/components/Fleets/Status/DetailsTab.jsx
Normal file
21
src/components/Fleets/Status/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 FleetDetails from "./Details";
|
||||
import useStyles from "../../useStyles";
|
||||
|
||||
const FleetDetailsTab = () => {
|
||||
const { name } = useParams();
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.paper, classes.tableSize)}>
|
||||
<Typography variant="h6">Fleet Details</Typography>
|
||||
<FleetDetails name={name} classes={classes} />
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
export default FleetDetailsTab;
|
||||
41
src/components/Fleets/Status/DetailsTab.test.jsx
Normal file
41
src/components/Fleets/Status/DetailsTab.test.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
jest.mock("../../Contexts/FleetContext");
|
||||
jest.mock("../../Contexts/StatusContext");
|
||||
jest.mock("../../Contexts/UserContext");
|
||||
jest.mock('@material-ui/core/utils/unstable_useId', () =>
|
||||
jest.fn().mockReturnValue('mui-test-id'),
|
||||
);
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { MemoryRouter, Route } from "react-router-dom";
|
||||
|
||||
import { FleetProvider } from "../../Contexts/FleetContext";
|
||||
import { StatusProvider } from "../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../utils/testing";
|
||||
import MainForm from "./DetailsTab"
|
||||
|
||||
const renderDetailsTab = async () => {
|
||||
const { container } = render(
|
||||
<FleetProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<MemoryRouter initialEntries={['/testroute/US-TEST']}>
|
||||
<Route path="/testroute/:name">
|
||||
<MainForm name="US-TEST" />
|
||||
</Route>
|
||||
</MemoryRouter >
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</FleetProvider>
|
||||
);
|
||||
await waitFor(() => { /* render */ });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("DetailsTab", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderDetailsTab();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FleetVehicleAdd Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-fleetprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-fleetprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3"
|
||||
>
|
||||
<form
|
||||
action="{onSubmit}"
|
||||
class="makeStyles-form-5"
|
||||
novalidate=""
|
||||
>
|
||||
<div
|
||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||
data-shrink="false"
|
||||
for="vin"
|
||||
id="vin-label"
|
||||
>
|
||||
VIN
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||
>
|
||||
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||
id="vin"
|
||||
maxlength="17"
|
||||
name="vin"
|
||||
required=""
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-62 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-64"
|
||||
>
|
||||
<span>
|
||||
VIN
|
||||
*
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label"
|
||||
>
|
||||
Submit
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
103
src/components/Fleets/Status/Vehicles/Add/index.jsx
Normal file
103
src/components/Fleets/Status/Vehicles/Add/index.jsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Redirect, useParams } from "react-router";
|
||||
import { Button, TextField } from "@material-ui/core";
|
||||
|
||||
import useStyles from "../../../../useStyles";
|
||||
import {
|
||||
useFleetContext,
|
||||
FleetProvider
|
||||
} from "../../../../Contexts/FleetContext";
|
||||
import { useStatusContext } from "../../../../Contexts/StatusContext";
|
||||
import { useUserContext } from "../../../../Contexts/UserContext";
|
||||
import { logger } from "../../../../../services/monitoring";
|
||||
|
||||
const MainForm = () => {
|
||||
const { name } = useParams();
|
||||
const { setMessage, setTitle, setSitePath } = useStatusContext();
|
||||
const { addFleetVehicle, busy } = useFleetContext();
|
||||
const {
|
||||
token: {
|
||||
idToken: { jwtToken: token },
|
||||
},
|
||||
} = useUserContext();
|
||||
const classes = useStyles();
|
||||
const vinEl = useRef(null);
|
||||
const [redirect, setRedirect] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const title = "Add Vehicle";
|
||||
setTitle(title);
|
||||
setSitePath([
|
||||
{
|
||||
label: "Fleets",
|
||||
link: "/fleets",
|
||||
},
|
||||
{
|
||||
label: `${name}`,
|
||||
link: `/fleet/${name}`
|
||||
},
|
||||
{
|
||||
label: title
|
||||
}
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (event) => {
|
||||
try {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = { vin: vinEl.current.value };
|
||||
const result = await addFleetVehicle(name, formData, token);
|
||||
|
||||
setMessage(`Added ${result.vin}`);
|
||||
setRedirect(`/fleet/${name}#vehicles`);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
};
|
||||
|
||||
if (redirect && redirect.length > 0) {
|
||||
return <Redirect to={redirect} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.paper}>
|
||||
<form className={classes.form} noValidate action="{onSubmit}">
|
||||
<TextField
|
||||
id="vin"
|
||||
name="vin"
|
||||
label="VIN"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
maxLength: "17",
|
||||
}}
|
||||
required
|
||||
fullWidth
|
||||
inputRef={vinEl}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.submit}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{busy ? "Submitting..." : "Submit"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FleetAddVehicleForm = () => (
|
||||
<FleetProvider>
|
||||
<MainForm />
|
||||
</FleetProvider>
|
||||
);
|
||||
|
||||
export default FleetAddVehicleForm;
|
||||
36
src/components/Fleets/Status/Vehicles/Add/index.test.jsx
Normal file
36
src/components/Fleets/Status/Vehicles/Add/index.test.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
jest.mock("../../../../Contexts/FleetContext");
|
||||
jest.mock("../../../../Contexts/StatusContext");
|
||||
jest.mock("../../../../Contexts/UserContext");
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { FleetProvider } from "../../../../Contexts/FleetContext";
|
||||
import { StatusProvider } from "../../../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../../../utils/testing";
|
||||
import MainForm from "./index"
|
||||
|
||||
const renderFleetVehicleAdd = async () => {
|
||||
const { container } = render(
|
||||
<FleetProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<MainForm />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</FleetProvider>
|
||||
);
|
||||
await waitFor(() => { /* render */ });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("FleetVehicleAdd", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderFleetVehicleAdd();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,374 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FleetVehiclesTable Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-fleetprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-fleetprovider"
|
||||
>
|
||||
<div
|
||||
class="makeStyles-paper-3 makeStyles-tableSize-53"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
|
||||
>
|
||||
<div
|
||||
class="MuiGrid-root makeStyles-textJustifyAlign-47 MuiGrid-item MuiGrid-grid-md-4"
|
||||
>
|
||||
<a
|
||||
class="makeStyles-labelInline-9"
|
||||
href="/fleet/undefined/vehicle-add"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
align="right"
|
||||
class="MuiGrid-root makeStyles-textCenterAlign-48 MuiGrid-item MuiGrid-grid-md-8"
|
||||
>
|
||||
<div
|
||||
class="MuiFormControl-root makeStyles-margin-28 makeStyles-fullWidth-50"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated"
|
||||
data-shrink="false"
|
||||
for="search"
|
||||
>
|
||||
Search
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedEnd"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedEnd"
|
||||
id="search"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
class="MuiInputAdornment-root MuiInputAdornment-positionEnd"
|
||||
>
|
||||
<button
|
||||
aria-label="search"
|
||||
class="MuiButtonBase-root MuiIconButton-root"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiIconButton-label"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table
|
||||
class="MuiTable-root"
|
||||
>
|
||||
<thead
|
||||
class="MuiTableHead-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root MuiTableRow-head"
|
||||
>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiTableSortLabel-root"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
VIN
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||
scope="col"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="MuiTableBody-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
<a
|
||||
href="/vehicle-status/USWESTVIN12345678"
|
||||
>
|
||||
USWESTVIN12345678
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="/"
|
||||
title="Delete \\"USWESTVIN12345678\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Delete USWESTVIN12345678"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="MuiTableRow-root"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
<a
|
||||
href="/vehicle-status/USWESTVIN12345679"
|
||||
>
|
||||
USWESTVIN12345679
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="/"
|
||||
title="Delete \\"USWESTVIN12345679\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Delete USWESTVIN12345679"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="MuiTableRow-root"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
<a
|
||||
href="/vehicle-status/USWESTVIN12345670"
|
||||
>
|
||||
USWESTVIN12345670
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="/"
|
||||
title="Delete \\"USWESTVIN12345670\\""
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
aria-label="Delete USWESTVIN12345670"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot
|
||||
class="MuiTableFooter-root"
|
||||
>
|
||||
<tr
|
||||
class="MuiTableRow-root MuiTableRow-footer"
|
||||
>
|
||||
<td
|
||||
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
|
||||
colspan="8"
|
||||
>
|
||||
<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>
|
||||
<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"
|
||||
>
|
||||
1-3 of 3
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
194
src/components/Fleets/Status/Vehicles/Table/index.jsx
Normal file
194
src/components/Fleets/Status/Vehicles/Table/index.jsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Grid,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TablePagination,
|
||||
TableRow,
|
||||
Tooltip,
|
||||
} from "@material-ui/core";
|
||||
import AddCircleIcon from "@material-ui/icons/AddCircle";
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import clsx from "clsx";
|
||||
|
||||
import TableHeaderSortable from "../../../../Table/HeaderSortable";
|
||||
import { useUserContext } from "../../../../Contexts/UserContext"
|
||||
import { useStatusContext } from "../../../../Contexts/StatusContext";
|
||||
import { FleetProvider, useFleetContext } from "../../../../Contexts/FleetContext"
|
||||
import useStyles from "../../../../useStyles";
|
||||
import SearchField from "../../../../Controls/SearchField";
|
||||
import { logger } from "../../../../../services/monitoring";
|
||||
import { Roles, hasRole } from "../../../../../utils/roles";
|
||||
|
||||
const tableColumns = [
|
||||
{
|
||||
id: "vin",
|
||||
label: "VIN"
|
||||
},
|
||||
{
|
||||
id: "",
|
||||
label: "Actions"
|
||||
}
|
||||
];
|
||||
|
||||
const MainForm = ({ name }) => {
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [orderBy, setOrderBy] = useState("id");
|
||||
const [order, setOrder] = useState("desc");
|
||||
const classes = useStyles();
|
||||
const { setMessage } = useStatusContext();
|
||||
const { fleetVehicles, totalFleetVehicles, getFleetVehicles, deleteFleetVehicle } = useFleetContext();
|
||||
const { token: { idToken: { jwtToken: token } }, groups } = useUserContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (!name || !token) return;
|
||||
await getFleetVehicles(
|
||||
name,
|
||||
{
|
||||
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
|
||||
}, [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);
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async (vin) => {
|
||||
try {
|
||||
await deleteFleetVehicle(name, { vin: vin }, token);
|
||||
setMessage(`Deleted ${vin}`)
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
logger.warn(e.stack);
|
||||
}
|
||||
};
|
||||
|
||||
const Actions = (vin) => {
|
||||
let actions = [];
|
||||
if (hasRole([Roles.DELETE], groups)) {
|
||||
actions.push({
|
||||
tip: `Delete "${vin}"`,
|
||||
id: vin,
|
||||
icon: <DeleteIcon aria-label={`Delete ${vin}`} />
|
||||
})
|
||||
}
|
||||
if (actions.length === 0) return ["No actions"];
|
||||
|
||||
return actions.map((action) => {
|
||||
if (action.link != null) {
|
||||
return (
|
||||
<Tooltip key={action.link} title={action.tip}>
|
||||
<Link to={action.link} style={{ margin: 5 }}>
|
||||
{action.icon}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tooltip key={`delete-${action.id}`} title={action.tip}>
|
||||
<Link to="#" onClick={() => onDelete(action.id)}>
|
||||
{action.icon}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.paper, classes.tableSize)}>
|
||||
<Grid container className={classes.root} spacing={2}>
|
||||
<Grid item md={4} className={classes.textJustifyAlign}>
|
||||
<Link to={`/fleet/${name}/vehicle-add`} className={classes.labelInline}>
|
||||
<AddCircleIcon fontSize="large" />
|
||||
</Link>
|
||||
</Grid>
|
||||
<Grid item md={8} align="right" className={classes.textCenterAlign}>
|
||||
<SearchField classes={classes} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Table>
|
||||
<TableHeaderSortable
|
||||
classes={classes}
|
||||
orderBy={orderBy}
|
||||
order={order}
|
||||
columnData={tableColumns}
|
||||
onSortRequest={handleSort}
|
||||
/>
|
||||
<TableBody>
|
||||
{fleetVehicles.map(vin => (
|
||||
<TableRow key={vin}>
|
||||
<TableCell align="center">
|
||||
<Link to={`/vehicle-status/${vin}`}>{vin}</Link>
|
||||
</TableCell>
|
||||
<TableCell align="center">{Actions(vin)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25, 100]}
|
||||
colSpan={8}
|
||||
count={totalFleetVehicles}
|
||||
rowsPerPage={pageSize}
|
||||
page={pageIndex}
|
||||
SelectProps={{
|
||||
inputProps: { "aria-label": "rows per page" },
|
||||
native: true,
|
||||
}}
|
||||
onPageChange={handleChangePageIndex}
|
||||
onRowsPerPageChange={handleChangePageSize}
|
||||
/>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
const FleetVehiclesTable = (props) => (
|
||||
<FleetProvider>
|
||||
<MainForm {...props} />
|
||||
</FleetProvider>
|
||||
);
|
||||
|
||||
export default FleetVehiclesTable;
|
||||
39
src/components/Fleets/Status/Vehicles/Table/index.test.jsx
Normal file
39
src/components/Fleets/Status/Vehicles/Table/index.test.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
jest.mock("../../../../Contexts/FleetContext");
|
||||
jest.mock("../../../../Contexts/StatusContext");
|
||||
jest.mock("../../../../Contexts/UserContext");
|
||||
jest.mock('@material-ui/core/utils/unstable_useId', () =>
|
||||
jest.fn().mockReturnValue('mui-test-id'),
|
||||
);
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { FleetProvider } from "../../../../Contexts/FleetContext";
|
||||
import { StatusProvider } from "../../../../Contexts/StatusContext";
|
||||
import { UserProvider, setToken } from "../../../../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../../../../utils/testing";
|
||||
import MainForm from "./index"
|
||||
|
||||
const renderFleetVehiclesTable = async () => {
|
||||
const { container } = render(
|
||||
<FleetProvider>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<MainForm />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</FleetProvider>
|
||||
);
|
||||
await waitFor(() => { /* render */ });
|
||||
return container;
|
||||
};
|
||||
|
||||
describe("FleetVehiclesTable", () => {
|
||||
it("Render", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderFleetVehiclesTable();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user