diff --git a/src/components/BulkActions/index.jsx b/src/components/BulkActions/index.jsx
new file mode 100644
index 0000000..81f5f3c
--- /dev/null
+++ b/src/components/BulkActions/index.jsx
@@ -0,0 +1,84 @@
+import { useEffect, useState } from "react";
+import TransformModal from "../TransformModal";
+import DropDownButton from "../Controls/DropDownButton";
+import { useUserContext } from "../Contexts/UserContext";
+import { useStatusContext } from "../Contexts/StatusContext";
+import useAddTags from "./useAddTags";
+import useUpdateConfig from "./useUpdateConfig";
+
+const transformArrayToCSV = (arr) => arr.join(", ");
+
+export default function BulkActions({
+ vins = [],
+}) {
+ const [vinCSV, setVinCSV] = useState(transformArrayToCSV(vins));
+ const [active, setActive] = useState(null);
+ const actions = [
+ {
+ name: "Update Configs",
+ disabled: vins.length === 0,
+ trigger: () => setActive("updateConfig"),
+ },
+ {
+ name: "Add Tags",
+ disabled: vins.length === 0,
+ trigger: () => setActive("addTags"),
+ },
+ ];
+
+ const updateConfig = useUpdateConfig();
+ const addTags = useAddTags();
+
+ const { setMessage } = useStatusContext();
+ const {
+ token: {
+ idToken: { jwtToken: token },
+ },
+ } = useUserContext();
+
+ const handleUpdateConfig = () => {
+ updateConfig.submit(vins, token)
+ .then(() => {
+ setMessage(`${vins.length} vehicles updated.`);
+ })
+ .catch((error) => {
+ setMessage(error.message);
+ });
+ }
+
+ const handleAddTags = () => {
+ addTags.submit(vins, token)
+ .then(() => setMessage(`Added ${addTags.data.tags.value.length} tags to ${vins.length} vehicles.`))
+ .catch((error) => setMessage(error.message));
+ }
+
+ const handleClose = () => setActive(null);
+
+ useEffect(() => {
+ setVinCSV(transformArrayToCSV(vins));
+ }, [vins]);
+
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/src/components/BulkActions/useAddTags.js b/src/components/BulkActions/useAddTags.js
new file mode 100644
index 0000000..7696595
--- /dev/null
+++ b/src/components/BulkActions/useAddTags.js
@@ -0,0 +1,22 @@
+import { useState } from "react";
+import vehiclesAPI from "../../services/vehiclesAPI";
+
+export default function useAddTags() {
+ const [tags, setTags] = useState({
+ tags: {
+ label: "Tags",
+ type: "list.string",
+ value: [],
+ },
+ });
+
+ const submit = async (vins, token) => {
+ return vehiclesAPI.addTags(vins, tags.tags.value, token);
+ }
+
+ return {
+ data: tags,
+ setData: setTags,
+ submit,
+ };
+}
diff --git a/src/components/BulkActions/useUpdateConfig.js b/src/components/BulkActions/useUpdateConfig.js
new file mode 100644
index 0000000..c648bd1
--- /dev/null
+++ b/src/components/BulkActions/useUpdateConfig.js
@@ -0,0 +1,40 @@
+import { useState } from "react";
+import TaskRunner from "../../utils/taskRunner";
+import vehiclesAPI from "../../services/vehiclesAPI";
+
+export default function useUpdateConfig() {
+ const [config, setConfig] = useState({
+ force: {
+ label: "Force Push",
+ type: "boolean",
+ value: false,
+ },
+ });
+
+ const submit = async (vins, token) => {
+ return new Promise((resolve, reject) => {
+ const taskRunner = new TaskRunner(5);
+
+ const task = (vin, isLast) => {
+ return async () => vehiclesAPI.updateConfig(vin, config.force.value, token)
+ .then((response) => {
+ if (isLast) {
+ if (response.error) {
+ reject(response);
+ }
+ resolve(response)
+ }
+ })
+ .catch((error) => reject(error));
+ }
+
+ vins.forEach((vin, index) => taskRunner.push(task(vin, index === vins.length - 1)));
+ });
+ }
+
+ return {
+ data: config,
+ setData: setConfig,
+ submit,
+ };
+}
\ No newline at end of file
diff --git a/src/components/Controls/DropDownButton/index.jsx b/src/components/Controls/DropDownButton/index.jsx
index 77a5562..6820877 100644
--- a/src/components/Controls/DropDownButton/index.jsx
+++ b/src/components/Controls/DropDownButton/index.jsx
@@ -40,6 +40,10 @@ const DropDownButton = ({ actions = [], payload = [] }) => {
setOpen(false);
};
+ if (!actions.length) {
+ return <>>;
+ }
+
return (
<>
+
diff --git a/src/components/Fleets/Status/Details/index.jsx b/src/components/Fleets/Status/Details/index.jsx
index 8b81643..2926b38 100644
--- a/src/components/Fleets/Status/Details/index.jsx
+++ b/src/components/Fleets/Status/Details/index.jsx
@@ -15,6 +15,7 @@ import { FleetProvider, useFleetContext } from "../../../Contexts/FleetContext"
import useStyles from "../../../useStyles";
import { logger } from "../../../../services/monitoring";
import DeleteConfirmation from "../../../DeleteConfirmation";
+import BulkActions from "../../../BulkActions";
const MainForm = ({ name }) => {
const classes = useStyles();
@@ -94,6 +95,9 @@ const MainForm = ({ name }) => {
+
+
+
setShowDeleteModal(false)} deleteFunction={onDelete} />
diff --git a/src/components/Fleets/Status/__snapshots__/DetailsTab.test.jsx.snap b/src/components/Fleets/Status/__snapshots__/DetailsTab.test.jsx.snap
index b62b7fd..b9ee191 100644
--- a/src/components/Fleets/Status/__snapshots__/DetailsTab.test.jsx.snap
+++ b/src/components/Fleets/Status/__snapshots__/DetailsTab.test.jsx.snap
@@ -151,6 +151,48 @@ exports[`DetailsTab Render 1`] = `
+
diff --git a/src/components/Fleets/Status/__snapshots__/index.test.jsx.snap b/src/components/Fleets/Status/__snapshots__/index.test.jsx.snap
index 19171b8..42fdb41 100644
--- a/src/components/Fleets/Status/__snapshots__/index.test.jsx.snap
+++ b/src/components/Fleets/Status/__snapshots__/index.test.jsx.snap
@@ -239,6 +239,48 @@ exports[`FleetStatus Render 1`] = `
+
diff --git a/src/components/TransformModal/index.jsx b/src/components/TransformModal/index.jsx
index 8a58a0c..ee0b13f 100644
--- a/src/components/TransformModal/index.jsx
+++ b/src/components/TransformModal/index.jsx
@@ -28,7 +28,7 @@ const TransformModal = ({
const handleChange = (key, value) => {
setData((data) => {
- const {[key]: toChange, ...rest} = data;
+ const { [key]: toChange, ...rest } = data;
switch (data[key].type) {
case "boolean":
toChange.value = !toChange.value;