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
This commit is contained in:
Drew Taylor
2022-03-03 11:33:07 -08:00
committed by GitHub
parent 3b9252097a
commit b7223b8bc6
37 changed files with 6673 additions and 944 deletions

View File

@@ -1,3 +1,7 @@
{ {
"editor.formatOnSave": true "editor.formatOnSave": true,
// spacing
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.detectIndentation": false
} }

2262
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,8 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@datadog/browser-logs": "^3.7.0", "@datadog/browser-logs": "^3.7.0",
"@emotion/react": "^11.8.1",
"@emotion/styled": "^11.8.1",
"@material-ui/core": "^4.12.3", "@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@testing-library/jest-dom": "^5.15.0", "@testing-library/jest-dom": "^5.15.0",
@@ -16,8 +18,9 @@
"material-ui-dropzone": "^3.5.0", "material-ui-dropzone": "^3.5.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-leaflet": "^3.2.2", "react-leaflet": "^3.2.5",
"react-router-dom": "^5.3.0", "react-router-dom": "^5.3.0",
"react-router-hash-link": "^2.4.3",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"web-vitals": "^2.1.2" "web-vitals": "^2.1.2"
}, },
@@ -44,7 +47,7 @@
"not op_mini all" "not op_mini all"
], ],
"engines": { "engines": {
"node": "12.20.1" "node": "14.17.6"
}, },
"devDependencies": { "devDependencies": {
"react-test-renderer": "^17.0.2" "react-test-renderer": "^17.0.2"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,177 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CANFiltersCreate 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-64 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-66"
>
<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-64 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-66"
>
<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-64 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-66"
>
<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>
`;

View 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;

View 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 renderCANFiltersCreate = async () => {
const { container } = render(
<CANFiltersProvider>
<StatusProvider>
<UserProvider>
<BrowserRouter>
<MainForm />
</BrowserRouter>
</UserProvider>
</StatusProvider>f
</CANFiltersProvider>
);
await waitFor(() => { });
return container;
};
describe("CANFiltersCreate", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderCANFiltersCreate();
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,402 @@
// 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-55"
>
<div
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
>
<div
class="MuiGrid-root makeStyles-textJustifyAlign-49 MuiGrid-item MuiGrid-grid-md-4"
>
<a
class="makeStyles-labelInline-9"
href="/filter-create?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-50 MuiGrid-item MuiGrid-grid-md-8"
>
<div
class="MuiFormControl-root makeStyles-margin-28 makeStyles-fullWidth-52"
>
<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>
</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>
</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>
</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>
`;

View File

@@ -0,0 +1,209 @@
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 EditIcon from '@material-ui/icons/Edit';
import DeleteIcon from "@material-ui/icons/Delete";
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);
} 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-create?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;

View 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();
});
});

View File

@@ -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-64 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-66"
>
<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-64 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-66"
>
<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-64 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-66"
>
<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>
`;

View 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;

View 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();
});
});

View File

@@ -0,0 +1,21 @@
import React from "react";
import { useParams } from "react-router";
import clsx from "clsx";
import { Typography } from "@material-ui/core";
import 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;

View 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(() => { });
return container;
};
describe("CANFiltersTab", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderCANFiltersTab();
expect(container).toMatchSnapshot();
});
});

View 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;

View 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(() => { });
return container;
};
describe("CarUpdatesTab", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderCarUpdatesTab();
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,399 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CANFiltersTab Render 1`] = `
<div>
<div
class="makeStyles-paper-3 makeStyles-tableSize-55"
>
<h6
class="MuiTypography-root MuiTypography-h6"
>
CAN Filters
</h6>
<div
data-testid="mocked-canfiltersprovider"
>
<div
class="makeStyles-paper-3 makeStyles-tableSize-55"
>
<div
class="MuiGrid-root makeStyles-root-14 MuiGrid-container MuiGrid-spacing-xs-2"
>
<div
class="MuiGrid-root makeStyles-textJustifyAlign-49 MuiGrid-item MuiGrid-grid-md-4"
>
<a
class="makeStyles-labelInline-9"
href="/filter-create?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-50 MuiGrid-item MuiGrid-grid-md-8"
>
<div
class="MuiFormControl-root makeStyles-margin-28 makeStyles-fullWidth-52"
>
<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>
</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>
</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>
</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>
`;

View File

@@ -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-55"
>
<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-49 MuiGrid-item MuiGrid-grid-md-4"
/>
<div
class="MuiGrid-root makeStyles-textCenterAlign-50 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-51 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-55"
>
<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>
`;

View File

@@ -0,0 +1,685 @@
// 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-55"
>
<div
class="MuiBox-root MuiBox-root-64 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"
>
Car Updates
</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"
>
CAN Filters
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
<span
class="PrivateTabIndicator-root-65 PrivateTabIndicator-colorSecondary-67 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-69"
>
<div
class="makeStyles-paper-3 makeStyles-tableSize-55"
>
<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-49 MuiGrid-item MuiGrid-grid-md-4"
/>
<div
class="MuiGrid-root makeStyles-textCenterAlign-50 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-51 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-55"
>
<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
aria-labelledby="tab-1"
hidden=""
id="tabpanel-1"
role="tabpanel"
/>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -1,38 +1,32 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useParams } from "react-router"; import { useParams } from "react-router";
import { useLocation } from "react-router-dom";
import clsx from "clsx"; 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 CarUpdatesTab from "./CarUpdatesTab";
import CarUpdatesTable from "../../Controls/CarUpdatesTable"; import CANFiltersTab from "./CANFiltersTab";
import { logger } from "../../../services/monitoring"; import TabPanel from "../../Controls/TabPanel";
import {
VehicleProvider,
useVehicleContext,
} from "../../Contexts/VehicleContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext"; import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles"; import useStyles from "../../useStyles";
const MainForm = () => { const tabHashes = [
"updates",
"filters"
]
const CarStatus = () => {
const { vin } = useParams(); const { vin } = useParams();
const classes = useStyles(); const classes = useStyles();
const { setTitle, setSitePath, setMessage } = useStatusContext(); const { setTitle, setSitePath } = useStatusContext();
const { busy, sendCommand } = useVehicleContext(); const { hash } = useLocation();
const { const [tabIndex, setTabIndex] = React.useState(0);
token: {
idToken: { jwtToken: token }, useEffect(() => {
}, const key = hash.replace("#", "")
} = useUserContext(); const index = tabHashes.findIndex(element => element === key);
const updateHandler = async (e) => { if (index >= 0) setTabIndex(index);
try { }, [hash]);
await sendCommand([vin], "ecu", "", token);
setMessage(`Sent command to ${vin}`);
} catch (error) {
setMessage(error.message);
logger.error(error.stack);
}
};
useEffect(() => { useEffect(() => {
const title = `Vehicle ${vin} Details`; const title = `Vehicle ${vin} Details`;
@@ -49,39 +43,35 @@ const MainForm = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [vin]); }, [vin]);
const handleTabChange = (event, newIndex) => {
setTabIndex(newIndex);
};
return ( return (
<div className={clsx(classes.paper, classes.tableSize)}> <div className={clsx(classes.paper, classes.tableSize)}>
<Typography variant="h6">Car Updates</Typography> <Box className={classes.tableToolbar} sx={{ borderBottom: 1, borderColor: 'divider' }}>
<CarUpdatesTable vin={vin} token={token} classes={classes} /> <Tabs value={tabIndex} onChange={handleTabChange} aria-label="car tabs" indicatorColor="secondary">
<Grid container className={classes.root} spacing={2}> <Tab label="Car Updates" {...tabProps(0)} />
<Grid item md={4} className={classes.textJustifyAlign}></Grid> <Tab label="CAN Filters" {...tabProps(1)} />
<Grid item md={4} className={classes.textCenterAlign}> </Tabs>
<Typography variant="h6" className={classes.labelInline}> </Box>
Car ECUs
</Typography> <TabPanel value={tabIndex} index={0}>
</Grid> <CarUpdatesTab />
<Grid item md={4} className={classes.textRightAlign}> </TabPanel>
<Button
type="submit" <TabPanel value={tabIndex} index={1}>
disabled={busy} <CANFiltersTab />
variant="contained" </TabPanel>
color="primary" </div >
className={clsx(classes.formControl, classes.textField)}
onClick={updateHandler}
>
{busy ? "Sending..." : "Refresh"}
</Button>
</Grid>
</Grid>
<CarECUsTable vin={vin} token={token} classes={classes} />
</div>
); );
}; };
const CarStatus = () => ( function tabProps(index) {
<VehicleProvider> return {
<MainForm /> id: `tab-${index}`,
</VehicleProvider> "aria-controls": `tabpanel-${index}`
); };
}
export default CarStatus; export default CarStatus;

View 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(() => { });
return container;
};
describe("CarStatus", () => {
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderCarStatus();
expect(container).toMatchSnapshot();
});
});

View 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);

View File

@@ -0,0 +1,149 @@
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");
});
});
});
const expectedFiltersData = [
{
can_id: "123",
interval: 1000
},
{
can_id: "456",
interval: 0
},
{
can_id: "789-1000",
interval: 5
},
];

View File

@@ -5,7 +5,7 @@ import api from "../../services/vehiclesAPI";
const VehicleContext = React.createContext(); const VehicleContext = React.createContext();
const validateAdd = (vehicle) => { const validateAdd = (vehicle) => {
if (vehicle === null) { if (vehicle == null) {
throw new Error("No vehicle data"); throw new Error("No vehicle data");
} }
@@ -175,7 +175,7 @@ export const VehicleProvider = ({ children }) => {
getState, getState,
getYears, getYears,
getVehicles, getVehicles,
sendCommand, sendCommand
}} }}
> >
{children} {children}

View 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(),
});

View 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),
});

View File

@@ -40,7 +40,7 @@ exports[`CarUpdateStatusProgress Render download_complete 1`] = `
<p <p
class="MuiTypography-root MuiTypography-body1" class="MuiTypography-root MuiTypography-body1"
> >
Recieved Received
</p> </p>
</div> </div>
<div <div
@@ -201,7 +201,7 @@ exports[`CarUpdateStatusProgress Render download_error 1`] = `
<p <p
class="MuiTypography-root MuiTypography-body1" class="MuiTypography-root MuiTypography-body1"
> >
Recieved Received
</p> </p>
</div> </div>
<div <div
@@ -362,7 +362,7 @@ exports[`CarUpdateStatusProgress Render download_start ECU 1`] = `
<p <p
class="MuiTypography-root MuiTypography-body1" class="MuiTypography-root MuiTypography-body1"
> >
Recieved Received
</p> </p>
</div> </div>
<div <div
@@ -523,7 +523,7 @@ exports[`CarUpdateStatusProgress Render download_started 1`] = `
<p <p
class="MuiTypography-root MuiTypography-body1" class="MuiTypography-root MuiTypography-body1"
> >
Recieved Received
</p> </p>
</div> </div>
<div <div
@@ -684,7 +684,7 @@ exports[`CarUpdateStatusProgress Render downloading 1`] = `
<p <p
class="MuiTypography-root MuiTypography-body1" class="MuiTypography-root MuiTypography-body1"
> >
Recieved Received
</p> </p>
</div> </div>
<div <div
@@ -845,7 +845,7 @@ exports[`CarUpdateStatusProgress Render install_complete ECU 1`] = `
<p <p
class="MuiTypography-root MuiTypography-body1" class="MuiTypography-root MuiTypography-body1"
> >
Recieved Received
</p> </p>
</div> </div>
<div <div
@@ -1006,7 +1006,7 @@ exports[`CarUpdateStatusProgress Render install_error 1`] = `
<p <p
class="MuiTypography-root MuiTypography-body1" class="MuiTypography-root MuiTypography-body1"
> >
Recieved Received
</p> </p>
</div> </div>
<div <div
@@ -1167,7 +1167,7 @@ exports[`CarUpdateStatusProgress Render installing 1`] = `
<p <p
class="MuiTypography-root MuiTypography-body1" class="MuiTypography-root MuiTypography-body1"
> >
Recieved Received
</p> </p>
</div> </div>
<div <div
@@ -1328,7 +1328,7 @@ exports[`CarUpdateStatusProgress Render manifest_accepted 1`] = `
<p <p
class="MuiTypography-root MuiTypography-body1" class="MuiTypography-root MuiTypography-body1"
> >
Recieved Received
</p> </p>
</div> </div>
<div <div
@@ -1489,7 +1489,7 @@ exports[`CarUpdateStatusProgress Render manifest_received 1`] = `
<p <p
class="MuiTypography-root MuiTypography-body1" class="MuiTypography-root MuiTypography-body1"
> >
Recieved Received
</p> </p>
</div> </div>
<div <div
@@ -1650,7 +1650,7 @@ exports[`CarUpdateStatusProgress Render package_download_complete 1`] = `
<p <p
class="MuiTypography-root MuiTypography-body1" class="MuiTypography-root MuiTypography-body1"
> >
Recieved Received
</p> </p>
</div> </div>
<div <div

View File

@@ -12,7 +12,7 @@ const PHASES = [
progress: () => 100, progress: () => 100,
}, },
{ {
label: "Recieved", label: "Received",
events: ["manifest_accepted", "manifest_received"], events: ["manifest_accepted", "manifest_received"],
progress: () => 100, progress: () => 100,
}, },

View File

@@ -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>
`;

View 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;

View 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();
});
});

View File

@@ -147,20 +147,19 @@ const MainForm = () => {
}); });
} }
if (hasRole([Roles.CREATE], groups)) { if (hasRole([Roles.CREATE], groups)) {
actions = actions.concat([ actions.push({
{ tip: `Deploy "${row.name} ${row.version}"`,
tip: `Deploy "${row.name} ${row.version}"`, link: `/package-deploy/${row.id}`,
link: `/package-deploy/${row.id}`, icon: <SendIcon aria-label={`Deploy ${row.name} ${row.version}`} />
icon: <SendIcon aria-label={`Deploy ${row.name} ${row.version}`} />, })
}, }
{ if (hasRole([Roles.DELETE], groups)) {
tip: `Delete "${row.name} ${row.version}"`, actions.push({
id: row.id, tip: `Delete "${row.name} ${row.version}"`,
icon: <DeleteIcon aria-label={`Delete ${row.name} ${row.version}`} />, id: row.id,
}, icon: <DeleteIcon aria-label={`Delete ${row.name} ${row.version}`} />
]); })
} }
if (actions.length === 0) return ["No actions"]; if (actions.length === 0) return ["No actions"];
return actions.map((action) => { return actions.map((action) => {

View File

@@ -7,6 +7,8 @@ import { useUserContext } from "../Contexts/UserContext";
import { Roles } from "../../utils/roles"; import { Roles } from "../../utils/roles";
const BatteryDatascope = React.lazy(() => import("../Datascope/Battery")); const BatteryDatascope = React.lazy(() => import("../Datascope/Battery"));
const CANFilterCreate = React.lazy(() => import("../CANFilter/Create"))
const CANFilterUpdate = React.lazy(() => import("../CANFilter/Update"))
const CarsList = React.lazy(() => import("../Cars/List")); const CarsList = React.lazy(() => import("../Cars/List"));
const CarStatus = React.lazy(() => import("../Cars/Status")); const CarStatus = React.lazy(() => import("../Cars/Status"));
const CarUpdateStatus = React.lazy(() => import("../Cars/UpdateStatus")); const CarUpdateStatus = React.lazy(() => import("../Cars/UpdateStatus"));
@@ -49,6 +51,22 @@ const SiteRoutes = () => {
groups={groups} groups={groups}
roles={[Roles.READ, Roles.CREATE]} roles={[Roles.READ, Roles.CREATE]}
/> />
<AuthRoute
path="/filter-create"
render={() => <CANFilterCreate />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
roles={[Roles.CREATE]}
/>
<AuthRoute
path="/filter-update"
render={() => <CANFilterUpdate />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
roles={[Roles.CREATE]}
/>
<AuthRoute <AuthRoute
path="/home" path="/home"
render={() => <Home />} render={() => <Home />}

View File

@@ -0,0 +1,49 @@
import {
getAuthHeaderOptions,
fetchRespHandler,
addQueryParams,
} from "../utils/http";
const API_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL;
const canFiltersAPI = {
addFilter: async (vin, filter, token) =>
fetch(`${API_ENDPOINT}/vehicle/${vin}/filter`, {
method: "POST",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
body: JSON.stringify(filter)
}).then(fetchRespHandler),
getFilters: async (vin, search, token) =>
fetch(addQueryParams(`${API_ENDPOINT}/vehicle/${vin}/filters`, search), {
method: "GET",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler),
updateFilter: async (vin, canID, filter, token) =>
fetch(`${API_ENDPOINT}/vehicle/${vin}/filter/${canID}`, {
method: "PUT",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
body: JSON.stringify(filter)
}).then(fetchRespHandler),
deleteFilter: async (vin, canID, token) =>
fetch(`${API_ENDPOINT}/vehicle/${vin}/filter/${canID}`, {
method: "DELETE",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
)
}).then(fetchRespHandler),
};
export default canFiltersAPI;

View File

@@ -0,0 +1,25 @@
const data = [
{ can_id: "123", interval: 1000 },
{ can_id: "456", interval: 0 },
{ can_id: "789-1000", interval: 5 }
];
const canFiltersAPI = {
addFilter: async (vin, filter, token) => {
data.push(filter);
return filter;
},
getFilters: async (vin, search, token) => {
return { data };
},
updateFilter: async (vin, canID, filter, token) => {
const index = data.findIndex(element => element.can_id === canID);
if (index >= 0) data[index] = filter;
},
deleteFilter: async (vin, canID, token) => {
const index = data.findIndex(element => element.can_id === canID);
if (index >= 0) data.splice(index, 1);
},
};
export default canFiltersAPI;

View File

@@ -1,8 +1,9 @@
import { parsePayload } from "./jwt"; import { parsePayload } from "./jwt";
export const Roles = { export const Roles = {
CREATE: "efcc3025-e2d8-4212-8227-805c7be39d2c",
READ: "a729bbd4-2038-4649-9127-16782bb1e701", READ: "a729bbd4-2038-4649-9127-16782bb1e701",
CREATE: "efcc3025-e2d8-4212-8227-805c7be39d2c",
DELETE: "8f78dce7-f5f9-4033-a10c-c9c7408bfcfe"
} }
export const hasRoleToken = (roles, token) => { export const hasRoleToken = (roles, token) => {