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 61249bcde..5b191bfea 100644 --- a/app/docker/views/networks/networksController.js +++ b/app/docker/views/networks/networksController.js @@ -1,5 +1,6 @@ import _ from 'lodash-es'; import DockerNetworkHelper from '@/docker/helpers/networkHelper'; +import { processItemsInBatches } from '@/react/common/processItemsInBatches'; angular.module('portainer.docker').controller('NetworksController', [ '$q', @@ -12,10 +13,9 @@ angular.module('portainer.docker').controller('NetworksController', [ 'AgentService', function ($q, $scope, $state, NetworkService, Notifications, HttpRequestHelper, endpoint, AgentService) { $scope.removeAction = async function (selectedItems) { - 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); @@ -23,14 +23,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 6b7cb68ca..f1d0696ec 100644 --- a/app/docker/views/secrets/secretsController.js +++ b/app/docker/views/secrets/secretsController.js @@ -1,3 +1,5 @@ +import { processItemsInBatches } from '@/react/common/processItemsInBatches'; + angular.module('portainer.docker').controller('SecretsController', [ '$scope', '$state', @@ -5,9 +7,8 @@ angular.module('portainer.docker').controller('SecretsController', [ 'Notifications', function ($scope, $state, SecretService, Notifications) { $scope.removeAction = async function (selectedItems) { - 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); @@ -15,14 +16,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 7f9740770..62de4a094 100644 --- a/app/docker/views/volumes/volumesController.js +++ b/app/docker/views/volumes/volumesController.js @@ -1,3 +1,5 @@ +import { processItemsInBatches } from '@/react/common/processItemsInBatches'; + angular.module('portainer.docker').controller('VolumesController', [ '$q', '$scope', @@ -10,11 +12,10 @@ angular.module('portainer.docker').controller('VolumesController', [ 'Authentication', 'endpoint', function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, Authentication, endpoint) { - $scope.removeAction = function (selectedItems) { - var actionCount = selectedItems.length; - angular.forEach(selectedItems, function (volume) { + $scope.removeAction = async function (selectedItems) { + async function doRemove(volume) { HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName); - VolumeService.remove(volume) + return VolumeService.remove(volume) .then(function success() { Notifications.success('Volume successfully removed', volume.Id); var index = $scope.volumes.indexOf(volume); @@ -22,14 +23,11 @@ angular.module('portainer.docker').controller('VolumesController', [ }) .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(); }; $scope.getVolumes = getVolumes; diff --git a/app/portainer/views/stacks/stacksController.js b/app/portainer/views/stacks/stacksController.js index 1a9e2d454..287e81727 100644 --- a/app/portainer/views/stacks/stacksController.js +++ b/app/portainer/views/stacks/stacksController.js @@ -1,3 +1,5 @@ +import { processItemsInBatches } from '@/react/common/processItemsInBatches'; + angular.module('portainer.app').controller('StacksController', StacksController); /* @ngInject */ @@ -6,11 +8,11 @@ function StacksController($scope, $state, Notifications, StackService, Authentic return deleteSelectedStacks(selectedItems); }; - 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); @@ -18,14 +20,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 09e48609c..c24aae7c8 100644 --- a/app/portainer/views/users/usersController.js +++ b/app/portainer/views/users/usersController.js @@ -1,5 +1,6 @@ import _ from 'lodash-es'; import { AuthenticationMethod } from '@/react/portainer/settings/types'; +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); + } +}