CEC-4523: Add bulk archive support (#379)
* CEC-4523: add archive endpoint and action
This commit is contained in:
@@ -6819,6 +6819,7 @@ exports[`App Route /packages authenticated 1`] = `
|
||||
</tfoot>
|
||||
</table>
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -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 = () => {
|
||||
<DropDownButton
|
||||
actions={[
|
||||
{
|
||||
name: "Archive",
|
||||
trigger: () => 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}
|
||||
/>
|
||||
<TableBody>
|
||||
{manifests.map((row) => {
|
||||
const isSelected = selected
|
||||
? !!selected.find(({ id }) => id === row.id)
|
||||
const isSelected = updateManifestIds
|
||||
? !!updateManifestIds.find((id) => id === row.id)
|
||||
: false;
|
||||
return (
|
||||
<TableRow key={row.id}>
|
||||
@@ -457,11 +483,18 @@ const MainForm = () => {
|
||||
</TableFooter>
|
||||
</Table>
|
||||
<DeleteConfirmation
|
||||
message={deleteRowName}
|
||||
message={`${updateManifestIds.length} records.`}
|
||||
open={showDeleteModal}
|
||||
close={() => setShowDeleteModal(false)}
|
||||
deleteFunction={() => onDelete()}
|
||||
/>
|
||||
<GeneralConfirmation
|
||||
title={`${archiveLabel} Resource?`}
|
||||
message={`${archiveLabel} ${updateManifestIds.length} records.`}
|
||||
open={showArchiveModal}
|
||||
close={() => setShowArchiveModal(false)}
|
||||
actionFunction={() => onArchive()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
35
src/components/Manifest/List/index.test.jsx
Normal file
35
src/components/Manifest/List/index.test.jsx
Normal file
@@ -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 = (
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<StatusProvider>
|
||||
<ManifestList />
|
||||
</StatusProvider>
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
);
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -1 +1 @@
|
||||
export { useArchiveManifests } from "./useArchiveManifests";
|
||||
export { useUpdateManifest } from "./useUpdateManifest";
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
47
src/hooks/useUpdateManifest.js
Normal file
47
src/hooks/useUpdateManifest.js
Normal file
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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" };
|
||||
},
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user