diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap
index 582864f..507ed57 100644
--- a/src/components/App/__snapshots__/App.test.js.snap
+++ b/src/components/App/__snapshots__/App.test.js.snap
@@ -2965,112 +2965,154 @@ exports[`App Route /package-deploy authenticated 1`] = `
class="MuiContainer-root MuiContainer-maxWidthLg"
>
-
-
-
-
- |
-
- Name
-
-
- |
-
-
- Log Level
-
-
- |
-
-
- CAN Bus Enabled
-
-
- |
-
-
- Data Logger Enabled
-
-
- |
-
-
- Vehicles
-
-
- |
-
-
- Filters
-
-
- |
-
-
-
-
- |
-
- US-WEST
-
- |
-
- info
- |
-
- true
- |
-
- true
- |
-
- 3
- |
-
- 3
- |
-
-
- |
-
- US-CENTRAL
-
- |
-
- warn
- |
-
- false
- |
-
- false
- |
-
- 3
- |
-
- 0
- |
-
-
- |
-
- US-EAST
-
- |
-
- error
- |
-
- true
- |
-
- false
- |
-
- 3
- |
-
- 0
- |
-
-
-
+
+ |
+
+ US-WEST
+
+ |
+
+ info
+ |
+
+ true
+ |
+
+ true
+ |
+
+ 3
+ |
+
+ 3
+ |
+
+
+ |
+
+ US-CENTRAL
+
+ |
+
+ warn
+ |
+
+ false
+ |
+
+ false
+ |
+
+ 3
+ |
+
+ 0
+ |
+
+
+ |
+
+ US-EAST
+
+ |
+
+ error
+ |
+
+ true
+ |
+
+ false
+ |
+
+ 3
+ |
+
+ 0
+ |
+
+
+
-
+
+
+
+
+
diff --git a/src/components/Fleets/Table/index.jsx b/src/components/Fleets/Table/index.jsx
index 2ffccf9..2a34897 100644
--- a/src/components/Fleets/Table/index.jsx
+++ b/src/components/Fleets/Table/index.jsx
@@ -1,66 +1,26 @@
-import React, { useEffect, useState } from "react";
-import { Link } from 'react-router-dom';
-import {
- Grid,
- Table,
- TableBody,
- TableCell,
- TableFooter,
- TablePagination,
- TableRow
-} from "@material-ui/core";
+import React, {useEffect, useState} from "react";
+import {Link} from 'react-router-dom';
+import {Grid,} from "@material-ui/core";
import AddCircleIcon from "@material-ui/icons/AddCircle";
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 {useUserContext} from "../../Contexts/UserContext"
+import {useStatusContext} from "../../Contexts/StatusContext";
+import {FleetProvider} from "../../Contexts/FleetContext"
import useStyles from "../../useStyles";
import SearchField from "../../Controls/SearchField";
-import { logger } from "../../../services/monitoring";
-import {useLocalStorage} from "../../useLocalStorage";
+import FleetSelectionTable from "../../Controls/FleetSelectionTable";
-const tableColumns = [
- {
- id: "name",
- label: "Name"
- },
- {
- id: "log_level",
- label: "Log Level"
- },
- {
- id: "canbus_enabled",
- label: "CAN Bus Enabled"
- },
- {
- id: "data_logger_enabled",
- label: "Data Logger Enabled"
- },
- {
- id: "num_vehicles",
- label: "Vehicles"
- },
- {
- id: "num_filters",
- label: "Filters"
- }
-];
-
-const PAGE_SIZE = "FLEET_TABLE_PAGE_SIZE";
const MainForm = () => {
- const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10);
- const [pageIndex, setPageIndex] = useState(0);
- const [orderBy, setOrderBy] = useState("id");
- const [order, setOrder] = useState("desc");
const classes = useStyles();
- const { setMessage, setSitePath, setTitle } = useStatusContext();
- const { fleets, totalFleets, getFleets } = useFleetContext();
- const { token: { idToken: { jwtToken: token } } } = useUserContext();
+ const [search, setSearch] = useState("");
+ const {setSitePath, setTitle} = useStatusContext();
+ const {token: {idToken: {jwtToken: token}}} = useUserContext();
+
+ const handleSearch = (query) => {
+ setSearch(query);
+ };
useEffect(() => {
setTitle("Fleets");
@@ -68,106 +28,26 @@ const MainForm = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- useEffect(() => {
- (async () => {
- try {
- if (!token) return;
- await getFleets(
- {
- 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);
- }
- };
-
return (
-
-
+
+
-
-
+
+
-
-
-
- {fleets.map((row) => (
-
-
- {row.name}
-
- {row.log_level}
- {row.canbus.enabled ? "true" : "false"}
- {row.canbus.data_logger_enabled ? "true" : "false"}
- {!row.vehicles ? 0 : row.vehicles.length}
- {!row.canbus.filters ? 0 : row.canbus.filters.length}
-
- ))}
-
-
-
-
-
-
-
-
+
+
);
};
diff --git a/src/components/Manifest/Deploy/FleetDeployForm.jsx b/src/components/Manifest/Deploy/FleetDeployForm.jsx
new file mode 100644
index 0000000..59890d4
--- /dev/null
+++ b/src/components/Manifest/Deploy/FleetDeployForm.jsx
@@ -0,0 +1,177 @@
+import React, { useEffect, useState } from "react";
+import { useParams, Redirect } from "react-router";
+import { Button, Grid, Typography } from "@material-ui/core";
+import clsx from "clsx";
+
+import {
+ ManifestsProvider,
+ useManifestsContext,
+} from "../../Contexts/ManifestsContext";
+import {
+ CarUpdatesProvider,
+ useCarUpdatesContext,
+} from "../../Contexts/CarUpdatesContext";
+import { VehicleProvider } from "../../Contexts/VehicleContext";
+import { useUserContext } from "../../Contexts/UserContext";
+import { useStatusContext } from "../../Contexts/StatusContext";
+import useStyles from "../../useStyles";
+import SearchField from "../../Controls/SearchField";
+import
+ CarSelectionTable from "../../Controls/CarSelectionTable";
+import { logger } from "../../../services/monitoring";
+import { LocalDateTimeString } from "../../../utils/dates";
+import {FleetProvider} from "../../Contexts/FleetContext";
+
+const MainForm = () => {
+ const { manifest_id } = useParams();
+ const { getManifests, manifests, busy } = useManifestsContext();
+ const { deployCarUpdates } = useCarUpdatesContext();
+ const {
+ token: {
+ idToken: { jwtToken: token },
+ },
+ } = useUserContext();
+ const { setMessage, setTitle, setSitePath } = useStatusContext();
+ const [manifestName, setManifestName] = useState("");
+ const [version, setVersion] = useState("");
+ const [createDate, setCreateDate] = useState("");
+ const [selected, setSelected] = useState([]);
+ const [search, setSearch] = useState("");
+ const [redirect, setRedirect] = useState("");
+ const classes = useStyles();
+
+ const handleSearch = (query) => {
+ setSelected([]);
+ setSearch(query);
+ };
+
+ 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 = {
+ manifest_id: parseInt(manifest_id),
+ vins: selected,
+ };
+ await deployCarUpdates(data, token);
+ setMessage(
+ `Deployed ${manifestName} ${version} to ${selected.length} cars`
+ );
+ setRedirect(`/package-status/${manifest_id}`);
+ } catch (e) {
+ setMessage(e.message);
+ logger.warn(e.stack);
+ }
+ };
+
+ const getData = async () => {
+ try {
+ getManifests({ id: parseInt(manifest_id) }, token);
+ } catch (e) {
+ setMessage(e.message);
+ logger.warn(e.stack);
+ }
+ };
+
+ useEffect(() => {
+ getData();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [token]);
+
+ useEffect(() => {
+ const title = `Deploy ${manifestName} ${version}`;
+ setTitle(title);
+ setSitePath([
+ {
+ label: "Deployments",
+ link: "/packages",
+ },
+ {
+ label: title,
+ },
+ ]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [manifestName, version]);
+
+ useEffect(() => {
+ if (!manifests || manifests.length === 0) return;
+ const data = manifests[0];
+
+ setManifestName(data.name);
+ setVersion(data.version);
+ setCreateDate(LocalDateTimeString(data.created));
+ }, [manifests]);
+
+ if (redirect.length > 0) {
+ return ;
+ }
+
+ return (
+
+ );
+};
+
+const FleetDeployForm = () => (
+
+
+
+
+
+
+
+);
+
+export default FleetDeployForm;
diff --git a/src/components/Manifest/Deploy/VehicleDeployForm.jsx b/src/components/Manifest/Deploy/VehicleDeployForm.jsx
new file mode 100644
index 0000000..e26f8c2
--- /dev/null
+++ b/src/components/Manifest/Deploy/VehicleDeployForm.jsx
@@ -0,0 +1,175 @@
+import React, { useEffect, useState } from "react";
+import { useParams, Redirect } from "react-router";
+import { Button, Grid, Typography } from "@material-ui/core";
+import clsx from "clsx";
+
+import {
+ ManifestsProvider,
+ useManifestsContext,
+} from "../../Contexts/ManifestsContext";
+import {
+ CarUpdatesProvider,
+ useCarUpdatesContext,
+} from "../../Contexts/CarUpdatesContext";
+import { VehicleProvider } from "../../Contexts/VehicleContext";
+import { useUserContext } from "../../Contexts/UserContext";
+import { useStatusContext } from "../../Contexts/StatusContext";
+import useStyles from "../../useStyles";
+import SearchField from "../../Controls/SearchField";
+import CarSelectionTable from "../../Controls/CarSelectionTable";
+import { logger } from "../../../services/monitoring";
+import { LocalDateTimeString } from "../../../utils/dates";
+
+const MainForm = () => {
+ const { manifest_id } = useParams();
+ const { getManifests, manifests, busy } = useManifestsContext();
+ const { deployCarUpdates } = useCarUpdatesContext();
+ const {
+ token: {
+ idToken: { jwtToken: token },
+ },
+ } = useUserContext();
+ const { setMessage, setTitle, setSitePath } = useStatusContext();
+ const [manifestName, setManifestName] = useState("");
+ const [version, setVersion] = useState("");
+ const [createDate, setCreateDate] = useState("");
+ const [selected, setSelected] = useState([]);
+ const [search, setSearch] = useState("");
+ const [redirect, setRedirect] = useState("");
+ const classes = useStyles();
+
+ const handleSearch = (query) => {
+ setSelected([]);
+ setSearch(query);
+ };
+
+ 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 = {
+ manifest_id: parseInt(manifest_id),
+ vins: selected,
+ };
+ await deployCarUpdates(data, token);
+ setMessage(
+ `Deployed ${manifestName} ${version} to ${selected.length} cars`
+ );
+ setRedirect(`/package-status/${manifest_id}`);
+ } catch (e) {
+ setMessage(e.message);
+ logger.warn(e.stack);
+ }
+ };
+
+ const getData = async () => {
+ try {
+ getManifests({ id: parseInt(manifest_id) }, token);
+ } catch (e) {
+ setMessage(e.message);
+ logger.warn(e.stack);
+ }
+ };
+
+ useEffect(() => {
+ getData();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [token]);
+
+ useEffect(() => {
+ const title = `Deploy ${manifestName} ${version}`;
+ setTitle(title);
+ setSitePath([
+ {
+ label: "Deployments",
+ link: "/packages",
+ },
+ {
+ label: title,
+ },
+ ]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [manifestName, version]);
+
+ useEffect(() => {
+ if (!manifests || manifests.length === 0) return;
+ const data = manifests[0];
+
+ setManifestName(data.name);
+ setVersion(data.version);
+ setCreateDate(LocalDateTimeString(data.created));
+ }, [manifests]);
+
+ if (redirect.length > 0) {
+ return ;
+ }
+
+ return (
+
+ );
+};
+
+const VehicleDeployForm = () => (
+
+
+
+
+
+
+
+);
+
+export default VehicleDeployForm;
diff --git a/src/components/Manifest/Deploy/index.jsx b/src/components/Manifest/Deploy/index.jsx
index 23318c8..eeb825b 100644
--- a/src/components/Manifest/Deploy/index.jsx
+++ b/src/components/Manifest/Deploy/index.jsx
@@ -1,35 +1,37 @@
-import React, { useEffect, useState } from "react";
-import { useParams, Redirect } from "react-router";
-import { Button, Grid, Typography } from "@material-ui/core";
+import React, {useEffect, useState} from "react";
+import {Redirect, useParams} from "react-router";
+import {Button, FormControlLabel, Grid, Switch, Typography} from "@material-ui/core";
import clsx from "clsx";
-import {
- ManifestsProvider,
- useManifestsContext,
-} from "../../Contexts/ManifestsContext";
-import {
- CarUpdatesProvider,
- useCarUpdatesContext,
-} from "../../Contexts/CarUpdatesContext";
-import { VehicleProvider } from "../../Contexts/VehicleContext";
-import { useUserContext } from "../../Contexts/UserContext";
-import { useStatusContext } from "../../Contexts/StatusContext";
+import {ManifestsProvider, useManifestsContext,} from "../../Contexts/ManifestsContext";
+import {CarUpdatesProvider, useCarUpdatesContext,} from "../../Contexts/CarUpdatesContext";
+import {VehicleProvider} from "../../Contexts/VehicleContext";
+import {useUserContext} from "../../Contexts/UserContext";
+import {useStatusContext} from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import SearchField from "../../Controls/SearchField";
import CarSelectionTable from "../../Controls/CarSelectionTable";
-import { logger } from "../../../services/monitoring";
-import { LocalDateTimeString } from "../../../utils/dates";
+import {logger} from "../../../services/monitoring";
+import {LocalDateTimeString} from "../../../utils/dates";
+import FleetSelectionTable from "../../Controls/FleetSelectionTable";
+import {FleetProvider} from "../../Contexts/FleetContext";
+
+
+const CAR_UPDATE = false;
+const FLEET_UPDATE = true;
const MainForm = () => {
- const { manifest_id } = useParams();
- const { getManifests, manifests, busy } = useManifestsContext();
- const { deployCarUpdates } = useCarUpdatesContext();
+ const [updateType, setUpdateType] = useState(CAR_UPDATE);
+
+ const {manifest_id} = useParams();
+ const {getManifests, manifests, busy} = useManifestsContext();
+ const {deployCarUpdates, deployFleetUpdates} = useCarUpdatesContext();
const {
token: {
- idToken: { jwtToken: token },
+ idToken: {jwtToken: token},
},
} = useUserContext();
- const { setMessage, setTitle, setSitePath } = useStatusContext();
+ const {setMessage, setTitle, setSitePath} = useStatusContext();
const [manifestName, setManifestName] = useState("");
const [version, setVersion] = useState("");
const [createDate, setCreateDate] = useState("");
@@ -38,6 +40,11 @@ const MainForm = () => {
const [redirect, setRedirect] = useState("");
const classes = useStyles();
+ const handleChange = (_) => {
+ setSelected([]);
+ setUpdateType(updateType === CAR_UPDATE ? FLEET_UPDATE : CAR_UPDATE);
+ }
+
const handleSearch = (query) => {
setSelected([]);
setSearch(query);
@@ -54,7 +61,7 @@ const MainForm = () => {
newSelected = [...selected];
newSelected.push(key);
} else {
- newSelected = selected.filter((vin) => vin !== key);
+ newSelected = selected.filter((vinOrName) => vinOrName !== key);
}
setSelected(newSelected);
} catch (e) {
@@ -67,9 +74,14 @@ const MainForm = () => {
event.preventDefault();
const data = {
manifest_id: parseInt(manifest_id),
- vins: selected,
- };
- await deployCarUpdates(data, token);
+ }
+ if (updateType === CAR_UPDATE) {
+ data.vins = selected;
+ await deployCarUpdates(data, token);
+ } else {
+ data.fleet_names = selected;
+ await deployFleetUpdates(data, token);
+ }
setMessage(
`Deployed ${manifestName} ${version} to ${selected.length} cars`
);
@@ -82,7 +94,7 @@ const MainForm = () => {
const getData = async () => {
try {
- getManifests({ id: parseInt(manifest_id) }, token);
+ getManifests({id: parseInt(manifest_id)}, token);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
@@ -119,7 +131,7 @@ const MainForm = () => {
}, [manifests]);
if (redirect.length > 0) {
- return ;
+ return ;
}
return (
@@ -127,13 +139,21 @@ const MainForm = () => {
);
};
const ManifestDeployForm = () => (
-
-
-
-
-
-
-
+
+
+
+
+
);
export default ManifestDeployForm;
diff --git a/src/services/__mocks__/updatesAPI.js b/src/services/__mocks__/updatesAPI.js
index 58c3ca6..e0efe59 100644
--- a/src/services/__mocks__/updatesAPI.js
+++ b/src/services/__mocks__/updatesAPI.js
@@ -5,6 +5,12 @@ const updatesAPI = {
return data;
},
+ createFleetUpdates: async (data, token) => {
+ if (!data.id) data.id = 0;
+ data.id++;
+ return data;
+ },
+
getCarUpdates: async (filter, token) => {
return { data: [] };
},
diff --git a/src/services/updatesAPI.js b/src/services/updatesAPI.js
index e5c56f7..42731a1 100644
--- a/src/services/updatesAPI.js
+++ b/src/services/updatesAPI.js
@@ -7,18 +7,23 @@ import {
const API_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL;
+const createDeployUpdatesClosure = (suffix) => {
+ return async (data, token) => fetch(`${API_ENDPOINT}/${suffix}`, {
+ method: "POST",
+ headers: Object.assign(
+ { "Content-Type": "application/json" },
+ getAuthHeaderOptions(token)
+ ),
+ body: JSON.stringify(data),
+ })
+ .then(fetchRespHandler)
+ .catch(errorHandler)
+}
+
const updatesAPI = {
- createCarUpdates: async (data, token) =>
- fetch(`${API_ENDPOINT}/carupdate`, {
- method: "POST",
- headers: Object.assign(
- { "Content-Type": "application/json" },
- getAuthHeaderOptions(token)
- ),
- body: JSON.stringify(data),
- })
- .then(fetchRespHandler)
- .catch(errorHandler),
+ createFleetUpdates: createDeployUpdatesClosure("fleetupdate"),
+
+ createCarUpdates: createDeployUpdatesClosure("carupdate"),
getCarUpdateLog: async (query, token) => {
const u = addQueryParams(`${API_ENDPOINT}/carupdateslog`, query);