diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap
index 8dc8aac..d9392bd 100644
--- a/src/components/App/__snapshots__/App.test.js.snap
+++ b/src/components/App/__snapshots__/App.test.js.snap
@@ -6284,7 +6284,44 @@ exports[`App Route /packages authenticated 1`] = `
+ |
+
+
+
+
+
+
+
+ |
+ |
+
+
+
+
+
+
+
+ |
diff --git a/src/components/Manifest/List/index.jsx b/src/components/Manifest/List/index.jsx
index 089e2a3..a115576 100644
--- a/src/components/Manifest/List/index.jsx
+++ b/src/components/Manifest/List/index.jsx
@@ -6,7 +6,8 @@ import {
TableFooter,
TablePagination,
TableRow,
- Tooltip
+ Tooltip,
+ Checkbox,
} from "@material-ui/core";
import DeleteIcon from "@material-ui/icons/Delete";
import SendIcon from "@material-ui/icons/Send";
@@ -37,6 +38,8 @@ import DeleteConfirmation from "../../DeleteConfirmation";
import TableHeaderSortable from "../../Table/HeaderSortable";
import { useLocalStorage } from "../../useLocalStorage";
import useStyles from "../../useStyles";
+import { useArchiveManifests } from "../../../hooks";
+import DropDownButton from "../../Controls/DropDownButton";
const tableColumns = [
{
@@ -92,12 +95,12 @@ const MainForm = () => {
const [order, setOrder] = useState("asc");
const [search, setSearch] = useLocalStorage("DEPLOYMENT_SEARCH", "");
const [active, setActive] = useLocalStorage("DEPLOYMENT_ACTIVE", "true");
+ const [selected, setSelected] = useState([]);
const [showDeleteModal, setShowDeleteModal] = useState(false);
- const [deleteId, setDeleteId] = useState("");
const [deleteRowName, setDeleteRowName] = useState("");
- const { getManifests, deleteManifest, manifests, totalManifests } =
+ const { getManifests, manifests, totalManifests } =
useManifestsContext();
const { setMessage, setTitle, setSitePath } = useStatusContext();
const {
@@ -107,6 +110,7 @@ const MainForm = () => {
groups,
providers,
} = useUserContext();
+ const { archive } = useArchiveManifests(token);
const sortHandler = (event, property) => {
if (property === orderBy) {
@@ -170,21 +174,45 @@ const MainForm = () => {
}
}
- const setDeletePopup = (id, row) => {
- setDeleteId(id);
- setDeleteRowName(`${row.name} ${row.version}`);
+ const handleSelectAll = () => {
+ setSelected((selected) => selected.length ? [] : manifests);
+ };
+
+ const handleSelect = (event, manifest) => {
+ setSelected((selected) => {
+ if (event.target.checked && selected.find(({ id }) => id === manifest.id)) {
+ return selected;
+ } else if (event.target.checked) {
+ return [...selected, manifest];
+ }
+ return selected.filter(({ id }) => id !== manifest.id);
+ });
+ };
+
+ const setDeletePopup = (row) => {
+ handleSelect({ target: { checked: true } }, row);
setShowDeleteModal(true);
};
- const onDelete = async (manifest_id) => {
+ const onDelete = async () => {
try {
- await deleteManifest(parseInt(manifest_id), token);
+ await archive(selected.map((manifest) => manifest.id))
+ .then(({ summary }) => {
+ setMessage(summary);
+ });
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
+ useEffect(() => {
+ setDeleteRowName(() => selected
+ .map((manifest) => `${manifest.name} ${manifest.version}`)
+ .join(", ")
+ );
+ }, [selected]);
+
const Actions = (row) => {
let actions = [];
if (hasRole(groups, Permissions.FiskerMagnaRead, providers)) {
@@ -232,7 +260,7 @@ const MainForm = () => {
return (
- setDeletePopup(action.id, row)}>
+ setDeletePopup(row)}>
{action.icon}
@@ -264,7 +292,17 @@ const MainForm = () => {
-
+
+ setShowDeleteModal(true),
+ disabled: !selected.length,
+ }
+ ]}
+ />
+
{
order={order}
columnData={tableColumns}
onSortRequest={sortHandler}
+ multiSelect
+ onSelectAll={handleSelectAll}
+ selectCount={selected ? selected.length : 0}
+ rowCount={manifests ? manifests.length : 0}
/>
- {manifests.map((row) => (
-
- {row.id}
-
- {row.name}
- {row.ecu_list && (
- <>
-
-
- >
- )}
-
- {row.version}
- {row.sums}
-
- {formatManifestType(row.type)}
-
-
- {LocalDateTimeString(row.created)}
-
-
- {LocalDateTimeString(row.updated)}
-
- {Actions(row)}
-
- ))}
+ {manifests.map((row) => {
+ const isSelected = selected
+ ? !!selected.find(({ id }) => id === row.id)
+ : false;
+ return (
+
+
+ handleSelect(event, row)}
+ />
+
+ {row.id}
+
+ {row.name}
+ {row.ecu_list && (
+ <>
+
+
+ >
+ )}
+
+ {row.version}
+ {row.sums}
+
+ {formatManifestType(row.type)}
+
+
+ {LocalDateTimeString(row.created)}
+
+
+ {LocalDateTimeString(row.updated)}
+
+ {Actions(row)}
+
+ );
+ })}
@@ -328,7 +381,7 @@ const MainForm = () => {
message={deleteRowName}
open={showDeleteModal}
close={() => setShowDeleteModal(false)}
- deleteFunction={() => onDelete(deleteId)}
+ deleteFunction={() => onDelete()}
/>
);
diff --git a/src/hooks/index.js b/src/hooks/index.js
new file mode 100644
index 0000000..7b34538
--- /dev/null
+++ b/src/hooks/index.js
@@ -0,0 +1 @@
+export { useArchiveManifests } from "./useArchiveManifests";
\ No newline at end of file
diff --git a/src/hooks/useArchiveManifests.js b/src/hooks/useArchiveManifests.js
new file mode 100644
index 0000000..dc6a2d2
--- /dev/null
+++ b/src/hooks/useArchiveManifests.js
@@ -0,0 +1,32 @@
+import manifestsAPI from "../services/manifestsAPI";
+import TaskRunner from "../utils/taskRunner";
+
+export const useArchiveManifests = (token) => {
+
+ const archive = async (ids) => {
+ return new Promise((resolve) => {
+ const taskRunner = new TaskRunner(5, ids.length);
+ let errorCount = 0;
+
+ const task = (id) => {
+ return async () => manifestsAPI.deleteManifest(id, token);
+ }
+
+ ids.forEach((id) => taskRunner.push(task(id))
+ .then((response) => {
+ if (response.error) {
+ errorCount += 1;
+ }
+ })
+ );
+ taskRunner.onComplete().then((responses) => resolve({
+ summary: `${ids.length - errorCount} out of ${ids.length} manifests were deleted.`,
+ responses,
+ }));
+ });
+ }
+
+ return {
+ archive,
+ };
+};
diff --git a/src/utils/taskRunner.js b/src/utils/taskRunner.js
index baa8e6b..b13ccec 100644
--- a/src/utils/taskRunner.js
+++ b/src/utils/taskRunner.js
@@ -1,36 +1,67 @@
export default class TaskRunner {
- constructor(concurrencyLimit = 1) {
- this.queue = [];
- this.running = 0;
- this.concurrencyLimit = concurrencyLimit;
+ constructor(concurrencyLimit = 1, total) {
+ this._queue = [];
+ this._index = 0;
+ this._running = 0;
+ this._complete = 0;
+ this._concurrencyLimit = concurrencyLimit;
+
+ if (total) {
+ this._total = total;
+ this._responses = new Array(total);
+ }
+
+ this._onComplete = new Promise((resolve, reject) => {
+ this._onCompleteResolve = resolve;
+ this._onCompleteReject = reject;
+ });
}
execute() {
- if (this.running >= this.concurrencyLimit || this.queue.length === 0) {
+ if (this._running >= this._concurrencyLimit || this._queue.length === 0) {
return;
}
-
- const task = this.queue.shift();
- this.running += 1;
- task();
+
+ const task = this._queue.shift();
+ this._running += 1;
+ task(this._index);
+ this._index += 1;
}
async push(fn) {
return new Promise((resolve, reject) => {
- const task = async () => {
+ const task = async (index) => {
try {
- const result = await fn();
- resolve(result);
+ const response = await fn();
+ if (this._responses) {
+ this._responses[index] = response;
+ }
+ resolve(response);
} catch (error) {
reject(error);
} finally {
- this.running -= 1;
+ this._running -= 1;
+ this.#progress();
this.execute();
}
}
-
- this.queue.push(task);
+
+ this._queue.push(task);
this.execute();
});
}
+
+ #progress() {
+ this._complete += 1;
+ if (this._complete === this._total) {
+ this._onCompleteResolve(this._responses);
+ }
+ }
+
+ async onComplete() {
+ if (!this._total) {
+ this._onCompleteReject(new Error("Total is required to determine onComplete."));
+ }
+ return this._onComplete;
+ }
}
\ No newline at end of file
diff --git a/src/utils/taskRunner.test.js b/src/utils/taskRunner.test.js
index 7e8b301..37749c3 100644
--- a/src/utils/taskRunner.test.js
+++ b/src/utils/taskRunner.test.js
@@ -4,6 +4,10 @@ const mockPromise = async (id, ms) => {
await new Promise(resolve => setTimeout(resolve, ms));
return id;
}
+const mockPromiseError = async (id, ms) => {
+ await new Promise(resolve => setTimeout(resolve, ms));
+ return new Error(`Task ${id} had an error`);
+}
const asyncFn1 = () => mockPromise(1, 200);
const asyncFn2 = () => mockPromise(2, 100);
@@ -12,19 +16,19 @@ const asyncFn3 = () => mockPromise(3, 50);
describe("TaskRunner", () => {
it("runs task added to queue, when space available", () => {
const taskRunner = new TaskRunner(2);
- expect(taskRunner.running).toEqual(0);
+ expect(taskRunner._running).toEqual(0);
taskRunner.push(() => mockPromise(1, 300));
- expect(taskRunner.running).toEqual(1);
+ expect(taskRunner._running).toEqual(1);
});
it("keeps task in queue when at concurrency limit", () => {
const taskRunner = new TaskRunner(2);
- expect(taskRunner.running).toEqual(0);
+ expect(taskRunner._running).toEqual(0);
taskRunner.push(() => mockPromise(1, 100));
taskRunner.push(() => mockPromise(2, 25));
taskRunner.push(() => mockPromise(3, 10));
- expect(taskRunner.running).toEqual(2);
- expect(taskRunner.queue.length).toEqual(1);
+ expect(taskRunner._running).toEqual(2);
+ expect(taskRunner._queue.length).toEqual(1);
});
it("runs queued tasks as space becomes available", async () => {
@@ -32,9 +36,9 @@ describe("TaskRunner", () => {
taskRunner.push(() => mockPromise(1, 600));
taskRunner.push(() => mockPromise(2, 300));
taskRunner.push(() => mockPromise(3, 100));
- expect(taskRunner.queue.length).toEqual(1);
+ expect(taskRunner._queue.length).toEqual(1);
await new Promise(r => setTimeout(r, 301));
- expect(taskRunner.queue.length).toEqual(0);
+ expect(taskRunner._queue.length).toEqual(0);
});
it("runs tasks in order", async () => {
@@ -52,7 +56,44 @@ describe("TaskRunner", () => {
.then((id) => {
actual.push(id);
});
- await new Promise(resolve => setTimeout(resolve, 500));
- expect(actual).toEqual([2, 3, 1]);
+ await new Promise(resolve => setTimeout(resolve, 500));
+ expect(actual).toEqual([2, 3, 1]);
});
-})
\ No newline at end of file
+
+ it("resolves a promise when all tasks are complete", async () => {
+ const taskRunner = new TaskRunner(2, 5);
+ taskRunner.push(() => mockPromise(1, 600));
+ taskRunner.push(() => mockPromise(2, 300));
+ taskRunner.push(() => mockPromise(3, 200));
+ taskRunner.push(() => mockPromise(4, 600));
+ taskRunner.push(() => mockPromise(5, 100));
+ await taskRunner.onComplete().then((actual) => {
+ expect(actual).toStrictEqual([1, 2, 3, 4, 5]);
+ });
+ });
+
+ it("resolves a promise when all tasks are complete, even if some fail", async () => {
+ const error = new Error(`Task 3 had an error`);
+ const taskRunner = new TaskRunner(2, 5);
+ taskRunner.push(() => mockPromise(1, 600));
+ taskRunner.push(() => mockPromise(2, 300));
+ taskRunner.push(() => mockPromiseError(3, 200));
+ taskRunner.push(() => mockPromise(4, 600));
+ taskRunner.push(() => mockPromise(5, 100));
+ await taskRunner.onComplete().then((actual) => {
+ expect(actual).toStrictEqual([1, 2, error, 4, 5]);
+ });
+ });
+
+ it("rejects a promise when the total number of tasks is unknown", async () => {
+ const taskRunner = new TaskRunner(2);
+ taskRunner.push(() => mockPromise(1, 600));
+ taskRunner.push(() => mockPromise(2, 300));
+ taskRunner.push(() => mockPromise(3, 200));
+ taskRunner.push(() => mockPromise(4, 600));
+ taskRunner.push(() => mockPromise(5, 100));
+ await taskRunner.onComplete().catch((error) => {
+ expect(error.message).toBe("Total is required to determine onComplete.");
+ });
+ });
+});
|