Merge CEC-394 Car update log (#82)

This commit is contained in:
John Wu
2021-08-26 15:03:45 -07:00
committed by GitHub
parent d1815e2ff9
commit 74eb2707a3
34 changed files with 3114 additions and 3583 deletions

View File

@@ -1,6 +1,5 @@
jest.mock("../Contexts/FileUploadContext");
jest.mock("../Contexts/VehicleContext");
jest.mock("../Contexts/UpdatesContext");
jest.mock("../Contexts/UserContext");
jest.mock("../Contexts/ManifestsContext");
jest.mock("../Contexts/CarUpdatesContext");
@@ -65,26 +64,6 @@ describe("App", () => {
await sleepAndCheck("/home", "span.MuiButton-label", "Sign In");
});
it("Route /vehicle-add unauthenticated", async () => {
await check("/vehicle-add", "span.MuiButton-label", "Sign In");
});
it("Route /carupdate-deploy unauthenticated", async () => {
await check("/carupdate-deploy/1", "span.MuiButton-label", "Sign In");
});
it("Route /carupdate-status unauthenticated", async () => {
await check("/carupdate-status/1", "span.MuiButton-label", "Sign In");
});
it("Route /vehicles unauthenticated", async () => {
await check("/vehicles", "span.MuiButton-label", "Sign In");
});
it("Route /vehicle-status unauthenticated", async () => {
await check("/vehicle-status/FISKER123", "span.MuiButton-label", "Sign In");
});
it("Route /datascope unauthenticated", async () => {
await sleepAndCheck("/datascope", "span.MuiButton-label", "Sign In");
});
@@ -109,6 +88,25 @@ describe("App", () => {
await check("/package-create", "span.MuiButton-label", "Sign In");
});
it("Route /vehicle-add unauthenticated", async () => {
await check("/vehicle-add", "span.MuiButton-label", "Sign In");
});
it("Route /vehicles unauthenticated", async () => {
await check("/vehicles", "span.MuiButton-label", "Sign In");
});
it("Route /vehicle-status unauthenticated", async () => {
await check("/vehicle-status/FISKER123", "span.MuiButton-label", "Sign In");
});
it("Route /vehicle-status/vin/carupdateid unauthenticated", async () => {
await check("/vehicle-status/1G1FP87S3GN100062/283", "span.MuiButton-label", "Sign In");
});
it("Route /page-not-found unauthenticated", async () => {
await check("/page-not-found", "h1", "Page Not Found");
});
it("Route / authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await sleepAndCheck("/", "h6", "Home");
@@ -119,40 +117,11 @@ describe("App", () => {
await sleepAndCheck("/home", "h6", "Home");
});
it("Route /vehicle-add authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/vehicle-add", "h6", "Add Vehicle");
});
it("Route /carupdate-status authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/carupdate-status/1", "h6", "Package Package 1.0");
});
it("Route /vehicles authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/vehicles", "h6", "Vehicles");
});
it("Route /vehicle-status authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/vehicle-status/FISKER123", "h6", "Vehicle FISKER123 Details");
});
it("Route /page-not-found unauthenticated", async () => {
await check("/page-not-found", "h1", "Page Not Found");
});
it("Route /page-not-found authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/page-not-found", "h1", "Page Not Found");
});
it("Route /carupdate-deploy authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/carupdate-deploy/1", "h6", "Deploy Package 1.0");
});
it("Route /datascope authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await sleepAndCheck("/datascope", "h6", "Datascope");
@@ -182,4 +151,24 @@ describe("App", () => {
setToken(TEST_AUTH_OBJECT);
await check("/package-create", "h6", "Create Deployments");
});
it("Route /vehicle-add authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/vehicle-add", "h6", "Add Vehicle");
});
it("Route /vehicles authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/vehicles", "h6", "Vehicles");
});
it("Route /vehicle-status authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/vehicle-status/FISKER123", "h6", "Vehicle FISKER123 Details");
});
it("Route /vehicle-status/vin/carupdateid authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await sleepAndCheck("/vehicle-status/1G1FP87S3GN100062/283", "h6", "Vehicle 1G1FP87S3GN100062, Update TEST UPDATE");
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,158 +0,0 @@
import React, { useEffect, useState } from "react";
import { useParams, Redirect } from "react-router";
import { Button, Grid, Typography } from "@material-ui/core";
import {
UpdatesProvider,
useUpdatesContext,
} from "../../Contexts/UpdatesContext";
import { VehicleProvider } from "../../Contexts/VehicleContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import { tsLocalDateTimeString } from "../../../utils/dates";
import SearchField from "../../Controls/SearchField";
import CarSelectionTable from "../../Cars/CarSelectionTable";
import { logger } from "../../../services/monitoring";
const MainForm = () => {
const { packageid } = useParams();
const { getPackages, createCarUpdates, packages, busy } = useUpdatesContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const { setMessage, setTitle } = useStatusContext();
const [packageName, setPackageName] = useState("");
const [version, setVersion] = useState("");
const [description, setDescription] = useState("");
const [createDate, setCreateDate] = useState("");
const [selected, setSelected] = useState([]);
const [search, setSearch] = useState("");
const [redirect, setRedirect] = useState("");
const classes = useStyles();
const handleSearch = (search) => {
setSelected([]);
setSearch(search);
};
const handleSelectAll = (cars) => {
setSelected(cars);
};
const handleSelect = (event, key) => {
try {
let newSelected;
if (event.target.checked) {
newSelected = [...selected];
newSelected.push(key);
} else {
newSelected = selected.filter((vin) => vin !== key);
}
setSelected(newSelected);
} catch (e) {
logger.warn(e.stack);
}
};
const onSubmit = async (event) => {
try {
event.preventDefault();
const data = {
package_id: parseInt(packageid),
vins: selected,
};
await createCarUpdates(data, token);
setMessage(
`Deployed ${packageName} ${version} to ${selected.length} cars`
);
setRedirect(`/carupdate-status/${packageid}`);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
const getData = async () => {
try {
getPackages({ id: parseInt(packageid) }, token);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
useEffect(() => {
getData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
useEffect(() => {
setTitle(`Deploy ${packageName} ${version}`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [packageName, version]);
useEffect(() => {
if (!packages || packages.length === 0) return;
var data = packages[0];
setPackageName(data.package_name);
setVersion(data.version);
setDescription(data.desc || "");
setCreateDate(tsLocalDateTimeString(data.timestamp));
}, [packages]);
if (redirect.length > 0) {
return <Redirect to={redirect} />;
}
return (
<div className={classes.paper}>
<form className={classes.form} noValidate action="{onSubmit}">
<Typography variant="body2">
Created {createDate}. {description || "No description"}
</Typography>
<Grid container className={classes.root} spacing={2}>
<Grid item md={10}>
<SearchField classes={classes} onSearch={handleSearch} />
<div
className={classes.labelInline}
>{`${selected.length} Selected`}</div>
</Grid>
<Grid item md={2} style={{ textAlign: "right" }}>
<Button
type="submit"
disabled={busy || selected.length === 0}
fullWidth
variant="contained"
color="primary"
className={classes.formControl}
onClick={onSubmit}
>
{busy ? "Deploying..." : "Deploy"}
</Button>
</Grid>
</Grid>
<CarSelectionTable
classes={classes}
token={token}
search={{ search }}
selected={selected}
onSelect={handleSelect}
onSelectAll={handleSelectAll}
/>
</form>
</div>
);
};
const UpdatePackageDeployForm = () => (
<VehicleProvider>
<UpdatesProvider>
<MainForm />
</UpdatesProvider>
</VehicleProvider>
);
export default UpdatePackageDeployForm;

View File

@@ -1,170 +0,0 @@
import React, { useEffect, useState } from "react";
import { useParams } from "react-router";
import { Link } from "react-router-dom";
import {
LinearProgress,
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TablePagination,
TableRow,
} from "@material-ui/core";
import clsx from "clsx";
import {
UpdatesProvider,
useUpdatesContext,
} from "../../Contexts/UpdatesContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import { LocalDateTimeString } from "../../../utils/dates";
import { logger } from "../../../services/monitoring";
const MainForm = () => {
const { packageid } = useParams();
const classes = useStyles();
const [pageSize, setPageSize] = useState(10);
const [pageIndex, setPageIndex] = useState(0);
const {
getCarUpdates,
carUpdates,
totalCarUpdates,
getPackages,
packages,
startMonitor,
stopMonitor,
} = useUpdatesContext();
const { setMessage, setTitle } = useStatusContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
useEffect(() => {
(async () => {
try {
await getPackages({ id: packageid }, token);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
useEffect(() => {
if (!packages || packages.length === 0) return;
setTitle(`Package ${packages[0].package_name} ${packages[0].version}`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [packages]);
useEffect(() => {
(async () => {
try {
stopMonitor();
await getCarUpdates(
{
packageid,
limit: pageSize,
offset: pageSize * pageIndex,
},
token
);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pageIndex, pageSize, token]);
useEffect(() => {
try {
if (carUpdates.length === 0) return;
startMonitor(token);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
return () => {
stopMonitor();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [carUpdates]);
const handleChangePageIndex = (event, newIndex) => {
setPageIndex(newIndex);
};
const handleChangePageSize = (event) => {
setPageSize(parseInt(event.target.value, 10));
setPageIndex(0);
};
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Table>
<TableHead>
<TableRow>
<TableCell align="center">ID</TableCell>
<TableCell align="center">Vehicle</TableCell>
<TableCell align="center">Status</TableCell>
<TableCell align="center">Created</TableCell>
<TableCell align="center">Updated</TableCell>
</TableRow>
</TableHead>
<TableBody>
{carUpdates.map((row) => (
<TableRow key={row.id}>
<TableCell align="center">{row.id}</TableCell>
<TableCell align="center">
<Link to={`/vehicle-status/${row.vin}`}>{row.vin}</Link>
</TableCell>
<TableCell align="center">
{row.status}
{row.progress > -1 && (
<LinearProgress variant="determinate" value={row.progress} />
)}
</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.created)}
</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.updated)}
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={5}
count={totalCarUpdates}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onPageChange={handleChangePageIndex}
onRowsPerPageChange={handleChangePageSize}
/>
</TableRow>
</TableFooter>
</Table>
</div>
);
};
const CarUpdatesStatus = () => (
<UpdatesProvider>
<MainForm />
</UpdatesProvider>
);
export default CarUpdatesStatus;

View File

@@ -8,9 +8,9 @@ import { VehicleProvider } from "../../Contexts/VehicleContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import SendCommand from "../SendCommand";
import SendCommand from "../../Controls/SendCommand";
import SearchField from "../../Controls/SearchField";
import CarSelectionTable from "../CarSelectionTable";
import CarSelectionTable from "../../Controls/CarSelectionTable";
import { logger } from "../../../services/monitoring";
const MainForm = () => {

View File

@@ -3,8 +3,8 @@ import { useParams } from "react-router";
import clsx from "clsx";
import { Button, Grid, Typography } from "@material-ui/core";
import CarECUs from "../CarECUs";
import CarUpdates from "../CarUpdates";
import CarECUsTable from "../../Controls/CarECUsTable";
import CarUpdatesTable from "../../Controls/CarUpdatesTable";
import {
VehicleProvider,
useVehicleContext,
@@ -52,7 +52,7 @@ const MainForm = () => {
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Typography variant="h6">Car Updates</Typography>
<CarUpdates vin={vin} token={token} />
<CarUpdatesTable vin={vin} token={token} />
<Grid container className={classes.root} spacing={2}>
<Grid item md={4} className={classes.textJustifyAlign}></Grid>
<Grid item md={4} className={classes.textCenterAlign}>
@@ -73,7 +73,7 @@ const MainForm = () => {
</Button>
</Grid>
</Grid>
<CarECUs vin={vin} token={token} />
<CarECUsTable vin={vin} token={token} />
</div>
);
};

View File

@@ -0,0 +1,145 @@
import React, { useEffect } from "react";
import { useState } from "react";
import { useParams } from "react-router-dom";
import { Grid, TextField } from "@material-ui/core";
import CarUpdateStatusProgress from "../../Controls/CarUpdateStatusProgress";
import CarUpdateStatusTable from "../../Controls/CarUpdateStatusTable";
import {
CarUpdatesProvider,
useCarUpdatesContext,
} from "../../Contexts/CarUpdatesContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import { useUserContext } from "../../Contexts/UserContext";
import { logger } from "../../../services/monitoring";
import { LocalDateTimeString } from "../../../utils/dates";
const MainForm = () => {
const { vin, carupdateid } = useParams();
const [manifest, setManifest] = useState(null);
const [status, setStatus] = useState(null);
const { setTitle, setSitePath, setMessage } = useStatusContext();
const { getCarUpdates, carUpdates, startMonitor, stopMonitor } =
useCarUpdatesContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const classes = useStyles();
useEffect(() => {
(async () => {
try {
const result = await getCarUpdates({ id: carupdateid }, token);
if (!result.data && result.data.length === 0)
throw new Error(`error getting update ${carupdateid}`);
setManifest(result.data[0]["updatemanifest"]);
} catch (e) {
setMessage(e.message);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!manifest) return;
const title = `Vehicle ${vin}, Update ${manifest.name}`;
setTitle(title);
setSitePath([
{
label: "Vehicles",
link: "/vehicles",
},
{
label: `Vehicle ${vin} Details`,
link: `/vehicle-status/${vin}`,
},
{
label: title,
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [manifest]);
useEffect(() => {
try {
if (carUpdates.length === 0) return;
setStatus(carUpdates[0]);
startMonitor(token);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
return () => {
stopMonitor();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [carUpdates]);
return (
<div className={classes.root}>
<Grid container spacing={2}>
{manifest && (
<>
<Grid item md={4} className={classes.textJustifyAlign}>
<TextField
label="Name"
defaultValue={manifest.name}
className={classes.fullWidth}
InputProps={{
readOnly: true,
}}
/>
</Grid>
<Grid item md={4} className={classes.textCenterAlign}>
<TextField
label="Version"
defaultValue={manifest.version}
className={classes.fullWidth}
InputProps={{
readOnly: true,
}}
/>
</Grid>
<Grid item md={4} className={classes.textRightAlign}>
<TextField
label="Created"
defaultValue={LocalDateTimeString(manifest.created)}
className={classes.fullWidth}
InputProps={{
readOnly: true,
}}
/>
</Grid>
<Grid item md={12}>
<TextField
label="Description"
defaultValue={manifest.description}
className={classes.fullWidth}
InputProps={{
readOnly: true,
}}
/>
</Grid>
</>
)}
<Grid item md={12}>
<CarUpdateStatusProgress status={status} />
</Grid>
<Grid item md={12}>
<CarUpdateStatusTable carupdateid={carupdateid} token={token} />
</Grid>
</Grid>
</div>
);
};
const UpdateStatus = () => (
<CarUpdatesProvider>
<MainForm />
</CarUpdatesProvider>
);
export default UpdateStatus;

View File

@@ -51,8 +51,11 @@ export const CarUpdatesProvider = ({ children }) => {
result = await api.getCarUpdates(search, token);
if (result.error)
throw new Error(`Get car updates error. ${result.message}`);
result.data.forEach((item) => (item.progress = 0));
console.log(result.data);
result.data.forEach((item) => {
item.progress = 0;
item.msg = item.status;
applyProgressStatus(item, item);
});
setCarUpdates(result.data);
if (search && search.offset === 0 && result.total) {
setTotalCarUpdates(result.total);
@@ -179,6 +182,21 @@ export const CarUpdatesProvider = ({ children }) => {
progressTimer = 0;
};
const getLog = async (query, token) => {
let result;
try {
setBusy(true);
result = await api.getCarUpdateLog(query, token);
if (result.error)
throw new Error(`Get car update log error. ${result.message}`);
} finally {
setBusy(false);
}
return result;
};
return (
<CarUpdatesContext.Provider
value={{
@@ -187,6 +205,7 @@ export const CarUpdatesProvider = ({ children }) => {
totalCarUpdates,
deployCarUpdates,
getCarUpdates,
getLog,
getVINUpdates,
startMonitor,
stopMonitor,

View File

@@ -1,252 +0,0 @@
import React, { useContext, useState } from "react";
import api from "../../services/updates";
const UpdatesContext = React.createContext();
export const UpdatesProvider = ({ children }) => {
const [busy, setBusy] = useState(false);
const [packages, setPackages] = useState([]);
const [carUpdates, setCarUpdates] = useState([]);
const [totalPackages, setTotalPackages] = useState(0);
const [totalCarUpdates, setTotalCarUpdates] = useState(0);
const [delayCount, setDelayCount] = useState(0);
let progressTimer = 0;
const getPackages = async (search, token) => {
let result;
try {
setBusy(true);
result = await api.getPackages(search, token);
if (result.error)
throw new Error(`Get packages error. ${result.message}`);
setPackages(result.data);
if (search && search.offset === 0 && result.total) {
setTotalPackages(result.total);
}
} finally {
setBusy(false);
}
return result;
};
const updatePackage = async (data, token) => {
let result = null;
try {
setBusy(true);
validateUpdatePackage(data);
result = await api.updatePackage(data, token);
if (result.error)
throw new Error(`Update package error. ${result.message}`);
} finally {
setBusy(false);
}
return result;
};
const deletePackage = async (package_id, token) => {
let result;
const index = packages.findIndex((element) => {
return element.id === package_id;
});
packages.splice(index, 1);
try {
setBusy(true);
result = await api.deletePackage(package_id, token);
if (result.error)
throw new Error(`Delete package error. ${result.message}`);
} finally {
setBusy(false);
}
return result;
};
const createCarUpdates = async (data, token) => {
let result;
try {
setBusy(true);
validateCreateCarUpdates(data);
result = await api.createCarUpdates(data, token);
if (result.error)
throw new Error(`Create car updates error. ${result.message}`);
} finally {
setBusy(false);
}
return result;
};
const getCarUpdates = async (search, token) => {
let result;
try {
setBusy(true);
result = await api.getCarUpdates(search, token);
if (result.error)
throw new Error(`Get packages error. ${result.message}`);
setCarUpdates(result.data);
if (search && search.offset === 0 && result.total) {
setTotalCarUpdates(result.total);
}
} finally {
setBusy(false);
}
return result;
};
const getVINUpdates = async (vin, token) => {
let result;
try {
setBusy(true);
result = await api.getVINUpdates(vin, token);
if (result.error)
throw new Error(`Get VIN updates error. ${result.message}`);
} finally {
setBusy(false);
}
return result;
};
const applyProgressStatus = (item, status) => {
if (status.msg === "DONE") {
delete item.progress;
item.status = "downloaded";
} else if (status.msg === "downloading" && status.total > 0) {
let progress = Math.floor((100 * status.bytes) / status.total);
if (progress > 99) progress = 0;
item.progress = progress;
item.status = `downloading ${progress}%`;
} else if (status.error > 0) {
item.status = "download error";
} else {
item.status = "downloading";
}
};
const applyProgressStatuses = (statuses) => {
let items = JSON.parse(JSON.stringify(carUpdates));
statuses.forEach((status) => {
let item = items.find((item) => status.id === item.id);
if (!item || status.id === 0) return;
applyProgressStatus(item, status);
});
setCarUpdates(items);
};
const updateStatusProgress = async (token) => {
stopMonitor();
if (!token || carUpdates.length === 0) return;
try {
setBusy(true);
const carupdateids = carUpdates.reduce((accum, update) => {
if (update.status !== "downloaded") accum.push(update.id);
return accum;
}, []);
if (carupdateids.length === 0) return;
const result = await api.getCarUpdateProgress(
carupdateids.join(","),
token
);
if (result.error)
throw new Error(`Get update progress error. ${result.message}`);
applyProgressStatuses(result.statuses);
} catch (e) {
} finally {
setBusy(false);
}
};
const getDelay = () => {
if (delayCount < 3) {
setDelayCount(delayCount + 1);
return 1000;
}
for (let i = 0, len = carUpdates.length; i < len; i++) {
if (carUpdates[i].status.indexOf("downloading") > -1) return 1000;
}
return 10000;
};
const startMonitor = async (token) => {
const delay = getDelay();
stopMonitor();
progressTimer = setTimeout(() => {
updateStatusProgress(token);
}, delay);
};
const stopMonitor = async () => {
if (progressTimer === 0) return;
clearTimeout(progressTimer);
progressTimer = 0;
};
return (
<UpdatesContext.Provider
value={{
busy,
packages,
totalPackages,
carUpdates,
totalCarUpdates,
getPackages,
updatePackage,
deletePackage,
createCarUpdates,
getCarUpdates,
getVINUpdates,
startMonitor,
stopMonitor,
}}
>
{children}
</UpdatesContext.Provider>
);
};
export const useUpdatesContext = () => useContext(UpdatesContext);
const validateUpdatePackage = (data) => {
if (data === null) {
throw new Error("No update data");
}
if (!data.package_name) {
throw new Error("Package name required");
}
if (!data.version) {
throw new Error("Version required");
}
};
const validateCreateCarUpdates = (data) => {
if (data === null) {
throw new Error("No car update data");
}
if (!data.package_id || data.package_id === 0) {
throw new Error("Package id required");
}
if (!data.vins || data.vins.length === 0) {
throw new Error("Cars are required");
}
};

View File

@@ -1,291 +0,0 @@
jest.mock("../../services/updates");
import {
render,
cleanup,
screen,
fireEvent,
waitFor,
} from "@testing-library/react";
import { UpdatesProvider, useUpdatesContext } from "./UpdatesContext";
import { StatusProvider, useStatusContext } from "./StatusContext";
import { TEST_AUTH_OBJECT } from "../../utils/testing";
describe("UpdatesContext", () => {
describe("getPackages", () => {
const expectedData = `[{"id":1,"package_name":"Test","version":"1.0","link":"http://cloudfront.com/download","ecu_list":"ECU1 1.0.0,ECU2 1.0.2"},{"id":2,"package_name":"Test","version":"1.1","link":"http://cloudfront.com/download","ecu_list":"ECU1 1.0.1,ECU2 1.0.2"},{"id":3,"package_name":"Test","version":"1.2","link":"http://cloudfront.com/download","ecu_list":"ECU1 1.1.0,ECU2 1.1.2"}]`;
const checkState = (busy, packages, message) => {
expect(screen.getByTestId("busy").innerHTML).toEqual(busy);
expect(screen.getByTestId("packages").innerHTML).toEqual(packages);
expect(screen.getByTestId("message").innerHTML).toEqual(message);
};
beforeEach(() => {
const TestComp = () => {
const { busy, packages, getPackages } = useUpdatesContext();
const { message, setMessage } = useStatusContext();
const exec = async (data, token) => {
try {
await getPackages(data, token);
} catch (e) {
setMessage(e.message);
}
};
return (
<>
<div data-testid="busy">{busy.toString()}</div>
<div data-testid="packages">{JSON.stringify(packages)}</div>
<div data-testid="message">{message}</div>
<button
data-testid="no-filter"
onClick={() => {
exec(null, TEST_AUTH_OBJECT);
}}
/>
<button
data-testid="with-filter"
onClick={() => {
exec({ package_name: "Test" }, TEST_AUTH_OBJECT);
}}
/>
</>
);
};
render(
<StatusProvider>
<UpdatesProvider>
<TestComp />
</UpdatesProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
});
it("Initial state", async () => {
checkState("false", "[]", "");
});
it("getPackages no filter", async () => {
fireEvent.click(screen.getByTestId("no-filter"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toBe("false")
);
checkState("false", expectedData, "");
});
it("getPackages with filter", async () => {
fireEvent.click(screen.getByTestId("with-filter"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toBe("false")
);
checkState("false", expectedData, "");
});
});
describe("updatePackage", () => {
let result = null;
const checkState = (busy, message, data) => {
expect(screen.getByTestId("busy").innerHTML).toEqual(busy);
expect(screen.getByTestId("message").innerHTML).toEqual(message);
expect(result).toEqual(data);
};
beforeEach(() => {
const TestComp = () => {
const { busy, updatePackage } = useUpdatesContext();
const { message, setMessage } = useStatusContext();
const exec = async (data, token) => {
var r = null;
try {
r = await updatePackage(data, token);
} catch (e) {
setMessage(e.message);
}
return r;
};
return (
<>
<div data-testid="busy">{busy.toString()}</div>
<div data-testid="message">{message}</div>
<button
data-testid="no-data"
onClick={async () => {
result = await exec(null, TEST_AUTH_OBJECT);
}}
/>
<button
data-testid="with-bad-data"
onClick={async () => {
result = await exec({ package_name: "Test" }, TEST_AUTH_OBJECT);
}}
/>
<button
data-testid="with-good-data"
onClick={async () => {
result = await exec(
{
package_name: "Test",
version: "1.0",
},
TEST_AUTH_OBJECT
);
}}
/>
</>
);
};
render(
<StatusProvider>
<UpdatesProvider>
<TestComp />
</UpdatesProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
result = null;
});
it("Initial state", () => {
checkState("false", "", null);
});
it("no-data", async () => {
fireEvent.click(screen.getByTestId("no-data"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toBe("false")
);
checkState("false", "No update data", null);
});
it("with-bad-data", async () => {
fireEvent.click(screen.getByTestId("with-bad-data"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toBe("false")
);
checkState("false", "Version required", null);
});
it("with-good-data", async () => {
fireEvent.click(screen.getByTestId("with-good-data"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toBe("false")
);
checkState("false", "", { package_name: "Test", version: "1.0" });
});
});
describe("createCarUpdates", () => {
let result = null;
const checkState = (busy, message, data) => {
expect(screen.getByTestId("busy").innerHTML).toEqual(busy);
expect(screen.getByTestId("message").innerHTML).toEqual(message);
expect(result).toEqual(data);
};
beforeEach(() => {
const TestComp = () => {
const { busy, createCarUpdates } = useUpdatesContext();
const { message, setMessage } = useStatusContext();
const exec = async (data, token) => {
var r = null;
try {
r = await createCarUpdates(data, token);
} catch (e) {
setMessage(e.message);
}
return r;
};
return (
<>
<div data-testid="busy">{busy.toString()}</div>
<div data-testid="message">{message}</div>
<button
data-testid="no-data"
onClick={async () => {
result = await exec(null, TEST_AUTH_OBJECT);
}}
/>
<button
data-testid="with-bad-data"
onClick={async () => {
result = await exec(
{ package_id: 1, vins: [] },
TEST_AUTH_OBJECT
);
}}
/>
<button
data-testid="with-good-data"
onClick={async () => {
result = await exec(
{
package_id: 1,
vins: ["FISKER123", "FISKER124", "FISKER125"],
},
TEST_AUTH_OBJECT
);
}}
/>
</>
);
};
render(
<StatusProvider>
<UpdatesProvider>
<TestComp />
</UpdatesProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
result = null;
});
it("Initial state", () => {
checkState("false", "", null);
});
it("no-data", async () => {
fireEvent.click(screen.getByTestId("no-data"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toBe("false")
);
checkState("false", "No car update data", null);
});
it("with-bad-data", async () => {
fireEvent.click(screen.getByTestId("with-bad-data"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toBe("false")
);
checkState("false", "Cars are required", null);
});
it("with-good-data", async () => {
fireEvent.click(screen.getByTestId("with-good-data"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toBe("false")
);
checkState("false", "", {
id: 1,
package_id: 1,
vins: ["FISKER123", "FISKER124", "FISKER125"],
});
});
});
});

View File

@@ -11,9 +11,38 @@ let carUpdates = [
status: "downloaded",
created: "2021-07-01T22:40:07.778509Z",
updated: "2021-07-12T18:22:13.736755Z",
updatemanifest: {
id: 283,
name: "TEST UPDATE",
version: "1000",
description: "UPDATE DESCRIPTION",
ecu_list: "AGS 1.0.0,AMP 1.0.0",
created: "2021-08-20T18:37:41.960397Z",
updated: "2021-08-20T18:37:50.007853Z",
},
},
];
let totalCarUpdates = 1;
let carUpdateLog = {
data: [
{
id: 90,
status: "package_install_complete",
error_code: 0,
created: "2021-08-23T17:06:38.410115Z",
updated: "2021-08-23T17:06:38.410115Z",
},
{
id: 89,
carupdate_id: 283,
status: "package_install_start",
error_code: 0,
created: "2021-08-23T17:06:38.030052Z",
updated: "2021-08-23T17:06:38.030052Z",
},
],
total: 2,
};
export const CarUpdatesProvider = ({ children }) => {
return <div data-testid="mocked-carupdatesprovider">{children}</div>;
@@ -24,7 +53,10 @@ export const useCarUpdatesContext = () => ({
carUpdates,
totalCarUpdates,
deployCarUpdates: jest.fn((data) => data),
getCarUpdates: jest.fn(() => carUpdates),
getCarUpdates: jest.fn(() => ({
data: carUpdates,
})),
getLog: jest.fn(() => carUpdateLog),
getVINUpdates: jest.fn(() => carUpdates),
startMonitor: jest.fn(),
stopMonitor: jest.fn(),

View File

@@ -1,39 +0,0 @@
import React from "react";
const UpdatesContext = React.createContext();
let busy = false;
let packages = [];
const examplePackage = {
id: 0,
package_name: "Package",
version: "1.0",
desc: "Description",
release_notes: "https://www.google.com/",
timestamp: 1625615299,
created: "2021-07-01T22:40:07.778509Z",
updated: "2021-07-12T18:22:13.736755Z",
};
packages.push(examplePackage);
let totalPackages = 0;
let carUpdates = [];
let totalCarUpdates = 0;
export const UpdatesProvider = ({ children }) => {
return <div data-testid="mocked-updatesprovider">{children}</div>;
};
export const useUpdatesContext = () => ({
busy,
packages,
totalPackages,
carUpdates,
totalCarUpdates,
getPackages: jest.fn(() => packages),
updatePackage: jest.fn((data) => data),
createCarUpdates: jest.fn((data) => data),
getCarUpdates: jest.fn(() => carUpdates),
getVINUpdates: jest.fn(() => carUpdates),
startMonitor: jest.fn(),
stopMonitor: jest.fn(),
});

View File

@@ -0,0 +1,174 @@
import React, { useEffect, useState } from "react";
import {
Table,
TableBody,
TableCell,
TableFooter,
TablePagination,
TableRow,
} from "@material-ui/core";
import clsx from "clsx";
import { LocalDateTimeString } from "../../../utils/dates";
import TableHeaderSortable from "../../Table/HeaderSortable";
import { useVehicleContext } from "../../Contexts/VehicleContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import { logger } from "../../../services/monitoring";
const tableColumns = [
{
id: "ecu",
label: "ECU",
},
{
id: "sw_version",
label: "SW Version",
},
{
id: "boot_loader_version",
label: "BL Version",
},
{
id: "hw_version",
label: "HW Version",
},
{
id: "vendor",
label: "Vendor",
},
{
id: "config",
label: "Config",
},
{
id: "fingerprint",
label: "Fingerprint",
},
{
id: "serial_number",
label: "Serial",
},
{
id: "created_at",
label: "Created",
},
{
id: "updated_at",
label: "Updated",
},
];
const CarECUsTable = ({ vin, token }) => {
const [ecus, setECUs] = useState([]);
const [total, setTotal] = useState(0);
const classes = useStyles();
const [pageSize, setPageSize] = useState(10);
const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("ecu");
const [order, setOrder] = useState("desc");
const { getECUs } = useVehicleContext();
const { setMessage } = useStatusContext();
useEffect(() => {
(async () => {
try {
if (!vin || !token) return;
const result = await getECUs(
{
vin,
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
},
token
);
setECUs(result.data);
if (result.total > -1) setTotal(result.total);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vin, token, pageIndex, pageSize, orderBy, order]);
const handleChangePageIndex = (event, newIndex) => {
setPageIndex(newIndex);
};
const handleChangePageSize = (event) => {
setPageSize(parseInt(event.target.value, 10));
setPageIndex(0);
};
const handleSort = (event, property) => {
try {
if (property === orderBy) {
if (order === "asc") {
setOrder("desc");
} else {
setOrder("asc");
}
} else {
setOrderBy(property);
setOrder("asc");
}
} catch (e) {
logger.warn(e.stack);
}
};
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Table>
<TableHeaderSortable
classes={classes}
orderBy={orderBy}
order={order}
columnData={tableColumns}
onSortRequest={handleSort}
/>
<TableBody>
{ecus.map((row) => (
<TableRow key={row.ecu}>
<TableCell align="center">{row.ecu}</TableCell>
<TableCell align="center">{row.sw_version}</TableCell>
<TableCell align="center">{row.boot_loader_version}</TableCell>
<TableCell align="center">{row.hw_version}</TableCell>
<TableCell align="center">{row.vendor}</TableCell>
<TableCell align="center">{row.config}</TableCell>
<TableCell align="center">{row.fingerprint}</TableCell>
<TableCell align="center">{row.serial_number}</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.created)}
</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.updated)}
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={10}
count={total}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onPageChange={handleChangePageIndex}
onRowsPerPageChange={handleChangePageSize}
/>
</TableRow>
</TableFooter>
</Table>
</div>
);
};
export default CarECUsTable;

View File

@@ -0,0 +1,128 @@
import React, { useState, useEffect } from "react";
import Typography from "@material-ui/core/Typography";
import { CheckCircle, RadioButtonUnchecked, Error } from "@material-ui/icons";
import clsx from "clsx";
import CircularProgress from "../CircularProgress";
import useStyles from "../../useStyles";
const Progress = ({ value }) => {
const classes = useStyles();
if (value === 100)
return (
<CheckCircle
className={clsx(classes.progressIcon, classes.progressSuccess)}
/>
);
if (value >= 0) return <CircularProgress value={value} />;
if (value < -1)
return (
<Error className={clsx(classes.progressIcon, classes.progressError)} />
);
return <RadioButtonUnchecked className={classes.progressIcon} />;
};
const CarUpdateStatus = ({ status }) => {
const classes = useStyles();
const [received, setReceived] = useState(-1);
const [approval, setApproval] = useState(-1);
const [precondition, setPrecondition] = useState(-1);
const [download, setDownload] = useState(-1);
const [install, setInstall] = useState(-1);
const [cleanup, setCleanup] = useState(-1);
const [updated, setUpdated] = useState(-1);
useEffect(() => {
/* eslint-disable no-fallthrough, default-case */
if (!status) return;
// update previous steps
switch (status.msg) {
case "cleanup_success":
setUpdated(100);
case "package_install_complete":
setInstall(100);
case "install_start":
case "installing":
case "install_complete":
case "install_error":
case "package_download_complete":
setDownload(100);
case "download_start":
case "downloading":
case "download_complete":
case "download_error":
case "install_approval_received":
setApproval(100);
case "requirements_succeeded":
setPrecondition(100);
case "manifest_received":
setReceived(100);
}
// update progress and errors
switch (status.msg) {
case "installing":
setInstall(status.progress);
break;
case "install_error":
setInstall(-100);
break;
case "downloading":
setDownload(status.progress);
break;
case "download_error":
setDownload(-100);
break;
case "cleanup_failed":
setCleanup(-100);
break;
}
}, [status]);
return (
<div
style={{
width: "100%",
display: "flex",
justifyContent: "space-between",
flexWrap: "wrap",
}}
>
<div className={classes.textCenterAlign}>
<Progress value={100} />
<Typography>Pending</Typography>
</div>
<div className={classes.textCenterAlign}>
<Progress value={received} />
<Typography>Recieved</Typography>
</div>
<div className={classes.textCenterAlign}>
<Progress value={approval} />
<Typography>Approved</Typography>
</div>
<div className={classes.textCenterAlign}>
<Progress value={precondition} />
<Typography>Precondition</Typography>
</div>
<div className={classes.textCenterAlign}>
<Progress value={download} />
<Typography>Download</Typography>
</div>
<div className={classes.textCenterAlign}>
<Progress value={install} />
<Typography>Install</Typography>
</div>
<div className={classes.textCenterAlign}>
<Progress value={cleanup} />
<Typography>Clean up</Typography>
</div>
<div className={classes.textCenterAlign}>
<Progress value={updated} />
<Typography>Updated</Typography>
</div>
</div>
);
};
export default CarUpdateStatus;

View File

@@ -0,0 +1,134 @@
import React, { useEffect, useState } from "react";
import {
Table,
TableBody,
TableCell,
TableFooter,
TablePagination,
TableRow,
} from "@material-ui/core";
import { LocalDateTimeString } from "../../../utils/dates";
import { logger } from "../../../services/monitoring";
import TableHeaderSortable from "../../Table/HeaderSortable";
import { useCarUpdatesContext } from "../../Contexts/CarUpdatesContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
const tableColumns = [
{
id: "created_at",
label: "Date",
},
{
id: "status",
label: "Status",
},
{
id: "error_code",
label: "Error",
},
];
const CarUpdateStatusTable = ({ carupdateid, token }) => {
const classes = useStyles();
const [log, setLog] = useState([]);
const [logTotal, setLogTotal] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("id");
const [order, setOrder] = useState("desc");
const { getLog } = useCarUpdatesContext();
const { setMessage } = useStatusContext();
useEffect(() => {
(async () => {
try {
if (!carupdateid || !token) return;
const result = await getLog(
{
carupdateid,
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
},
token
);
setLog(result.data);
if (pageIndex === 0 && result.total) setLogTotal(result.total);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [carupdateid, token, pageIndex, pageSize, orderBy, order]);
const handleChangePageIndex = (event, newIndex) => {
setPageIndex(newIndex);
};
const handleChangePageSize = (event) => {
setPageSize(parseInt(event.target.value, 10));
setPageIndex(0);
};
const handleSort = (event, property) => {
try {
if (property === orderBy) {
if (order === "asc") {
setOrder("desc");
} else {
setOrder("asc");
}
} else {
setOrderBy(property);
setOrder("asc");
}
} catch (e) {
logger.warn(e.stack);
}
};
return (
<Table>
<TableHeaderSortable
classes={classes}
orderBy={orderBy}
order={order}
columnData={tableColumns}
onSortRequest={handleSort}
/>
<TableBody>
{log.map((row) => (
<TableRow key={row.id}>
<TableCell align="center">
{LocalDateTimeString(row.created)}
</TableCell>
<TableCell align="center">{row.status}</TableCell>
<TableCell align="center">{row.error}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={5}
count={logTotal}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onPageChange={handleChangePageIndex}
onRowsPerPageChange={handleChangePageSize}
/>
</TableRow>
</TableFooter>
</Table>
);
};
export default CarUpdateStatusTable;

View File

@@ -0,0 +1,162 @@
import React, { useEffect, useState } from "react";
import {
Table,
TableBody,
TableCell,
TableFooter,
TablePagination,
TableRow,
} from "@material-ui/core";
import { Link } from "react-router-dom";
import { LocalDateTimeString } from "../../../utils/dates";
import TableHeaderSortable from "../../Table/HeaderSortable";
import {
CarUpdatesProvider,
useCarUpdatesContext,
} from "../../Contexts/CarUpdatesContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import { logger } from "../../../services/monitoring";
const tableColumns = [
{
id: "id",
label: "ID",
},
{
id: "update_package_id",
label: "Name",
},
{
id: "status",
label: "Status",
},
{
id: "created_at",
label: "Created",
},
{
id: "updated_at",
label: "Updated",
},
];
const MainForm = ({ vin, token }) => {
const classes = useStyles();
const [pageSize, setPageSize] = useState(10);
const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("id");
const [order, setOrder] = useState("desc");
const { getCarUpdates, carUpdates, totalCarUpdates } = useCarUpdatesContext();
const { setMessage } = useStatusContext();
useEffect(() => {
(async () => {
try {
if (!vin || !token) return;
await getCarUpdates(
{
vin,
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
},
token
);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vin, token, pageIndex, pageSize, orderBy, order]);
const handleChangePageIndex = (event, newIndex) => {
setPageIndex(newIndex);
};
const handleChangePageSize = (event) => {
setPageSize(parseInt(event.target.value, 10));
setPageIndex(0);
};
const handleSort = (event, property) => {
try {
if (property === orderBy) {
if (order === "asc") {
setOrder("desc");
} else {
setOrder("asc");
}
} else {
setOrderBy(property);
setOrder("asc");
}
} catch (e) {
logger.warn(e.stack);
}
};
const updateName = (row) => {
if (row.updatemanifest)
return `${row.updatemanifest.name} ${row.updatemanifest.version}`;
return "None";
};
return (
<Table>
<TableHeaderSortable
classes={classes}
orderBy={orderBy}
order={order}
columnData={tableColumns}
onSortRequest={handleSort}
/>
<TableBody>
{carUpdates.map((row) => (
<TableRow key={row.id}>
<TableCell align="center">{row.id}</TableCell>
<TableCell align="center">
<Link to={`/vehicle-status/${row.vin}/${row.id}`}>
{updateName(row)}
</Link>
</TableCell>
<TableCell align="center">{row.status}</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.created)}
</TableCell>
<TableCell align="center">
{LocalDateTimeString(row.updated)}
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={5}
count={totalCarUpdates}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onPageChange={handleChangePageIndex}
onRowsPerPageChange={handleChangePageSize}
/>
</TableRow>
</TableFooter>
</Table>
);
};
const CarUpdatesTable = (props) => (
<CarUpdatesProvider>
<MainForm {...props} />
</CarUpdatesProvider>
);
export default CarUpdatesTable;

View File

@@ -0,0 +1,43 @@
import React from "react";
import PropTypes from "prop-types";
import CircularProgress from "@material-ui/core/CircularProgress";
import Typography from "@material-ui/core/Typography";
import Box from "@material-ui/core/Box";
const CircularProgressWithLabel = (props) => {
return (
<Box position="relative" display="inline-flex">
<CircularProgress
variant="determinate"
{...props}
style={{ color: "green" }}
/>
<Box
top={0}
left={0}
bottom={0}
right={0}
position="absolute"
display="flex"
alignItems="center"
justifyContent="center"
>
<Typography
variant="caption"
component="div"
color="textSecondary"
>{`${Math.round(props.value)}%`}</Typography>
</Box>
</Box>
);
};
CircularProgressWithLabel.propTypes = {
/**
* The value of the progress indicator for the determinate variant.
* Value between 0 and 100.
*/
value: PropTypes.number.isRequired,
};
export default CircularProgressWithLabel;

View File

@@ -6,6 +6,7 @@ import api from "../../../services/grafana";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import ResponsiveIFrame from "../../Controls/ResponsiveIFrame";
import { logger } from "../../../services/monitoring";
const Datascope = () => {
const classes = useStyles();
@@ -23,7 +24,7 @@ const Datascope = () => {
api
.getCarsCount()
.then((result) => setCarsCount(result))
.catch((error) => console.log(error));
.catch((error) => logger.warn(error.stack));
}, []);
const [signalsCount, setSignalsCount] = useState("0");
@@ -45,7 +46,7 @@ const Datascope = () => {
let num = result.toLocaleString();
setSignalsCount(num);
})
.catch((error) => console.log(error));
.catch((error) => logger.warn(error.stack));
};
return (

View File

@@ -2,6 +2,8 @@ import React, { Component } from "react";
import PropTypes from "prop-types";
import { Typography } from "@material-ui/core";
import { logger } from "../services/monitoring";
const reload = () => {
window.location.reload();
};
@@ -22,6 +24,10 @@ export default class ErrorBoundary extends Component {
render() {
if (this.state.hasError) {
try {
logger.error(this.state.error.stack);
} catch (e) {}
if (this.state.error && this.state.error.name === "ChunkLoadError") {
reload();
return;

View File

@@ -16,7 +16,7 @@ import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import SearchField from "../../Controls/SearchField";
import CarSelectionTable from "../../Cars/CarSelectionTable";
import CarSelectionTable from "../../Controls/CarSelectionTable";
import { logger } from "../../../services/monitoring";
import { LocalDateTimeString } from "../../../utils/dates";
@@ -111,7 +111,7 @@ const MainForm = () => {
useEffect(() => {
if (!manifests || manifests.length === 0) return;
var data = manifests[0];
const data = manifests[0];
setManifestName(data.name);
setVersion(data.version);

View File

@@ -68,6 +68,10 @@ const MainForm = () => {
label: "Deployments",
link: "/packages",
},
{
label: `Deploy ${manifests[0].name} ${manifests[0].version}`,
link: `/package-deploy/${manifests[0].id}`,
},
{
label: title,
},

View File

@@ -6,20 +6,19 @@ import { MessageBar } from "../MessageBar";
import { useUserContext } from "../Contexts/UserContext";
import { Roles } from "../../utils/roles";
const SSOForm = React.lazy(() => import("../SSOForm"));
const Home = React.lazy(() => import("../Home"));
const VehicleAddForm = React.lazy(() => import("../Cars/Add"));
const PageNotFound = React.lazy(() => import("../404"));
const CarUpdatesDeploy = React.lazy(() => import("../CarUpdates/Deploy"));
const CarUpdatesStatus = React.lazy(() => import("../CarUpdates/Status"));
const CarUpdates = React.lazy(() => import("../Cars/Status"));
const SendCommandBulk = React.lazy(() => import("../Cars/SendCommandBulk"));
const Datascope = React.lazy(() => import("../Datascope/Home"));
const BatteryDatascope = React.lazy(() => import("../Datascope/Battery"));
const CarsList = React.lazy(() => import("../Cars/List"));
const CarStatus = React.lazy(() => import("../Cars/Status"));
const CarUpdateStatus = React.lazy(() => import("../Cars/UpdateStatus"));
const Datascope = React.lazy(() => import("../Datascope/Home"));
const Home = React.lazy(() => import("../Home"));
const Manifests = React.lazy(() => import("../Manifest/List"));
const ManifestDeploy = React.lazy(() => import("../Manifest/Deploy"));
const ManifestStatus = React.lazy(() => import("../Manifest/Status"));
const ManifestCreate = React.lazy(() => import("../Manifest/Create"));
const PageNotFound = React.lazy(() => import("../404"));
const SSOForm = React.lazy(() => import("../SSOForm"));
const VehicleAddForm = React.lazy(() => import("../Cars/Add"));
const SiteRoutes = () => {
const { token, groups } = useUserContext();
@@ -34,52 +33,6 @@ const SiteRoutes = () => {
type={TYPES.GUEST}
token={token}
/>
<AuthRoute
path="/home"
render={() => <Home />}
type={TYPES.PROTECTED}
token={token}
/>
<AuthRoute
path="/carupdate-deploy/:packageid"
render={() => <CarUpdatesDeploy />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
roles={[Roles.CREATE]}
/>
<AuthRoute
path="/carupdate-status/:packageid"
render={() => <CarUpdatesStatus />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
roles={[Roles.READ, Roles.CREATE]}
/>
<AuthRoute
path="/vehicles"
render={() => <SendCommandBulk />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
roles={[Roles.CREATE]}
/>
<AuthRoute
path="/vehicle-add"
render={() => <VehicleAddForm />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
roles={[Roles.CREATE]}
/>
<AuthRoute
path="/vehicle-status/:vin"
render={() => <CarUpdates />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
roles={[Roles.READ, Roles.CREATE]}
/>
<AuthRoute
path="/datascope/battery"
render={() => <BatteryDatascope />}
@@ -96,6 +49,12 @@ const SiteRoutes = () => {
groups={groups}
roles={[Roles.READ, Roles.CREATE]}
/>
<AuthRoute
path="/home"
render={() => <Home />}
type={TYPES.PROTECTED}
token={token}
/>
<AuthRoute
path="/packages"
render={() => <Manifests />}
@@ -104,6 +63,14 @@ const SiteRoutes = () => {
groups={groups}
roles={[Roles.READ, Roles.CREATE]}
/>
<AuthRoute
path="/package-create"
render={() => <ManifestCreate />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
roles={[Roles.CREATE]}
/>
<AuthRoute
path="/package-deploy/:manifest_id"
render={() => <ManifestDeploy />}
@@ -121,13 +88,37 @@ const SiteRoutes = () => {
roles={[Roles.READ, Roles.CREATE]}
/>
<AuthRoute
path="/package-create"
render={() => <ManifestCreate />}
path="/vehicles"
render={() => <CarsList />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
roles={[Roles.CREATE]}
/>
<AuthRoute
path="/vehicle-add"
render={() => <VehicleAddForm />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
roles={[Roles.CREATE]}
/>
<AuthRoute
path="/vehicle-status/:vin/:carupdateid"
render={() => <CarUpdateStatus />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
roles={[Roles.READ, Roles.CREATE]}
/>
<AuthRoute
path="/vehicle-status/:vin"
render={() => <CarStatus />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
roles={[Roles.READ, Roles.CREATE]}
/>
<PageNotFound />
</Switch>
</Suspense>

View File

@@ -9,13 +9,14 @@ import { useVehicleContext, VehicleProvider } from "../Contexts/VehicleContext";
import { VehiclePopUp } from "./popup";
import GreenMarkerIcon from "../../assets/green-marker.png";
import GrayMarkerIcon from "../../assets/gray-marker.png";
import { logger } from "../../services/monitoring";
const Component = () => {
const classes = useStyles();
const {
token: {
idToken: { jwtToken: token },
}
},
} = useUserContext();
const { getConnections, getLocations, getState } = useVehicleContext();
@@ -26,29 +27,34 @@ const Component = () => {
const [markers, setMarkers] = useState([]);
useEffect(() => {
retrieveAndStoreLocations()
.then(points => {
centerAroundMarkers(points);
})
retrieveAndStoreLocations().then((points) => {
centerAroundMarkers(points);
});
const id = setInterval(function () {
retrieveAndStoreLocations();
}, REQUEST_INTERVAL);
return () => { clearInterval(id) };
return () => {
clearInterval(id);
};
// eslint-disable-next-line
}, []);
const retrieveAndStoreLocations = () => {
return getLocations(token)
.then(result => {
.then((result) => {
if (result.data != null) {
const points = result.data.map(point => [point.latitude, point.longitude, point.vin]);
const points = result.data.map((point) => [
point.latitude,
point.longitude,
point.vin,
]);
setMarkers(points);
return points
return points;
}
return []
return [];
})
.catch(error => console.log(error));
}
.catch((error) => logger.warn(error.stack));
};
const centerAroundMarkers = (markers) => {
// if (markers == null) {
@@ -62,20 +68,19 @@ const Component = () => {
setCenter([37.0902, -95.7129]);
setZoom(4.5);
}
};
const [connections, setConnections] = useState({});
useEffect(() => {
if (markers.length > 0) {
const vins = markers.map(marker => marker[2]);
getConnections(vins, token)
.then(connections => {
setConnections(connections);
})
const vins = markers.map((marker) => marker[2]);
getConnections(vins, token).then((connections) => {
setConnections(connections);
});
}
// eslint-disable-next-line
}, [markers, token])
}, [markers, token]);
const [selectedVIN, setSelectedVIN] = useState(null);
const [carState, setCarState] = useState(null);
@@ -86,7 +91,9 @@ const Component = () => {
const id = setInterval(function () {
retrieveAndStoreCarState(selectedVIN);
}, REQUEST_INTERVAL);
return () => { clearInterval(id) };
return () => {
clearInterval(id);
};
}
// eslint-disable-next-line
}, [selectedVIN]);
@@ -94,14 +101,13 @@ const Component = () => {
const selectCar = (e, vin) => {
e.preventDefault();
setSelectedVIN(vin);
}
};
const retrieveAndStoreCarState = (vin) => {
getState(token, vin)
.then(results => {
setCarState({ ...results.data, vin: vin });
});
}
getState(token, vin).then((results) => {
setCarState({ ...results.data, vin: vin });
});
};
const handleClose = () => {
setSelectedVIN(null);
@@ -117,7 +123,7 @@ const Component = () => {
return new L.Icon({
iconUrl: icon,
iconAnchor: [24, 42]
iconAnchor: [24, 42],
});
}
@@ -126,8 +132,8 @@ const Component = () => {
center={center}
zoom={zoom}
style={{
width: '100%',
height: '900px'
width: "100%",
height: "900px",
}}
>
<TileLayer
@@ -146,37 +152,36 @@ const Component = () => {
>
<Popup>
<div align="center">
<p className={classes.markerTitle}><b>{marker[2]}</b></p>
<p className={classes.markerTitle}>
<b>{marker[2]}</b>
</p>
<Button
type="submit"
variant="contained"
color="primary"
onClick={e => selectCar(e, marker[2])}
onClick={(e) => selectCar(e, marker[2])}
>
View Stats
</Button>
</div>
</Popup>
</Marker>
))
}
))}
{
carState ? (
<VehiclePopUp
key={carState.vin}
vin={carState.vin}
online={carState.online}
battery={carState.battery}
doors={carState.doors}
location={carState.location}
windows={carState.windows}
className={classes.popup}
onClose={handleClose}
/>
) : null
}
</MapContainer >
{carState ? (
<VehiclePopUp
key={carState.vin}
vin={carState.vin}
online={carState.online}
battery={carState.battery}
doors={carState.doors}
location={carState.location}
windows={carState.windows}
className={classes.popup}
onClose={handleClose}
/>
) : null}
</MapContainer>
);
};
@@ -192,12 +197,12 @@ const CenterFocus = ({ center, zoom }) => {
}, [center, zoom, map]);
return null;
}
};
const VehicleMap = () => (
<VehicleProvider>
<Component />
</VehicleProvider>
)
);
export default VehicleMap;

View File

@@ -262,6 +262,9 @@ const useStyles = makeStyles((theme) => ({
},
tableSize: { height: 700, width: "100%" },
whiteBackground: { backgroundColor: "White" },
progressIcon: { width: 40, height: 40 },
progressSuccess: { color: "green" },
progressError: { color: "red" },
}));
export default useStyles;