CEC-4523: Add bulk archive support (#379)

* CEC-4523: add archive endpoint and action
This commit is contained in:
Tristan Timblin
2023-07-03 12:07:19 -04:00
committed by GitHub
parent a7c13306c5
commit 11406aa8da
8 changed files with 163 additions and 63 deletions

View File

@@ -6819,6 +6819,7 @@ exports[`App Route /packages authenticated 1`] = `
</tfoot>
</table>
<div />
<div />
</div>
</div>
</main>

View File

@@ -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>
);
};

View 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");
});
});

View File

@@ -1 +1 @@
export { useArchiveManifests } from "./useArchiveManifests";
export { useUpdateManifest } from "./useUpdateManifest";

View File

@@ -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,
};
};

View 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,
};
};

View File

@@ -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" };
},

View File

@@ -5,6 +5,18 @@ import {
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",
@@ -80,7 +92,7 @@ const manifestsAPI = {
.catch(errorHandler),
migrateManifest: async (manifest_id, token) =>
fetch(`${API_ENDPOINT}/manifestmigrate/${manifest_id}`,{
fetch(`${API_ENDPOINT}/manifestmigrate/${manifest_id}`, {
method: "POST",
headers: Object.assign(
{ "Content-Type": "application/json" },