CEC-5432: expand bulk-action to add, remove, and move vehicles between fleets (#484)

* battery indicator

* optimistic response remove from fleet

* update api to use params

* expand add to fleet to also remove

* typo

* update to post request

* CEC-5432: remove unused deps

* update mocks for delete
This commit is contained in:
Tristan Timblin
2023-11-17 10:44:56 -08:00
committed by GitHub
parent 94ba59ea77
commit f4652b5de7
15 changed files with 239 additions and 189 deletions

View File

@@ -19,7 +19,8 @@ const Batteries = [
]; ];
const chargingStates = [ const chargingStates = [
"V2L_trunk_active", ["V2L_trunk_active", "info"],
["AC_charging", "warning"],
]; ];
function getBatteryByPercent(percent) { function getBatteryByPercent(percent) {
@@ -35,7 +36,7 @@ function getBatteryByPercent(percent) {
return BatteryFull; return BatteryFull;
} }
const unit = 14.2857142857; const unit = 14.2857142857; // 1/7 * 100
const range = Math.floor(percent / unit); const range = Math.floor(percent / unit);
return Batteries[range]; return Batteries[range];
@@ -48,14 +49,16 @@ export default function Battery({
const classes = useStyles(); const classes = useStyles();
let Battery = getBatteryByPercent(percent); let Battery = getBatteryByPercent(percent);
if (chargingStates.includes(charge)) { const charging = chargingStates.find(([state]) => state === charge) || [];
if (charging[0]) {
Battery = BatteryCharging; Battery = BatteryCharging;
} }
return ( return (
<Tooltip title={`State: ${charge}`}> <Tooltip title={`State: ${charge}`}>
<div className={classes.alignCenter}> <div className={classes.alignBaseline}>
<Battery color="disabled" /> {percent && `${percent}%`} <Battery color={charging[1] || "disabled"} /> {percent && `${percent}%`}
</div> </div>
</Tooltip> </Tooltip>
); );

View File

@@ -1,74 +0,0 @@
import { useState, forwardRef, useImperativeHandle } from "react";
import {
FormControl,
} from '@material-ui/core';
import SearchSelect from "../../SearchSelect/SearchSelect";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import fleetsAPI from "../../../services/fleetsAPI";
export default forwardRef(({
ids,
idCSV,
}, ref) => {
const { setMessage } = useStatusContext();
const { token: { idToken: { jwtToken: token } } } = useUserContext();
const [fleet, setFleet] = useState(null);
useImperativeHandle(ref, () => ({
async submit() {
if (!fleet) {
setMessage(`Select a valid fleet, "${fleet}" is invalid.`);
return Promise.reject("Invalid Fleet");
}
return fleetsAPI
.addFleetVehicles(fleet, { vins: ids }, token)
.then((response) => {
if (response.error) {
setMessage(`${response.error}: ${response.message}`);
}
if (response.vins) {
setMessage(`Added ${response.vins.length} vehicles to ${fleet}.`);
return;
}
setMessage(`Something unexpected happened while attempting to add vehicles to fleet.`);
})
.catch((error) => {
setMessage(JSON.stringify(error));
});
},
}));
async function searchFleets(search) {
return fleetsAPI
.getFleets({
search,
limit: 10,
offset: 0,
order: `id desc`,
}, token)
.then(response => response.data.map(fleet => fleet.name))
.catch(() => []);
}
return (
<div>
<p>
You are adding the following VINs to a fleet: {idCSV}.
</p>
<FormControl variant="filled" fullWidth={true}>
<SearchSelect
label="Fleet"
value={fleet}
setValue={setFleet}
getData={searchFleets}
research={true}
/>
</FormControl>
</div>
);
});

View File

@@ -0,0 +1,117 @@
import { useState, forwardRef, useImperativeHandle } from "react";
import {
FormControl,
} from '@material-ui/core';
import SearchSelect from "../../SearchSelect/SearchSelect";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import fleetsAPI from "../../../services/fleetsAPI";
export default forwardRef(({
ids,
idCSV,
fleet,
}, ref) => {
const { setMessage } = useStatusContext();
const { token: { idToken: { jwtToken: token } } } = useUserContext();
const [fromFleet, setFromFleet] = useState(fleet);
const [toFleet, setToFleet] = useState();
useImperativeHandle(ref, () => ({
async submit() {
const errorTracking = [false, false];
if (toFleet) {
await fleetsAPI
.addFleetVehicles(toFleet, ids, token)
.then((response) => {
if (response.error) {
errorTracking[0] = true;
setMessage(`${response.error}: ${response.message}`);
}
if (response.vins) {
setMessage(`Added ${response.vins.length} vehicles to ${toFleet}.`);
return;
}
setMessage(`Something unexpected happened while attempting to add vehicles to fleet.`);
})
.catch((error) => {
setMessage(JSON.stringify(error));
return Promise.reject("Could not add vehicles to fleet.");
});
}
if (fromFleet) {
await fleetsAPI
.deleteFleetVehicles(fromFleet, ids, token)
.then((response) => {
if (response.error) {
errorTracking[1] = true;
setMessage(`${response.error}: ${response.message}`);
}
if (response.message === "Deleted") {
setMessage(`Removed ${ids.length} vehicles from ${fromFleet}.`);
return;
}
})
.catch((error) => {
setMessage(JSON.stringify(error));
return Promise.reject("Could not remove vehicles from fleet.");
});
}
return {
fromFleet,
fromVehicles: errorTracking[1] ? [] : ids,
toFleet,
toVehicles: errorTracking[0] ? [] : ids,
};
},
}));
async function searchFleets(search) {
return fleetsAPI
.getFleets({
search,
limit: 10,
offset: 0,
order: `id desc`,
}, token)
.then(response => response.data.map(fleet => fleet.name))
.catch(() => []);
}
return (
<div>
<p>
This operation will affect fleet membership for the following VINs: {idCSV}.
</p>
<p>
VINs will be removed from the "From Fleet" and added to the "To Fleet". If you would
like to only add or only remove the other field can be left blank.
</p>
<FormControl variant="filled" fullWidth={true}>
<SearchSelect
label="Remove From Fleet"
value={fromFleet}
setValue={setFromFleet}
getData={searchFleets}
research={true}
/>
</FormControl>
<FormControl variant="filled" fullWidth={true}>
<SearchSelect
label="Add To Fleet"
value={toFleet}
setValue={setToFleet}
getData={searchFleets}
research={true}
/>
</FormControl>
</div>
);
});

View File

@@ -10,7 +10,7 @@ import {
import { UserProvider, setToken } from "../../Contexts/UserContext"; import { UserProvider, setToken } from "../../Contexts/UserContext";
import { StatusProvider } from "../../Contexts/StatusContext"; import { StatusProvider } from "../../Contexts/StatusContext";
import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing"; import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing";
import AddToFleet from "./AddToFleet"; import UpdateFleetVehicles from "./UpdateFleetVehicles";
import fleetsAPI from "../../../services/fleetsAPI"; import fleetsAPI from "../../../services/fleetsAPI";
jest.mock('react', () => ({ jest.mock('react', () => ({
@@ -23,22 +23,25 @@ jest.mock('@material-ui/core/FormControl', () => {
return () => <div data-testid="mock-form-control" />; return () => <div data-testid="mock-form-control" />;
}); });
describe("BulkActions/AddToFleet", () => { describe("BulkActions/UpdateFleetVehicles", () => {
beforeAll(() => { beforeAll(() => {
setToken(TEST_AUTH_OBJECT_FISKER); setToken(TEST_AUTH_OBJECT_FISKER);
}); });
it("makes request to update the config of multiple vehicles", async () => { it("makes request to update the config of multiple vehicles", async () => {
useState useState
.mockReturnValueOnce(["Default-Test", jest.fn()])
.mockReturnValueOnce([["Default-Test"], jest.fn()])
.mockReturnValueOnce(["Default-Test", jest.fn()]) .mockReturnValueOnce(["Default-Test", jest.fn()])
.mockReturnValueOnce([["Default-Test"], jest.fn()]); .mockReturnValueOnce([["Default-Test"], jest.fn()]);
const api = jest.spyOn(fleetsAPI, "addFleetVehicles"); const add = jest.spyOn(fleetsAPI, "addFleetVehicles");
const remove = jest.spyOn(fleetsAPI, "deleteFleetVehicles");
const ref = React.createRef(); const ref = React.createRef();
render( render(
<StatusProvider> <StatusProvider>
<UserProvider> <UserProvider>
<AddToFleet <UpdateFleetVehicles
ref={ref} ref={ref}
ids={["TESTVIN1234567890"]} ids={["TESTVIN1234567890"]}
idCSV="" idCSV=""
@@ -48,6 +51,7 @@ describe("BulkActions/AddToFleet", () => {
); );
await act(async () => ref.current.submit()); await act(async () => ref.current.submit());
expect(api).toHaveBeenCalled(); expect(add).toHaveBeenCalled();
expect(remove).toHaveBeenCalled();
}); });
}); });

View File

@@ -8,17 +8,19 @@ import truncateCSV from "../../utils/truncateCSV";
// Code-splitting individual actions // Code-splitting individual actions
// https://react.dev/reference/react/lazy // https://react.dev/reference/react/lazy
const AddTags = lazy(() => import("./actions/AddTags")); const AddTags = lazy(() => import("./actions/AddTags"));
const AddToFleet = lazy(() => import("./actions/AddToFleet"));
const UpdateConfig = lazy(() => import("./actions/UpdateConfig"));
const SendSMS = lazy(() => import("./actions/SendSMS"));
const Cancel = lazy(() => import("./actions/Cancel")); const Cancel = lazy(() => import("./actions/Cancel"));
const Diagnostic = lazy(() => import("./actions/Diagnostic"));
const Redeploy = lazy(() => import("./actions/Redeploy")); const Redeploy = lazy(() => import("./actions/Redeploy"));
const RemoteCommand = lazy(() => import("./actions/RemoteCommand")); const RemoteCommand = lazy(() => import("./actions/RemoteCommand"));
const Diagnostic = lazy(() => import("./actions/Diagnostic")); const SendSMS = lazy(() => import("./actions/SendSMS"));
const UpdateConfig = lazy(() => import("./actions/UpdateConfig"));
const UpdateFleetVehicles = lazy(() => import("./actions/UpdateFleetVehicles"));
export default function BulkActions({ export default function BulkActions({
ids = [], ids = [],
actions = [], actions = [],
fleet = undefined,
callback = (active, ids, context) => { }, // context is raised from the action itself
}) { }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [title, setTitle] = useState("Action"); const [title, setTitle] = useState("Action");
@@ -34,30 +36,18 @@ export default function BulkActions({
disabled: false, disabled: false,
trigger: () => setActive("addTags"), trigger: () => setActive("addTags"),
}, },
{
id: "addToFleet",
name: "Add To Fleet",
disabled: false,
trigger: () => setActive("addToFleet"),
},
{
id: "updateConfig",
name: "Update Config",
disabled: false,
trigger: () => setActive("updateConfig"),
},
{
id: "sms",
name: "Send SMS",
disabled: !hasRole(groups, Permissions.FiskerCreate, providers),
trigger: () => setActive("sms"),
},
{ {
id: "cancel", id: "cancel",
name: "Cancel Updates", name: "Cancel Updates",
disabled: false, disabled: false,
trigger: () => setActive("cancel"), trigger: () => setActive("cancel"),
}, },
{
id: "diagnostic",
name: "Send Diagnostic",
disabled: !hasRole(groups, Permissions.CarDiagnostic, providers),
trigger: () => setActive("diagnostic"),
},
{ {
id: "redeploy", id: "redeploy",
name: "Redploy Updates", name: "Redploy Updates",
@@ -72,16 +62,29 @@ export default function BulkActions({
embedded: true, embedded: true,
}, },
{ {
id: "diagnostic", id: "sms",
name: "Send Diagnostic", name: "Send SMS",
disabled: !hasRole(groups, Permissions.CarDiagnostic, providers), disabled: !hasRole(groups, Permissions.FiskerCreate, providers),
trigger: () => setActive("diagnostic"), trigger: () => setActive("sms"),
} },
{
id: "updateConfig",
name: "Update Config",
disabled: false,
trigger: () => setActive("updateConfig"),
},
{
id: "updateFleetVehicles",
name: "Move Vehicles",
disabled: false,
trigger: () => setActive("updateFleetVehicles"),
},
].filter((action) => actions.includes(action.id)); ].filter((action) => actions.includes(action.id));
const payload = { const payload = {
ids, ids,
idCSV: (ids && ids.length > 0) ? truncateCSV(ids, 10) : "N/A", idCSV: (ids && ids.length > 0) ? truncateCSV(ids, 10) : "N/A",
fleet,
ref: activeRef ref: activeRef
}; };
@@ -91,7 +94,7 @@ export default function BulkActions({
const handleSubmit = () => { const handleSubmit = () => {
if (activeRef.current.submit) { if (activeRef.current.submit) {
activeRef.current.submit(); activeRef.current.submit().then((error) => callback(active, ids, error));
} }
handleClose(); handleClose();
} }
@@ -117,13 +120,13 @@ export default function BulkActions({
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={<div>Loading...</div>}>
<section> <section>
{active === "addTags" && <AddTags {...payload} />} {active === "addTags" && <AddTags {...payload} />}
{active === "addToFleet" && <AddToFleet {...payload} />}
{active === "updateConfig" && <UpdateConfig {...payload} />}
{active === "sms" && <SendSMS {...payload} />}
{active === "cancel" && <Cancel {...payload} />} {active === "cancel" && <Cancel {...payload} />}
{active === "diagnostic" && <Diagnostic {...payload} />}
{active === "redeploy" && <Redeploy {...payload} />} {active === "redeploy" && <Redeploy {...payload} />}
{active === "remoteCommand" && <RemoteCommand {...payload} />} {active === "remoteCommand" && <RemoteCommand {...payload} />}
{active === "diagnostic" && <Diagnostic {...payload} />} {active === "sms" && <SendSMS {...payload} />}
{active === "updateConfig" && <UpdateConfig {...payload} />}
{active === "updateFleetVehicles" && <UpdateFleetVehicles {...payload} />}
</section> </section>
</Suspense> </Suspense>
</Modal> </Modal>

View File

@@ -75,7 +75,7 @@ const MainForm = () => {
<AddCircleIcon fontSize="large" /> <AddCircleIcon fontSize="large" />
</Link> </Link>
</RoleWrap> </RoleWrap>
<BulkActions ids={selectedVins} actions={["addTags", "addToFleet", "updateConfig"]} /> <BulkActions ids={selectedVins} actions={["addTags", "addToFleet", "updateConfig", "updateFleetVehicles"]} />
</Grid> </Grid>
<Grid item md={4} className={classes.textCenterAlign}> <Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} savedSearchValue={query} /> <SearchField classes={classes} onSearch={handleSearch} savedSearchValue={query} />

View File

@@ -239,26 +239,39 @@ export const FleetProvider = ({ children }) => {
} }
}; };
const deleteFleetVehicle = async (name, vehicle, token) => { const deleteFleetVehicles = async (name, vins, token) => {
if (!Array.isArray(vins)) {
throw new Error(`VINs are required`);
}
try { try {
setBusy(true); setBusy(true);
validateFleetName(name); validateFleetName(name);
validateVIN(vehicle.vin); for (let i = 0; i < vins.length; i++) {
validateVIN(vins[0]);
}
const result = await api.deleteFleetVehicle(name, vehicle, token); const result = await api.deleteFleetVehicles(name, vins, token);
if (result.error) { if (result?.error) {
throw new Error(`Delete fleet vehicle error. ${result.message}`); throw new Error(`Delete fleet vehicle error. ${result.message}`);
} }
const index = fleetVehicles.findIndex(element => element === vehicle.vin); removeFleetVehiclesLocal(vins);
if (index >= 0) fleetVehicles.splice(index, 1);
return result;
} finally { } finally {
setBusy(false); setBusy(false);
} }
}; };
const removeFleetVehiclesLocal = (vins = []) => {
setFleetVehicles((fleetVehicles) => {
return fleetVehicles.filter((element) => !vins.includes(element.vin));
});
setTotalFleetVehicles((totalFleetVehicles) => {
return totalFleetVehicles - vins.length;
});
}
const getFleetCANFilters = async (name, search, token) => { const getFleetCANFilters = async (name, search, token) => {
try { try {
setBusy(true); setBusy(true);
@@ -358,7 +371,8 @@ export const FleetProvider = ({ children }) => {
watchFleetVehicles, watchFleetVehicles,
getFleetVehicles, getFleetVehicles,
addFleetVehicles, addFleetVehicles,
deleteFleetVehicle, deleteFleetVehicles,
removeFleetVehiclesLocal,
fleetCANFilters, fleetCANFilters,
totalFleetCANFilters, totalFleetCANFilters,

View File

@@ -465,14 +465,14 @@ describe("FleetContext", () => {
}); });
}); });
describe("deleteFleetVehicle", () => { describe("deleteFleetVehicles", () => {
beforeEach(async () => { beforeEach(async () => {
const TestComp = () => { const TestComp = () => {
const { busy, deleteFleetVehicle } = useFleetContext(); const { busy, deleteFleetVehicles } = useFleetContext();
const { message, setMessage } = useStatusContext(); const { message, setMessage } = useStatusContext();
const deleteFV = async (name, vehicle) => { const deleteFV = async (name, vins) => {
try { try {
await deleteFleetVehicle(name, vehicle); await deleteFleetVehicles(name, vins);
} catch (e) { } catch (e) {
setMessage(e.message); setMessage(e.message);
} }
@@ -488,11 +488,11 @@ describe("FleetContext", () => {
/> />
<button <button
data-testid="deleteFleetVehicleInvalid" data-testid="deleteFleetVehicleInvalid"
onClick={() => deleteFV("US-WEST", "INVALID")} onClick={() => deleteFV("US-WEST", ["INVALID"])}
/> />
<button <button
data-testid="deleteFleetVehicle" data-testid="deleteFleetVehicles"
onClick={() => deleteFV("US-WEST", { vin: "USWESTVIN12345678" })} onClick={() => deleteFV("US-WEST", ["USWESTVIN12345678"])}
/> />
</> </>
); );
@@ -519,10 +519,7 @@ describe("FleetContext", () => {
await waitFor(() => await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false") expect(screen.getByTestId("busy").innerHTML).toEqual("false")
); );
checkBaseResults( checkBaseResults("VINs are required", "false");
"Cannot read properties of null (reading 'vin')",
"false"
);
}); });
it("deleteFleetVehicleNonexistent", async () => { it("deleteFleetVehicleNonexistent", async () => {
@@ -533,8 +530,8 @@ describe("FleetContext", () => {
checkBaseResults("Invalid VIN", "false"); checkBaseResults("Invalid VIN", "false");
}); });
it("deleteFleetVehicle", async () => { it("deleteFleetVehicles", async () => {
fireEvent.click(screen.getByTestId("deleteFleetVehicle")); fireEvent.click(screen.getByTestId("deleteFleetVehicles"));
await waitFor(() => await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false") expect(screen.getByTestId("busy").innerHTML).toEqual("false")
); );

View File

@@ -93,7 +93,7 @@ export const useFleetContext = () => ({
return Promise.resolve(result); return Promise.resolve(result);
}), }),
addFleetVehicles: jest.fn(), addFleetVehicles: jest.fn(),
deleteFleetVehicle: jest.fn(), deleteFleetVehicles: jest.fn(),
fleetCANFilters, fleetCANFilters,
totalFleetCANFilters, totalFleetCANFilters,

View File

@@ -301,12 +301,6 @@ exports[`FleetVehiclesTable Render 1`] = `
</svg> </svg>
</span> </span>
</th> </th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
Actions
</th>
</tr> </tr>
</thead> </thead>
<tbody <tbody
@@ -320,7 +314,7 @@ exports[`FleetVehiclesTable Render 1`] = `
> >
<td <td
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root" class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
colspan="9" colspan="8"
> >
<div <div
class="MuiToolbar-root MuiToolbar-regular MuiTablePagination-toolbar MuiToolbar-gutters" class="MuiToolbar-root MuiToolbar-regular MuiTablePagination-toolbar MuiToolbar-gutters"

View File

@@ -9,10 +9,8 @@ import {
TablePagination, TablePagination,
TableRow, TableRow,
Tooltip, Tooltip,
IconButton,
} from "@material-ui/core"; } from "@material-ui/core";
import AddCircleIcon from "@material-ui/icons/AddCircle"; import AddCircleIcon from "@material-ui/icons/AddCircle";
import DeleteIcon from "@material-ui/icons/Delete";
import clsx from "clsx"; import clsx from "clsx";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -61,10 +59,6 @@ const tableColumns = [
id: "park", // TODO: Update to 'gear' when we confirm each possible state id: "park", // TODO: Update to 'gear' when we confirm each possible state
label: "Park", label: "Park",
}, },
{
id: "",
label: "Actions",
},
]; ];
const PAGE_SIZE = "FLEET_STATUS_VEHICLES_TABLE_PAGE_SIZE"; const PAGE_SIZE = "FLEET_STATUS_VEHICLES_TABLE_PAGE_SIZE";
@@ -83,7 +77,7 @@ const MainForm = ({ name }) => {
totalFleetVehicles, totalFleetVehicles,
watchFleetVehicles, watchFleetVehicles,
getFleetVehicles, getFleetVehicles,
deleteFleetVehicle, removeFleetVehiclesLocal,
} = useFleetContext(); } = useFleetContext();
const { const {
token: { token: {
@@ -113,6 +107,7 @@ const MainForm = ({ name }) => {
} catch (e) { } catch (e) {
setMessage(e.message); setMessage(e.message);
logger.warn(e.stack); logger.warn(e.stack);
watchFleetVehicles.end();
} }
})(); })();
return () => { return () => {
@@ -160,15 +155,12 @@ const MainForm = ({ name }) => {
: fleetVehicles.map((vehicle) => vehicle.vin)); : fleetVehicles.map((vehicle) => vehicle.vin));
} }
const onDelete = async (vin) => { const bulkActionCallback = (action, _, context) => {
try { if (action === "updateFleetVehicles") {
await deleteFleetVehicle(name, { vin: vin }, token); const vinsToRemove = context.fromFleet === name ? context.fromVehicles : [];
setMessage(`Deleted ${vin}`); removeFleetVehiclesLocal(vinsToRemove);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
} }
}; }
return ( return (
<div className={clsx(classes.paper, classes.tableSize)}> <div className={clsx(classes.paper, classes.tableSize)}>
@@ -179,7 +171,12 @@ const MainForm = ({ name }) => {
</Link> </Link>
</Grid> </Grid>
<Grid item md={4}> <Grid item md={4}>
<BulkActions ids={selected} actions={["addTags", "sms", "updateConfig", "remoteCommand", "diagnostic"]} /> <BulkActions
actions={["updateFleetVehicles", "addTags", "sms", "updateConfig", "remoteCommand", "diagnostic"]}
ids={selected}
fleet={name}
callback={bulkActionCallback}
/>
</Grid> </Grid>
<Grid item md={7} align="right" className={classes.textCenterAlign}> <Grid item md={7} align="right" className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} /> <SearchField classes={classes} onSearch={handleSearch} />
@@ -232,7 +229,7 @@ const MainForm = ({ name }) => {
<LinearProgress variant="determinate" value={car.car_update_progress} /> <LinearProgress variant="determinate" value={car.car_update_progress} />
)} )}
</TableCell> </TableCell>
<TableCell key={"cell5" + car.vin}> <TableCell key={"cell5" + car.vin} align="left">
<Battery percent={car.charge} charge={car.charge_type} /> <Battery percent={car.charge} charge={car.charge_type} />
</TableCell> </TableCell>
<TableCell key={"cell6" + car.vin}> <TableCell key={"cell6" + car.vin}>
@@ -241,13 +238,6 @@ const MainForm = ({ name }) => {
<TableCell key={"cell7" + car.vin}> <TableCell key={"cell7" + car.vin}>
{car.park ? "Yes" : "No"} {car.park ? "Yes" : "No"}
</TableCell> </TableCell>
<TableCell key={"cell8" + car.vin} align="center">
<Tooltip key={`delete-${car.vin}`} title={`Delete "${car.vin}"`}>
<IconButton onClick={() => onDelete(car.vin)}>
<DeleteIcon aria-label={`Delete ${car.vin}`} />
</IconButton>
</Tooltip>
</TableCell>
</TableRow> </TableRow>
) )
})} })}
@@ -256,7 +246,7 @@ const MainForm = ({ name }) => {
<TableRow> <TableRow>
<TablePagination <TablePagination
rowsPerPageOptions={[5, 10, 25, 100]} rowsPerPageOptions={[5, 10, 25, 100]}
colSpan={9} colSpan={8}
count={totalFleetVehicles} count={totalFleetVehicles}
rowsPerPage={pageSize} rowsPerPage={pageSize}
page={pageIndex} page={pageIndex}

View File

@@ -300,12 +300,6 @@ exports[`VehiclesTab Render 1`] = `
</svg> </svg>
</span> </span>
</th> </th>
<th
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col"
>
Actions
</th>
</tr> </tr>
</thead> </thead>
<tbody <tbody
@@ -319,7 +313,7 @@ exports[`VehiclesTab Render 1`] = `
> >
<td <td
class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root" class="MuiTableCell-root MuiTableCell-footer MuiTablePagination-root"
colspan="9" colspan="8"
> >
<div <div
class="MuiToolbar-root MuiToolbar-regular MuiTablePagination-toolbar MuiToolbar-gutters" class="MuiToolbar-root MuiToolbar-regular MuiTablePagination-toolbar MuiToolbar-gutters"

View File

@@ -335,7 +335,11 @@ const useStyles = makeStyles((theme) => ({
overflow: "hidden", overflow: "hidden",
display: "inline-block", display: "inline-block",
whiteSpace: "nowrap", whiteSpace: "nowrap",
} },
alignBaseline: {
display: "flex",
alignItems: "center",
},
})); }));
export default useStyles; export default useStyles;

View File

@@ -90,10 +90,13 @@ const fleetsAPI = {
})))); }))));
return payload; return payload;
}, },
deleteFleetVehicle: async (_name, vehicle) => { deleteFleetVehicles: async (_name, vins) => {
const index = vehicles.findIndex(element => element.vin === vehicle.vin); for (let i = 0; i < vins; i++) {
if (index >= 0) vehicles.splice(index, 1); const vin = vins[i];
return vehicle.vin; const index = vehicles.findIndex(element => element.vin === vin);
if (index >= 0) vehicles.splice(index, 1);
}
return { message: "Deleted" };
}, },
getFleetCANFilters: async () => { getFleetCANFilters: async () => {

View File

@@ -74,25 +74,26 @@ const fleetsAPI = {
.then(fetchRespHandler) .then(fetchRespHandler)
.catch(errorHandler), .catch(errorHandler),
addFleetVehicles: async (name, vehicles, token) => addFleetVehicles: async (name, payload, token) =>
fetch(`${API_ENDPOINT}/fleet/${name}/vehicle`, { fetch(`${API_ENDPOINT}/fleet/${name}/vehicles/add`, {
method: "POST", method: "POST",
headers: Object.assign( headers: Object.assign(
{ "Content-Type": "application/json" }, { "Content-Type": "application/json" },
getAuthHeaderOptions(token) getAuthHeaderOptions(token)
), ),
body: JSON.stringify(vehicles), body: JSON.stringify(payload),
}) })
.then(fetchRespHandler) .then(fetchRespHandler)
.catch(errorHandler), .catch(errorHandler),
deleteFleetVehicle: async (name, vehicle, token) => deleteFleetVehicles: async (name, vins, token) =>
fetch(`${API_ENDPOINT}/fleet/${name}/vehicle/${vehicle.vin}`, { fetch(`${API_ENDPOINT}/fleet/${name}/vehicles/delete`, {
method: "DELETE", method: "POST",
headers: Object.assign( headers: Object.assign(
{ "Content-Type": "application/json" }, { "Content-Type": "application/json" },
getAuthHeaderOptions(token) getAuthHeaderOptions(token)
), ),
body: JSON.stringify({ vins }),
}) })
.then(fetchRespHandler) .then(fetchRespHandler)
.catch(errorHandler), .catch(errorHandler),