CEC-244 Remote car commands, search, sortable tables (#42)

* Add sortable table header

* Send bulk commands page
Update table page sizes
All tables are sortable

* Update site layout
Add search to update packages

* Reenable Datadog

* remove dev stuff
This commit is contained in:
John Wu
2021-05-26 15:46:46 -07:00
committed by GitHub
parent 64995ef7a6
commit 931e1521e8
29 changed files with 1886 additions and 1541 deletions

View File

@@ -1,4 +1,4 @@
import React, { useRef } from "react";
import React, { useEffect, useRef } from "react";
import useStyles from "../../useStyles";
import {
@@ -7,11 +7,11 @@ import {
} from "../../Contexts/VehicleContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import { Button, TextField, Typography } from "@material-ui/core";
import { Button, TextField } from "@material-ui/core";
const MainForm = () => {
const { addVehicle, busy } = useVehicleContext();
const { setMessage } = useStatusContext();
const { setMessage, setTitle } = useStatusContext();
const {
token: {
idToken: { jwtToken: token },
@@ -22,6 +22,10 @@ const MainForm = () => {
const modelEl = useRef(null);
const yearEl = useRef(null);
useEffect(() => {
setTitle("Add Vehicle");
// eslint-disable-next-line
}, []);
const onSubmit = async (event) => {
try {
event.preventDefault();
@@ -43,9 +47,6 @@ const MainForm = () => {
return (
<div className={classes.paper}>
<Typography component="h1" variant="h5">
Add Vehicle
</Typography>
<form className={classes.form} noValidate action="{onSubmit}">
<TextField
id="vin"

View File

@@ -6,10 +6,9 @@ import {
TableCell,
TableContainer,
TableFooter,
TableHead,
TablePagination,
TableRow,
Typography,
Toolbar,
} from "@material-ui/core";
import {
@@ -20,25 +19,77 @@ import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import { LocalDateTimeString } from "../../../utils/dates";
import TableHeaderSortable from "../../Table/HeaderSortable";
import SearchField from "../../Controls/SearchField";
const tableColumns = [
{
id: "vin",
label: "VIN",
},
{
id: "model",
label: "Model",
},
{
id: "year",
label: "Year",
},
{
id: "trim",
label: "Trim",
},
{
id: "created_at",
label: "Created",
},
{
id: "updated_at",
label: "Updated",
},
];
const MainForm = () => {
const classes = useStyles();
const [pageSize, setPageSize] = useState(25);
const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("vin");
const [order, setOrder] = useState("asc");
const [search, setSearch] = useState("");
const { getVehicles, vehicles, totalVehicles } = useVehicleContext();
const { setMessage } = useStatusContext();
const { setMessage, setTitle } = useStatusContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const sortHandler = (event, property) => {
if (property === orderBy) {
if (order === "asc") {
setOrder("desc");
} else {
setOrder("asc");
}
} else {
setOrderBy(property);
setOrder("asc");
}
};
useEffect(() => {
setTitle("Vehicles");
// eslint-disable-next-line
}, []);
useEffect(() => {
try {
getVehicles(
{
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
search,
},
token
);
@@ -46,7 +97,7 @@ const MainForm = () => {
setMessage(e.message);
}
// eslint-disable-next-line
}, [pageIndex, pageSize, token]);
}, [pageIndex, pageSize, token, orderBy, order, search]);
const handleChangePageIndex = (event, newIndex) => {
setPageIndex(newIndex);
@@ -57,30 +108,33 @@ const MainForm = () => {
setPageIndex(0);
};
const handleSearch = (search) => {
setSearch(search);
};
return (
<div className={classes.paper} style={{ height: 700, width: "100%" }}>
<Typography component="h1" variant="h5">
Vehicles
</Typography>
<Toolbar className={classes.tableToolbar}>
<SearchField classes={classes} onSearch={handleSearch} />
</Toolbar>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell align="center">VIN</TableCell>
<TableCell align="center">Model</TableCell>
<TableCell align="center">Year</TableCell>
<TableCell align="center">Created</TableCell>
<TableCell align="center">Updated</TableCell>
</TableRow>
</TableHead>
<TableHeaderSortable
classes={classes}
orderBy={orderBy}
order={order}
columnData={tableColumns}
onSortRequest={sortHandler}
/>
<TableBody>
{vehicles.map((row) => (
<TableRow key={row.vin}>
<TableCell align="center">
<TableCell align="center" sortDirection={true}>
<Link to={`/vehicle-status/${row.vin}`}>{row.vin}</Link>
</TableCell>
<TableCell align="center">{row.model}</TableCell>
<TableCell align="center">{row.year}</TableCell>
<TableCell align="center">{row.trim || ""}</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.created)}
</TableCell>
@@ -93,7 +147,7 @@ const MainForm = () => {
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={6}
count={totalVehicles}
rowsPerPage={pageSize}

View File

@@ -1,123 +0,0 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import {
FormControlLabel,
Grid,
IconButton,
TextField,
Switch,
} from "@material-ui/core";
import SendIcon from "@material-ui/icons/Send";
import { useVehicleContext } from "../../Contexts/VehicleContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
const LogFilter = ({ vin }) => {
const { sendLogFilter, busy } = useVehicleContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const { setMessage } = useStatusContext();
const [filter, setFilter] = useState("");
const [enableLog, setEnableLog] = useState(true);
const [freq, setFreq] = useState(0);
const changeFilterHandler = (e) => {
setFilter(e.target.value);
};
const changeStartHandler = (e) => {
setEnableLog(e.target.checked);
};
const changeFreqHandler = (e) => {
setFreq(e.target.value);
};
const clickHandler = async (e) => {
try {
const data = {
enable: enableLog,
frequency: freq,
filter,
};
await sendLogFilter(vin, data, token);
setMessage(`Sent log command to ${vin}`);
} catch (e) {
setMessage(e.message);
}
};
return (
<Grid
container
direction="row"
justify="flex-start"
alignItems="center"
spacing={2}
>
<Grid item>
<FormControlLabel
control={
<Switch
checked={enableLog}
onChange={changeStartHandler}
name="logStart"
color="primary"
/>
}
labelPlacement="top"
label="Logging"
/>
</Grid>
<Grid item>
<TextField
id="log-filter"
name="log-filter"
label="Filter"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "17",
}}
disabled={!enableLog}
value={filter}
onChange={changeFilterHandler}
/>
</Grid>
<Grid item>
<TextField
id="log-frequency"
label="Frequency"
type="number"
disabled={!enableLog}
InputLabelProps={{
shrink: true,
}}
value={freq}
onChange={changeFreqHandler}
/>
</Grid>
<Grid item>
<IconButton
color="primary"
aria-label="send log command"
component="span"
onClick={clickHandler}
disabled={busy}
>
<SendIcon fontSize="large" />
</IconButton>
</Grid>
</Grid>
);
};
LogFilter.propTypes = {
vin: PropTypes.string.isRequired,
};
export default LogFilter;

View File

@@ -9,7 +9,7 @@ import useStyles from "../../useStyles";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
const SendCommand = ({ vin }) => {
const SendCommand = ({ vins }) => {
const classes = useStyles();
const { sendCommand, busy } = useVehicleContext();
const {
@@ -17,25 +17,59 @@ const SendCommand = ({ vin }) => {
idToken: { jwtToken: token },
},
} = useUserContext();
const NoParameters = {
value: "",
label: "None",
};
const { setMessage } = useStatusContext();
const [command, setCommand] = useState("");
const [parameters, setParameters] = useState([NoParameters]);
const [parameter, setParameter] = useState("");
const changeCommandHandler = (e) => {
selectCommand(e.target.value);
};
const changeHandler = (e) => {
setCommand(e.target.value);
const selectCommand = (cmd) => {
const params = getParameters(cmd);
setCommand(cmd);
setParameters(params);
setParameter(params[0].value);
};
const changeParametersHandler = (e) => {
setParameter(e.target.value);
};
const clickHandler = async (e) => {
try {
await sendCommand(vin, command, token);
setMessage(`Sent command to ${vin}`);
await sendCommand(vins, command, parameter, token);
if (vins.length === 1) {
setMessage(`Sent command to ${vins[0]}`);
} else {
setMessage(`Sent command to ${vins.length} cars`);
}
} catch (e) {
setMessage(e.message);
}
};
const getParameters = (command) => {
for (let i = 0, len = commands.length; i < len; i += 1) {
const item = commands[i];
if (item.value === command) {
if (!item.parameters) {
break;
}
return item.parameters;
}
}
return [NoParameters];
};
useEffect(() => {
if (!commands || commands.length === 0) return;
setCommand(commands[0].value);
selectCommand(commands[0].value);
// eslint-disable-next-line
}, []);
return (
@@ -52,7 +86,7 @@ const SendCommand = ({ vin }) => {
name: "send-command",
id: "send-command",
}}
onChange={changeHandler}
onChange={changeCommandHandler}
>
{commands.map((item) => (
<option key={item.value} value={item.value}>
@@ -61,12 +95,37 @@ const SendCommand = ({ vin }) => {
))}
</Select>
</FormControl>
<FormControl className={classes.formControlInline} variant="outlined">
<InputLabel
htmlFor="send-parameter"
style={{ backgroundColor: "White" }}
>
Parameter
</InputLabel>
<Select
native
value={parameter}
variant="outlined"
inputProps={{
name: "send-parameter",
id: "send-parameter",
}}
onChange={changeParametersHandler}
disabled={parameters.length === 0}
>
{parameters.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</Select>
</FormControl>
<IconButton
color="primary"
aria-label="send command"
component="span"
onClick={clickHandler}
disabled={busy}
disabled={busy || vins.length === 0}
>
<SendIcon fontSize="large" />
</IconButton>
@@ -75,7 +134,7 @@ const SendCommand = ({ vin }) => {
};
SendCommand.propTypes = {
vin: PropTypes.string.isRequired,
vins: PropTypes.array.isRequired,
};
export default SendCommand;

View File

@@ -0,0 +1,203 @@
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import {
Checkbox,
Table,
TableBody,
TableCell,
TableContainer,
TableFooter,
TablePagination,
TableRow,
} from "@material-ui/core";
import {
useVehicleContext,
VehicleProvider,
} from "../../Contexts/VehicleContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import { LocalDateTimeString } from "../../../utils/dates";
import TableHeaderSortable from "../../Table/HeaderSortable";
import SendCommand from "../SendCommand";
const tableColumns = [
{
id: "vin",
label: "VIN",
},
{
id: "model",
label: "Model",
},
{
id: "year",
label: "Year",
},
{
id: "trim",
label: "Trim",
},
{
id: "created_at",
label: "Created",
},
{
id: "updated_at",
label: "Updated",
},
];
const MainForm = () => {
const classes = useStyles();
const [pageSize, setPageSize] = useState(25);
const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("vin");
const [order, setOrder] = useState("asc");
const [selected, setSelected] = useState([]);
const { getVehicles, vehicles, totalVehicles } = useVehicleContext();
const { setMessage, setTitle } = useStatusContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const sortHandler = (event, property) => {
if (property === orderBy) {
if (order === "asc") {
setOrder("desc");
} else {
setOrder("asc");
}
} else {
setOrderBy(property);
setOrder("asc");
}
};
useEffect(() => {
setTitle("Send Command");
// eslint-disable-next-line
}, []);
useEffect(() => {
try {
getVehicles(
{
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
},
token
);
} catch (e) {
setMessage(e.message);
}
// eslint-disable-next-line
}, [pageIndex, pageSize, token, orderBy, order]);
const handleChangePageIndex = (event, newIndex) => {
setPageIndex(newIndex);
};
const handleChangePageSize = (event) => {
setPageSize(parseInt(event.target.value, 10));
setPageIndex(0);
};
const handleSelectAll = (event) => {
const newSelected = [];
if (event.target.checked) {
vehicles.forEach((car) => {
newSelected.push(car.vin);
});
}
setSelected(newSelected);
};
const handleSelect = (event, key) => {
let newSelected;
if (event.target.checked) {
newSelected = [...selected];
newSelected.push(key);
} else {
newSelected = selected.filter((vin) => vin !== key);
}
setSelected(newSelected);
};
return (
<div className={classes.paper} style={{ height: 700, width: "100%" }}>
<TableContainer>
<Table>
<TableHeaderSortable
classes={classes}
orderBy={orderBy}
order={order}
columnData={tableColumns}
onSortRequest={sortHandler}
multiSelect={true}
onSelectAll={handleSelectAll}
selectCount={selected.length}
rowCount={totalVehicles}
/>
<TableBody>
{vehicles.map((row) => {
const isSelected = selected.indexOf(row.vin) !== -1;
return (
<TableRow key={row.vin}>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
onChange={(event) => handleSelect(event, row.vin)}
/>
</TableCell>
<TableCell align="center" sortDirection={true}>
<Link to={`/vehicle-status/${row.vin}`}>{row.vin}</Link>
</TableCell>
<TableCell align="center">{row.model}</TableCell>
<TableCell align="center">{row.year}</TableCell>
<TableCell align="center">{row.trim || ""}</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={7}
count={totalVehicles}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onChangePage={handleChangePageIndex}
onChangeRowsPerPage={handleChangePageSize}
/>
</TableRow>
</TableFooter>
</Table>
</TableContainer>
<SendCommand vins={selected} />
</div>
);
};
const VehiclesList = () => (
<VehicleProvider>
<MainForm />
</VehicleProvider>
);
export default VehiclesList;

View File

@@ -7,10 +7,8 @@ import {
TableCell,
TableContainer,
TableFooter,
TableHead,
TablePagination,
TableRow,
Typography,
} from "@material-ui/core";
import {
@@ -23,22 +21,51 @@ import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import { LocalDateTimeString } from "../../../utils/dates";
import TableHeaderSortable from "../../Table/HeaderSortable";
import SendCommand from "../SendCommand";
import LogFilter from "../LogFilter";
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 = () => {
const { vin } = useParams();
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();
const { setMessage, setTitle } = useStatusContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
useEffect(() => {
setTitle(`Vehicle ${vin} Details`);
// eslint-disable-next-line
}, [vin]);
useEffect(() => {
try {
getCarUpdates(
@@ -46,6 +73,7 @@ const MainForm = () => {
vin,
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
},
token
);
@@ -53,7 +81,7 @@ const MainForm = () => {
setMessage(e.message);
}
// eslint-disable-next-line
}, [pageIndex, pageSize, token]);
}, [pageIndex, pageSize, token, orderBy, order]);
const handleChangePageIndex = (event, newIndex) => {
setPageIndex(newIndex);
@@ -64,22 +92,30 @@ const MainForm = () => {
setPageIndex(0);
};
const handleSort = (event, property) => {
if (property === orderBy) {
if (order === "asc") {
setOrder("desc");
} else {
setOrder("asc");
}
} else {
setOrderBy(property);
setOrder("asc");
}
};
return (
<div className={classes.paper} style={{ height: 700, width: "100%" }}>
<Typography component="h1" variant="h5">
{vin} Updates
</Typography>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell align="center">ID</TableCell>
<TableCell align="center">Update</TableCell>
<TableCell align="center">Status</TableCell>
<TableCell align="center">Created</TableCell>
<TableCell align="center">Updated</TableCell>
</TableRow>
</TableHead>
<TableHeaderSortable
classes={classes}
orderBy={orderBy}
order={order}
columnData={tableColumns}
onSortRequest={handleSort}
/>
<TableBody>
{carUpdates.map((row) => (
<TableRow key={row.id}>
@@ -98,7 +134,7 @@ const MainForm = () => {
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={5}
count={totalCarUpdates}
rowsPerPage={pageSize}
@@ -116,10 +152,7 @@ const MainForm = () => {
</TableContainer>
<Grid container className={classes.root} spacing={2}>
<Grid item lg={6} md={12}>
<SendCommand vin={vin} />
</Grid>
<Grid item lg={6} md={12} style={{ textAlign: "right" }}>
<LogFilter vin={vin} />
<SendCommand vins={[vin]} />
</Grid>
</Grid>
</div>