diff --git a/app/docker/views/images/imagesController.js b/app/docker/views/images/imagesController.js index 3250301e8..cd723236a 100644 --- a/app/docker/views/images/imagesController.js +++ b/app/docker/views/images/imagesController.js @@ -3,6 +3,7 @@ import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; import { confirmImageExport } from '@/react/docker/images/common/ConfirmExportModal'; import { confirmDestructive } from '@@/modals/confirm'; import { buildConfirmButton } from '@@/modals/utils'; +import { processItemsInBatches } from '@/react/common/processItemsInBatches'; angular.module('portainer.docker').controller('ImagesController', [ '$scope', @@ -157,24 +158,20 @@ angular.module('portainer.docker').controller('ImagesController', [ * @param {Array} selectedItems * @param {boolean} force */ - function removeAction(selectedItems, force) { - var actionCount = selectedItems.length; - angular.forEach(selectedItems, function (image) { + async function removeAction(selectedItems, force) { + async function doRemove(image) { HttpRequestHelper.setPortainerAgentTargetHeader(image.nodeName); - ImageService.deleteImage(image.id, force) + return ImageService.deleteImage(image.id, force) .then(function success() { Notifications.success('Image successfully removed', image.id); }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to remove image'); - }) - .finally(function final() { - --actionCount; - if (actionCount === 0) { - $state.reload(); - } }); - }); + } + + await processItemsInBatches(selectedItems, doRemove); + $state.reload(); } $scope.setPullImageValidity = setPullImageValidity; diff --git a/app/docker/views/networks/networksController.js b/app/docker/views/networks/networksController.js index 87d4b3797..d657c5e01 100644 --- a/app/docker/views/networks/networksController.js +++ b/app/docker/views/networks/networksController.js @@ -1,6 +1,7 @@ import _ from 'lodash-es'; import DockerNetworkHelper from '@/docker/helpers/networkHelper'; import { confirmDelete } from '@@/modals/confirm'; +import { processItemsInBatches } from '@/react/common/processItemsInBatches'; angular.module('portainer.docker').controller('NetworksController', [ '$q', @@ -17,10 +18,10 @@ angular.module('portainer.docker').controller('NetworksController', [ if (!confirmed) { return null; } - var actionCount = selectedItems.length; - angular.forEach(selectedItems, function (network) { + + async function doRemove(network) { HttpRequestHelper.setPortainerAgentTargetHeader(network.NodeName); - NetworkService.remove(network.Id) + return NetworkService.remove(network.Id) .then(function success() { Notifications.success('Network successfully removed', network.Name); var index = $scope.networks.indexOf(network); @@ -28,14 +29,11 @@ angular.module('portainer.docker').controller('NetworksController', [ }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to remove network'); - }) - .finally(function final() { - --actionCount; - if (actionCount === 0) { - $state.reload(); - } }); - }); + } + + await processItemsInBatches(selectedItems, doRemove); + $state.reload(); }; $scope.getNetworks = getNetworks; diff --git a/app/docker/views/secrets/secretsController.js b/app/docker/views/secrets/secretsController.js index d59a69472..9b6742795 100644 --- a/app/docker/views/secrets/secretsController.js +++ b/app/docker/views/secrets/secretsController.js @@ -1,4 +1,6 @@ import { confirmDelete } from '@@/modals/confirm'; +import { processItemsInBatches } from '@/react/common/processItemsInBatches'; + angular.module('portainer.docker').controller('SecretsController', [ '$scope', '$state', @@ -10,9 +12,9 @@ angular.module('portainer.docker').controller('SecretsController', [ if (!confirmed) { return null; } - var actionCount = selectedItems.length; - angular.forEach(selectedItems, function (secret) { - SecretService.remove(secret.Id) + + async function doRemove(secret) { + return SecretService.remove(secret.Id) .then(function success() { Notifications.success('Secret successfully removed', secret.Name); var index = $scope.secrets.indexOf(secret); @@ -20,14 +22,11 @@ angular.module('portainer.docker').controller('SecretsController', [ }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to remove secret'); - }) - .finally(function final() { - --actionCount; - if (actionCount === 0) { - $state.reload(); - } }); - }); + } + + await processItemsInBatches(selectedItems, doRemove); + $state.reload(); }; $scope.getSecrets = getSecrets; diff --git a/app/docker/views/volumes/volumesController.js b/app/docker/views/volumes/volumesController.js index 0ca1352d6..6398459b1 100644 --- a/app/docker/views/volumes/volumesController.js +++ b/app/docker/views/volumes/volumesController.js @@ -1,5 +1,7 @@ import { confirmDelete } from '@@/modals/confirm'; +import { processItemsInBatches } from '@/react/common/processItemsInBatches'; + angular.module('portainer.docker').controller('VolumesController', [ '$q', '$scope', @@ -13,27 +15,23 @@ angular.module('portainer.docker').controller('VolumesController', [ 'endpoint', function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, Authentication, endpoint) { $scope.removeAction = function (selectedItems) { - confirmDelete('Do you want to remove the selected volume(s)?').then((confirmed) => { + confirmDelete('Do you want to remove the selected volume(s)?').then(async (confirmed) => { + async function doRemove(volume) { + HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName); + return VolumeService.remove(volume) + .then(function success() { + Notifications.success('Volume successfully removed', volume.Id); + var index = $scope.volumes.indexOf(volume); + $scope.volumes.splice(index, 1); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove volume'); + }); + } + if (confirmed) { - var actionCount = selectedItems.length; - angular.forEach(selectedItems, function (volume) { - HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName); - VolumeService.remove(volume) - .then(function success() { - Notifications.success('Volume successfully removed', volume.Id); - var index = $scope.volumes.indexOf(volume); - $scope.volumes.splice(index, 1); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove volume'); - }) - .finally(function final() { - --actionCount; - if (actionCount === 0) { - $state.reload(); - } - }); - }); + await processItemsInBatches(selectedItems, doRemove); + $state.reload(); } }); }; diff --git a/app/portainer/views/stacks/stacksController.js b/app/portainer/views/stacks/stacksController.js index 1d7543d55..89a94a68e 100644 --- a/app/portainer/views/stacks/stacksController.js +++ b/app/portainer/views/stacks/stacksController.js @@ -1,5 +1,7 @@ import { confirmDelete } from '@@/modals/confirm'; +import { processItemsInBatches } from '@/react/common/processItemsInBatches'; + angular.module('portainer.app').controller('StacksController', StacksController); /* @ngInject */ @@ -13,11 +15,11 @@ function StacksController($scope, $state, Notifications, StackService, Authentic }); }; - function deleteSelectedStacks(stacks) { + async function deleteSelectedStacks(selectedItems) { const endpointId = endpoint.Id; - let actionCount = stacks.length; - angular.forEach(stacks, function (stack) { - StackService.remove(stack, stack.External, endpointId) + + async function doRemove(stack) { + return StackService.remove(stack, stack.External, endpointId) .then(function success() { Notifications.success('Stack successfully removed', stack.Name); var index = $scope.stacks.indexOf(stack); @@ -25,14 +27,11 @@ function StacksController($scope, $state, Notifications, StackService, Authentic }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to remove stack ' + stack.Name); - }) - .finally(function final() { - --actionCount; - if (actionCount === 0) { - $state.reload(); - } }); - }); + } + + await processItemsInBatches(selectedItems, doRemove); + $state.reload(); } $scope.createEnabled = false; diff --git a/app/portainer/views/users/usersController.js b/app/portainer/views/users/usersController.js index c898ea732..fe5f55c57 100644 --- a/app/portainer/views/users/usersController.js +++ b/app/portainer/views/users/usersController.js @@ -1,5 +1,6 @@ import _ from 'lodash-es'; import { confirmDelete } from '@@/modals/confirm'; +import { processItemsInBatches } from '@/react/common/processItemsInBatches'; angular.module('portainer.app').controller('UsersController', [ '$q', @@ -69,10 +70,9 @@ angular.module('portainer.app').controller('UsersController', [ }); }; - function deleteSelectedUsers(selectedItems) { - var actionCount = selectedItems.length; - angular.forEach(selectedItems, function (user) { - UserService.deleteUser(user.Id) + async function deleteSelectedUsers(selectedItems) { + async function doRemove(user) { + return UserService.deleteUser(user.Id) .then(function success() { Notifications.success('User successfully removed', user.Username); var index = $scope.users.indexOf(user); @@ -80,14 +80,10 @@ angular.module('portainer.app').controller('UsersController', [ }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to remove user'); - }) - .finally(function final() { - --actionCount; - if (actionCount === 0) { - $state.reload(); - } }); - }); + } + await processItemsInBatches(selectedItems, doRemove); + $state.reload(); } $scope.removeAction = function (selectedItems) { diff --git a/app/react/common/processItemsInBatches.ts b/app/react/common/processItemsInBatches.ts new file mode 100644 index 000000000..adeaaa058 --- /dev/null +++ b/app/react/common/processItemsInBatches.ts @@ -0,0 +1,30 @@ +/** + * Type definition for the callback function used in processItemsInBatches. + * It should accept an item from the array as its first argument + * and additional arguments (if any) as its second argument, and should return a Promise. + */ +type ProcessItemsCallback = ( + item: T, + ...args: Args +) => Promise; + +/** + * Asynchronously processes an array of items in batches. + * @param items An array of items to be processed. + * @param processor A callback function of type ProcessItemsCallback that will be called for each item in the array. + * @param batchSize The maximum number of items to process in each batch. Defaults to 100 if not provided. + * @param args Additional arguments to be passed to the callback function for each item. + */ +export async function processItemsInBatches( + items: T[], + processor: ProcessItemsCallback, + batchSize = 100, + ...args: Args +): Promise { + while (items.length) { + const batch = items.splice(0, batchSize); + const batchPromises = batch.map((item) => processor(item, ...args)); + + await Promise.all(batchPromises); + } +}