diff --git a/app/controllers/api/v2/hosts_bulk_actions_controller.rb b/app/controllers/api/v2/hosts_bulk_actions_controller.rb
index 0ff9fe2462c..3287ce4c118 100644
--- a/app/controllers/api/v2/hosts_bulk_actions_controller.rb
+++ b/app/controllers/api/v2/hosts_bulk_actions_controller.rb
@@ -5,7 +5,7 @@ class HostsBulkActionsController < V2::BaseController
include Api::V2::BulkHostsExtension
before_action :find_deletable_hosts, :only => [:bulk_destroy]
- before_action :find_editable_hosts, :only => [:build]
+ before_action :find_editable_hosts, :only => [:build, :reassign_hostgroup]
def_param_group :bulk_host_ids do
param :organization_id, :number, :required => true, :desc => N_("ID of the organization")
@@ -63,6 +63,21 @@ def build
end
end
+ api :PUT, "/hosts/bulk/reassign_hostgroups", N_("Reassign hostgroups")
+ param_group :bulk_host_ids
+ param :hostgroup_id, :number, :desc => N_("ID of the hostgroup to reassign the hosts to")
+ def reassign_hostgroup
+ hostgroup = params[:hostgroup_id].present? ? Hostgroup.find(params[:hostgroup_id]) : nil
+ BulkHostsManager.new(hosts: @hosts).reassign_hostgroups(hostgroup)
+ if hostgroup
+ process_response(true, { :message => n_("Reassigned %{count} host to hostgroup %{hostgroup}",
+ "Reassigned %{count} hosts to hostgroup %{hostgroup}", @hosts.count) % {count: @hosts.count, hostgroup: hostgroup.name} })
+ else
+ process_response(true, { :message => n_("Removed assignment of host group from %s host",
+ "Removed assignment of host group from %s hosts", @hosts.count) % @hosts.count })
+ end
+ end
+
protected
def action_permission
diff --git a/app/controllers/hosts_controller.rb b/app/controllers/hosts_controller.rb
index 1dc45447db2..f7fb6fad18d 100644
--- a/app/controllers/hosts_controller.rb
+++ b/app/controllers/hosts_controller.rb
@@ -435,12 +435,7 @@ def update_multiple_hostgroup
return
end
hg = Hostgroup.find_by_id(id)
- # update the hosts
- @hosts.each do |host|
- host.hostgroup = hg
- host.save(:validate => false)
- end
-
+ BulkHostsManager.new(hosts: @hosts).reassign_hostgroups(hg)
success _('Updated hosts: changed host group')
# We prefer to go back as this does not lose the current search
redirect_back_or_to hosts_path
diff --git a/app/registries/foreman/access_permissions.rb b/app/registries/foreman/access_permissions.rb
index f3d4ef2e263..16b21a02bba 100644
--- a/app/registries/foreman/access_permissions.rb
+++ b/app/registries/foreman/access_permissions.rb
@@ -273,7 +273,7 @@
:"api/v2/hosts" => [:update, :disassociate, :forget_status],
:"api/v2/interfaces" => [:create, :update, :destroy],
:"api/v2/compute_resources" => [:associate],
- :"api/v2/hosts_bulk_actions" => [:build],
+ :"api/v2/hosts_bulk_actions" => [:build, :reassign_hostgroup],
}
map.permission :destroy_hosts, {:hosts => [:destroy, :multiple_actions, :reset_multiple, :multiple_destroy, :submit_multiple_destroy],
:"api/v2/hosts" => [:destroy],
diff --git a/app/services/bulk_hosts_manager.rb b/app/services/bulk_hosts_manager.rb
index 7c379fec1c2..ef93a27f663 100644
--- a/app/services/bulk_hosts_manager.rb
+++ b/app/services/bulk_hosts_manager.rb
@@ -19,6 +19,13 @@ def build(reboot: false)
end
end
+ def reassign_hostgroups(hostgroup)
+ @hosts.each do |host|
+ host.hostgroup = hostgroup
+ host.save(:validate => false)
+ end
+ end
+
def rebuild_configuration
# returns a hash with a key/value configuration
all_fails = {}
diff --git a/config/routes/api/v2.rb b/config/routes/api/v2.rb
index 08fad846475..3eca161f873 100644
--- a/config/routes/api/v2.rb
+++ b/config/routes/api/v2.rb
@@ -5,6 +5,7 @@
scope "(:apiv)", :module => :v2, :defaults => {:apiv => 'v2'}, :apiv => /v2/, :constraints => ApiConstraints.new(:version => 2, :default => true) do
match 'hosts/bulk', :to => 'hosts_bulk_actions#bulk_destroy', :via => [:delete]
match 'hosts/bulk/build', :to => 'hosts_bulk_actions#build', :via => [:put]
+ match 'hosts/bulk/reassign_hostgroup', :to => 'hosts_bulk_actions#reassign_hostgroup', :via => [:put]
resources :architectures, :except => [:new, :edit] do
constraints(:id => /[^\/]+/) do
diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/buildHosts/BulkBuildHostModal.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/buildHosts/BulkBuildHostModal.js
index e25dc7da9d3..1c02884c859 100644
--- a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/buildHosts/BulkBuildHostModal.js
+++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/buildHosts/BulkBuildHostModal.js
@@ -110,7 +110,7 @@ const BulkBuildHostModal = ({
singular: __('selected host'),
plural: __('selected hosts'),
}}
- id="ccs-options-i18n"
+ id="bulk-build-hosts-selected-hosts"
/>
),
diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/BulkReassignHostgroupModal.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/BulkReassignHostgroupModal.js
new file mode 100644
index 00000000000..1faa23613bf
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/BulkReassignHostgroupModal.js
@@ -0,0 +1,197 @@
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { useDispatch, useSelector } from 'react-redux';
+import { FormattedMessage } from 'react-intl';
+import {
+ Modal,
+ Button,
+ TextContent,
+ Text,
+ SelectOption,
+} from '@patternfly/react-core';
+import { addToast } from '../../../ToastsList/slice';
+import { translate as __ } from '../../../../common/I18n';
+import { failedHostsToastParams } from '../helpers';
+import { STATUS } from '../../../../constants';
+import {
+ selectAPIStatus,
+ selectAPIResponse,
+} from '../../../../redux/API/APISelectors';
+import {
+ BULK_REASSIGN_HOSTGROUP_KEY,
+ bulkReassignHostgroups,
+ fetchHostgroups,
+ HOSTGROUP_KEY,
+} from './actions';
+import { foremanUrl } from '../../../../common/helpers';
+import { APIActions } from '../../../../redux/API';
+import HostGroupSelect from './HostGroupSelect';
+import {
+ HOSTS_API_PATH,
+ API_REQUEST_KEY,
+} from '../../../../routes/Hosts/constants';
+
+const BulkReassignHostgroupModal = ({
+ isOpen,
+ closeModal,
+ selectedCount,
+ fetchBulkParams,
+}) => {
+ const dispatch = useDispatch();
+ const [hostgroupId, setHostgroupId] = useState('');
+ const hostgroups = useSelector(state =>
+ selectAPIResponse(state, HOSTGROUP_KEY)
+ );
+ const hostgroupStatus = useSelector(state =>
+ selectAPIStatus(state, HOSTGROUP_KEY)
+ );
+ const hostUpdateStatus = useSelector(state =>
+ selectAPIStatus(state, BULK_REASSIGN_HOSTGROUP_KEY)
+ );
+ const handleModalClose = () => {
+ setHostgroupId('');
+ closeModal();
+ };
+
+ const [hgSelectOpen, setHgSelectOpen] = useState(false);
+
+ useEffect(() => {
+ dispatch(fetchHostgroups());
+ }, [dispatch]);
+
+ const handleError = response => {
+ handleModalClose();
+ dispatch(
+ addToast(
+ failedHostsToastParams({
+ ...response.data.error,
+ key: BULK_REASSIGN_HOSTGROUP_KEY,
+ })
+ )
+ );
+ };
+
+ const handleSuccess = response => {
+ dispatch(
+ addToast({
+ type: 'success',
+ message: response.data.message,
+ })
+ );
+ dispatch(
+ APIActions.get({
+ key: API_REQUEST_KEY,
+ url: foremanUrl(HOSTS_API_PATH),
+ })
+ );
+ handleModalClose();
+ };
+ const handleSave = () => {
+ const requestBody = {
+ included: {
+ search: fetchBulkParams(),
+ },
+ hostgroup_id: hostgroupId,
+ };
+
+ dispatch(bulkReassignHostgroups(requestBody, handleSuccess, handleError));
+ };
+
+ const handleHgSelect = (event, selection) => {
+ setHostgroupId(selection);
+ setHgSelectOpen(false);
+ };
+
+ const modalActions = [
+ ,
+ ,
+ ];
+ return (
+
+