From 41bf18220bf3d90dad9ab3b3ed23918c37e6b6df Mon Sep 17 00:00:00 2001 From: Nadja Heitmann Date: Mon, 22 Jun 2026 07:40:03 +0000 Subject: [PATCH] Fixes #39437 - Apply taxonomies to UI bulk actions Before this patch, the Foreman UI did not send any information about taxonomies on the 'All Hosts' page for bulk actions. This could cause bulk actions to be applied to different hosts than the ones that were actually selected in the UI because the session in the backend would calculate the hosts independent from UI input. Assisted-by: OpenAI Codex --- .../api/v2/hosts_bulk_actions_controller.rb | 1 - .../concerns/api/v2/bulk_hosts_extension.rb | 10 +++++-- .../v2/hosts_bulk_actions_controller_test.rb | 28 +++++++++++++++++++ .../BulkActions/__tests__/bulkDelete.test.js | 24 ++++++++++++++-- .../assignTaxonomy/BulkAssignTaxonomyModal.js | 17 +++++++---- .../BulkActions/assignTaxonomy/index.js | 24 ++++++++++++---- .../buildHosts/BulkBuildHostModal.js | 18 ++++++++---- .../BulkActions/buildHosts/index.js | 11 ++++++-- .../HostsIndex/BulkActions/bulkDelete.js | 9 +++++- .../changeOwner/BulkChangeOwnerModal.js | 18 ++++++++---- .../BulkActions/changeOwner/index.js | 4 +++ .../disassociate/BulkDisassociateModal.js | 19 +++++++++---- .../BulkActions/disassociate/index.js | 4 +++ .../HostsIndex/BulkActions/helpers.js | 22 +++++++++++++++ .../powerState/BulkPowerStateModal.js | 18 ++++++++---- .../BulkActions/powerState/index.js | 11 ++++++-- .../BulkReassignHostgroupModal.js | 18 ++++++++---- .../BulkActions/reassignHostGroup/index.js | 11 ++++++-- .../react_app/components/HostsIndex/index.js | 8 ++++++ .../components/HostsIndex/index.test.js | 2 ++ 20 files changed, 221 insertions(+), 56 deletions(-) diff --git a/app/controllers/api/v2/hosts_bulk_actions_controller.rb b/app/controllers/api/v2/hosts_bulk_actions_controller.rb index bed35b6f1c..0c7bce1830 100644 --- a/app/controllers/api/v2/hosts_bulk_actions_controller.rb +++ b/app/controllers/api/v2/hosts_bulk_actions_controller.rb @@ -9,7 +9,6 @@ class HostsBulkActionsController < V2::BaseController before_action :validate_power_action, :only => [:change_power_state] def_param_group :bulk_host_ids do - param :organization_id, :number, :required => true, :desc => N_("ID of the organization") param :included, Hash, :desc => N_("Hosts to include in the action"), :required => true, :action_aware => true do param :search, String, :required => false, :desc => N_("Search string describing which hosts to perform the action on") param :ids, Array, :required => false, :desc => N_("List of host ids to perform the action on") diff --git a/app/controllers/concerns/api/v2/bulk_hosts_extension.rb b/app/controllers/concerns/api/v2/bulk_hosts_extension.rb index b6d720964d..49e53b2b77 100644 --- a/app/controllers/concerns/api/v2/bulk_hosts_extension.rb +++ b/app/controllers/concerns/api/v2/bulk_hosts_extension.rb @@ -1,9 +1,10 @@ module Api::V2::BulkHostsExtension extend ActiveSupport::Concern - def bulk_hosts_relation(permission, org) + def bulk_hosts_relation(permission, org, location) relation = ::Host::Managed.authorized(permission) relation = relation.where(organization: org) if org + relation = relation.where(location: location) if location relation end @@ -18,7 +19,8 @@ def find_bulk_hosts(permission, bulk_params, restrict_to = nil) end find_organization - @hosts = bulk_hosts_relation(permission, @organization) + find_location + @hosts = bulk_hosts_relation(permission, @organization, @location) if bulk_params[:included][:ids].present? @hosts = @hosts.where(id: bulk_params[:included][:ids]) @@ -42,4 +44,8 @@ def find_bulk_hosts(permission, bulk_params, restrict_to = nil) def find_organization @organization ||= Organization.find_by_id(params[:organization_id]) end + + def find_location + @location ||= Location.find_by_id(params[:location_id]) + end end diff --git a/test/controllers/api/v2/hosts_bulk_actions_controller_test.rb b/test/controllers/api/v2/hosts_bulk_actions_controller_test.rb index 0312d16308..f03af11fd5 100644 --- a/test/controllers/api/v2/hosts_bulk_actions_controller_test.rb +++ b/test/controllers/api/v2/hosts_bulk_actions_controller_test.rb @@ -17,6 +17,7 @@ def setup def valid_bulk_params(host_ids = @host_ids) { :organization_id => @organization.id, + :location_id => @location.id, :included => { :ids => host_ids, }, @@ -107,6 +108,33 @@ def valid_power_params(host_ids = @host_ids, action = 'start') end end + test "should scope searched hosts by organization and location" do + other_location = FactoryBot.create(:location) + other_host = FactoryBot.create(:host, :managed, :organization => @organization, :location => other_location) + + put :change_owner, params: { + :organization_id => @organization.id, + :location_id => @location.id, + :included => { + :search => 'name ~ *', + }, + :excluded => { + :ids => [], + }, + :owner_id => @user.id_and_type, + } + + assert_response :success + + [@host1, @host2, @host3].each do |host| + host.reload + assert_equal @user.id_and_type, host.is_owned_by + end + + other_host.reload + refute_equal @user.id_and_type, other_host.is_owned_by + end + context "change_power_state" do test "successfully changes power state for all hosts" do Host.any_instance.stubs(:supports_power?).returns(true) diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/__tests__/bulkDelete.test.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/__tests__/bulkDelete.test.js index 5c76648968..f132aaffae 100644 --- a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/__tests__/bulkDelete.test.js +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/__tests__/bulkDelete.test.js @@ -37,12 +37,16 @@ describe('bulkDeleteHosts', () => { }); const bulkParams = 'id ^ (1,2,3)'; + const organizationId = 1; + const locationId = 2; const selectedCount = 3; const destroyVmOnHostDelete = true; it('dispatches openConfirmModal with correct parameters', () => { bulkDeleteHosts({ bulkParams, + organizationId, + locationId, selectedCount, destroyVmOnHostDelete, })(dispatch); @@ -61,6 +65,8 @@ describe('bulkDeleteHosts', () => { it('calls visit with /new/hosts when onDeleteSuccess is not provided', () => { bulkDeleteHosts({ bulkParams, + organizationId, + locationId, selectedCount, destroyVmOnHostDelete, })(dispatch); @@ -70,8 +76,16 @@ describe('bulkDeleteHosts', () => { expect(APIActions.delete).toHaveBeenCalledTimes(1); const deleteParams = APIActions.delete.mock.calls[0][0]; - - expect(deleteParams.url).toBe(`/api/v2/hosts/bulk?search=${bulkParams}`); + const requestUrl = new URL(deleteParams.url, 'https://example.test'); + + expect(requestUrl.pathname).toBe('/api/v2/hosts/bulk'); + expect(requestUrl.searchParams.get('search')).toBe(bulkParams); + expect(requestUrl.searchParams.get('organization_id')).toBe( + String(organizationId) + ); + expect(requestUrl.searchParams.get('location_id')).toBe( + String(locationId) + ); expect(deleteParams.key).toBe('BULK-HOSTS-DELETE'); expect(typeof deleteParams.successToast).toBe('function'); expect(typeof deleteParams.errorToast).toBe('function'); @@ -86,6 +100,8 @@ describe('bulkDeleteHosts', () => { bulkDeleteHosts({ bulkParams, + organizationId, + locationId, selectedCount, destroyVmOnHostDelete, onDeleteSuccess, @@ -106,6 +122,8 @@ describe('bulkDeleteHosts', () => { it('returns formatted success message with host count', () => { bulkDeleteHosts({ bulkParams, + organizationId, + locationId, selectedCount, destroyVmOnHostDelete, })(dispatch); @@ -124,6 +142,8 @@ describe('bulkDeleteHosts', () => { it('returns error message from response', () => { bulkDeleteHosts({ bulkParams, + organizationId, + locationId, selectedCount, destroyVmOnHostDelete, })(dispatch); diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/assignTaxonomy/BulkAssignTaxonomyModal.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/assignTaxonomy/BulkAssignTaxonomyModal.js index 5bc8273709..3ae3450866 100644 --- a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/assignTaxonomy/BulkAssignTaxonomyModal.js +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/assignTaxonomy/BulkAssignTaxonomyModal.js @@ -38,6 +38,7 @@ import { API_REQUEST_KEY, } from '../../../../routes/Hosts/constants'; import TaxonomySelect from './TaxonomySelect'; +import { buildBulkRequestBody } from '../helpers'; export const BulkAssignOrganizationModal = props => ( @@ -52,6 +53,8 @@ const BulkAssignTaxonomyModal = ({ selectAllHostsMode, selectedCount, fetchBulkParams, + organizationId, + locationId, modalType, }) => { const org = modalType === MODAL_TYPES.ORGANIZATION; @@ -136,13 +139,13 @@ const BulkAssignTaxonomyModal = ({ }; const handleSave = () => { - const requestBody = { - included: { - search: fetchBulkParams(), - }, + const requestBody = buildBulkRequestBody({ + fetchBulkParams, + organizationId, + locationId, id: taxId, mismatch_setting: fixRadioChecked, - }; + }); org ? dispatch( @@ -243,10 +246,14 @@ BulkAssignTaxonomyModal.propTypes = { selectedCount: PropTypes.number.isRequired, selectAllHostsMode: PropTypes.bool.isRequired, fetchBulkParams: PropTypes.func.isRequired, + organizationId: PropTypes.number, + locationId: PropTypes.number, modalType: PropTypes.string.isRequired, }; BulkAssignTaxonomyModal.defaultProps = { isOpen: false, closeModal: () => {}, + organizationId: undefined, + locationId: undefined, }; diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/assignTaxonomy/index.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/assignTaxonomy/index.js index 61d2fdfc5d..4375e6b447 100644 --- a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/assignTaxonomy/index.js +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/assignTaxonomy/index.js @@ -7,15 +7,21 @@ import { } from './BulkAssignTaxonomyModal'; export const BulkAssignOrganizationModalScene = ({ isOpen, closeModal }) => { - const { selectAllHostsMode, selectedCount, fetchBulkParams } = useContext( - ForemanActionsBarContext - ); + const { + selectAllHostsMode, + selectedCount, + fetchBulkParams, + organizationId, + locationId, + } = useContext(ForemanActionsBarContext); return ( @@ -23,15 +29,21 @@ export const BulkAssignOrganizationModalScene = ({ isOpen, closeModal }) => { }; export const BulkAssignLocationModalScene = ({ isOpen, closeModal }) => { - const { selectAllHostsMode, selectedCount, fetchBulkParams } = useContext( - ForemanActionsBarContext - ); + const { + selectAllHostsMode, + selectedCount, + fetchBulkParams, + organizationId, + locationId, + } = useContext(ForemanActionsBarContext); return ( 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 e492e0b0fc..eea9df3c7e 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 @@ -12,7 +12,7 @@ import { } from '@patternfly/react-core'; import { addToast } from '../../../ToastsList/slice'; import { translate as __ } from '../../../../common/I18n'; -import { failedHostsToastParams } from '../helpers'; +import { buildBulkRequestBody, failedHostsToastParams } from '../helpers'; import { STATUS } from '../../../../constants'; import { selectAPIStatus } from '../../../../redux/API/APISelectors'; import { bulkBuildHosts, HOST_BUILD_KEY } from './actions'; @@ -22,6 +22,8 @@ const BulkBuildHostModal = ({ closeModal, selectedCount, fetchBulkParams, + organizationId, + locationId, }) => { const dispatch = useDispatch(); const [buildRadioChecked, setBuildRadioChecked] = useState(true); @@ -44,13 +46,13 @@ const BulkBuildHostModal = ({ ); }; const handleSave = () => { - const requestBody = { - included: { - search: fetchBulkParams(), - }, + const requestBody = buildBulkRequestBody({ + fetchBulkParams, + organizationId, + locationId, reboot: rebootChecked, rebuild_configuration: !buildRadioChecked, - }; + }); dispatch(bulkBuildHosts(requestBody, handleModalClose, handleError)); }; @@ -156,11 +158,15 @@ BulkBuildHostModal.propTypes = { closeModal: PropTypes.func, selectedCount: PropTypes.number.isRequired, fetchBulkParams: PropTypes.func.isRequired, + organizationId: PropTypes.number, + locationId: PropTypes.number, }; BulkBuildHostModal.defaultProps = { isOpen: false, closeModal: () => {}, + organizationId: undefined, + locationId: undefined, }; export default BulkBuildHostModal; diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/buildHosts/index.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/buildHosts/index.js index f7e413862e..b143c30973 100644 --- a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/buildHosts/index.js +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/buildHosts/index.js @@ -4,14 +4,19 @@ import { ForemanActionsBarContext } from '../../../../components/HostDetails/Act import BulkBuildHostModal from './BulkBuildHostModal'; const BulkBuildHostModalScene = ({ isOpen, closeModal }) => { - const { selectedCount, fetchBulkParams } = useContext( - ForemanActionsBarContext - ); + const { + selectedCount, + fetchBulkParams, + organizationId, + locationId, + } = useContext(ForemanActionsBarContext); return ( diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/bulkDelete.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/bulkDelete.js index 6ab889bd29..5c12e9d7f7 100644 --- a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/bulkDelete.js +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/bulkDelete.js @@ -4,17 +4,24 @@ import { visit, foremanUrl } from '../../../common/helpers'; import { sprintf, translate as __ } from '../../../common/I18n'; import { openConfirmModal } from '../../ConfirmModal'; import { APIActions } from '../../../redux/API'; +import { bulkActionTaxonomyParams } from './helpers'; import './bulkDeleteModal.scss'; export const bulkDeleteHosts = ({ bulkParams, + organizationId, + locationId, selectedCount, destroyVmOnHostDelete, onDeleteSuccess, }) => dispatch => { const successToast = () => sprintf(__('%s hosts deleted'), selectedCount); const errorToast = ({ message }) => message; - const url = foremanUrl(`/api/v2/hosts/bulk?search=${bulkParams}`); + const queryParams = new URLSearchParams({ + search: bulkParams, + ...bulkActionTaxonomyParams({ organizationId, locationId }), + }); + const url = foremanUrl(`/api/v2/hosts/bulk?${queryParams.toString()}`); // TODO: Replace with a checkbox instead of a global setting for cascade host destroy const cascadeMessage = () => diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/changeOwner/BulkChangeOwnerModal.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/changeOwner/BulkChangeOwnerModal.js index 34e7d9b456..d48e9d9e0c 100644 --- a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/changeOwner/BulkChangeOwnerModal.js +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/changeOwner/BulkChangeOwnerModal.js @@ -34,7 +34,7 @@ import { HOSTS_API_PATH, API_REQUEST_KEY, } from '../../../../routes/Hosts/constants'; -import { failedHostsToastParams } from '../helpers'; +import { buildBulkRequestBody, failedHostsToastParams } from '../helpers'; const BulkChangeOwnerModal = ({ isOpen, @@ -42,6 +42,8 @@ const BulkChangeOwnerModal = ({ selectAllHostsMode, selectedCount, fetchBulkParams, + organizationId, + locationId, }) => { const dispatch = useDispatch(); const [ownerId, setOwnerId] = useState(''); @@ -131,12 +133,12 @@ const BulkChangeOwnerModal = ({ }; const handleConfirm = () => { - const requestBody = { - included: { - search: fetchBulkParams(), - }, + const requestBody = buildBulkRequestBody({ + fetchBulkParams, + organizationId, + locationId, owner_id: ownerId, - }; + }); dispatch(bulkChangeOwner(requestBody, handleSuccess, handleError)); }; @@ -250,11 +252,15 @@ BulkChangeOwnerModal.propTypes = { fetchBulkParams: PropTypes.func.isRequired, selectedCount: PropTypes.number.isRequired, selectAllHostsMode: PropTypes.bool.isRequired, + organizationId: PropTypes.number, + locationId: PropTypes.number, }; BulkChangeOwnerModal.defaultProps = { isOpen: false, closeModal: () => {}, + organizationId: undefined, + locationId: undefined, }; export default BulkChangeOwnerModal; diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/changeOwner/index.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/changeOwner/index.js index fd554d8662..454e56ca50 100644 --- a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/changeOwner/index.js +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/changeOwner/index.js @@ -9,6 +9,8 @@ const BulkChangeOwnerModalScene = ({ isOpen, closeModal }) => { selectedCount, selectedResults, fetchBulkParams, + organizationId, + locationId, } = useContext(ForemanActionsBarContext); return ( { selectedCount={selectedCount} selectedResults={selectedResults} fetchBulkParams={fetchBulkParams} + organizationId={organizationId} + locationId={locationId} isOpen={isOpen} closeModal={closeModal} /> diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/disassociate/BulkDisassociateModal.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/disassociate/BulkDisassociateModal.js index 17a51be23d..d8b7ec3300 100644 --- a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/disassociate/BulkDisassociateModal.js +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/disassociate/BulkDisassociateModal.js @@ -18,7 +18,7 @@ import { HOSTS_API_PATH, API_REQUEST_KEY, } from '../../../../routes/Hosts/constants'; -import { failedHostsToastParams } from '../helpers'; +import { buildBulkRequestBody, failedHostsToastParams } from '../helpers'; const BulkDisassociateModal = ({ isOpen, @@ -27,6 +27,8 @@ const BulkDisassociateModal = ({ selectedCount, selectedResults, fetchBulkParams, + organizationId, + locationId, }) => { const dispatch = useDispatch(); const hostsWithComputeResource = selectedResults?.filter( @@ -91,11 +93,12 @@ const BulkDisassociateModal = ({ const queryString = selectedResultsEmpty ? fetchBulkParams() : `id ^ (${hostsWithComputeResource.map(h => h.id).join(',')})`; - const requestBody = { - included: { - search: queryString, - }, - }; + const requestBody = buildBulkRequestBody({ + fetchBulkParams, + organizationId, + locationId, + includedSearch: queryString, + }); dispatch(bulkDisassociate(requestBody, handleSuccess, handleError)); }; @@ -188,12 +191,16 @@ BulkDisassociateModal.propTypes = { fetchBulkParams: PropTypes.func.isRequired, selectedCount: PropTypes.number.isRequired, selectAllHostsMode: PropTypes.bool.isRequired, + organizationId: PropTypes.number, + locationId: PropTypes.number, }; BulkDisassociateModal.defaultProps = { isOpen: false, closeModal: () => {}, selectedResults: [], + organizationId: undefined, + locationId: undefined, }; export default BulkDisassociateModal; diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/disassociate/index.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/disassociate/index.js index 45438eb4f2..169eb72f36 100644 --- a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/disassociate/index.js +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/disassociate/index.js @@ -9,6 +9,8 @@ const BulkDisassociateModalScene = ({ isOpen, closeModal }) => { selectedCount, selectedResults, fetchBulkParams, + organizationId, + locationId, } = useContext(ForemanActionsBarContext); return ( { selectedCount={selectedCount} selectedResults={selectedResults} fetchBulkParams={fetchBulkParams} + organizationId={organizationId} + locationId={locationId} isOpen={isOpen} closeModal={closeModal} /> diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/helpers.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/helpers.js index 675c0c5bfb..e51c1ba8cc 100644 --- a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/helpers.js +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/helpers.js @@ -8,6 +8,28 @@ export const searchLink = ({ query, message, baseUrl }) => ({ href: urlWithSearch(baseUrl, query), }); +export const bulkActionTaxonomyParams = ({ + organizationId, + locationId, +} = {}) => ({ + ...(organizationId != null ? { organization_id: organizationId } : {}), + ...(locationId != null ? { location_id: locationId } : {}), +}); + +export const buildBulkRequestBody = ({ + fetchBulkParams, + organizationId, + locationId, + includedSearch, + ...params +}) => ({ + included: { + search: includedSearch || fetchBulkParams(), + }, + ...bulkActionTaxonomyParams({ organizationId, locationId }), + ...params, +}); + export const failedHostsToastParams = ({ message, failed_host_ids: failedHostIds, diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/powerState/BulkPowerStateModal.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/powerState/BulkPowerStateModal.js index a21ab5bfcd..8cb93e5b10 100644 --- a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/powerState/BulkPowerStateModal.js +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/powerState/BulkPowerStateModal.js @@ -19,7 +19,7 @@ import './BulkPowerStateModal.scss'; import { HostsPowerRefreshContext } from '../../HostsPowerRefreshContext'; import { POWER_STATES, BULK_POWER_STATE_KEY } from './constants'; import { bulkChangePowerState } from './actions'; -import { failedHostsToastParams } from '../helpers'; +import { buildBulkRequestBody, failedHostsToastParams } from '../helpers'; import { addToast } from '../../../ToastsList/slice'; import { HOSTS_API_PATH, @@ -31,6 +31,8 @@ import { APIActions } from '../../../../redux/API'; const BulkPowerStateModal = ({ selectedHostsCount, fetchBulkParams, + organizationId, + locationId, isOpen, closeModal, }) => { @@ -102,12 +104,12 @@ const BulkPowerStateModal = ({ const handleSubmit = () => { setIsLoading(true); - const payload = { - included: { - search: fetchBulkParams(), - }, + const payload = buildBulkRequestBody({ + fetchBulkParams, + organizationId, + locationId, power: selectedPowerState, - }; + }); dispatch(bulkChangePowerState(payload, handleSuccess, handleError)); }; @@ -206,12 +208,16 @@ const BulkPowerStateModal = ({ BulkPowerStateModal.propTypes = { selectedHostsCount: PropTypes.number, fetchBulkParams: PropTypes.func.isRequired, + organizationId: PropTypes.number, + locationId: PropTypes.number, isOpen: PropTypes.bool, closeModal: PropTypes.func, }; BulkPowerStateModal.defaultProps = { selectedHostsCount: 0, + organizationId: undefined, + locationId: undefined, isOpen: false, closeModal: () => {}, }; diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/powerState/index.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/powerState/index.js index 80dcf1ba13..f67e800a9e 100644 --- a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/powerState/index.js +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/powerState/index.js @@ -4,13 +4,18 @@ import { ForemanActionsBarContext } from '../../../../components/HostDetails/Act import BulkPowerStateModal from './BulkPowerStateModal'; const BulkPowerStateModalScene = ({ isOpen, closeModal }) => { - const { fetchBulkParams, selectedCount = 0 } = useContext( - ForemanActionsBarContext - ); + const { + fetchBulkParams, + selectedCount = 0, + organizationId, + locationId, + } = useContext(ForemanActionsBarContext); return ( 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 index 308e4ee60f..3e6c051a85 100644 --- a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/BulkReassignHostgroupModal.js +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/BulkReassignHostgroupModal.js @@ -11,7 +11,7 @@ import { } from '@patternfly/react-core'; import { addToast } from '../../../ToastsList/slice'; import { translate as __ } from '../../../../common/I18n'; -import { failedHostsToastParams } from '../helpers'; +import { buildBulkRequestBody, failedHostsToastParams } from '../helpers'; import { STATUS } from '../../../../constants'; import { selectAPIStatus, @@ -77,6 +77,8 @@ const BulkReassignHostgroupModal = ({ closeModal, selectedCount, fetchBulkParams, + organizationId, + locationId, }) => { const dispatch = useDispatch(); const [hostgroupId, setHostgroupId] = useState(''); @@ -151,12 +153,12 @@ const BulkReassignHostgroupModal = ({ handleModalClose(); }; const handleSave = () => { - const requestBody = { - included: { - search: fetchBulkParams(), - }, + const requestBody = buildBulkRequestBody({ + fetchBulkParams, + organizationId, + locationId, hostgroup_id: hostgroupId, - }; + }); dispatch(bulkReassignHostgroups(requestBody, handleSuccess, handleError)); }; @@ -285,11 +287,15 @@ BulkReassignHostgroupModal.propTypes = { closeModal: PropTypes.func, selectedCount: PropTypes.number.isRequired, fetchBulkParams: PropTypes.func.isRequired, + organizationId: PropTypes.number, + locationId: PropTypes.number, }; BulkReassignHostgroupModal.defaultProps = { isOpen: false, closeModal: () => {}, + organizationId: undefined, + locationId: undefined, }; export default BulkReassignHostgroupModal; 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 index 4712eaad4d..d7a4669a73 100644 --- a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/index.js +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/reassignHostGroup/index.js @@ -4,14 +4,19 @@ import { ForemanActionsBarContext } from '../../../../components/HostDetails/Act import BulkReassignHostgroupModal from './BulkReassignHostgroupModal'; const BulkReassignHostgroupModalScene = ({ isOpen, closeModal }) => { - const { selectedCount, fetchBulkParams } = useContext( - ForemanActionsBarContext - ); + const { + selectedCount, + fetchBulkParams, + organizationId, + locationId, + } = useContext(ForemanActionsBarContext); return ( diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/index.js b/webpack/assets/javascripts/react_app/components/HostsIndex/index.js index 83c173ce6a..f8b82efd1e 100644 --- a/webpack/assets/javascripts/react_app/components/HostsIndex/index.js +++ b/webpack/assets/javascripts/react_app/components/HostsIndex/index.js @@ -45,6 +45,8 @@ import { useForemanSettings, useForemanHostsPageUrl, useForemanContext, + useForemanOrganization, + useForemanLocation, } from '../../Root/Context/ForemanContext'; import { bulkDeleteHosts } from './BulkActions/bulkDelete'; import { @@ -114,6 +116,8 @@ const HostsIndex = () => { syncWithOptions: true, }); const contextData = useForemanContext(); + const currentOrganization = useForemanOrganization(); + const currentLocation = useForemanLocation(); const { response: { @@ -240,6 +244,8 @@ const HostsIndex = () => { dispatch( bulkDeleteHosts({ bulkParams, + organizationId: currentOrganization?.id, + locationId: currentLocation?.id, selectedCount, destroyVmOnHostDelete, onDeleteSuccess: () => { @@ -589,6 +595,8 @@ const HostsIndex = () => { selectedCount, selectedResults, fetchBulkParams, + organizationId: currentOrganization?.id, + locationId: currentLocation?.id, }} > ({ })), useForemanHostsPageUrl: jest.fn(() => '/hosts'), useForemanContext: jest.fn(() => ({})), + useForemanOrganization: jest.fn(() => undefined), + useForemanLocation: jest.fn(() => undefined), })); jest.mock('../common/Slot', () => ({