From 9ea08433ff27029f992c431019502075f1220c61 Mon Sep 17 00:00:00 2001 From: Samir Jha Date: Fri, 7 Jun 2024 17:15:43 +0000 Subject: [PATCH] Fixes #37551 - Hosts : Allow bulk reassignment of hostgroups --- .../api/v2/hosts_bulk_actions_controller.rb | 17 +- app/controllers/hosts_controller.rb | 7 +- app/registries/foreman/access_permissions.rb | 2 +- app/services/bulk_hosts_manager.rb | 7 + config/routes/api/v2.rb | 1 + .../buildHosts/BulkBuildHostModal.js | 2 +- .../BulkReassignHostgroupModal.js | 183 ++++++++++++++++++ .../reassignHostGroup/HostGroupSelect.js | 41 ++++ .../BulkActions/reassignHostGroup/actions.js | 30 +++ .../BulkActions/reassignHostGroup/index.js | 27 +++ .../react_app/components/HostsIndex/index.js | 24 ++- 11 files changed, 330 insertions(+), 11 deletions(-) create mode 100644 webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/BulkReassignHostgroupModal.js create mode 100644 webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/HostGroupSelect.js create mode 100644 webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/actions.js create mode 100644 webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/index.js diff --git a/app/controllers/api/v2/hosts_bulk_actions_controller.rb b/app/controllers/api/v2/hosts_bulk_actions_controller.rb index 0ff9fe2462c..1b4a40e5cf2 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 #{@hosts.count} host to hostgroup #{hostgroup.name}", + "Reassigned #{@hosts.count} hosts to hostgroup #{hostgroup.name}", @hosts.count) }) + else + process_response(true, { :message => n_("Removed assignment of host group from #{@hosts.count} host", + "Removed assignment of host group from #{@hosts.count} hosts", @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..a2ea867c4e6 --- /dev/null +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/BulkReassignHostgroupModal.js @@ -0,0 +1,183 @@ +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 { visit } from '../../../../../foreman_navigation'; +import { foremanUrl } from '../../../../common/helpers'; +import HostGroupSelect from './HostGroupSelect'; + +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 handleSave = () => { + const requestBody = { + included: { + search: fetchBulkParams(), + }, + hostgroup_id: hostgroupId, + }; + + dispatch( + bulkReassignHostgroups( + requestBody, + () => visit(foremanUrl('/new/hosts')), + handleError + ) + ); + }; + + const handleHgSelect = (event, selection) => { + setHostgroupId(selection); + setHgSelectOpen(false); + }; + + const modalActions = [ + , + , + ]; + return ( + + + + + + + ), + }} + id="bulk-reassign-hg-description" + /> + + + {hostgroups && hostgroupStatus === STATUS.RESOLVED && ( + setHostgroupId('')} + headerText={__('Select host group')} + selections={hostgroupId} + onChange={value => setHostgroupId(value)} + isOpen={hgSelectOpen} + onToggle={isExpanded => setHgSelectOpen(isExpanded)} + onSelect={handleHgSelect} + > + {hostgroups?.results?.map(hg => ( + + {hg.name} + + ))} + + )} +
+
+ ); +}; + +BulkReassignHostgroupModal.propTypes = { + isOpen: PropTypes.bool, + closeModal: PropTypes.func, + selectedCount: PropTypes.number.isRequired, + fetchBulkParams: PropTypes.func.isRequired, +}; + +BulkReassignHostgroupModal.defaultProps = { + isOpen: false, + closeModal: () => {}, +}; + +export default BulkReassignHostgroupModal; diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/HostGroupSelect.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/HostGroupSelect.js new file mode 100644 index 00000000000..94f2c726f35 --- /dev/null +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/HostGroupSelect.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { Select, SelectVariant } from '@patternfly/react-core'; +import PropTypes from 'prop-types'; +import { translate as __ } from '../../../../common/I18n'; + +const HostGroupSelect = ({ + headerText, + children, + onClear, + ...pfSelectProps +}) => ( +
+

{headerText}

+ +
+); + +HostGroupSelect.propTypes = { + headerText: PropTypes.string, + onClear: PropTypes.func.isRequired, + children: PropTypes.node, +}; + +HostGroupSelect.defaultProps = { + headerText: __('Select host group'), + children: [], +}; + +export default HostGroupSelect; diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/actions.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/actions.js new file mode 100644 index 00000000000..9cb6d8c3c1e --- /dev/null +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/actions.js @@ -0,0 +1,30 @@ +import { APIActions } from '../../../../redux/API'; +import { foremanUrl } from '../../../../common/helpers'; + +export const BULK_REASSIGN_HOSTGROUP_KEY = 'BULK_REASSIGN_HOSTGROUP_KEY'; +export const bulkReassignHostgroups = (params, handleSuccess, handleError) => { + const url = foremanUrl(`/api/v2/hosts/bulk/reassign_hostgroup`); + return APIActions.put({ + key: BULK_REASSIGN_HOSTGROUP_KEY, + url, + successToast: response => response.data.message, + handleSuccess, + handleError, + params, + }); +}; + +export const HOSTGROUP_KEY = 'HOSTGROUP_KEY'; + +export const fetchHostgroups = () => { + const url = foremanUrl('/api/v2/hostgroups'); + return APIActions.get({ + key: HOSTGROUP_KEY, + url, + params: { + per_page: 'all', + }, + }); +}; + +export default bulkReassignHostgroups; diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/index.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/index.js new file mode 100644 index 00000000000..59743d6d7d1 --- /dev/null +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/index.js @@ -0,0 +1,27 @@ +import React, { useContext } from 'react'; +import { ForemanActionsBarContext } from '../../../../components/HostDetails/ActionsBar'; +import { useForemanModal } from '../../../../components/ForemanModal/ForemanModalHooks'; +import { useForemanOrganization } from '../../../../Root/Context/ForemanContext'; +import BulkReassignHostgroupModal from './BulkReassignHostgroupModal'; + +const BulkReassignHostgroupModalScene = () => { + const { selectedCount, fetchBulkParams } = useContext( + ForemanActionsBarContext + ); + const { modalOpen, setModalClosed } = useForemanModal({ + id: 'bulk-reassign-hg-modal', + }); + const org = useForemanOrganization(); + return ( + + ); +}; + +export default BulkReassignHostgroupModalScene; diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/index.js b/webpack/assets/javascripts/react_app/components/HostsIndex/index.js index ca806bfc8d4..a7ccc45a4a6 100644 --- a/webpack/assets/javascripts/react_app/components/HostsIndex/index.js +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/index.js @@ -36,6 +36,7 @@ import { deleteHost } from '../HostDetails/ActionsBar/actions'; import { useForemanSettings } from '../../Root/Context/ForemanContext'; import { bulkDeleteHosts } from './BulkActions/bulkDelete'; import BulkBuildHostModal from './BulkActions/buildHosts'; +import BulkReassignHostgroupModal from './BulkActions/reassignHostGroup'; import { foremanUrl } from '../../common/helpers'; import Slot from '../common/Slot'; import forceSingleton from '../../common/forceSingleton'; @@ -189,9 +190,19 @@ const HostsIndex = () => { id: 'bulk-build-hosts-modal', }) ); + dispatch( + addModal({ + id: 'bulk-reassign-hg-modal', + }) + ); }, [dispatch]); - const { setModalOpen } = useForemanModal({ id: 'bulk-build-hosts-modal' }); + const { setModalOpen: setHgModalOpen } = useForemanModal({ + id: 'bulk-reassign-hg-modal', + }); + const { setModalOpen: setBuildModalOpen } = useForemanModal({ + id: 'bulk-build-hosts-modal', + }); const dropdownItems = [ { {__('Build management')} , + + {__('Change Host Group')} + , ]; const registeredItems = useSelector(selectKebabItems, shallowEqual); @@ -367,6 +386,7 @@ const HostsIndex = () => { value={{ selectedCount, fetchBulkParams }} > +