CEC-4882: add send sms bulk action (#416)

* CEC-4882: add send sms bulk action

* npm audit fix

* upgrade to version specified by react-scripts

* override transitive package

* hoist ejs override

* add dep

* force blackduck scan
This commit is contained in:
Tristan Timblin
2023-08-17 14:09:56 -07:00
committed by GitHub
parent f4d021b658
commit 9ab36d5a1b
10 changed files with 2333 additions and 3171 deletions

View File

@@ -1,9 +1,9 @@
name: Blackduck name: Blackduck
on: on:
schedule: push:
# run scans twice a month branches:
- cron: '0 2 1,15 * *' - CEC-4882-off-main
jobs: jobs:
blackduck: blackduck:

5319
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -35,12 +35,17 @@
"react-leaflet": "^3.2.5", "react-leaflet": "^3.2.5",
"react-router-dom": "^5.3.0", "react-router-dom": "^5.3.0",
"react-router-hash-link": "^2.4.3", "react-router-hash-link": "^2.4.3",
"react-scripts": "5.0.0", "react-scripts": "^5.1.0-next.14",
"semver-compare": "^1.0.0", "semver-compare": "^1.0.0",
"usehooks-ts": "^2.7.1", "usehooks-ts": "^2.7.1",
"web-vitals": "^2.1.4", "web-vitals": "^2.1.4",
"webpack": "^5.74.0" "webpack": "^5.74.0"
}, },
"overrides": {
"react-scripts": {
"ejs": "3.1.9"
}
},
"scripts": { "scripts": {
"start": "env-cmd -f .env.local react-scripts start", "start": "env-cmd -f .env.local react-scripts start",
"build": "env-cmd -f .env.local react-scripts build", "build": "env-cmd -f .env.local react-scripts build",

View File

@@ -2,7 +2,7 @@ jest.mock("../../Contexts/UserContext");
jest.mock("../../Contexts/StatusContext"); jest.mock("../../Contexts/StatusContext");
jest.mock("../../../services/fleetsAPI"); jest.mock("../../../services/fleetsAPI");
import React, { useEffect, useState } from "react"; import React, { useState } from "react";
import { import {
render, render,
act, act,

View File

@@ -0,0 +1,77 @@
import { forwardRef, useImperativeHandle, useState } from "react";
import {
TextField,
FormControlLabel,
Checkbox,
} from "@material-ui/core";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import TaskRunner from "../../../utils/taskRunner";
import vehiclesAPI from "../../../services/vehiclesAPI";
import smsAPI from "../../../services/smsAPI";
import { SMS } from "../../Contexts/SMSContext";
export default forwardRef(({
vins,
vinCSV,
}, ref) => {
const [shouldAwait, setShouldAwait] = useState(false);
const [smsMessage, setSmsMessage] = useState("");
const { setMessage } = useStatusContext();
const { token: { idToken: { jwtToken: token } } } = useUserContext();
useImperativeHandle(ref, () => ({
async submit() {
return new Promise((resolve) => {
const taskRunner = new TaskRunner(5, vins.length);
vins.forEach((vin) => {
taskRunner.push(async () => {
const { iccid } = await vehiclesAPI.getVehicle(vin, token);
if (!iccid) {
throw new Error(`Missing ICC ID for ${vin}`);
}
const sms = new SMS(smsMessage, iccid, shouldAwait);
const result = await smsAPI.send(sms, token);
if (result.error) {
throw new Error(result.message);
}
return vin
})
.then((vin) => setMessage(`Sent message to ${vin}`))
.catch((error) => setMessage(`Failed to send SMS: ${error}`));
});
taskRunner.onComplete()
.then((responses) => {
setMessage(`Sent ${vins.length} SMS messages`);
resolve(responses);
});
});
},
}));
return (
<div>
<p>
You are about to send SMS to the following vins: {vinCSV}.
</p>
<TextField
fullWidth
label="Message"
variant="filled"
onChange={(event) => setSmsMessage(event.target.value)}
/>
<FormControlLabel
label="Await"
control={<Checkbox
checked={shouldAwait}
onChange={() => setShouldAwait(shouldAwait => !shouldAwait)}
/>}
/>
</div>
);
});

View File

@@ -0,0 +1,67 @@
jest.mock("../../Contexts/UserContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../../services/vehiclesAPI");
jest.mock("../../../services/smsAPI");
import React, { useState } from "react";
import {
render,
act,
} from "@testing-library/react";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { StatusProvider } from "../../Contexts/StatusContext";
import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing";
import SendSMS from "./SendSMS";
import vehiclesAPI from "../../../services/vehiclesAPI";
import smsAPI from "../../../services/smsAPI";
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: jest.fn(),
}));
jest.mock('@material-ui/core/FormControlLabel', () => {
const React = require('react');
return () => <div data-testid="mock-form-control-label" />;
});
jest.mock('@material-ui/core/TextField', () => {
const React = require('react');
return () => <div data-testid="mock-text-field" />;
});
jest.mock('@material-ui/core/Checkbox', () => {
const React = require('react');
return () => <div data-testid="mock-checkbox" />;
});
describe("BulkActions/SendSMS", () => {
beforeAll(() => {
setToken(TEST_AUTH_OBJECT_FISKER);
});
it("makes request to send multiple SMS messages", async () => {
useState
.mockReturnValueOnce([true, jest.fn()])
.mockReturnValueOnce(["body", jest.fn()]);
const vehiclesAPIMock = jest.spyOn(vehiclesAPI, "getVehicle");
const smsAPIMock = jest.spyOn(smsAPI, "send");
const ref = React.createRef();
render(
<StatusProvider>
<UserProvider>
<SendSMS
ref={ref}
vins={["3C4PDCBG0ET127145"]}
vinCSV=""
/>
</UserProvider>
</StatusProvider>
);
await act(async () => ref.current.submit());
expect(vehiclesAPIMock).toHaveBeenCalledTimes(1);
expect(smsAPIMock).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,5 +1,7 @@
import { useEffect, useState, useRef, Suspense, lazy } from "react"; import { useEffect, useState, useRef, Suspense, lazy } from "react";
import { hasRole, Permissions } from "../../utils/roles";
import DropDownButton from "../Controls/DropDownButton"; import DropDownButton from "../Controls/DropDownButton";
import { useUserContext } from "../Contexts/UserContext";
import { Modal } from "./Modal"; import { Modal } from "./Modal";
// Code-splitting individual actions // Code-splitting individual actions
@@ -8,6 +10,7 @@ const AddTags = lazy(() => import("./actions/AddTags"));
const AddToFleet = lazy(() => import("./actions/AddToFleet")); const AddToFleet = lazy(() => import("./actions/AddToFleet"));
const DeleteVehicles = lazy(() => import("./actions/DeleteVehicles")); const DeleteVehicles = lazy(() => import("./actions/DeleteVehicles"));
const UpdateConfig = lazy(() => import("./actions/UpdateConfig")); const UpdateConfig = lazy(() => import("./actions/UpdateConfig"));
const SendSMS = lazy(() => import("./actions/SendSMS"));
export default function BulkActions({ export default function BulkActions({
vins = [], vins = [],
@@ -16,6 +19,7 @@ export default function BulkActions({
const [title, setTitle] = useState("Action"); const [title, setTitle] = useState("Action");
const [active, setActive] = useState(null); const [active, setActive] = useState(null);
const activeRef = useRef(); const activeRef = useRef();
const { groups, providers } = useUserContext();
const filteredActions = [ const filteredActions = [
{ {
@@ -41,7 +45,13 @@ export default function BulkActions({
name: "Update Config", name: "Update Config",
disabled: false, disabled: false,
trigger: () => setActive("updateConfig"), trigger: () => setActive("updateConfig"),
} },
{
id: "sms",
name: "Send SMS",
disabled: !hasRole(groups, Permissions.FiskerCreate, providers),
trigger: () => setActive("sms"),
},
].filter((action) => actions.includes(action.id)); ].filter((action) => actions.includes(action.id));
const payload = { const payload = {
@@ -80,6 +90,7 @@ export default function BulkActions({
{active === "addToFleet" && <AddToFleet {...payload} />} {active === "addToFleet" && <AddToFleet {...payload} />}
{active === "deleteVehicles" && <DeleteVehicles {...payload} />} {active === "deleteVehicles" && <DeleteVehicles {...payload} />}
{active === "updateConfig" && <UpdateConfig {...payload} />} {active === "updateConfig" && <UpdateConfig {...payload} />}
{active === "sms" && <SendSMS {...payload} />}
</section> </section>
</Suspense> </Suspense>
</Modal> </Modal>

View File

@@ -468,6 +468,7 @@ const expectedVehicleData = {
max_disk_buffer_size: 2, max_disk_buffer_size: 2,
filters: expectedFilters, filters: expectedFilters,
}, },
iccid: "8988300000000000000",
connected: true, connected: true,
connectedHMI: false, connectedHMI: false,
}; };
@@ -487,6 +488,7 @@ const expectedVehiclesData = [
max_disk_buffer_size: 2, max_disk_buffer_size: 2,
filters: expectedFilters, filters: expectedFilters,
}, },
iccid: "8988300000000000000",
connected: true, connected: true,
connectedHMI: false, connectedHMI: false,
}, },

View File

@@ -96,7 +96,7 @@ const MainForm = ({ name }) => {
</Tooltip> </Tooltip>
</Grid> </Grid>
<Grid item md={12} className={classes.textCenterAlign}> <Grid item md={12} className={classes.textCenterAlign}>
<BulkActions vins={fleet.vehicles} actions={["addTags", "updateConfig"]} /> <BulkActions vins={fleet.vehicles} actions={["addTags", "updateConfig", "sms"]} />
</Grid> </Grid>
</Grid> </Grid>
<DeleteConfirmation message={name} open={showDeleteModal} close={() => setShowDeleteModal(false)} deleteFunction={onDelete} /> <DeleteConfirmation message={name} open={showDeleteModal} close={() => setShowDeleteModal(false)} deleteFunction={onDelete} />

View File

@@ -22,6 +22,7 @@ const data = [
ecu_list: "ECUA 2.0.0, ECUB 2.1.1", ecu_list: "ECUA 2.0.0, ECUB 2.1.1",
log_level: "info", log_level: "info",
canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: filters }, canbus: { enabled: true, data_logger_enabled: true, max_mem_buffer_size: 1, max_disk_buffer_size: 2, filters: filters },
iccid: "8988300000000000000",
}, },
{ vin: "1G1FP87S3GN100062" }, { vin: "1G1FP87S3GN100062" },
{ vin: "1HGCG325XYA062256", year: 2021 }, { vin: "1HGCG325XYA062256", year: 2021 },