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:
1
src/assets/fisker-badge.svg
Normal file
1
src/assets/fisker-badge.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 48 KiB |
@@ -99,37 +99,37 @@ describe("App", () => {
|
||||
|
||||
it("Route /package-upload authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/package-upload", "h1", "Create Update Package");
|
||||
await check("/package-upload", "h6", "Create Update Package");
|
||||
});
|
||||
|
||||
it("Route /vehicle-add authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/vehicle-add", "h1", "Add Vehicle");
|
||||
await check("/vehicle-add", "h6", "Add Vehicle");
|
||||
});
|
||||
|
||||
it("Route /updates authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/updates", "h1", "Update Packages");
|
||||
await check("/updates", "h6", "Deploy Packages");
|
||||
});
|
||||
|
||||
it("Route /update authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/update/1", "h1", "Edit Update Package 1");
|
||||
await check("/update/1", "h6", "Edit Update Package 1");
|
||||
});
|
||||
|
||||
it("Route /carupdate-status authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/carupdate-status/1", "h1", "");
|
||||
await check("/carupdate-status/1", "h6", "");
|
||||
});
|
||||
|
||||
it("Route /vehicles authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/vehicles", "h1", "Vehicles");
|
||||
await check("/vehicles", "h6", "Vehicles");
|
||||
});
|
||||
|
||||
it("Route /vehicle-status authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/vehicle-status/FISKER123", "h1", "FISKER123 Updates");
|
||||
await check("/vehicle-status/FISKER123", "h6", "Vehicle FISKER123 Details");
|
||||
});
|
||||
|
||||
it("Route /page-not-found unauthenticated", async () => {
|
||||
@@ -143,6 +143,6 @@ describe("App", () => {
|
||||
|
||||
it("Route /carupdate-deploy authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
await check("/carupdate-deploy/1", "h1", "Deploy [1]");
|
||||
await check("/carupdate-deploy/1", "h6", "Deploy ");
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, Redirect } from "react-router";
|
||||
import { Button, TextField, Typography } from "@material-ui/core";
|
||||
|
||||
import { Button, Typography } from "@material-ui/core";
|
||||
import {
|
||||
UpdatesProvider,
|
||||
useUpdatesContext,
|
||||
@@ -20,12 +19,10 @@ const MainForm = () => {
|
||||
idToken: { jwtToken: token },
|
||||
},
|
||||
} = useUserContext();
|
||||
const { setMessage } = useStatusContext();
|
||||
const { setMessage, setTitle } = useStatusContext();
|
||||
const [packageName, setPackageName] = useState("");
|
||||
const [version, setVersion] = useState("");
|
||||
const [link, setLink] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [releaseNotesLink, setReleaseNotesLink] = useState("");
|
||||
const [createDate, setCreateDate] = useState("");
|
||||
const [selectedVehicles, setSelectedVehicles] = useState([]);
|
||||
const [redirect, setRedirect] = useState("");
|
||||
@@ -60,15 +57,18 @@ const MainForm = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(`Deploy ${packageName} ${version}`);
|
||||
// eslint-disable-next-line
|
||||
}, [packageName, version]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!packages || packages.length === 0) return;
|
||||
var data = packages[0];
|
||||
|
||||
setPackageName(data.package_name);
|
||||
setVersion(data.version);
|
||||
setLink(data.link);
|
||||
setDescription(data.desc || "");
|
||||
setReleaseNotesLink(data.release_notes || "");
|
||||
setCreateDate(tsLocalDateTimeString(data.timestamp));
|
||||
}, [packages]);
|
||||
|
||||
@@ -78,64 +78,11 @@ const MainForm = () => {
|
||||
|
||||
return (
|
||||
<div className={classes.paper}>
|
||||
<Typography component="h1" variant="h5">
|
||||
Deploy {`${packageName} ${version} [${packageid}]`}
|
||||
</Typography>
|
||||
<form className={classes.form} noValidate action="{onSubmit}">
|
||||
<TextField
|
||||
label="Create Date"
|
||||
variant="filled"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
readOnly: true,
|
||||
}}
|
||||
value={createDate}
|
||||
size="small"
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
id="link"
|
||||
name="link"
|
||||
label="Package link"
|
||||
variant="filled"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
readOnly: true,
|
||||
}}
|
||||
value={link}
|
||||
size="small"
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
id="description"
|
||||
name="description"
|
||||
label="Description"
|
||||
variant="filled"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
readOnly: true,
|
||||
}}
|
||||
value={description}
|
||||
size="small"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
placeholder="Package description"
|
||||
/>
|
||||
<TextField
|
||||
id="releasenotes"
|
||||
name="releasenotes"
|
||||
label="Release Notes URL"
|
||||
variant="filled"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
readOnly: true,
|
||||
}}
|
||||
value={releaseNotesLink}
|
||||
size="small"
|
||||
fullWidth
|
||||
placeholder="Release Notes URL"
|
||||
/>
|
||||
<Typography variant="body2">
|
||||
Created {createDate}. {description}
|
||||
</Typography>
|
||||
<hr style={{ marginBottom: 30, marginTop: 30 }} />
|
||||
<CarSelection onSelection={setSelectedVehicles} />
|
||||
<Button
|
||||
type="submit"
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
TableHead,
|
||||
TablePagination,
|
||||
TableRow,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import {
|
||||
@@ -38,7 +37,7 @@ const MainForm = () => {
|
||||
startMonitor,
|
||||
stopMonitor,
|
||||
} = useUpdatesContext();
|
||||
const { setMessage } = useStatusContext();
|
||||
const { setMessage, setTitle } = useStatusContext();
|
||||
const {
|
||||
token: {
|
||||
idToken: { jwtToken: token },
|
||||
@@ -54,6 +53,13 @@ const MainForm = () => {
|
||||
// eslint-disable-next-line
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!packages || packages.length === 0) return;
|
||||
setTitle(`Package ${packages[0].package_name} ${packages[0].version}`);
|
||||
|
||||
// eslint-disable-next-line
|
||||
}, [packages]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
stopMonitor();
|
||||
@@ -104,11 +110,6 @@ const MainForm = () => {
|
||||
|
||||
return (
|
||||
<div className={classes.paper} style={{ height: 700, width: "100%" }}>
|
||||
<Typography component="h1" variant="h5">
|
||||
{packages &&
|
||||
packages.length > 0 &&
|
||||
`${packages[0].package_name} ${packages[0].version}`}
|
||||
</Typography>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
@@ -150,7 +151,7 @@ const MainForm = () => {
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
rowsPerPageOptions={[5, 10, 25, 100]}
|
||||
colSpan={5}
|
||||
count={totalCarUpdates}
|
||||
rowsPerPage={pageSize}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
203
src/components/Cars/SendCommandBulk/index.jsx
Normal file
203
src/components/Cars/SendCommandBulk/index.jsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -4,12 +4,15 @@ const StatusContext = React.createContext();
|
||||
|
||||
export const StatusProvider = ({ children }) => {
|
||||
const [message, setMessage] = useState(null);
|
||||
const [title, setTitle] = useState("");
|
||||
|
||||
return (
|
||||
<StatusContext.Provider
|
||||
value={{
|
||||
message,
|
||||
setMessage,
|
||||
title,
|
||||
setTitle,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -84,10 +84,10 @@ export const VehicleProvider = ({ children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const sendCommand = async (vin, command, token) => {
|
||||
const sendCommand = async (vins, command, parameters, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
const result = await api.sendCommand(vin, command, token);
|
||||
const result = await api.sendCommand(vins, command, parameters, token);
|
||||
if (result.error)
|
||||
throw new Error(`Send command error. ${result.message}`);
|
||||
return result;
|
||||
@@ -96,18 +96,6 @@ export const VehicleProvider = ({ children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const sendLogFilter = async (vin, filter, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
const result = await api.sendLog(vin, filter, token);
|
||||
if (result.error)
|
||||
throw new Error(`Send log filter error. ${result.message}`);
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<VehicleContext.Provider
|
||||
value={{
|
||||
@@ -121,7 +109,6 @@ export const VehicleProvider = ({ children }) => {
|
||||
getModels,
|
||||
getYears,
|
||||
sendCommand,
|
||||
sendLogFilter,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -25,13 +25,10 @@ export const useVehicleContext = () => ({
|
||||
getYears: jest.fn(() => {
|
||||
years = [2023, 2024];
|
||||
}),
|
||||
sendCommand: jest.fn((vin, command, token) => ({
|
||||
sendCommand: jest.fn((vin, command, parameters, token) => ({
|
||||
vin,
|
||||
command,
|
||||
})),
|
||||
sendLogFilter: jest.fn((vin, filter, token) => ({
|
||||
vin,
|
||||
filter,
|
||||
parameters,
|
||||
})),
|
||||
});
|
||||
|
||||
|
||||
49
src/components/Controls/SearchField/index.jsx
Normal file
49
src/components/Controls/SearchField/index.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
} from "@material-ui/core";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
import clsx from "clsx";
|
||||
|
||||
const SearchField = (props) => {
|
||||
const { classes, onSearch } = props;
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const handleChange = (e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
};
|
||||
const handleSearch = (e) => {
|
||||
if (!onSearch) return;
|
||||
onSearch(searchTerm);
|
||||
};
|
||||
const handleEnterPress = (e) => {
|
||||
if (e.keyCode !== 13) return;
|
||||
e.preventDefault();
|
||||
handleSearch(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormControl className={clsx(classes.margin, classes.textField)}>
|
||||
<InputLabel htmlFor="search">Search</InputLabel>
|
||||
<Input
|
||||
id="search"
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleEnterPress}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton aria-label="search" onClick={handleSearch}>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchField;
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { Typography } from "@material-ui/core";
|
||||
import useStyles from "../useStyles";
|
||||
|
||||
import { useUserContext } from "../Contexts/UserContext";
|
||||
import { useStatusContext } from "../Contexts/StatusContext";
|
||||
import { parsePayload } from "../../utils/jwt";
|
||||
|
||||
const DEFAULT_GREETING = "Welcome";
|
||||
@@ -22,6 +23,12 @@ const Home = () => {
|
||||
const classes = useStyles();
|
||||
const { token } = useUserContext();
|
||||
const greeting = getGreeting(token);
|
||||
const { setTitle } = useStatusContext();
|
||||
|
||||
useEffect(() => {
|
||||
setTitle("");
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={classes.paper}>
|
||||
|
||||
@@ -1,34 +1,22 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import { useTheme } from "@material-ui/core/styles";
|
||||
import Drawer from "@material-ui/core/Drawer";
|
||||
import AppBar from "@material-ui/core/AppBar";
|
||||
import Toolbar from "@material-ui/core/Toolbar";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Divider from "@material-ui/core/Divider";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
import MenuIcon from "@material-ui/icons/Menu";
|
||||
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
|
||||
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
|
||||
|
||||
import SideMenu from "./SideMenu";
|
||||
import useStyles from "../useStyles";
|
||||
import { useUserContext } from "../Contexts/UserContext";
|
||||
import { useStatusContext } from "../Contexts/StatusContext";
|
||||
import { Button, Container } from "@material-ui/core";
|
||||
import logo from "../../assets/fisker-badge.svg";
|
||||
|
||||
export default function MenuDrawer({ children }) {
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
const { title } = useStatusContext();
|
||||
const { signOut, token } = useUserContext();
|
||||
const [open, setOpen] = React.useState(true);
|
||||
|
||||
const handleDrawerOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleDrawerClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const onSignOut = () => {
|
||||
document.location = signOut();
|
||||
@@ -39,26 +27,12 @@ export default function MenuDrawer({ children }) {
|
||||
<AppBar
|
||||
position="fixed"
|
||||
className={clsx(classes.appBar, {
|
||||
[classes.appBarShift]: open && token !== null,
|
||||
[classes.appBarShift]: token !== null,
|
||||
})}
|
||||
>
|
||||
<Toolbar>
|
||||
{token !== null && (
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
onClick={handleDrawerOpen}
|
||||
edge="start"
|
||||
className={clsx(
|
||||
classes.menuButton,
|
||||
open && classes.hide && token !== null
|
||||
)}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<Typography variant="h6" noWrap>
|
||||
Fisker OTA Portal
|
||||
{title}
|
||||
</Typography>
|
||||
{token !== null && (
|
||||
<Button
|
||||
@@ -76,19 +50,17 @@ export default function MenuDrawer({ children }) {
|
||||
className={classes.drawer}
|
||||
variant="persistent"
|
||||
anchor="left"
|
||||
open={open}
|
||||
open={true}
|
||||
classes={{
|
||||
paper: classes.drawerPaper,
|
||||
}}
|
||||
>
|
||||
<div className={classes.drawerHeader}>
|
||||
<IconButton onClick={handleDrawerClose}>
|
||||
{theme.direction === "ltr" ? (
|
||||
<ChevronLeftIcon />
|
||||
) : (
|
||||
<ChevronRightIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
<img
|
||||
src={logo}
|
||||
alt="Fisker Admin Portal"
|
||||
className={classes.logo}
|
||||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
<SideMenu />
|
||||
@@ -96,7 +68,7 @@ export default function MenuDrawer({ children }) {
|
||||
)}
|
||||
<main
|
||||
className={clsx(classes.content, {
|
||||
[classes.contentShift]: open && token !== null,
|
||||
[classes.contentShift]: token !== null,
|
||||
})}
|
||||
>
|
||||
<div className={classes.drawerHeader} />
|
||||
|
||||
@@ -11,7 +11,7 @@ const menuData = [
|
||||
roles: [],
|
||||
},
|
||||
{
|
||||
label: "View Packages",
|
||||
label: "Deploy Packages",
|
||||
to: "/updates",
|
||||
roles: [Roles.CREATE, Roles.READ],
|
||||
},
|
||||
@@ -30,6 +30,11 @@ const menuData = [
|
||||
to: "/vehicle-add",
|
||||
roles: [Roles.CREATE],
|
||||
},
|
||||
{
|
||||
label: "Send Command",
|
||||
to: "/vehicles-command",
|
||||
roles: [Roles.CREATE],
|
||||
},
|
||||
];
|
||||
|
||||
export default function SideMenu() {
|
||||
|
||||
@@ -44,7 +44,7 @@ exports[`SideMenu Authenticated 1`] = `
|
||||
<span
|
||||
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
View Packages
|
||||
Deploy Packages
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
@@ -118,6 +118,28 @@ exports[`SideMenu Authenticated 1`] = `
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
||||
href="/vehicles-command"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
Send Command
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ const CarUpdatesDeploy = React.lazy(() => import("../CarUpdates/Deploy"));
|
||||
const CarUpdatesStatus = React.lazy(() => import("../CarUpdates/Status"));
|
||||
const CarUpdates = React.lazy(() => import("../Cars/Status"));
|
||||
const VehiclesList = React.lazy(() => import("../Cars/List"));
|
||||
const SendCommandBulk = React.lazy(() => import("../Cars/SendCommandBulk"));
|
||||
|
||||
const SiteRoutes = () => {
|
||||
const { token, groups } = useUserContext();
|
||||
@@ -101,6 +102,14 @@ const SiteRoutes = () => {
|
||||
groups={groups}
|
||||
roles={[Roles.READ, Roles.CREATE]}
|
||||
/>
|
||||
<AuthRoute
|
||||
path="/vehicles-command"
|
||||
render={() => <SendCommandBulk />}
|
||||
type={TYPES.PROTECTED}
|
||||
token={token}
|
||||
groups={groups}
|
||||
roles={[Roles.CREATE]}
|
||||
/>
|
||||
<PageNotFound />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
|
||||
101
src/components/Table/HeaderSortable/index.jsx
Normal file
101
src/components/Table/HeaderSortable/index.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
Checkbox,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableSortLabel,
|
||||
} from "@material-ui/core";
|
||||
|
||||
const HeaderSortable = (props) => {
|
||||
const {
|
||||
classes,
|
||||
order,
|
||||
orderBy,
|
||||
onSortRequest,
|
||||
columnData,
|
||||
multiSelect,
|
||||
onSelectAll,
|
||||
selectCount,
|
||||
rowCount,
|
||||
} = props;
|
||||
const sortHandler = (property) => (event) => {
|
||||
if (!onSortRequest) return;
|
||||
onSortRequest(event, property);
|
||||
};
|
||||
const selectAllHandler = (event) => {
|
||||
if (!onSelectAll) return;
|
||||
onSelectAll(event);
|
||||
};
|
||||
const ColumnLabel = (column) => {
|
||||
if (column.id) {
|
||||
return (
|
||||
<TableSortLabel
|
||||
active={orderBy === column.id}
|
||||
direction={orderBy === column.id ? order : "asc"}
|
||||
onClick={sortHandler(column.id)}
|
||||
>
|
||||
{column.label}
|
||||
{orderBy === column.id ? (
|
||||
<span className={classes.hiddenSortSpan}>
|
||||
{order === "desc" ? "sorted descending" : "sorted ascending"}
|
||||
</span>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
);
|
||||
}
|
||||
|
||||
return column.label;
|
||||
};
|
||||
|
||||
if (multiSelect) {
|
||||
const errors = [];
|
||||
if (onSelectAll === undefined) errors.push("onSelectAll required");
|
||||
if (selectCount === undefined) errors.push("selectCount required");
|
||||
if (rowCount === undefined) errors.push("rowCount required");
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join(". "));
|
||||
}
|
||||
}
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{multiSelect && (
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
indeterminate={selectCount > 0 && selectCount < rowCount}
|
||||
checked={rowCount > 0 && selectCount === rowCount}
|
||||
onChange={selectAllHandler}
|
||||
inputProps={{ "aria-label": "select all desserts" }}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
{columnData.map((column) => (
|
||||
<TableCell
|
||||
key={column.id}
|
||||
align={column.numeric ? "right" : "center"}
|
||||
padding={column.disablePadding ? "none" : "default"}
|
||||
sortDirection={orderBy === column.id ? order : false}
|
||||
>
|
||||
{ColumnLabel(column)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
};
|
||||
|
||||
HeaderSortable.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
onSortRequest: PropTypes.func.isRequired,
|
||||
order: PropTypes.oneOf(["asc", "desc"]).isRequired,
|
||||
orderBy: PropTypes.string.isRequired,
|
||||
columnData: PropTypes.array.isRequired,
|
||||
multiSelect: PropTypes.bool,
|
||||
selectCount: PropTypes.number,
|
||||
totalRows: PropTypes.number,
|
||||
onSelectAll: PropTypes.func,
|
||||
};
|
||||
|
||||
export default HeaderSortable;
|
||||
@@ -8,11 +8,6 @@ exports[`File Upload Form Should render 1`] = `
|
||||
<div
|
||||
class="makeStyles-paper-3"
|
||||
>
|
||||
<h1
|
||||
class="MuiTypography-root MuiTypography-h5"
|
||||
>
|
||||
Create Update Package
|
||||
</h1>
|
||||
<form
|
||||
action="{onSubmit}"
|
||||
class="makeStyles-form-5"
|
||||
@@ -51,10 +46,10 @@ exports[`File Upload Form Should render 1`] = `
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-26 MuiOutlinedInput-notchedOutline"
|
||||
class="PrivateNotchedOutline-root-31 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-28"
|
||||
class="PrivateNotchedOutline-legendLabelled-33"
|
||||
>
|
||||
<span>
|
||||
Package name
|
||||
@@ -97,10 +92,10 @@ exports[`File Upload Form Should render 1`] = `
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-26 MuiOutlinedInput-notchedOutline"
|
||||
class="PrivateNotchedOutline-root-31 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-28"
|
||||
class="PrivateNotchedOutline-legendLabelled-33"
|
||||
>
|
||||
<span>
|
||||
Version
|
||||
@@ -143,10 +138,10 @@ exports[`File Upload Form Should render 1`] = `
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-26 MuiOutlinedInput-notchedOutline"
|
||||
class="PrivateNotchedOutline-root-31 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-28"
|
||||
class="PrivateNotchedOutline-legendLabelled-33"
|
||||
>
|
||||
<span>
|
||||
Description
|
||||
@@ -190,10 +185,10 @@ exports[`File Upload Form Should render 1`] = `
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-26 MuiOutlinedInput-notchedOutline"
|
||||
class="PrivateNotchedOutline-root-31 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-28"
|
||||
class="PrivateNotchedOutline-legendLabelled-33"
|
||||
>
|
||||
<span>
|
||||
Release Notes URL
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Button, TextField, Typography } from "@material-ui/core";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Button, TextField } from "@material-ui/core";
|
||||
import { DropzoneArea } from "material-ui-dropzone";
|
||||
import { useUserContext } from "../../Contexts/UserContext";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
@@ -42,13 +42,19 @@ const FileUploadZone = ({ classes, token }) => {
|
||||
const MainForm = () => {
|
||||
const { uploading, upload, files, cancel } = useFileUploadContext();
|
||||
const { token } = useUserContext();
|
||||
const { setMessage } = useStatusContext();
|
||||
const { setMessage, setTitle } = useStatusContext();
|
||||
const [redirect, setRedirect] = useState(null);
|
||||
const classes = useStyles();
|
||||
const packagenameEl = useRef(null);
|
||||
const versionEl = useRef(null);
|
||||
const descEl = useRef(null);
|
||||
const releasenotesEl = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle("Create Update Package");
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (event) => {
|
||||
try {
|
||||
event.preventDefault();
|
||||
@@ -79,9 +85,6 @@ const MainForm = () => {
|
||||
|
||||
return (
|
||||
<div className={classes.paper}>
|
||||
<Typography component="h1" variant="h5">
|
||||
Create Update Package
|
||||
</Typography>
|
||||
<form className={classes.form} noValidate action="{onSubmit}">
|
||||
<TextField
|
||||
id="packagename"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import { Button, TextField, Typography } from "@material-ui/core";
|
||||
import { Button, TextField } from "@material-ui/core";
|
||||
|
||||
import {
|
||||
UpdatesProvider,
|
||||
@@ -19,7 +19,7 @@ const MainForm = () => {
|
||||
idToken: { jwtToken: token },
|
||||
},
|
||||
} = useUserContext();
|
||||
const { setMessage } = useStatusContext();
|
||||
const { setMessage, setTitle } = useStatusContext();
|
||||
const [packageName, setPackageName] = useState("");
|
||||
const [version, setVersion] = useState("");
|
||||
const [link, setLink] = useState("");
|
||||
@@ -68,10 +68,16 @@ const MainForm = () => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(`Edit Update Package ${id}`);
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!packages || packages.length === 0) return;
|
||||
var data = packages[0];
|
||||
@@ -86,9 +92,6 @@ const MainForm = () => {
|
||||
|
||||
return (
|
||||
<div className={classes.paper}>
|
||||
<Typography component="h1" variant="h5">
|
||||
Edit Update Package {id}
|
||||
</Typography>
|
||||
<form className={classes.form} noValidate action="{onSubmit}">
|
||||
<TextField
|
||||
label="Create Date"
|
||||
|
||||
@@ -6,13 +6,11 @@ import {
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TablePagination,
|
||||
TableRow,
|
||||
Toolbar,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import EditIcon from "@material-ui/icons/Edit";
|
||||
import SendIcon from "@material-ui/icons/Send";
|
||||
import VisibilityIcon from "@material-ui/icons/Visibility";
|
||||
import useStyles from "../../useStyles";
|
||||
@@ -21,13 +19,42 @@ import {
|
||||
useUpdatesContext,
|
||||
} from "../../Contexts/UpdatesContext";
|
||||
import { useUserContext } from "../../Contexts/UserContext";
|
||||
import { tsLocalDateTimeString } from "../../../utils/dates";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import { LocalDateTimeString } from "../../../utils/dates";
|
||||
import { Roles, hasRole } from "../../../utils/roles";
|
||||
import TableHeaderSortable from "../../Table/HeaderSortable";
|
||||
import SearchField from "../../Controls/SearchField";
|
||||
|
||||
const tableColumns = [
|
||||
{
|
||||
id: "id",
|
||||
label: "ID",
|
||||
},
|
||||
{
|
||||
id: "package_name",
|
||||
label: "Name",
|
||||
},
|
||||
{
|
||||
id: "version",
|
||||
label: "Version",
|
||||
},
|
||||
{
|
||||
id: "created_at",
|
||||
label: "Created",
|
||||
},
|
||||
{
|
||||
id: "",
|
||||
label: "Actions",
|
||||
},
|
||||
];
|
||||
|
||||
const UpdatePackagesList = () => {
|
||||
const classes = useStyles();
|
||||
const [pageSize, setPageSize] = useState(25);
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [orderBy, setOrderBy] = useState("id");
|
||||
const [order, setOrder] = useState("desc");
|
||||
const [search, setSearch] = useState("");
|
||||
const { getPackages, packages, totalPackages } = useUpdatesContext();
|
||||
const {
|
||||
token: {
|
||||
@@ -35,11 +62,25 @@ const UpdatePackagesList = () => {
|
||||
},
|
||||
groups,
|
||||
} = useUserContext();
|
||||
const { setTitle } = useStatusContext();
|
||||
|
||||
useEffect(() => {
|
||||
getPackages({ limit: pageSize, offset: pageSize * pageIndex }, token);
|
||||
setTitle("Deploy Packages");
|
||||
// eslint-disable-next-line
|
||||
}, [pageIndex, pageSize, token]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getPackages(
|
||||
{
|
||||
limit: pageSize,
|
||||
offset: pageSize * pageIndex,
|
||||
order: `${orderBy} ${order}`,
|
||||
search,
|
||||
},
|
||||
token
|
||||
);
|
||||
// eslint-disable-next-line
|
||||
}, [pageIndex, pageSize, token, orderBy, order, search]);
|
||||
|
||||
const handleChangePageIndex = (event, newIndex) => {
|
||||
setPageIndex(newIndex);
|
||||
@@ -50,6 +91,23 @@ const UpdatePackagesList = () => {
|
||||
setPageIndex(0);
|
||||
};
|
||||
|
||||
const handleSort = (event, property) => {
|
||||
if (property === orderBy) {
|
||||
if (order === "asc") {
|
||||
setOrder("desc");
|
||||
} else {
|
||||
setOrder("asc");
|
||||
}
|
||||
} else {
|
||||
setOrderBy(property);
|
||||
setOrder("asc");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (search) => {
|
||||
setSearch(search);
|
||||
};
|
||||
|
||||
const Actions = (row) => {
|
||||
let actions = [];
|
||||
if (hasRole([Roles.CREATE, Roles.READ], groups)) {
|
||||
@@ -65,13 +123,6 @@ const UpdatePackagesList = () => {
|
||||
}
|
||||
if (hasRole([Roles.CREATE], groups)) {
|
||||
actions = actions.concat([
|
||||
{
|
||||
tip: `Edit "${row.package_name} ${row.version}"`,
|
||||
link: `/update/${row.id}`,
|
||||
icon: (
|
||||
<EditIcon aria-label={`Edit ${row.package_name} ${row.version}`} />
|
||||
),
|
||||
},
|
||||
{
|
||||
tip: `Deploy "${row.package_name} ${row.version}"`,
|
||||
link: `/carupdate-deploy/${row.id}`,
|
||||
@@ -97,20 +148,18 @@ const UpdatePackagesList = () => {
|
||||
|
||||
return (
|
||||
<div className={classes.paper} style={{ height: 700, width: "100%" }}>
|
||||
<Typography component="h1" variant="h5">
|
||||
Update Packages
|
||||
</Typography>
|
||||
<Toolbar className={classes.tableToolbar}>
|
||||
<SearchField classes={classes} onSearch={handleSearch} />
|
||||
</Toolbar>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell align="center">ID</TableCell>
|
||||
<TableCell align="center">Name</TableCell>
|
||||
<TableCell align="center">Version</TableCell>
|
||||
<TableCell align="center">Created</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableHeaderSortable
|
||||
classes={classes}
|
||||
orderBy={orderBy}
|
||||
order={order}
|
||||
columnData={tableColumns}
|
||||
onSortRequest={handleSort}
|
||||
/>
|
||||
<TableBody>
|
||||
{packages.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
@@ -118,16 +167,16 @@ const UpdatePackagesList = () => {
|
||||
<TableCell align="center">{row.package_name}</TableCell>
|
||||
<TableCell align="center">{row.version}</TableCell>
|
||||
<TableCell align="center">
|
||||
{tsLocalDateTimeString(row.timestamp)}
|
||||
{LocalDateTimeString(row.created)}
|
||||
</TableCell>
|
||||
<TableCell align="right">{Actions(row)}</TableCell>
|
||||
<TableCell align="center">{Actions(row)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
rowsPerPageOptions={[5, 10, 25, 100]}
|
||||
colSpan={5}
|
||||
count={totalPackages}
|
||||
rowsPerPage={pageSize}
|
||||
|
||||
@@ -17,7 +17,7 @@ const useStyles = makeStyles((theme) => ({
|
||||
padding: theme.spacing(2, 4, 3),
|
||||
},
|
||||
paper: {
|
||||
marginTop: theme.spacing(8),
|
||||
marginTop: theme.spacing(1),
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
@@ -105,7 +105,7 @@ const useStyles = makeStyles((theme) => ({
|
||||
padding: theme.spacing(0, 1),
|
||||
// necessary for content to be below app bar
|
||||
...theme.mixins.toolbar,
|
||||
justifyContent: "flex-end",
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
@@ -134,6 +134,34 @@ const useStyles = makeStyles((theme) => ({
|
||||
textDecorationLine: "underline",
|
||||
color: "Blue",
|
||||
},
|
||||
hiddenSortSpan: {
|
||||
border: 0,
|
||||
clip: "rect(0 0 0 0)",
|
||||
height: 1,
|
||||
margin: -1,
|
||||
overflow: "hidden",
|
||||
padding: 0,
|
||||
position: "absolute",
|
||||
top: 20,
|
||||
width: 1,
|
||||
},
|
||||
margin: {
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
textField: {
|
||||
width: "25ch",
|
||||
},
|
||||
tableToolbar: {
|
||||
textAlign: "left",
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(1),
|
||||
width: "100%",
|
||||
},
|
||||
logo: {
|
||||
height: 60,
|
||||
width: 60,
|
||||
margin: "auto",
|
||||
},
|
||||
}));
|
||||
|
||||
export default useStyles;
|
||||
|
||||
@@ -25,14 +25,11 @@ const vehiclesAPI = {
|
||||
data: [2021, 2022],
|
||||
};
|
||||
},
|
||||
sendCommand: async (vin, command, token) => {
|
||||
sendCommand: async (vin, command, parameters, token) => {
|
||||
return {
|
||||
vin, command,
|
||||
vin, command, parameters
|
||||
}
|
||||
},
|
||||
sendLog: async (vin, filter, token) => {
|
||||
return Object.assign(filter, {vin});
|
||||
},
|
||||
};
|
||||
|
||||
export default vehiclesAPI;
|
||||
|
||||
@@ -1,41 +1,68 @@
|
||||
const LockUnlockParams = [
|
||||
{
|
||||
value: "LOCK",
|
||||
label: "Lock",
|
||||
},
|
||||
{
|
||||
value: "UNLOCK",
|
||||
label: "Unlock",
|
||||
},
|
||||
];
|
||||
|
||||
const OpenCloseParams = [
|
||||
{
|
||||
value: "OPEN",
|
||||
label: "Open",
|
||||
},
|
||||
{
|
||||
value: "CLOSE",
|
||||
label: "Close",
|
||||
},
|
||||
];
|
||||
|
||||
const Commands = [{
|
||||
value: "LFRD",
|
||||
label: "Lock front right door",
|
||||
value: "LOG",
|
||||
label: "Log level",
|
||||
parameters: [
|
||||
{
|
||||
value: "INFO",
|
||||
label: "Info",
|
||||
},
|
||||
{
|
||||
value: "DEBUG",
|
||||
label: "Debug",
|
||||
},
|
||||
{
|
||||
value: "TRACE",
|
||||
label: "Trace",
|
||||
},
|
||||
],
|
||||
},{
|
||||
value: "UFRD",
|
||||
label: "Unlock front right door",
|
||||
value: "FRONT-RIGHT",
|
||||
label: "Front right door",
|
||||
parameters: LockUnlockParams,
|
||||
},{
|
||||
value: "LFLD",
|
||||
label: "Lock front left door",
|
||||
value: "FRONT-LEFT",
|
||||
label: "Front left door",
|
||||
parameters: LockUnlockParams,
|
||||
},{
|
||||
value: "UFLD",
|
||||
label: "Unlock front left door",
|
||||
value: "REAR-RIGHT",
|
||||
label: "Rear right door",
|
||||
parameters: LockUnlockParams,
|
||||
},{
|
||||
value: "LRRD",
|
||||
label: "Lock rear right door",
|
||||
value: "REAR-LEFT",
|
||||
label: "Rear left door",
|
||||
parameters: LockUnlockParams,
|
||||
},{
|
||||
value: "URRD",
|
||||
label: "Unlock rear right door",
|
||||
value: "TRUNK",
|
||||
label: "Trunk",
|
||||
parameters: LockUnlockParams,
|
||||
},{
|
||||
value: "LRLD",
|
||||
label: "Lock rear left door",
|
||||
value: "WINDOWS",
|
||||
label: "Windows",
|
||||
parameters: OpenCloseParams,
|
||||
},{
|
||||
value: "URLD",
|
||||
label: "Unlock rear left door",
|
||||
},{
|
||||
value: "LTRK",
|
||||
label: "Lock trunk",
|
||||
},{
|
||||
value: "UTRK",
|
||||
label: "Unlock trunk",
|
||||
},{
|
||||
value: "OWIN",
|
||||
label: "Open Windows",
|
||||
},{
|
||||
value: "CWIN",
|
||||
label: "Close Windows",
|
||||
},{
|
||||
value: "FLASH",
|
||||
value: "FLASH-HEADLIGHTS",
|
||||
label: "Flash headlights",
|
||||
}];
|
||||
|
||||
|
||||
@@ -31,22 +31,14 @@ const vehiclesAPI = {
|
||||
})
|
||||
.then(fetchRespHandler),
|
||||
|
||||
sendCommand: async (vin, command, token) => fetch(`${API_ENDPOINT}/vehiclecommand`, {
|
||||
sendCommand: async (vins, command, parameters, token) => fetch(`${API_ENDPOINT}/vehiclecommand`, {
|
||||
method: "POST",
|
||||
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
||||
body: JSON.stringify({
|
||||
vin, command,
|
||||
vins, command, parameters
|
||||
}),
|
||||
})
|
||||
.then(fetchRespHandler),
|
||||
|
||||
sendLog: async (vin, filter, token) => fetch(`${API_ENDPOINT}/vehiclelog`, {
|
||||
method: "POST",
|
||||
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
||||
body: JSON.stringify(Object.assign(filter, {vin})),
|
||||
})
|
||||
.then(fetchRespHandler),
|
||||
|
||||
};
|
||||
|
||||
export default vehiclesAPI;
|
||||
|
||||
Reference in New Issue
Block a user