From 11406aa8dac8c236abf9125f43b67592006d0ad6 Mon Sep 17 00:00:00 2001 From: Tristan Timblin Date: Mon, 3 Jul 2023 12:07:19 -0400 Subject: [PATCH] CEC-4523: Add bulk archive support (#379) * CEC-4523: add archive endpoint and action --- .../App/__snapshots__/App.test.js.snap | 1 + src/components/Manifest/List/index.jsx | 87 +++++++++++++------ src/components/Manifest/List/index.test.jsx | 35 ++++++++ src/hooks/index.js | 2 +- src/hooks/useArchiveManifests.js | 32 ------- src/hooks/useUpdateManifest.js | 47 ++++++++++ src/services/__mocks__/manifestsAPI.js | 4 + src/services/manifestsAPI.js | 18 +++- 8 files changed, 163 insertions(+), 63 deletions(-) create mode 100644 src/components/Manifest/List/index.test.jsx delete mode 100644 src/hooks/useArchiveManifests.js create mode 100644 src/hooks/useUpdateManifest.js diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap index c691a1a..e4a3185 100644 --- a/src/components/App/__snapshots__/App.test.js.snap +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -6819,6 +6819,7 @@ exports[`App Route /packages authenticated 1`] = `
+
diff --git a/src/components/Manifest/List/index.jsx b/src/components/Manifest/List/index.jsx index ad52162..da6dd49 100644 --- a/src/components/Manifest/List/index.jsx +++ b/src/components/Manifest/List/index.jsx @@ -21,7 +21,6 @@ import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import EditIcon from "@material-ui/icons/Edit"; -import { useArchiveManifests } from "../../../hooks"; import { logger } from "../../../services/monitoring"; import { LocalDateTimeString } from "../../../utils/dates"; import { TYPE_MANIFEST_AFTERSALES, TYPE_MANIFEST_CONFIG, TYPE_MANIFEST_SOFTWARE } from "../../../utils/manifest_types"; @@ -40,6 +39,8 @@ import DeleteConfirmation from "../../DeleteConfirmation"; import TableHeaderSortable from "../../Table/HeaderSortable"; import { useLocalStorage } from "../../useLocalStorage"; import useStyles from "../../useStyles"; +import { useUpdateManifest } from "../../../hooks"; +import GeneralConfirmation from "../../GeneralConfirmation"; const tableColumns = [ { @@ -114,10 +115,10 @@ const MainForm = () => { const [order, setOrder] = useState("asc"); const [search, setSearch] = useLocalStorage("DEPLOYMENT_SEARCH", ""); const [active, setActive] = useLocalStorage("DEPLOYMENT_TAB_TOGGLE", "software"); - const [selected, setSelected] = useState([]); const [showDeleteModal, setShowDeleteModal] = useState(false); - const [deleteRowName, setDeleteRowName] = useState(""); + const [showArchiveModal, setShowArchiveModal] = useState(false); + const [archiveLabel, setArchiveLabel] = useState("Archive"); const { getManifests, manifests, totalManifests } = useManifestsContext(); @@ -129,7 +130,13 @@ const MainForm = () => { groups, providers, } = useUserContext(); - const { archive } = useArchiveManifests(token); + const { + remove, + archive, + updateManifestIds, + setUpdateManifestIds, + setMakeActive, + } = useUpdateManifest(token); const sortHandler = (event, property) => { if (property === orderBy) { @@ -227,7 +234,11 @@ const MainForm = () => { } })(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pageIndex, pageSize, token, orderBy, order, search, active]); + }, [pageIndex, pageSize, token, orderBy, order, search, active, updateManifestIds]); + + useEffect(() => { + setUpdateManifestIds([]); + }, [active, setUpdateManifestIds]); const handleChangePageIndex = (_event, newIndex) => { setPageIndex(newIndex); @@ -245,20 +256,28 @@ const MainForm = () => { const handleActiveChange = (event, newAlignment) => { if (newAlignment !== null) { - setActive(newAlignment) + setActive(newAlignment); + setMakeActive(newAlignment === 'archived'); + setArchiveLabel(() => { + if (newAlignment === "archived") { + return "Activate"; + } + + return "Archive"; + }); } } const handleSelectAll = () => { - setSelected((selected) => selected.length ? [] : manifests); + setUpdateManifestIds((selected) => selected.length ? [] : manifests.map((manifest) => manifest.id)); }; const handleSelect = (event, manifest) => { - setSelected((selected) => { - if (event.target.checked && selected.find(({ id }) => id === manifest.id)) { + setUpdateManifestIds((selected) => { + if (event.target.checked && selected.find((id) => id === manifest.id)) { return selected; } else if (event.target.checked) { - return [...selected, manifest]; + return [...selected, manifest.id]; } return selected.filter(({ id }) => id !== manifest.id); }); @@ -269,11 +288,12 @@ const MainForm = () => { setShowDeleteModal(true); }; - const onDelete = async () => { + const onArchive = async () => { try { - await archive(selected.map((manifest) => manifest.id)) - .then(({ summary }) => { - setMessage(summary); + await archive() + .then(({ message }) => { + setUpdateManifestIds([]); + setMessage(message); }); } catch (e) { setMessage(e.message); @@ -281,12 +301,18 @@ const MainForm = () => { } }; - useEffect(() => { - setDeleteRowName(() => selected - .map((manifest) => `${manifest.name} ${manifest.version}`) - .join(", ") - ); - }, [selected]); + const onDelete = async () => { + try { + await remove() + .then(({ summary }) => { + setUpdateManifestIds([]); + setMessage(summary); + }); + } catch (e) { + setMessage(e.message); + logger.warn(e.stack); + } + }; const Actions = (row) => { let actions = []; @@ -372,9 +398,9 @@ const MainForm = () => { setShowDeleteModal(true), - disabled: !selected.length, + name: archiveLabel, + trigger: () => setShowArchiveModal(true), + disabled: !updateManifestIds.length || active === "all", } ]} /> @@ -389,13 +415,13 @@ const MainForm = () => { onSortRequest={sortHandler} multiSelect onSelectAll={handleSelectAll} - selectCount={selected ? selected.length : 0} + selectCount={updateManifestIds ? updateManifestIds.length : 0} rowCount={manifests ? manifests.length : 0} /> {manifests.map((row) => { - const isSelected = selected - ? !!selected.find(({ id }) => id === row.id) + const isSelected = updateManifestIds + ? !!updateManifestIds.find((id) => id === row.id) : false; return ( @@ -457,11 +483,18 @@ const MainForm = () => { setShowDeleteModal(false)} deleteFunction={() => onDelete()} /> + setShowArchiveModal(false)} + actionFunction={() => onArchive()} + /> ); }; diff --git a/src/components/Manifest/List/index.test.jsx b/src/components/Manifest/List/index.test.jsx new file mode 100644 index 0000000..e7f2ab4 --- /dev/null +++ b/src/components/Manifest/List/index.test.jsx @@ -0,0 +1,35 @@ +jest.mock("../../Contexts/ManifestsContext"); +jest.mock("../../Contexts/UserContext"); + +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { BrowserRouter } from "react-router-dom"; + +import { UserProvider, setToken } from "../../Contexts/UserContext"; +import { StatusProvider } from "../../Contexts/StatusContext"; +import ManifestList from "."; +import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing"; + +const Page = ( + + + + + + + +); + +describe("Manifest List Component", () => { + beforeAll(() => { + setToken(TEST_AUTH_OBJECT_FISKER); + }); + + it("adjusts the active state on switch to archived tab", async () => { + render(Page); + + const archiveActionEl = screen.getByText("Archive"); + fireEvent.click(screen.getByText("Archived")); + expect(archiveActionEl.innerHTML).toBe("Activate"); + }); +}); diff --git a/src/hooks/index.js b/src/hooks/index.js index 7b34538..5ee20bb 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -1 +1 @@ -export { useArchiveManifests } from "./useArchiveManifests"; \ No newline at end of file +export { useUpdateManifest } from "./useUpdateManifest"; \ No newline at end of file diff --git a/src/hooks/useArchiveManifests.js b/src/hooks/useArchiveManifests.js deleted file mode 100644 index dc6a2d2..0000000 --- a/src/hooks/useArchiveManifests.js +++ /dev/null @@ -1,32 +0,0 @@ -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/hooks/useUpdateManifest.js b/src/hooks/useUpdateManifest.js new file mode 100644 index 0000000..39b0b9b --- /dev/null +++ b/src/hooks/useUpdateManifest.js @@ -0,0 +1,47 @@ +import { useState } from "react"; +import manifestsAPI from "../services/manifestsAPI"; +import TaskRunner from "../utils/taskRunner"; + +export const useUpdateManifest = (token) => { + const [updateManifestIds, setUpdateManifestIds] = useState([]); + const [makeActive, setMakeActive] = useState(false); + + const remove = async () => { + return new Promise((resolve) => { + const taskRunner = new TaskRunner(5, updateManifestIds.length); + let errorCount = 0; + + const task = (id) => { + return async () => manifestsAPI.deleteManifest(id, token); + } + + updateManifestIds.forEach((id) => taskRunner.push(task(id)) + .then((response) => { + if (response.error) { + errorCount += 1; + } + }) + ); + taskRunner.onComplete().then((responses) => resolve({ + summary: `${updateManifestIds.length - errorCount} out of ${updateManifestIds.length} manifests were deleted.`, + responses, + })); + }); + } + + const archive = async () => { + return manifestsAPI.archiveManifest({ + ids: updateManifestIds, + active: makeActive, + }, token); + } + + return { + updateManifestIds, + setUpdateManifestIds, + makeActive, + setMakeActive, + archive, + remove, + }; +}; diff --git a/src/services/__mocks__/manifestsAPI.js b/src/services/__mocks__/manifestsAPI.js index 6404bc5..302e6d4 100644 --- a/src/services/__mocks__/manifestsAPI.js +++ b/src/services/__mocks__/manifestsAPI.js @@ -5,6 +5,10 @@ const manifestsAPI = { return data; }, + archiveManifest: async (data, token) => { + return { message: "Archived 1 update manifests" }; + }, + deleteManifest: async (manifest_id, token) => { return { message: "OK" }; }, diff --git a/src/services/manifestsAPI.js b/src/services/manifestsAPI.js index 8fbe7a8..25782f7 100644 --- a/src/services/manifestsAPI.js +++ b/src/services/manifestsAPI.js @@ -1,10 +1,22 @@ import { - addQueryParams, errorHandler, fetchRespHandler, getAuthHeaderOptions + addQueryParams, errorHandler, fetchRespHandler, getAuthHeaderOptions } from "../utils/http"; const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL; const manifestsAPI = { + archiveManifest: async (data, token) => + fetch(`${API_ENDPOINT}/vehicles/archive`, { + method: "PUT", + headers: Object.assign( + { "Content-Type": "application/json" }, + getAuthHeaderOptions(token) + ), + body: JSON.stringify(data) + }) + .then(fetchRespHandler) + .catch(errorHandler), + deleteManifest: async (manifest_id, token) => fetch(`${API_ENDPOINT}/manifest?id=${manifest_id}`, { method: "DELETE", @@ -79,8 +91,8 @@ const manifestsAPI = { .then(fetchRespHandler) .catch(errorHandler), - migrateManifest: async (manifest_id, token) => - fetch(`${API_ENDPOINT}/manifestmigrate/${manifest_id}`,{ + migrateManifest: async (manifest_id, token) => + fetch(`${API_ENDPOINT}/manifestmigrate/${manifest_id}`, { method: "POST", headers: Object.assign( { "Content-Type": "application/json" },