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

@@ -2965,112 +2965,154 @@ exports[`App Route /package-deploy authenticated 1`] = `
class="MuiContainer-root MuiContainer-maxWidthLg" class="MuiContainer-root MuiContainer-maxWidthLg"
> >
<div <div
data-testid="mocked-vehicleprovider" data-testid="mocked-manifestsprovider"
> >
<div <div
data-testid="mocked-manifestsprovider" data-testid="mocked-carupdatesprovider"
> >
<div <div
data-testid="mocked-carupdatesprovider" class="makeStyles-paper-0"
> >
<div <form
class="makeStyles-paper-0" action="{onSubmit}"
class="makeStyles-form-0"
novalidate=""
> >
<form <p
action="{onSubmit}" class="MuiTypography-root MuiTypography-body2"
class="makeStyles-form-0" >
novalidate="" Created
7/1/2021 10:40:07 PM
.
</p>
<div
class="MuiGrid-root makeStyles-root-0 MuiGrid-container MuiGrid-spacing-xs-2"
> >
<p
class="MuiTypography-root MuiTypography-body2"
>
Created
7/1/2021 10:40:07 PM
.
</p>
<div <div
class="MuiGrid-root makeStyles-root-0 MuiGrid-container MuiGrid-spacing-xs-2" class="MuiGrid-root MuiGrid-item MuiGrid-grid-md-2"
> >
<div <div
class="MuiGrid-root MuiGrid-item MuiGrid-grid-md-4" class="makeStyles-labelInline-0"
> >
<div 0 Selected
class="makeStyles-labelInline-0"
>
0 Selected
</div>
</div> </div>
<div </div>
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-4" <div
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-2"
>
<label
class="MuiFormControlLabel-root"
> >
<div <span
class="MuiFormControl-root makeStyles-margin-0 makeStyles-fullWidth-0" class="MuiSwitch-root"
> >
<label <span
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated" aria-disabled="false"
data-shrink="false" class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-0 MuiSwitch-switchBase MuiSwitch-colorSecondary PrivateSwitchBase-checked-0 Mui-checked"
for="search"
> >
Search <span
</label> class="MuiIconButton-label"
<div
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedEnd"
>
<input
aria-invalid="false"
class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedEnd"
id="search"
type="text"
value=""
/>
<div
class="MuiInputAdornment-root MuiInputAdornment-positionEnd"
> >
<button <input
aria-label="search" aria-label="controlled"
class="MuiButtonBase-root MuiIconButton-root" checked=""
tabindex="0" class="PrivateSwitchBase-input-0 MuiSwitch-input"
type="button" 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"
>
<div
class="MuiFormControl-root makeStyles-margin-0 makeStyles-fullWidth-0"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated"
data-shrink="false"
for="search"
>
Search
</label>
<div
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedEnd"
>
<input
aria-invalid="false"
class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedEnd"
id="search"
type="text"
value=""
/>
<div
class="MuiInputAdornment-root MuiInputAdornment-positionEnd"
>
<button
aria-label="search"
class="MuiButtonBase-root MuiIconButton-root"
tabindex="0"
type="button"
>
<span
class="MuiIconButton-label"
> >
<span <svg
class="MuiIconButton-label" aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
> >
<svg <path
aria-hidden="true" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
class="MuiSvgIcon-root" />
focusable="false" </svg>
viewBox="0 0 24 24" </span>
> <span
<path class="MuiTouchRipple-root"
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
/> </button>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<div
class="MuiGrid-root makeStyles-textRightAlign-0 MuiGrid-item MuiGrid-grid-md-4"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-formControl-0 makeStyles-textField-0 MuiButton-containedPrimary Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
type="submit"
>
<span
class="MuiButton-label"
>
Deploy
</span>
</button>
</div>
</div> </div>
<div
class="MuiGrid-root makeStyles-textRightAlign-0 MuiGrid-item MuiGrid-grid-md-4"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-formControl-0 makeStyles-textField-0 MuiButton-containedPrimary Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
type="submit"
>
<span
class="MuiButton-label"
>
Deploy
</span>
</button>
</div>
</div>
<div
data-testid="mocked-vehicleprovider"
>
<div <div
class="makeStyles-paper-0 makeStyles-tableSize-0" class="makeStyles-paper-0 makeStyles-tableSize-0"
> >
@@ -3393,8 +3435,8 @@ exports[`App Route /package-deploy authenticated 1`] = `
</tfoot> </tfoot>
</table> </table>
</div> </div>
</form> </div>
</div> </form>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@@ -60,6 +60,7 @@ const CarSelectionTable = (props) => {
onSelect, onSelect,
onSelectAll, onSelectAll,
} = props; } = props;
const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10); const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10);
const [pageIndex, setPageIndex] = useState(0); const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("vin"); const [orderBy, setOrderBy] = useState("vin");
@@ -67,7 +68,8 @@ const CarSelectionTable = (props) => {
const { getVehicles, vehicles, totalVehicles } = useVehicleContext(); const { getVehicles, vehicles, totalVehicles } = useVehicleContext();
const { setMessage } = useStatusContext(); const { setMessage } = useStatusContext();
const { search: searchTerm } = search; const { search: searchTerm } = search;
const sortHandler = (_event, property) => {
const handleSort = (_event, property) => {
if (property === orderBy) { if (property === orderBy) {
if (order === "asc") { if (order === "asc") {
setOrder("desc"); setOrder("desc");
@@ -137,7 +139,7 @@ const CarSelectionTable = (props) => {
orderBy={orderBy} orderBy={orderBy}
order={order} order={order}
columnData={tableColumns} columnData={tableColumns}
onSortRequest={sortHandler} onSortRequest={handleSort}
multiSelect={multiSelect} multiSelect={multiSelect}
onSelectAll={handleSelectAll} onSelectAll={handleSelectAll}
selectCount={selected ? selected.length : 0} 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" class="MuiGrid-root makeStyles-textJustifyAlign-0 MuiGrid-item MuiGrid-grid-md-4"
> >
<a <a
class="makeStyles-labelInline-0"
href="/fleet-add" href="/fleet-add"
> >
<svg <svg
@@ -40,7 +39,6 @@ exports[`FleetTable Render 1`] = `
</a> </a>
</div> </div>
<div <div
align="right"
class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-4" class="MuiGrid-root makeStyles-textCenterAlign-0 MuiGrid-item MuiGrid-grid-md-4"
> >
<div <div
@@ -98,397 +96,401 @@ exports[`FleetTable Render 1`] = `
class="MuiGrid-root makeStyles-textRightAlign-0 MuiGrid-item MuiGrid-grid-md-4" class="MuiGrid-root makeStyles-textRightAlign-0 MuiGrid-item MuiGrid-grid-md-4"
/> />
</div> </div>
<table <div
class="MuiTable-root" class="makeStyles-paper-0 makeStyles-tableSize-0"
> >
<thead <table
class="MuiTableHead-root" class="MuiTable-root"
> >
<tr <thead
class="MuiTableRow-root MuiTableRow-head" class="MuiTableHead-root"
> >
<th <tr
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter" class="MuiTableRow-root MuiTableRow-head"
scope="col"
> >
<span <th
aria-disabled="false" class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
class="MuiButtonBase-root MuiTableSortLabel-root" scope="col"
role="button"
tabindex="0"
> >
Name <span
<svg aria-disabled="false"
aria-hidden="true" class="MuiButtonBase-root MuiTableSortLabel-root"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc" role="button"
focusable="false" tabindex="0"
viewBox="0 0 24 24"
> >
<path Name
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Log Level
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
CAN Bus Enabled
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Data Logger Enabled
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Vehicles
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Filters
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
</tr>
</thead>
<tbody
class="MuiTableBody-root"
>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
href="/fleet/US-WEST"
>
US-WEST
</a>
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
info
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
true
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
true
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
3
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
3
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
href="/fleet/US-CENTRAL"
>
US-CENTRAL
</a>
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
warn
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
false
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
false
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
3
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
0
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
href="/fleet/US-EAST"
>
US-EAST
</a>
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
error
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
true
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
false
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
3
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
0
</td>
</tr>
</tbody>
<tfoot
class="MuiTableFooter-root"
>
<tr
class="MuiTableRow-root MuiTableRow-footer"
>
<td
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
colspan="8"
>
<div
class="MuiToolbar-root MuiToolbar-regular MuiTablePagination-toolbar MuiToolbar-gutters"
>
<div
class="MuiTablePagination-spacer"
/>
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
Rows per page:
</p>
<div
class="MuiInputBase-root MuiTablePagination-input MuiTablePagination-selectRoot"
>
<select
aria-label="rows per page"
class="MuiSelect-root MuiSelect-select MuiTablePagination-select MuiInputBase-input"
>
<option
class="MuiTablePagination-menuItem"
value="5"
>
5
</option>
<option
class="MuiTablePagination-menuItem"
value="10"
>
10
</option>
<option
class="MuiTablePagination-menuItem"
value="25"
>
25
</option>
<option
class="MuiTablePagination-menuItem"
value="100"
>
100
</option>
</select>
<svg <svg
aria-hidden="true" aria-hidden="true"
class="MuiSvgIcon-root MuiSelect-icon MuiTablePagination-selectIcon" class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false" focusable="false"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<path <path
d="M7 10l5 5 5-5z" d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/> />
</svg> </svg>
</div> </span>
<p </th>
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit" <th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
> >
1-3 of 3 Log Level
</p> <svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
CAN Bus Enabled
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Data Logger Enabled
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Vehicles
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root"
role="button"
tabindex="0"
>
Filters
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"
/>
</svg>
</span>
</th>
</tr>
</thead>
<tbody
class="MuiTableBody-root"
>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
href="/fleet/US-WEST"
>
US-WEST
</a>
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
info
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
true
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
true
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
3
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
3
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
href="/fleet/US-CENTRAL"
>
US-CENTRAL
</a>
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
warn
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
false
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
false
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
3
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
0
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
<a
href="/fleet/US-EAST"
>
US-EAST
</a>
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
error
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
true
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
false
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
3
</td>
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
0
</td>
</tr>
</tbody>
<tfoot
class="MuiTableFooter-root"
>
<tr
class="MuiTableRow-root MuiTableRow-footer"
>
<td
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
colspan="8"
>
<div <div
class="MuiTablePagination-actions" class="MuiToolbar-root MuiToolbar-regular MuiTablePagination-toolbar MuiToolbar-gutters"
> >
<button <div
aria-label="Previous page" class="MuiTablePagination-spacer"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled" />
disabled="" <p
tabindex="-1" class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
title="Previous page"
type="button"
> >
<span Rows per page:
class="MuiIconButton-label" </p>
> <div
<svg class="MuiInputBase-root MuiTablePagination-input MuiTablePagination-selectRoot"
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z"
/>
</svg>
</span>
</button>
<button
aria-label="Next page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Next page"
type="button"
> >
<span <select
class="MuiIconButton-label" aria-label="rows per page"
class="MuiSelect-root MuiSelect-select MuiTablePagination-select MuiInputBase-input"
> >
<svg <option
aria-hidden="true" class="MuiTablePagination-menuItem"
class="MuiSvgIcon-root" value="5"
focusable="false"
viewBox="0 0 24 24"
> >
<path 5
d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z" </option>
/> <option
</svg> class="MuiTablePagination-menuItem"
</span> value="10"
</button> >
10
</option>
<option
class="MuiTablePagination-menuItem"
value="25"
>
25
</option>
<option
class="MuiTablePagination-menuItem"
value="100"
>
100
</option>
</select>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSelect-icon MuiTablePagination-selectIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
</div>
<p
class="MuiTypography-root MuiTablePagination-caption MuiTypography-body2 MuiTypography-colorInherit"
>
1-3 of 3
</p>
<div
class="MuiTablePagination-actions"
>
<button
aria-label="Previous page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Previous page"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z"
/>
</svg>
</span>
</button>
<button
aria-label="Next page"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
title="Next page"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"
/>
</svg>
</span>
</button>
</div>
</div> </div>
</div> </td>
</td> </tr>
</tr> </tfoot>
</tfoot> </table>
</table> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,66 +1,26 @@
import React, { useEffect, useState } from "react"; import React, {useEffect, useState} from "react";
import { Link } from 'react-router-dom'; import {Link} from 'react-router-dom';
import { import {Grid,} from "@material-ui/core";
Grid,
Table,
TableBody,
TableCell,
TableFooter,
TablePagination,
TableRow
} from "@material-ui/core";
import AddCircleIcon from "@material-ui/icons/AddCircle"; import AddCircleIcon from "@material-ui/icons/AddCircle";
import clsx from "clsx"; import clsx from "clsx";
import TableHeaderSortable from "../../Table/HeaderSortable"; import {useUserContext} from "../../Contexts/UserContext"
import { import {useStatusContext} from "../../Contexts/StatusContext";
useUserContext import {FleetProvider} from "../../Contexts/FleetContext"
} from "../../Contexts/UserContext"
import { useStatusContext } from "../../Contexts/StatusContext";
import { FleetProvider, useFleetContext } from "../../Contexts/FleetContext"
import useStyles from "../../useStyles"; import useStyles from "../../useStyles";
import SearchField from "../../Controls/SearchField"; import SearchField from "../../Controls/SearchField";
import { logger } from "../../../services/monitoring"; import FleetSelectionTable from "../../Controls/FleetSelectionTable";
import {useLocalStorage} from "../../useLocalStorage";
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 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 classes = useStyles();
const { setMessage, setSitePath, setTitle } = useStatusContext(); const [search, setSearch] = useState("");
const { fleets, totalFleets, getFleets } = useFleetContext(); const {setSitePath, setTitle} = useStatusContext();
const { token: { idToken: { jwtToken: token } } } = useUserContext(); const {token: {idToken: {jwtToken: token}}} = useUserContext();
const handleSearch = (query) => {
setSearch(query);
};
useEffect(() => { useEffect(() => {
setTitle("Fleets"); setTitle("Fleets");
@@ -68,106 +28,26 @@ const MainForm = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // 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 ( return (
<div className={clsx(classes.paper, classes.tableSize)}> <div className={clsx(classes.paper, classes.tableSize)}>
<Grid container className={classes.root} spacing={2}> <Grid container className={classes.root} spacing={2}>
<Grid item md={4} className={classes.textJustifyAlign}> <Grid item md={4} className={classes.textJustifyAlign}>
<Link to={"/fleet-add"} className={classes.labelInline}> <Link to={"/fleet-add"}>
<AddCircleIcon fontSize="large" /> <AddCircleIcon fontSize="large"/>
</Link> </Link>
</Grid> </Grid>
<Grid item md={4} align="right" className={classes.textCenterAlign}> <Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} /> <SearchField classes={classes} onSearch={handleSearch}/>
</Grid> </Grid>
<Grid item md={4} className={classes.textRightAlign}></Grid> <Grid item md={4} className={classes.textRightAlign}></Grid>
</Grid> </Grid>
<Table> <FleetSelectionTable
<TableHeaderSortable token={token}
classes={classes} classes={classes}
orderBy={orderBy} search={{search}}
order={order} multiSelect={false}
columnData={tableColumns} />
onSortRequest={handleSort} </div>
/>
<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,35 +1,37 @@
import React, { useEffect, useState } from "react"; import React, {useEffect, useState} from "react";
import { useParams, Redirect } from "react-router"; import {Redirect, useParams} from "react-router";
import { Button, Grid, Typography } from "@material-ui/core"; import {Button, FormControlLabel, Grid, Switch, Typography} from "@material-ui/core";
import clsx from "clsx"; import clsx from "clsx";
import { import {ManifestsProvider, useManifestsContext,} from "../../Contexts/ManifestsContext";
ManifestsProvider, import {CarUpdatesProvider, useCarUpdatesContext,} from "../../Contexts/CarUpdatesContext";
useManifestsContext, import {VehicleProvider} from "../../Contexts/VehicleContext";
} from "../../Contexts/ManifestsContext"; import {useUserContext} from "../../Contexts/UserContext";
import { import {useStatusContext} from "../../Contexts/StatusContext";
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 useStyles from "../../useStyles";
import SearchField from "../../Controls/SearchField"; import SearchField from "../../Controls/SearchField";
import CarSelectionTable from "../../Controls/CarSelectionTable"; import CarSelectionTable from "../../Controls/CarSelectionTable";
import { logger } from "../../../services/monitoring"; import {logger} from "../../../services/monitoring";
import { LocalDateTimeString } from "../../../utils/dates"; 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 MainForm = () => {
const { manifest_id } = useParams(); const [updateType, setUpdateType] = useState(CAR_UPDATE);
const { getManifests, manifests, busy } = useManifestsContext();
const { deployCarUpdates } = useCarUpdatesContext(); const {manifest_id} = useParams();
const {getManifests, manifests, busy} = useManifestsContext();
const {deployCarUpdates, deployFleetUpdates} = useCarUpdatesContext();
const { const {
token: { token: {
idToken: { jwtToken: token }, idToken: {jwtToken: token},
}, },
} = useUserContext(); } = useUserContext();
const { setMessage, setTitle, setSitePath } = useStatusContext(); const {setMessage, setTitle, setSitePath} = useStatusContext();
const [manifestName, setManifestName] = useState(""); const [manifestName, setManifestName] = useState("");
const [version, setVersion] = useState(""); const [version, setVersion] = useState("");
const [createDate, setCreateDate] = useState(""); const [createDate, setCreateDate] = useState("");
@@ -38,6 +40,11 @@ const MainForm = () => {
const [redirect, setRedirect] = useState(""); const [redirect, setRedirect] = useState("");
const classes = useStyles(); const classes = useStyles();
const handleChange = (_) => {
setSelected([]);
setUpdateType(updateType === CAR_UPDATE ? FLEET_UPDATE : CAR_UPDATE);
}
const handleSearch = (query) => { const handleSearch = (query) => {
setSelected([]); setSelected([]);
setSearch(query); setSearch(query);
@@ -54,7 +61,7 @@ const MainForm = () => {
newSelected = [...selected]; newSelected = [...selected];
newSelected.push(key); newSelected.push(key);
} else { } else {
newSelected = selected.filter((vin) => vin !== key); newSelected = selected.filter((vinOrName) => vinOrName !== key);
} }
setSelected(newSelected); setSelected(newSelected);
} catch (e) { } catch (e) {
@@ -67,9 +74,14 @@ const MainForm = () => {
event.preventDefault(); event.preventDefault();
const data = { const data = {
manifest_id: parseInt(manifest_id), manifest_id: parseInt(manifest_id),
vins: selected, }
}; if (updateType === CAR_UPDATE) {
await deployCarUpdates(data, token); data.vins = selected;
await deployCarUpdates(data, token);
} else {
data.fleet_names = selected;
await deployFleetUpdates(data, token);
}
setMessage( setMessage(
`Deployed ${manifestName} ${version} to ${selected.length} cars` `Deployed ${manifestName} ${version} to ${selected.length} cars`
); );
@@ -82,7 +94,7 @@ const MainForm = () => {
const getData = async () => { const getData = async () => {
try { try {
getManifests({ id: parseInt(manifest_id) }, token); getManifests({id: parseInt(manifest_id)}, token);
} catch (e) { } catch (e) {
setMessage(e.message); setMessage(e.message);
logger.warn(e.stack); logger.warn(e.stack);
@@ -119,7 +131,7 @@ const MainForm = () => {
}, [manifests]); }, [manifests]);
if (redirect.length > 0) { if (redirect.length > 0) {
return <Redirect to={redirect} />; return <Redirect to={redirect}/>;
} }
return ( return (
@@ -127,13 +139,21 @@ const MainForm = () => {
<form className={classes.form} noValidate action="{onSubmit}"> <form className={classes.form} noValidate action="{onSubmit}">
<Typography variant="body2">Created {createDate}.</Typography> <Typography variant="body2">Created {createDate}.</Typography>
<Grid container className={classes.root} spacing={2}> <Grid container className={classes.root} spacing={2}>
<Grid item md={4}> <Grid item md={2}>
<div <div
className={classes.labelInline} className={classes.labelInline}
>{`${selected.length} Selected`}</div> >{`${selected.length} Selected`}</div>
</Grid> </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}> <Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} /> <SearchField classes={classes} onSearch={handleSearch}/>
</Grid> </Grid>
<Grid item md={4} className={classes.textRightAlign}> <Grid item md={4} className={classes.textRightAlign}>
<Button <Button
@@ -148,28 +168,39 @@ const MainForm = () => {
</Button> </Button>
</Grid> </Grid>
</Grid> </Grid>
<CarSelectionTable {updateType === CAR_UPDATE ?
classes={classes} <VehicleProvider>
token={token} <CarSelectionTable
multiSelect={true} classes={classes}
search={{ search }} token={token}
selected={selected} multiSelect={true}
onSelect={handleSelect} search={{search}}
onSelectAll={handleSelectAll} 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> </form>
</div> </div>
); );
}; };
const ManifestDeployForm = () => ( const ManifestDeployForm = () => (
<VehicleProvider> <ManifestsProvider>
<ManifestsProvider> <CarUpdatesProvider>
<CarUpdatesProvider> <MainForm/>
<MainForm /> </CarUpdatesProvider>
</CarUpdatesProvider> </ManifestsProvider>
</ManifestsProvider>
</VehicleProvider>
); );
export default ManifestDeployForm; export default ManifestDeployForm;

View File

@@ -5,6 +5,12 @@ const updatesAPI = {
return data; return data;
}, },
createFleetUpdates: async (data, token) => {
if (!data.id) data.id = 0;
data.id++;
return data;
},
getCarUpdates: async (filter, token) => { getCarUpdates: async (filter, token) => {
return { data: [] }; return { data: [] };
}, },

View File

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