CEC-4523: add bulk archive to /packages (#372)
* CEC-4523: add bulk archive to /packages
This commit is contained in:
@@ -6284,7 +6284,44 @@ exports[`App Route /packages authenticated 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="MuiGrid-root makeStyles-textRightAlign-0 MuiGrid-item MuiGrid-grid-md-4"
|
class="MuiGrid-root makeStyles-textRightAlign-0 MuiGrid-item MuiGrid-grid-md-4"
|
||||||
/>
|
>
|
||||||
|
<div
|
||||||
|
aria-label="choose action"
|
||||||
|
class="MuiButtonGroup-root MuiButtonGroup-contained css-zqcytf-MuiButtonGroup-root"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium Mui-disabled MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-sghohy-MuiButtonBase-root-MuiButton-root"
|
||||||
|
disabled=""
|
||||||
|
tabindex="-1"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Archive
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-label="select action"
|
||||||
|
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-11qr2p8-MuiButtonBase-root-MuiButton-root"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-i4bv87-MuiSvgIcon-root"
|
||||||
|
data-testid="ArrowDropDownIcon"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m7 10 5 5 5-5z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table
|
<table
|
||||||
class="MuiTable-root"
|
class="MuiTable-root"
|
||||||
@@ -6295,6 +6332,40 @@ exports[`App Route /packages authenticated 1`] = `
|
|||||||
<tr
|
<tr
|
||||||
class="MuiTableRow-root MuiTableRow-head"
|
class="MuiTableRow-root MuiTableRow-head"
|
||||||
>
|
>
|
||||||
|
<th
|
||||||
|
class="MuiTableCell-root MuiTableCell-head MuiTableCell-paddingCheckbox"
|
||||||
|
scope="col"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-disabled="false"
|
||||||
|
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-0 MuiCheckbox-root MuiCheckbox-colorSecondary MuiIconButton-colorSecondary"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiIconButton-label"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-label="select all desserts"
|
||||||
|
class="PrivateSwitchBase-input-0"
|
||||||
|
data-indeterminate="false"
|
||||||
|
type="checkbox"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
<th
|
<th
|
||||||
aria-sort="ascending"
|
aria-sort="ascending"
|
||||||
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
|
||||||
@@ -6476,6 +6547,38 @@ exports[`App Route /packages authenticated 1`] = `
|
|||||||
<tr
|
<tr
|
||||||
class="MuiTableRow-root"
|
class="MuiTableRow-root"
|
||||||
>
|
>
|
||||||
|
<td
|
||||||
|
class="MuiTableCell-root MuiTableCell-body MuiTableCell-paddingCheckbox"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-disabled="false"
|
||||||
|
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-0 MuiCheckbox-root MuiCheckbox-colorSecondary MuiIconButton-colorSecondary"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiIconButton-label"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="PrivateSwitchBase-input-0"
|
||||||
|
data-indeterminate="false"
|
||||||
|
type="checkbox"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td
|
<td
|
||||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import {
|
|||||||
TableFooter,
|
TableFooter,
|
||||||
TablePagination,
|
TablePagination,
|
||||||
TableRow,
|
TableRow,
|
||||||
Tooltip
|
Tooltip,
|
||||||
|
Checkbox,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import DeleteIcon from "@material-ui/icons/Delete";
|
import DeleteIcon from "@material-ui/icons/Delete";
|
||||||
import SendIcon from "@material-ui/icons/Send";
|
import SendIcon from "@material-ui/icons/Send";
|
||||||
@@ -37,6 +38,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 { useArchiveManifests } from "../../../hooks";
|
||||||
|
import DropDownButton from "../../Controls/DropDownButton";
|
||||||
|
|
||||||
const tableColumns = [
|
const tableColumns = [
|
||||||
{
|
{
|
||||||
@@ -92,12 +95,12 @@ 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_ACTIVE", "true");
|
const [active, setActive] = useLocalStorage("DEPLOYMENT_ACTIVE", "true");
|
||||||
|
const [selected, setSelected] = useState([]);
|
||||||
|
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [deleteId, setDeleteId] = useState("");
|
|
||||||
const [deleteRowName, setDeleteRowName] = useState("");
|
const [deleteRowName, setDeleteRowName] = useState("");
|
||||||
|
|
||||||
const { getManifests, deleteManifest, manifests, totalManifests } =
|
const { getManifests, manifests, totalManifests } =
|
||||||
useManifestsContext();
|
useManifestsContext();
|
||||||
const { setMessage, setTitle, setSitePath } = useStatusContext();
|
const { setMessage, setTitle, setSitePath } = useStatusContext();
|
||||||
const {
|
const {
|
||||||
@@ -107,6 +110,7 @@ const MainForm = () => {
|
|||||||
groups,
|
groups,
|
||||||
providers,
|
providers,
|
||||||
} = useUserContext();
|
} = useUserContext();
|
||||||
|
const { archive } = useArchiveManifests(token);
|
||||||
|
|
||||||
const sortHandler = (event, property) => {
|
const sortHandler = (event, property) => {
|
||||||
if (property === orderBy) {
|
if (property === orderBy) {
|
||||||
@@ -170,21 +174,45 @@ const MainForm = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setDeletePopup = (id, row) => {
|
const handleSelectAll = () => {
|
||||||
setDeleteId(id);
|
setSelected((selected) => selected.length ? [] : manifests);
|
||||||
setDeleteRowName(`${row.name} ${row.version}`);
|
};
|
||||||
|
|
||||||
|
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);
|
setShowDeleteModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDelete = async (manifest_id) => {
|
const onDelete = async () => {
|
||||||
try {
|
try {
|
||||||
await deleteManifest(parseInt(manifest_id), token);
|
await archive(selected.map((manifest) => manifest.id))
|
||||||
|
.then(({ summary }) => {
|
||||||
|
setMessage(summary);
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setMessage(e.message);
|
setMessage(e.message);
|
||||||
logger.warn(e.stack);
|
logger.warn(e.stack);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDeleteRowName(() => selected
|
||||||
|
.map((manifest) => `${manifest.name} ${manifest.version}`)
|
||||||
|
.join(", ")
|
||||||
|
);
|
||||||
|
}, [selected]);
|
||||||
|
|
||||||
const Actions = (row) => {
|
const Actions = (row) => {
|
||||||
let actions = [];
|
let actions = [];
|
||||||
if (hasRole(groups, Permissions.FiskerMagnaRead, providers)) {
|
if (hasRole(groups, Permissions.FiskerMagnaRead, providers)) {
|
||||||
@@ -232,7 +260,7 @@ const MainForm = () => {
|
|||||||
return (
|
return (
|
||||||
<span key={`delete-${action.id}-of-key`}>
|
<span key={`delete-${action.id}-of-key`}>
|
||||||
<Tooltip key={`delete-${action.id}`} title={action.tip}>
|
<Tooltip key={`delete-${action.id}`} title={action.tip}>
|
||||||
<Link to="#" onClick={() => setDeletePopup(action.id, row)}>
|
<Link to="#" onClick={() => setDeletePopup(row)}>
|
||||||
{action.icon}
|
{action.icon}
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -264,7 +292,17 @@ const MainForm = () => {
|
|||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
</RoleWrap>
|
</RoleWrap>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={4} className={classes.textRightAlign}></Grid>
|
<Grid item md={4} className={classes.textRightAlign}>
|
||||||
|
<DropDownButton
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
name: "Archive",
|
||||||
|
trigger: () => setShowDeleteModal(true),
|
||||||
|
disabled: !selected.length,
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeaderSortable
|
<TableHeaderSortable
|
||||||
@@ -273,38 +311,53 @@ const MainForm = () => {
|
|||||||
order={order}
|
order={order}
|
||||||
columnData={tableColumns}
|
columnData={tableColumns}
|
||||||
onSortRequest={sortHandler}
|
onSortRequest={sortHandler}
|
||||||
|
multiSelect
|
||||||
|
onSelectAll={handleSelectAll}
|
||||||
|
selectCount={selected ? selected.length : 0}
|
||||||
|
rowCount={manifests ? manifests.length : 0}
|
||||||
/>
|
/>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{manifests.map((row) => (
|
{manifests.map((row) => {
|
||||||
<TableRow key={row.id}>
|
const isSelected = selected
|
||||||
<TableCell align="center">{row.id}</TableCell>
|
? !!selected.find(({ id }) => id === row.id)
|
||||||
<TableCell align="center">
|
: false;
|
||||||
{row.name}
|
return (
|
||||||
{row.ecu_list && (
|
<TableRow key={row.id}>
|
||||||
<>
|
<TableCell padding="checkbox">
|
||||||
<br />
|
<Checkbox
|
||||||
<ECUList
|
checked={isSelected}
|
||||||
list={row.ecu_list}
|
onChange={(event) => handleSelect(event, row)}
|
||||||
search={search}
|
/>
|
||||||
searchedOnly={true}
|
</TableCell>
|
||||||
/>
|
<TableCell align="center">{row.id}</TableCell>
|
||||||
</>
|
<TableCell align="center">
|
||||||
)}
|
{row.name}
|
||||||
</TableCell>
|
{row.ecu_list && (
|
||||||
<TableCell align="center">{row.version}</TableCell>
|
<>
|
||||||
<TableCell align="center">{row.sums}</TableCell>
|
<br />
|
||||||
<TableCell align="center">
|
<ECUList
|
||||||
{formatManifestType(row.type)}
|
list={row.ecu_list}
|
||||||
</TableCell>
|
search={search}
|
||||||
<TableCell align="center">
|
searchedOnly={true}
|
||||||
{LocalDateTimeString(row.created)}
|
/>
|
||||||
</TableCell>
|
</>
|
||||||
<TableCell align="center">
|
)}
|
||||||
{LocalDateTimeString(row.updated)}
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell align="center">{row.version}</TableCell>
|
||||||
<TableCell align="center">{Actions(row)}</TableCell>
|
<TableCell align="center">{row.sums}</TableCell>
|
||||||
</TableRow>
|
<TableCell align="center">
|
||||||
))}
|
{formatManifestType(row.type)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{LocalDateTimeString(row.created)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{LocalDateTimeString(row.updated)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">{Actions(row)}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
<TableFooter>
|
<TableFooter>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -328,7 +381,7 @@ const MainForm = () => {
|
|||||||
message={deleteRowName}
|
message={deleteRowName}
|
||||||
open={showDeleteModal}
|
open={showDeleteModal}
|
||||||
close={() => setShowDeleteModal(false)}
|
close={() => setShowDeleteModal(false)}
|
||||||
deleteFunction={() => onDelete(deleteId)}
|
deleteFunction={() => onDelete()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
1
src/hooks/index.js
Normal file
1
src/hooks/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { useArchiveManifests } from "./useArchiveManifests";
|
||||||
32
src/hooks/useArchiveManifests.js
Normal file
32
src/hooks/useArchiveManifests.js
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,36 +1,67 @@
|
|||||||
export default class TaskRunner {
|
export default class TaskRunner {
|
||||||
constructor(concurrencyLimit = 1) {
|
constructor(concurrencyLimit = 1, total) {
|
||||||
this.queue = [];
|
this._queue = [];
|
||||||
this.running = 0;
|
this._index = 0;
|
||||||
this.concurrencyLimit = concurrencyLimit;
|
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() {
|
execute() {
|
||||||
if (this.running >= this.concurrencyLimit || this.queue.length === 0) {
|
if (this._running >= this._concurrencyLimit || this._queue.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const task = this.queue.shift();
|
const task = this._queue.shift();
|
||||||
this.running += 1;
|
this._running += 1;
|
||||||
task();
|
task(this._index);
|
||||||
|
this._index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
async push(fn) {
|
async push(fn) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const task = async () => {
|
const task = async (index) => {
|
||||||
try {
|
try {
|
||||||
const result = await fn();
|
const response = await fn();
|
||||||
resolve(result);
|
if (this._responses) {
|
||||||
|
this._responses[index] = response;
|
||||||
|
}
|
||||||
|
resolve(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
} finally {
|
} finally {
|
||||||
this.running -= 1;
|
this._running -= 1;
|
||||||
|
this.#progress();
|
||||||
this.execute();
|
this.execute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.queue.push(task);
|
this._queue.push(task);
|
||||||
this.execute();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,10 @@ const mockPromise = async (id, ms) => {
|
|||||||
await new Promise(resolve => setTimeout(resolve, ms));
|
await new Promise(resolve => setTimeout(resolve, ms));
|
||||||
return id;
|
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 asyncFn1 = () => mockPromise(1, 200);
|
||||||
const asyncFn2 = () => mockPromise(2, 100);
|
const asyncFn2 = () => mockPromise(2, 100);
|
||||||
@@ -12,19 +16,19 @@ const asyncFn3 = () => mockPromise(3, 50);
|
|||||||
describe("TaskRunner", () => {
|
describe("TaskRunner", () => {
|
||||||
it("runs task added to queue, when space available", () => {
|
it("runs task added to queue, when space available", () => {
|
||||||
const taskRunner = new TaskRunner(2);
|
const taskRunner = new TaskRunner(2);
|
||||||
expect(taskRunner.running).toEqual(0);
|
expect(taskRunner._running).toEqual(0);
|
||||||
taskRunner.push(() => mockPromise(1, 300));
|
taskRunner.push(() => mockPromise(1, 300));
|
||||||
expect(taskRunner.running).toEqual(1);
|
expect(taskRunner._running).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps task in queue when at concurrency limit", () => {
|
it("keeps task in queue when at concurrency limit", () => {
|
||||||
const taskRunner = new TaskRunner(2);
|
const taskRunner = new TaskRunner(2);
|
||||||
expect(taskRunner.running).toEqual(0);
|
expect(taskRunner._running).toEqual(0);
|
||||||
taskRunner.push(() => mockPromise(1, 100));
|
taskRunner.push(() => mockPromise(1, 100));
|
||||||
taskRunner.push(() => mockPromise(2, 25));
|
taskRunner.push(() => mockPromise(2, 25));
|
||||||
taskRunner.push(() => mockPromise(3, 10));
|
taskRunner.push(() => mockPromise(3, 10));
|
||||||
expect(taskRunner.running).toEqual(2);
|
expect(taskRunner._running).toEqual(2);
|
||||||
expect(taskRunner.queue.length).toEqual(1);
|
expect(taskRunner._queue.length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("runs queued tasks as space becomes available", async () => {
|
it("runs queued tasks as space becomes available", async () => {
|
||||||
@@ -32,9 +36,9 @@ describe("TaskRunner", () => {
|
|||||||
taskRunner.push(() => mockPromise(1, 600));
|
taskRunner.push(() => mockPromise(1, 600));
|
||||||
taskRunner.push(() => mockPromise(2, 300));
|
taskRunner.push(() => mockPromise(2, 300));
|
||||||
taskRunner.push(() => mockPromise(3, 100));
|
taskRunner.push(() => mockPromise(3, 100));
|
||||||
expect(taskRunner.queue.length).toEqual(1);
|
expect(taskRunner._queue.length).toEqual(1);
|
||||||
await new Promise(r => setTimeout(r, 301));
|
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 () => {
|
it("runs tasks in order", async () => {
|
||||||
@@ -52,7 +56,44 @@ describe("TaskRunner", () => {
|
|||||||
.then((id) => {
|
.then((id) => {
|
||||||
actual.push(id);
|
actual.push(id);
|
||||||
});
|
});
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
expect(actual).toEqual([2, 3, 1]);
|
expect(actual).toEqual([2, 3, 1]);
|
||||||
});
|
});
|
||||||
})
|
|
||||||
|
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.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user