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>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
<div />
|
<div />
|
||||||
|
<div />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import EditIcon from "@material-ui/icons/Edit";
|
import EditIcon from "@material-ui/icons/Edit";
|
||||||
import { useArchiveManifests } from "../../../hooks";
|
|
||||||
import { logger } from "../../../services/monitoring";
|
import { logger } from "../../../services/monitoring";
|
||||||
import { LocalDateTimeString } from "../../../utils/dates";
|
import { LocalDateTimeString } from "../../../utils/dates";
|
||||||
import { TYPE_MANIFEST_AFTERSALES, TYPE_MANIFEST_CONFIG, TYPE_MANIFEST_SOFTWARE } from "../../../utils/manifest_types";
|
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 TableHeaderSortable from "../../Table/HeaderSortable";
|
||||||
import { useLocalStorage } from "../../useLocalStorage";
|
import { useLocalStorage } from "../../useLocalStorage";
|
||||||
import useStyles from "../../useStyles";
|
import useStyles from "../../useStyles";
|
||||||
|
import { useUpdateManifest } from "../../../hooks";
|
||||||
|
import GeneralConfirmation from "../../GeneralConfirmation";
|
||||||
|
|
||||||
const tableColumns = [
|
const tableColumns = [
|
||||||
{
|
{
|
||||||
@@ -114,10 +115,10 @@ const MainForm = () => {
|
|||||||
const [order, setOrder] = useState("asc");
|
const [order, setOrder] = useState("asc");
|
||||||
const [search, setSearch] = useLocalStorage("DEPLOYMENT_SEARCH", "");
|
const [search, setSearch] = useLocalStorage("DEPLOYMENT_SEARCH", "");
|
||||||
const [active, setActive] = useLocalStorage("DEPLOYMENT_TAB_TOGGLE", "software");
|
const [active, setActive] = useLocalStorage("DEPLOYMENT_TAB_TOGGLE", "software");
|
||||||
const [selected, setSelected] = useState([]);
|
|
||||||
|
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [deleteRowName, setDeleteRowName] = useState("");
|
const [showArchiveModal, setShowArchiveModal] = useState(false);
|
||||||
|
const [archiveLabel, setArchiveLabel] = useState("Archive");
|
||||||
|
|
||||||
const { getManifests, manifests, totalManifests } =
|
const { getManifests, manifests, totalManifests } =
|
||||||
useManifestsContext();
|
useManifestsContext();
|
||||||
@@ -129,7 +130,13 @@ const MainForm = () => {
|
|||||||
groups,
|
groups,
|
||||||
providers,
|
providers,
|
||||||
} = useUserContext();
|
} = useUserContext();
|
||||||
const { archive } = useArchiveManifests(token);
|
const {
|
||||||
|
remove,
|
||||||
|
archive,
|
||||||
|
updateManifestIds,
|
||||||
|
setUpdateManifestIds,
|
||||||
|
setMakeActive,
|
||||||
|
} = useUpdateManifest(token);
|
||||||
|
|
||||||
const sortHandler = (event, property) => {
|
const sortHandler = (event, property) => {
|
||||||
if (property === orderBy) {
|
if (property === orderBy) {
|
||||||
@@ -227,7 +234,11 @@ const MainForm = () => {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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) => {
|
const handleChangePageIndex = (_event, newIndex) => {
|
||||||
setPageIndex(newIndex);
|
setPageIndex(newIndex);
|
||||||
@@ -245,20 +256,28 @@ const MainForm = () => {
|
|||||||
|
|
||||||
const handleActiveChange = (event, newAlignment) => {
|
const handleActiveChange = (event, newAlignment) => {
|
||||||
if (newAlignment !== null) {
|
if (newAlignment !== null) {
|
||||||
setActive(newAlignment)
|
setActive(newAlignment);
|
||||||
|
setMakeActive(newAlignment === 'archived');
|
||||||
|
setArchiveLabel(() => {
|
||||||
|
if (newAlignment === "archived") {
|
||||||
|
return "Activate";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Archive";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
setSelected((selected) => selected.length ? [] : manifests);
|
setUpdateManifestIds((selected) => selected.length ? [] : manifests.map((manifest) => manifest.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = (event, manifest) => {
|
const handleSelect = (event, manifest) => {
|
||||||
setSelected((selected) => {
|
setUpdateManifestIds((selected) => {
|
||||||
if (event.target.checked && selected.find(({ id }) => id === manifest.id)) {
|
if (event.target.checked && selected.find((id) => id === manifest.id)) {
|
||||||
return selected;
|
return selected;
|
||||||
} else if (event.target.checked) {
|
} else if (event.target.checked) {
|
||||||
return [...selected, manifest];
|
return [...selected, manifest.id];
|
||||||
}
|
}
|
||||||
return selected.filter(({ id }) => id !== manifest.id);
|
return selected.filter(({ id }) => id !== manifest.id);
|
||||||
});
|
});
|
||||||
@@ -269,11 +288,12 @@ const MainForm = () => {
|
|||||||
setShowDeleteModal(true);
|
setShowDeleteModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDelete = async () => {
|
const onArchive = async () => {
|
||||||
try {
|
try {
|
||||||
await archive(selected.map((manifest) => manifest.id))
|
await archive()
|
||||||
.then(({ summary }) => {
|
.then(({ message }) => {
|
||||||
setMessage(summary);
|
setUpdateManifestIds([]);
|
||||||
|
setMessage(message);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setMessage(e.message);
|
setMessage(e.message);
|
||||||
@@ -281,12 +301,18 @@ const MainForm = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const onDelete = async () => {
|
||||||
setDeleteRowName(() => selected
|
try {
|
||||||
.map((manifest) => `${manifest.name} ${manifest.version}`)
|
await remove()
|
||||||
.join(", ")
|
.then(({ summary }) => {
|
||||||
);
|
setUpdateManifestIds([]);
|
||||||
}, [selected]);
|
setMessage(summary);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
logger.warn(e.stack);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const Actions = (row) => {
|
const Actions = (row) => {
|
||||||
let actions = [];
|
let actions = [];
|
||||||
@@ -372,9 +398,9 @@ const MainForm = () => {
|
|||||||
<DropDownButton
|
<DropDownButton
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
name: "Archive",
|
name: archiveLabel,
|
||||||
trigger: () => setShowDeleteModal(true),
|
trigger: () => setShowArchiveModal(true),
|
||||||
disabled: !selected.length,
|
disabled: !updateManifestIds.length || active === "all",
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -389,13 +415,13 @@ const MainForm = () => {
|
|||||||
onSortRequest={sortHandler}
|
onSortRequest={sortHandler}
|
||||||
multiSelect
|
multiSelect
|
||||||
onSelectAll={handleSelectAll}
|
onSelectAll={handleSelectAll}
|
||||||
selectCount={selected ? selected.length : 0}
|
selectCount={updateManifestIds ? updateManifestIds.length : 0}
|
||||||
rowCount={manifests ? manifests.length : 0}
|
rowCount={manifests ? manifests.length : 0}
|
||||||
/>
|
/>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{manifests.map((row) => {
|
{manifests.map((row) => {
|
||||||
const isSelected = selected
|
const isSelected = updateManifestIds
|
||||||
? !!selected.find(({ id }) => id === row.id)
|
? !!updateManifestIds.find((id) => id === row.id)
|
||||||
: false;
|
: false;
|
||||||
return (
|
return (
|
||||||
<TableRow key={row.id}>
|
<TableRow key={row.id}>
|
||||||
@@ -457,11 +483,18 @@ const MainForm = () => {
|
|||||||
</TableFooter>
|
</TableFooter>
|
||||||
</Table>
|
</Table>
|
||||||
<DeleteConfirmation
|
<DeleteConfirmation
|
||||||
message={deleteRowName}
|
message={`${updateManifestIds.length} records.`}
|
||||||
open={showDeleteModal}
|
open={showDeleteModal}
|
||||||
close={() => setShowDeleteModal(false)}
|
close={() => setShowDeleteModal(false)}
|
||||||
deleteFunction={() => onDelete()}
|
deleteFunction={() => onDelete()}
|
||||||
/>
|
/>
|
||||||
|
<GeneralConfirmation
|
||||||
|
title={`${archiveLabel} Resource?`}
|
||||||
|
message={`${archiveLabel} ${updateManifestIds.length} records.`}
|
||||||
|
open={showArchiveModal}
|
||||||
|
close={() => setShowArchiveModal(false)}
|
||||||
|
actionFunction={() => onArchive()}
|
||||||
|
/>
|
||||||
</div>
|
</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;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
archiveManifest: async (data, token) => {
|
||||||
|
return { message: "Archived 1 update manifests" };
|
||||||
|
},
|
||||||
|
|
||||||
deleteManifest: async (manifest_id, token) => {
|
deleteManifest: async (manifest_id, token) => {
|
||||||
return { message: "OK" };
|
return { message: "OK" };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,18 @@ import {
|
|||||||
const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL;
|
const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL;
|
||||||
|
|
||||||
const manifestsAPI = {
|
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) =>
|
deleteManifest: async (manifest_id, token) =>
|
||||||
fetch(`${API_ENDPOINT}/manifest?id=${manifest_id}`, {
|
fetch(`${API_ENDPOINT}/manifest?id=${manifest_id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
|||||||
Reference in New Issue
Block a user