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:
6
.github/workflows/blackduck.yml
vendored
6
.github/workflows/blackduck.yml
vendored
@@ -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:
|
||||||
|
|||||||
5325
package-lock.json
generated
5325
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
77
src/components/BulkActions/actions/SendSMS.jsx
Normal file
77
src/components/BulkActions/actions/SendSMS.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
67
src/components/BulkActions/actions/SendSMS.test.jsx
Normal file
67
src/components/BulkActions/actions/SendSMS.test.jsx
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
Reference in New Issue
Block a user