+
+
+`;
diff --git a/src/components/Fleets/Status/Vehicles/Add/index.jsx b/src/components/Fleets/Status/Vehicles/Add/index.jsx
new file mode 100644
index 0000000..debf7b6
--- /dev/null
+++ b/src/components/Fleets/Status/Vehicles/Add/index.jsx
@@ -0,0 +1,103 @@
+import React, { useEffect, useRef, useState } from "react";
+import { Redirect, useParams } from "react-router";
+import { Button, TextField } from "@material-ui/core";
+
+import useStyles from "../../../../useStyles";
+import {
+ useFleetContext,
+ FleetProvider
+} from "../../../../Contexts/FleetContext";
+import { useStatusContext } from "../../../../Contexts/StatusContext";
+import { useUserContext } from "../../../../Contexts/UserContext";
+import { logger } from "../../../../../services/monitoring";
+
+const MainForm = () => {
+ const { name } = useParams();
+ const { setMessage, setTitle, setSitePath } = useStatusContext();
+ const { addFleetVehicle, busy } = useFleetContext();
+ const {
+ token: {
+ idToken: { jwtToken: token },
+ },
+ } = useUserContext();
+ const classes = useStyles();
+ const vinEl = useRef(null);
+ const [redirect, setRedirect] = useState(null);
+
+ useEffect(() => {
+ const title = "Add Vehicle";
+ setTitle(title);
+ setSitePath([
+ {
+ label: "Fleets",
+ link: "/fleets",
+ },
+ {
+ label: `${name}`,
+ link: `/fleet/${name}`
+ },
+ {
+ label: title
+ }
+ ]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const onSubmit = async (event) => {
+ try {
+ event.preventDefault();
+
+ const formData = { vin: vinEl.current.value };
+ const result = await addFleetVehicle(name, formData, token);
+
+ setMessage(`Added ${result.vin}`);
+ setRedirect(`/fleet/${name}#vehicles`);
+ } catch (e) {
+ setMessage(e.message);
+ logger.warn(e.stack);
+ }
+ };
+
+ if (redirect && redirect.length > 0) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+};
+
+const FleetAddVehicleForm = () => (
+
+
+
+);
+
+export default FleetAddVehicleForm;
diff --git a/src/components/Fleets/Status/Vehicles/Add/index.test.jsx b/src/components/Fleets/Status/Vehicles/Add/index.test.jsx
new file mode 100644
index 0000000..31afb5b
--- /dev/null
+++ b/src/components/Fleets/Status/Vehicles/Add/index.test.jsx
@@ -0,0 +1,36 @@
+jest.mock("../../../../Contexts/CANFiltersContext");
+jest.mock("../../../../Contexts/StatusContext");
+jest.mock("../../../../Contexts/UserContext");
+
+import { render, waitFor } from "@testing-library/react";
+import { BrowserRouter } from "react-router-dom";
+
+import { CANFiltersProvider } from "../../../../Contexts/CANFiltersContext";
+import { StatusProvider } from "../../../../Contexts/StatusContext";
+import { UserProvider, setToken } from "../../../../Contexts/UserContext";
+import { TEST_AUTH_OBJECT } from "../../../../../utils/testing";
+import MainForm from "./index"
+
+const renderCANFiltersAdd = async () => {
+ const { container } = render(
+
+
+
+
+
+
+
+ f
+
+ );
+ await waitFor(() => { });
+ return container;
+};
+
+describe("FleetVehicleAdd", () => {
+ it("Render", async () => {
+ setToken(TEST_AUTH_OBJECT);
+ const container = await renderCANFiltersAdd();
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/src/components/Fleets/Status/Vehicles/Table/__snapshots__/index.test.jsx.snap b/src/components/Fleets/Status/Vehicles/Table/__snapshots__/index.test.jsx.snap
new file mode 100644
index 0000000..71732df
--- /dev/null
+++ b/src/components/Fleets/Status/Vehicles/Table/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,326 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FleetVehiclesTable Render 1`] = `
+
+`;
diff --git a/src/components/Fleets/Status/Vehicles/Table/index.jsx b/src/components/Fleets/Status/Vehicles/Table/index.jsx
new file mode 100644
index 0000000..0fdfc23
--- /dev/null
+++ b/src/components/Fleets/Status/Vehicles/Table/index.jsx
@@ -0,0 +1,194 @@
+import React, { useEffect, useState } from "react";
+import { Link } from 'react-router-dom';
+import {
+ Grid,
+ Table,
+ TableBody,
+ TableCell,
+ TableFooter,
+ TablePagination,
+ TableRow,
+ Tooltip,
+} from "@material-ui/core";
+import AddCircleIcon from "@material-ui/icons/AddCircle";
+import DeleteIcon from "@material-ui/icons/Delete";
+import clsx from "clsx";
+
+import TableHeaderSortable from "../../../../Table/HeaderSortable";
+import { useUserContext } from "../../../../Contexts/UserContext"
+import { useStatusContext } from "../../../../Contexts/StatusContext";
+import { FleetProvider, useFleetContext } from "../../../../Contexts/FleetContext"
+import useStyles from "../../../../useStyles";
+import SearchField from "../../../../Controls/SearchField";
+import { logger } from "../../../../../services/monitoring";
+import { Roles, hasRole } from "../../../../../utils/roles";
+
+const tableColumns = [
+ {
+ id: "vin",
+ label: "VIN"
+ },
+ {
+ id: "",
+ label: "Actions"
+ }
+];
+
+const MainForm = ({ name }) => {
+ const [pageSize, setPageSize] = useState(10);
+ const [pageIndex, setPageIndex] = useState(0);
+ const [orderBy, setOrderBy] = useState("id");
+ const [order, setOrder] = useState("desc");
+ const classes = useStyles();
+ const { setMessage } = useStatusContext();
+ const { fleetVehicles, totalFleetVehicles, getFleetVehicles, deleteFleetVehicle } = useFleetContext();
+ const { token: { idToken: { jwtToken: token } }, groups } = useUserContext();
+
+ useEffect(() => {
+ (async () => {
+ try {
+ if (!token) return;
+ await getFleetVehicles(
+ name,
+ {
+ limit: pageSize,
+ offset: pageSize * pageIndex,
+ order: `${orderBy} ${order}`,
+ },
+ token
+ );
+ } catch (e) {
+ setMessage(e.message);
+ logger.warn(e.stack);
+ }
+ })();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [token, pageIndex, pageSize, orderBy, order]);
+
+ const handleChangePageIndex = (event, newIndex) => {
+ setPageIndex(newIndex);
+ };
+
+ const handleChangePageSize = (event) => {
+ setPageSize(parseInt(event.target.value, 10));
+ setPageIndex(0);
+ };
+
+ const handleSort = (event, property) => {
+ try {
+ if (property === orderBy) {
+ if (order === "asc") {
+ setOrder("desc");
+ } else {
+ setOrder("asc");
+ }
+ } else {
+ setOrderBy(property);
+ setOrder("asc");
+ }
+ } catch (e) {
+ logger.warn(e.stack);
+ }
+ };
+
+ const onDelete = async (vin) => {
+ try {
+ await deleteFleetVehicle(name, { vin: vin }, token);
+ setMessage(`Deleted ${vin}`)
+ } catch (e) {
+ setMessage(e.message);
+ logger.warn(e.stack);
+ }
+ };
+
+ const Actions = (vin) => {
+ let actions = [];
+ if (hasRole([Roles.DELETE], groups)) {
+ actions.push({
+ tip: `Delete "${vin}"`,
+ id: vin,
+ icon:
+ })
+ }
+ if (actions.length === 0) return ["No actions"];
+
+ return actions.map((action) => {
+ if (action.link != null) {
+ return (
+
+
+ {action.icon}
+
+
+ );
+ } else {
+ return (
+
+ onDelete(action.id)}>
+ {action.icon}
+
+
+ );
+ }
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {fleetVehicles.map(vin => (
+
+
+ {vin}
+
+ {Actions(vin)}
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+};
+
+const FleetVehiclesTable = (props) => (
+
+
+
+);
+
+export default FleetVehiclesTable;
diff --git a/src/components/Fleets/Status/Vehicles/Table/index.test.jsx b/src/components/Fleets/Status/Vehicles/Table/index.test.jsx
new file mode 100644
index 0000000..6559abd
--- /dev/null
+++ b/src/components/Fleets/Status/Vehicles/Table/index.test.jsx
@@ -0,0 +1,39 @@
+jest.mock("../../../../Contexts/FleetContext");
+jest.mock("../../../../Contexts/StatusContext");
+jest.mock("../../../../Contexts/UserContext");
+jest.mock('@material-ui/core/utils/unstable_useId', () =>
+ jest.fn().mockReturnValue('mui-test-id'),
+);
+
+import { render, waitFor } from "@testing-library/react";
+import { BrowserRouter } from "react-router-dom";
+
+import { FleetProvider } from "../../../../Contexts/FleetContext";
+import { StatusProvider } from "../../../../Contexts/StatusContext";
+import { UserProvider, setToken } from "../../../../Contexts/UserContext";
+import { TEST_AUTH_OBJECT } from "../../../../../utils/testing";
+import MainForm from "./index"
+
+const renderFleetTable = async () => {
+ const { container } = render(
+
+
+
+
+
+
+
+
+
+ );
+ await waitFor(() => { });
+ return container;
+};
+
+describe("FleetVehiclesTable", () => {
+ it("Render", async () => {
+ setToken(TEST_AUTH_OBJECT);
+ const container = await renderFleetTable();
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/src/components/Fleets/Status/VehiclesTab.jsx b/src/components/Fleets/Status/VehiclesTab.jsx
new file mode 100644
index 0000000..ca4bf28
--- /dev/null
+++ b/src/components/Fleets/Status/VehiclesTab.jsx
@@ -0,0 +1,21 @@
+import React from "react";
+import { useParams } from "react-router";
+import clsx from "clsx";
+import { Typography } from "@material-ui/core";
+
+import FleetVehiclesTable from "./Vehicles/Table";
+import useStyles from "../../useStyles";
+
+const FleetVehiclesTab = () => {
+ const { name } = useParams();
+ const classes = useStyles();
+
+ return (
+
+ Vehicles
+
+
+ );
+};
+
+export default FleetVehiclesTab;
diff --git a/src/components/Fleets/Status/VehiclesTab.test.jsx b/src/components/Fleets/Status/VehiclesTab.test.jsx
new file mode 100644
index 0000000..5284c39
--- /dev/null
+++ b/src/components/Fleets/Status/VehiclesTab.test.jsx
@@ -0,0 +1,31 @@
+jest.mock("../../Contexts/FleetContext");
+jest.mock("../../Contexts/StatusContext");
+jest.mock("../../Contexts/UserContext");
+jest.mock('@material-ui/core/utils/unstable_useId', () =>
+ jest.fn().mockReturnValue('mui-test-id'),
+);
+
+import { render, waitFor } from "@testing-library/react";
+import { BrowserRouter } from "react-router-dom";
+
+import { setToken } from "../../Contexts/UserContext";
+import { TEST_AUTH_OBJECT } from "../../../utils/testing";
+import VehiclesTab from "./VehiclesTab"
+
+const renderVehiclesTab = async () => {
+ const { container } = render(
+
+
+
+ );
+ await waitFor(() => { });
+ return container;
+};
+
+describe("VehiclesTab", () => {
+ it("Render", async () => {
+ setToken(TEST_AUTH_OBJECT);
+ const container = await renderVehiclesTab();
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/src/components/Fleets/Status/__snapshots__/VehiclesTab.test.jsx.snap b/src/components/Fleets/Status/__snapshots__/VehiclesTab.test.jsx.snap
new file mode 100644
index 0000000..ecd8f29
--- /dev/null
+++ b/src/components/Fleets/Status/__snapshots__/VehiclesTab.test.jsx.snap
@@ -0,0 +1,323 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VehiclesTab Render 1`] = `
+
+`;
diff --git a/src/components/Fleets/Status/__snapshots__/index.test.jsx.snap b/src/components/Fleets/Status/__snapshots__/index.test.jsx.snap
new file mode 100644
index 0000000..d5c1309
--- /dev/null
+++ b/src/components/Fleets/Status/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,414 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FleetStatus Render 1`] = `
+
+`;
diff --git a/src/components/Fleets/Status/index.jsx b/src/components/Fleets/Status/index.jsx
new file mode 100644
index 0000000..7d70f3c
--- /dev/null
+++ b/src/components/Fleets/Status/index.jsx
@@ -0,0 +1,76 @@
+import React, { useEffect } from "react";
+import { useParams } from "react-router";
+import { useLocation } from "react-router-dom";
+import clsx from "clsx";
+import { Box, Tab, Tabs } from "@material-ui/core";
+
+import FleetVehiclesTab from "./VehiclesTab";
+import TabPanel from "../../Controls/TabPanel";
+import { useStatusContext } from "../../Contexts/StatusContext";
+import useStyles from "../../useStyles";
+
+const tabHashes = [
+ "updates",
+ "filters"
+]
+
+const FleetStatus = () => {
+ const { name } = useParams();
+ const classes = useStyles();
+ const { setTitle, setSitePath } = useStatusContext();
+ const { hash } = useLocation();
+ const [tabIndex, setTabIndex] = React.useState(0);
+
+ useEffect(() => {
+ const key = hash.replace("#", "")
+ const index = tabHashes.findIndex(element => element === key);
+ if (index >= 0) setTabIndex(index);
+ }, [hash]);
+
+ useEffect(() => {
+ const title = `Fleet ${name} Details`;
+ setTitle(title);
+ setSitePath([
+ {
+ label: "Fleets",
+ link: "/fleets",
+ },
+ {
+ label: title,
+ },
+ ]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [name]);
+
+ const handleTabChange = (event, newIndex) => {
+ setTabIndex(newIndex);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* */}
+
+
+ );
+};
+
+function tabProps(index) {
+ return {
+ id: `tab-${index}`,
+ "aria-controls": `tabpanel-${index}`
+ };
+}
+
+export default FleetStatus;
diff --git a/src/components/Fleets/Status/index.test.jsx b/src/components/Fleets/Status/index.test.jsx
new file mode 100644
index 0000000..4c5f52f
--- /dev/null
+++ b/src/components/Fleets/Status/index.test.jsx
@@ -0,0 +1,39 @@
+jest.mock("../../Contexts/FleetContext");
+jest.mock("../../Contexts/StatusContext");
+jest.mock("../../Contexts/UserContext");
+jest.mock('@material-ui/core/utils/unstable_useId', () =>
+ jest.fn().mockReturnValue('mui-test-id'),
+);
+
+import { render, waitFor } from "@testing-library/react";
+import { BrowserRouter } from "react-router-dom";
+
+import { FleetProvider } from "../../Contexts/FleetContext";
+import { StatusProvider } from "../../Contexts/StatusContext";
+import { UserProvider, setToken } from "../../Contexts/UserContext";
+import { TEST_AUTH_OBJECT } from "../../../utils/testing";
+import FleetStatus from "./index"
+
+const renderCarStatus = async () => {
+ const { container } = render(
+
+
+
+
+
+
+
+
+
+ );
+ await waitFor(() => { });
+ return container;
+};
+
+describe("FleetStatus", () => {
+ it("Render", async () => {
+ setToken(TEST_AUTH_OBJECT);
+ const container = await renderCarStatus();
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/src/components/Fleets/Table/__snapshots__/index.test.jsx.snap b/src/components/Fleets/Table/__snapshots__/index.test.jsx.snap
index e7a7815..425c94c 100644
--- a/src/components/Fleets/Table/__snapshots__/index.test.jsx.snap
+++ b/src/components/Fleets/Table/__snapshots__/index.test.jsx.snap
@@ -240,7 +240,7 @@ exports[`FleetTable Render 1`] = `
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
US-WEST
@@ -258,7 +258,7 @@ exports[`FleetTable Render 1`] = `
- 0
+ 3
|
US-CENTRAL
@@ -313,7 +313,7 @@ exports[`FleetTable Render 1`] = `
|
- 0
+ 3
|
US-EAST
@@ -368,7 +368,7 @@ exports[`FleetTable Render 1`] = `
|
- 0
+ 3
|
{
const [orderBy, setOrderBy] = useState("id");
const [order, setOrder] = useState("desc");
const classes = useStyles();
- const { setMessage, setTitle } = useStatusContext();
+ const { setMessage, setSitePath, setTitle } = useStatusContext();
const { fleets, totalFleets, getFleets, deleteFleet } = useFleetContext();
const { token: { idToken: { jwtToken: token } }, groups } = useUserContext();
useEffect(() => {
setTitle("Fleets");
+ setSitePath([]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
(async () => {
try {
if (!token) return;
@@ -131,7 +136,7 @@ const MainForm = ({ vin }) => {
}
if (hasRole([Roles.DELETE], groups)) {
actions.push({
- tip: `Delete ""${row.name}""`,
+ tip: `Delete "${row.name}"`,
id: row.name,
icon:
})
@@ -184,7 +189,7 @@ const MainForm = ({ vin }) => {
{fleets.map((row) => (
- {row.name}
+ {row.name}
{row.canbus.enabled ? "true" : "false"}
{row.log_level}
diff --git a/src/components/Routes/SiteRoutes.jsx b/src/components/Routes/SiteRoutes.jsx
index 0880085..4aabca3 100644
--- a/src/components/Routes/SiteRoutes.jsx
+++ b/src/components/Routes/SiteRoutes.jsx
@@ -14,8 +14,10 @@ const CarStatus = React.lazy(() => import("../Cars/Status"));
const CarUpdateStatus = React.lazy(() => import("../Cars/UpdateStatus"));
const Datascope = React.lazy(() => import("../Datascope/Home"));
const FleetsList = React.lazy(() => import("../Fleets/Table"));
+const FleetStatus = React.lazy(() => import("../Fleets/Status"));
const FleetAddForm = React.lazy(() => import("../Fleets/Add"));
const FleetUpdateForm = React.lazy(() => import("../Fleets/Update"))
+const FleetAddVehicleForm = React.lazy(() => import("../Fleets/Status/Vehicles/Add"))
const Home = React.lazy(() => import("../Home"));
const Manifests = React.lazy(() => import("../Manifest/List"));
const ManifestDeploy = React.lazy(() => import("../Manifest/Deploy"));
@@ -78,6 +80,22 @@ const SiteRoutes = () => {
groups={groups}
roles={[Roles.READ, Roles.CREATE]}
/>
+ }
+ type={TYPES.PROTECTED}
+ token={token}
+ groups={groups}
+ roles={[Roles.READ, Roles.CREATE]}
+ />
+ }
+ type={TYPES.PROTECTED}
+ token={token}
+ groups={groups}
+ roles={[Roles.READ, Roles.CREATE]}
+ />
}
diff --git a/src/services/__mocks__/fleetsAPI.js b/src/services/__mocks__/fleetsAPI.js
index 9f4376b..2ca7cf0 100644
--- a/src/services/__mocks__/fleetsAPI.js
+++ b/src/services/__mocks__/fleetsAPI.js
@@ -1,27 +1,57 @@
-const data = [
- { name: "US-WEST", log_level: "info", canbus: { enabled: true } },
- { name: "US-CENTRAL", log_level: "warn", canbus: { enabled: false } },
- { name: "US-EAST", log_level: "error", canbus: { enabled: true } },
+const fleets = [
+ {
+ name: "US-WEST",
+ log_level: "info",
+ canbus: { enabled: true },
+ vehicles: ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"]
+ },
+ {
+ name: "US-CENTRAL",
+ log_level: "warn",
+ canbus: { enabled: false },
+ vehicles: ["USCENTVIN12345678", "USCENTVIN12345679", "USCENTVIN12345670"]
+ },
+ {
+ name: "US-EAST",
+ log_level: "error",
+ canbus: { enabled: true },
+ vehicles: ["USEASTVIN12345678", "USEASTVIN12345679", "USEASTVIN12345670"]
+ },
];
+const vehicles = ["USWESTVIN12345678", "USWESTVIN12345679", "USWESTVIN12345670"];
+
const fleetsAPI = {
addFleet: async (fleet, token) => {
- data.push(fleet);
+ fleets.push(fleet);
return fleet;
},
getFleets: async (search, token) => {
- return { data };
+ return {data: fleets};
},
updateFleet: async (name, fleet, token) => {
- const index = data.findIndex(element => element.name === name);
- if (index >= 0) data[index] = fleet;
+ const index = fleets.findIndex(element => element.name === name);
+ if (index >= 0) fleets[index] = fleet;
return fleet;
},
deleteFleet: async (name, token) => {
- const index = data.findIndex(element => element.name === name);
- if (index >= 0) data.splice(index, 1);
+ const index = fleets.findIndex(element => element.name === name);
+ if (index >= 0) fleets.splice(index, 1);
return name;
},
+
+ getFleetVehicles: async (name, search, token) => {
+ return {data: vehicles};
+ },
+ addFleetVehicle: async (name, vehicle, token) => {
+ vehicles.push(vehicle.vin);
+ return vehicle;
+ },
+ deleteFleetVehicle: async (name, vehicle, token) => {
+ const index = vehicles.findIndex(element => element === vehicle.vin);
+ if (index >= 0) vehicles.splice(index, 1);
+ return vehicle;
+ }
};
export default fleetsAPI;
diff --git a/src/services/fleetsAPI.js b/src/services/fleetsAPI.js
index 36e58c6..60100fa 100644
--- a/src/services/fleetsAPI.js
+++ b/src/services/fleetsAPI.js
@@ -44,6 +44,34 @@ const fleetsAPI = {
getAuthHeaderOptions(token)
)
}).then(fetchRespHandler),
+
+ getFleetVehicles: async (name, search, token) =>
+ fetch(addQueryParams(`${API_ENDPOINT}/fleet/${name}/vehicles`, search), {
+ method: "GET",
+ headers: Object.assign(
+ { "Content-Type": "application/json" },
+ getAuthHeaderOptions(token)
+ )
+ }).then(fetchRespHandler),
+
+ addFleetVehicle: async (name, vehicle, token) =>
+ fetch(`${API_ENDPOINT}/fleet/${name}/vehicle`, {
+ method: "POST",
+ headers: Object.assign(
+ { "Content-Type": "application/json" },
+ getAuthHeaderOptions(token)
+ ),
+ body: JSON.stringify(vehicle)
+ }).then(fetchRespHandler),
+
+ deleteFleetVehicle: async (name, vehicle, token) =>
+ fetch(`${API_ENDPOINT}/fleet/${name}/vehicle/${vehicle.vin}`, {
+ method: "DELETE",
+ headers: Object.assign(
+ { "Content-Type": "application/json" },
+ getAuthHeaderOptions(token)
+ )
+ }).then(fetchRespHandler),
};
export default fleetsAPI;
|