CEC-2144, CEC-2338 Add deploy by fleets and fix fleets table (#192)

* Add fix for fleets search

* Decompose fleets table

* Add deploy by fleets

* Add snapshots
This commit is contained in:
arpanetus
2022-08-30 03:31:26 +06:00
committed by GitHub
parent aa36b2eb91
commit 8d0dbf8030
11 changed files with 1210 additions and 663 deletions

View File

@@ -2963,9 +2963,6 @@ exports[`App Route /package-deploy authenticated 1`] = `
/>
<main
class="MuiContainer-root MuiContainer-maxWidthLg"
>
<div
data-testid="mocked-vehicleprovider"
>
<div
data-testid="mocked-manifestsprovider"
@@ -2992,7 +2989,7 @@ exports[`App Route /package-deploy authenticated 1`] = `
class="MuiGrid-root makeStyles-root-0 MuiGrid-container MuiGrid-spacing-xs-2"
>
<div
class="MuiGrid-root MuiGrid-item MuiGrid-grid-md-4"
class="MuiGrid-root MuiGrid-item MuiGrid-grid-md-2"
>
<div
class="makeStyles-labelInline-0"
@@ -3000,6 +2997,48 @@ exports[`App Route /package-deploy authenticated 1`] = `
0 Selected
</div>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-2"
>
<label
class="MuiFormControlLabel-root"
>
<span
class="MuiSwitch-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-0 MuiSwitch-switchBase MuiSwitch-colorSecondary PrivateSwitchBase-checked-0 Mui-checked"
>
<span
class="MuiIconButton-label"
>
<input
aria-label="controlled"
checked=""
class="PrivateSwitchBase-input-0 MuiSwitch-input"
type="checkbox"
value=""
/>
<span
class="MuiSwitch-thumb"
/>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiSwitch-track"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Car(default) or Fleet
</span>
</label>
</div>
<div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-4"
>
@@ -3071,6 +3110,9 @@ exports[`App Route /package-deploy authenticated 1`] = `
</button>
</div>
</div>
<div
data-testid="mocked-vehicleprovider"
>
<div
class="makeStyles-paper-0 makeStyles-tableSize-0"
>
@@ -3393,8 +3435,8 @@ exports[`App Route /package-deploy authenticated 1`] = `
</tfoot>
</table>
</div>
</form>
</div>
</form>
</div>
</div>
</div>

View File

@@ -6,7 +6,8 @@ import { validateStatusMessage } from "../../utils/statusMessage";
const FINAL_UPDATE_STATES = ["package_install_complete"];
const CarUpdatesContext = React.createContext();
const validateDeployCarUpdates = (data) => {
const validateDeployClosure = (data, propertyName, errPfx) => {
if (data === null) {
throw new Error("No car update data");
}
@@ -15,11 +16,21 @@ const validateDeployCarUpdates = (data) => {
throw new Error("Manifest id required");
}
if (!data.vins || data.vins.length === 0) {
throw new Error("Cars are required");
const { [propertyName]: value } = data;
if (!value || value.length === 0) {
throw new Error(`${errPfx} are required`);
}
}
const validateDeployCarUpdates = (data) => {
return validateDeployClosure(data, 'vins', 'Cars')
};
const validateDeployFleetUpdates = (data) => {
return validateDeployClosure(data, 'fleet_names', 'Fleets')
};
export const CarUpdatesProvider = ({ children }) => {
const [busy, setBusy] = useState(false);
const [carUpdates, setCarUpdates] = useState([]);
@@ -43,6 +54,22 @@ export const CarUpdatesProvider = ({ children }) => {
return result;
};
const deployFleetUpdates = async (data, token) => {
let result;
try {
setBusy(true);
validateDeployFleetUpdates(data);
result = await api.createFleetUpdates(data, token);
if (result.error)
throw new Error(`Deploy fleet updates error. ${result.message}`);
} finally {
setBusy(false);
}
return result;
}
const getCarUpdates = async (search, token) => {
let result;
@@ -204,6 +231,7 @@ export const CarUpdatesProvider = ({ children }) => {
carUpdates,
totalCarUpdates,
deployCarUpdates,
deployFleetUpdates,
getCarUpdates,
getLog,
getVINUpdates,

View File

@@ -60,6 +60,7 @@ const CarSelectionTable = (props) => {
onSelect,
onSelectAll,
} = props;
const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10);
const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("vin");
@@ -67,7 +68,8 @@ const CarSelectionTable = (props) => {
const { getVehicles, vehicles, totalVehicles } = useVehicleContext();
const { setMessage } = useStatusContext();
const { search: searchTerm } = search;
const sortHandler = (_event, property) => {
const handleSort = (_event, property) => {
if (property === orderBy) {
if (order === "asc") {
setOrder("desc");
@@ -137,7 +139,7 @@ const CarSelectionTable = (props) => {
orderBy={orderBy}
order={order}
columnData={tableColumns}
onSortRequest={sortHandler}
onSortRequest={handleSort}
multiSelect={multiSelect}
onSelectAll={handleSelectAll}
selectCount={selected ? selected.length : 0}

View File

@@ -0,0 +1,199 @@
import React, {useEffect, useState} from "react";
import {Link} from 'react-router-dom';
import {Checkbox, Table, TableBody, TableCell, TableFooter, TablePagination, TableRow} from "@material-ui/core";
import clsx from "clsx";
import TableHeaderSortable from "../../Table/HeaderSortable";
import {useStatusContext} from "../../Contexts/StatusContext";
import {useFleetContext} from "../../Contexts/FleetContext"
import {logger} from "../../../services/monitoring";
import {useLocalStorage} from "../../useLocalStorage";
import PropTypes from "prop-types";
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_SELECTION_TABLE_PAGE_SIZE";
const FleetSelectionTable = (props) => {
const {
token,
classes,
search,
multiSelect,
selected,
onSelect,
onSelectAll,
} = props;
const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10);
const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("id");
const [order, setOrder] = useState("desc");
const { fleets, getFleets, totalFleets } = useFleetContext();
const { setMessage } = useStatusContext();
const { search: searchTerm } = search;
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 handleChangePageIndex = (_event, newIndex) => {
setPageIndex(newIndex);
};
const handleChangePageSize = (event) => {
setPageSize(parseInt(event.target.value, 10));
setPageIndex(0);
};
const handleSelectAll = (event) => {
if (!onSelectAll) return;
const newSelected = [];
if (event.target.checked) {
fleets.forEach((car) => {
newSelected.push(car.name);
});
}
onSelectAll(newSelected);
};
const handleSelect = (event, key) => {
if (!onSelect) return;
onSelect(event, key);
};
useEffect(() => {
const options = { search: searchTerm,
limit: pageSize,
offset: pageSize * pageIndex,
order: `${orderBy} ${order}`,
};
(async () => {
try {
if (!token) return;
await getFleets(options, token);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token, pageIndex, pageSize, orderBy, order, searchTerm]);
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Table>
<TableHeaderSortable
classes={classes}
orderBy={orderBy}
order={order}
columnData={tableColumns}
onSortRequest={handleSort}
multiSelect={multiSelect}
onSelectAll={handleSelectAll}
selectCount={selected ? selected.length : 0}
rowCount={fleets ? fleets.length : 0}
/>
<TableBody>
{fleets.map((row) => {
const isSelected = selected
? selected.indexOf(row.name) !== -1
: false;
return (
<TableRow key={row.name}>
{multiSelect && (
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
onChange={(event) => handleSelect(event, row.name)}
/>
</TableCell>
)}
<TableCell align="center">
<Link to={`/fleet/${row.name}`}>{row.name}</Link>
</TableCell>
<TableCell align="center">{row.log_level}</TableCell>
<TableCell align="center">{row.canbus.enabled ? "true" : "false"}</TableCell>
<TableCell align="center">{row.canbus.data_logger_enabled ? "true" : "false"}</TableCell>
<TableCell align="center">{!row.vehicles ? 0 : row.vehicles.length}</TableCell>
<TableCell align="center">{!row.canbus.filters ? 0 : row.canbus.filters.length}</TableCell>
</TableRow>)
}
)}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={8}
count={totalFleets}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: {"aria-label": "rows per page"},
native: true,
}}
onPageChange={handleChangePageIndex}
onRowsPerPageChange={handleChangePageSize}
/>
</TableRow>
</TableFooter>
</Table>
</div>
);
};
FleetSelectionTable.propTypes = {
token: PropTypes.string.isRequired,
classes: PropTypes.object.isRequired,
search: PropTypes.object.isRequired,
multiSelect: PropTypes.bool.isRequired,
selected: PropTypes.array,
onSelect: PropTypes.func,
onSelectAll: PropTypes.func,
connectionStatus: PropTypes.bool,
}
export default FleetSelectionTable;

View File

@@ -24,7 +24,6 @@ exports[`FleetTable Render 1`] = `
class="MuiGrid-root makeStyles-textJustifyAlign-0 MuiGrid-item MuiGrid-grid-md-4"
>
<a
class="makeStyles-labelInline-0"
href="/fleet-add"
>
<svg
@@ -40,7 +39,6 @@ exports[`FleetTable Render 1`] = `
</a>
</div>
<div
align="right"
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-4"
>
<div
@@ -98,6 +96,9 @@ exports[`FleetTable Render 1`] = `
class="MuiGrid-root makeStyles-textRightAlign-0 MuiGrid-item MuiGrid-grid-md-4"
/>
</div>
<div
class="makeStyles-paper-0 makeStyles-tableSize-0"
>
<table
class="MuiTable-root"
>
@@ -495,4 +496,5 @@ exports[`FleetTable Render 1`] = `
</div>
</div>
</div>
</div>
`;

View File

@@ -1,172 +1,52 @@
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 {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 {useUserContext} from "../../Contexts/UserContext"
import {useStatusContext} from "../../Contexts/StatusContext";
import { FleetProvider, useFleetContext } from "../../Contexts/FleetContext"
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 [search, setSearch] = useState("");
const {setSitePath, setTitle} = useStatusContext();
const {token: {idToken: {jwtToken: token}}} = useUserContext();
const handleSearch = (query) => {
setSearch(query);
};
useEffect(() => {
setTitle("Fleets");
setSitePath([]);
// 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 (
<div className={clsx(classes.paper, classes.tableSize)}>
<Grid container className={classes.root} spacing={2}>
<Grid item md={4} className={classes.textJustifyAlign}>
<Link to={"/fleet-add"} className={classes.labelInline}>
<Link to={"/fleet-add"}>
<AddCircleIcon fontSize="large"/>
</Link>
</Grid>
<Grid item md={4} align="right" className={classes.textCenterAlign}>
<SearchField classes={classes} />
<Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch}/>
</Grid>
<Grid item md={4} className={classes.textRightAlign}></Grid>
</Grid>
<Table>
<TableHeaderSortable
<FleetSelectionTable
token={token}
classes={classes}
orderBy={orderBy}
order={order}
columnData={tableColumns}
onSortRequest={handleSort}
search={{search}}
multiSelect={false}
/>
<TableBody>
{fleets.map((row) => (
<TableRow key={row.name}>
<TableCell align="center">
<Link to={`/fleet/${row.name}`}>{row.name}</Link>
</TableCell>
<TableCell align="center">{row.log_level}</TableCell>
<TableCell align="center">{row.canbus.enabled ? "true" : "false"}</TableCell>
<TableCell align="center">{row.canbus.data_logger_enabled ? "true" : "false"}</TableCell>
<TableCell align="center">{!row.vehicles ? 0 : row.vehicles.length}</TableCell>
<TableCell align="center">{!row.canbus.filters ? 0 : row.canbus.filters.length}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={8}
count={totalFleets}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onPageChange={handleChangePageIndex}
onRowsPerPageChange={handleChangePageSize}
/>
</TableRow>
</TableFooter>
</Table>
</div>
);
};

View File

@@ -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 <Redirect to={redirect} />;
}
return (
<div className={classes.paper}>
<form className={classes.form} noValidate action="{onSubmit}">
<Typography variant="body2">Created {createDate}.</Typography>
<Grid container className={classes.root} spacing={2}>
<Grid item md={4}>
<div
className={classes.labelInline}
>{`${selected.length} Selected`}</div>
</Grid>
<Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} />
</Grid>
<Grid item md={4} className={classes.textRightAlign}>
<Button
type="submit"
disabled={busy || selected.length === 0}
variant="contained"
color="primary"
className={clsx(classes.formControl, classes.textField)}
onClick={onSubmit}
>
{busy ? "Deploying..." : "Deploy"}
</Button>
</Grid>
</Grid>
<CarSelectionTable
classes={classes}
token={token}
multiSelect={true}
search={{ search }}
selected={selected}
onSelect={handleSelect}
onSelectAll={handleSelectAll}
/>
</form>
</div>
);
};
const FleetDeployForm = () => (
<FleetProvider>
<ManifestsProvider>
<CarUpdatesProvider>
<MainForm />
</CarUpdatesProvider>
</ManifestsProvider>
</FleetProvider>
);
export default FleetDeployForm;

View File

@@ -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 <Redirect to={redirect} />;
}
return (
<div className={classes.paper}>
<form className={classes.form} noValidate action="{onSubmit}">
<Typography variant="body2">Created {createDate}.</Typography>
<Grid container className={classes.root} spacing={2}>
<Grid item md={4}>
<div
className={classes.labelInline}
>{`${selected.length} Selected`}</div>
</Grid>
<Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} />
</Grid>
<Grid item md={4} className={classes.textRightAlign}>
<Button
type="submit"
disabled={busy || selected.length === 0}
variant="contained"
color="primary"
className={clsx(classes.formControl, classes.textField)}
onClick={onSubmit}
>
{busy ? "Deploying..." : "Deploy"}
</Button>
</Grid>
</Grid>
<CarSelectionTable
classes={classes}
token={token}
multiSelect={true}
search={{ search }}
selected={selected}
onSelect={handleSelect}
onSelectAll={handleSelectAll}
/>
</form>
</div>
);
};
const VehicleDeployForm = () => (
<VehicleProvider>
<ManifestsProvider>
<CarUpdatesProvider>
<MainForm />
</CarUpdatesProvider>
</ManifestsProvider>
</VehicleProvider>
);
export default VehicleDeployForm;

View File

@@ -1,16 +1,10 @@
import React, {useEffect, useState} from "react";
import { useParams, Redirect } from "react-router";
import { Button, Grid, Typography } from "@material-ui/core";
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 {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";
@@ -19,11 +13,19 @@ import SearchField from "../../Controls/SearchField";
import CarSelectionTable from "../../Controls/CarSelectionTable";
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 [updateType, setUpdateType] = useState(CAR_UPDATE);
const {manifest_id} = useParams();
const {getManifests, manifests, busy} = useManifestsContext();
const { deployCarUpdates } = useCarUpdatesContext();
const {deployCarUpdates, deployFleetUpdates} = useCarUpdatesContext();
const {
token: {
idToken: {jwtToken: token},
@@ -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,
};
}
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`
);
@@ -127,11 +139,19 @@ const MainForm = () => {
<form className={classes.form} noValidate action="{onSubmit}">
<Typography variant="body2">Created {createDate}.</Typography>
<Grid container className={classes.root} spacing={2}>
<Grid item md={4}>
<Grid item md={2}>
<div
className={classes.labelInline}
>{`${selected.length} Selected`}</div>
</Grid>
<Grid item md={2} className={classes.textCenterAlign}>
<FormControlLabel control={<Switch
checked={updateType === CAR_UPDATE}
onChange={handleChange}
inputProps={{'aria-label': 'controlled'}}
/>} label="Car(default) or Fleet"/>
</Grid>
<Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch}/>
</Grid>
@@ -148,6 +168,8 @@ const MainForm = () => {
</Button>
</Grid>
</Grid>
{updateType === CAR_UPDATE ?
<VehicleProvider>
<CarSelectionTable
classes={classes}
token={token}
@@ -156,20 +178,29 @@ const MainForm = () => {
selected={selected}
onSelect={handleSelect}
onSelectAll={handleSelectAll}
/>
/></VehicleProvider> :
<FleetProvider>
<FleetSelectionTable
classes={classes}
token={token}
multiSelect={true}
search={{search}}
selected={selected}
onSelect={handleSelect}
onSelectAll={handleSelectAll}/>
</FleetProvider>
}
</form>
</div>
);
};
const ManifestDeployForm = () => (
<VehicleProvider>
<ManifestsProvider>
<CarUpdatesProvider>
<MainForm/>
</CarUpdatesProvider>
</ManifestsProvider>
</VehicleProvider>
);
export default ManifestDeployForm;

View File

@@ -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: [] };
},

View File

@@ -7,9 +7,8 @@ import {
const API_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL;
const updatesAPI = {
createCarUpdates: async (data, token) =>
fetch(`${API_ENDPOINT}/carupdate`, {
const createDeployUpdatesClosure = (suffix) => {
return async (data, token) => fetch(`${API_ENDPOINT}/${suffix}`, {
method: "POST",
headers: Object.assign(
{ "Content-Type": "application/json" },
@@ -18,7 +17,13 @@ const updatesAPI = {
body: JSON.stringify(data),
})
.then(fetchRespHandler)
.catch(errorHandler),
.catch(errorHandler)
}
const updatesAPI = {
createFleetUpdates: createDeployUpdatesClosure("fleetupdate"),
createCarUpdates: createDeployUpdatesClosure("carupdate"),
getCarUpdateLog: async (query, token) => {
const u = addQueryParams(`${API_ENDPOINT}/carupdateslog`, query);