Skip to content

Commit

Permalink
Fixes #37551 - Hosts : Allow bulk reassignment of hostgroups
Browse files Browse the repository at this point in the history
  • Loading branch information
sjha4 committed Jul 10, 2024
1 parent f0e2655 commit 9ea0843
Show file tree
Hide file tree
Showing 11 changed files with 330 additions and 11 deletions.
17 changes: 16 additions & 1 deletion app/controllers/api/v2/hosts_bulk_actions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
7 changes: 1 addition & 6 deletions app/controllers/hosts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/registries/foreman/access_permissions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
7 changes: 7 additions & 0 deletions app/services/bulk_hosts_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down
1 change: 1 addition & 0 deletions config/routes/api/v2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ const BulkBuildHostModal = ({
singular: __('selected host'),
plural: __('selected hosts'),
}}
id="ccs-options-i18n"
id="bulk-build-hosts-selected-hosts"
/>
</strong>
),
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = [
<Button
key="add"
ouiaId="bulk-reassign-hg-modal-add-button"
variant="primary"
onClick={handleSave}
isDisabled={hostUpdateStatus === STATUS.PENDING}
isLoading={hostUpdateStatus === STATUS.PENDING}
>
{__('Save')}
</Button>,
<Button
key="cancel"
ouiaId="bulk-reassign-hg-modal-cancel-button"
variant="link"
onClick={handleModalClose}
>
{__('Cancel')}
</Button>,
];
return (
<Modal
isOpen={isOpen}
onClose={handleModalClose}
onEscapePress={handleModalClose}
title={__('Change host group')}
width="50%"
position="top"
actions={modalActions}
id="bulk-reassign-hg-modal"
key="bulk-reassign-hg-modal"
ouiaId="bulk-reassign-hg-modal"
>
<TextContent>
<Text ouiaId="bulk-reassign-hg-options">
<FormattedMessage
defaultMessage={__(
'Change the host group of {hosts}. Some hosts may already be in your chosen host group.'
)}
values={{
hosts: (
<strong>
<FormattedMessage
defaultMessage="{count, plural, one {# {singular}} other {# {plural}}}"
values={{
count: selectedCount,
singular: __('selected host'),
plural: __('selected hosts'),
}}
id="bulk-hg-selected-host-options"
/>
</strong>
),
}}
id="bulk-reassign-hg-description"
/>
</Text>
</TextContent>
{hostgroups && hostgroupStatus === STATUS.RESOLVED && (
<HostGroupSelect
onClear={() => setHostgroupId('')}
headerText={__('Select host group')}
selections={hostgroupId}
onChange={value => setHostgroupId(value)}
isOpen={hgSelectOpen}
onToggle={isExpanded => setHgSelectOpen(isExpanded)}
onSelect={handleHgSelect}
>
{hostgroups?.results?.map(hg => (
<SelectOption key={hg.id} value={hg.id}>
{hg.name}
</SelectOption>
))}
</HostGroupSelect>
)}
<hr />
</Modal>
);
};

BulkReassignHostgroupModal.propTypes = {
isOpen: PropTypes.bool,
closeModal: PropTypes.func,
selectedCount: PropTypes.number.isRequired,
fetchBulkParams: PropTypes.func.isRequired,
};

BulkReassignHostgroupModal.defaultProps = {
isOpen: false,
closeModal: () => {},
};

export default BulkReassignHostgroupModal;
Original file line number Diff line number Diff line change
@@ -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
}) => (
<div style={{ marginTop: '1em' }}>
<h3>{headerText}</h3>
<Select
variant={SelectVariant.typeahead}
onClear={onClear}
maxHeight="20rem"
menuAppendTo="parent"
ouiaId="select-host-group"
id="selectHostGroup"
name="selectHostGroup"
aria-label="selectHostGroup"
{...pfSelectProps}
>
{children}
</Select>
</div>
);

HostGroupSelect.propTypes = {
headerText: PropTypes.string,
onClear: PropTypes.func.isRequired,
children: PropTypes.node,
};

HostGroupSelect.defaultProps = {
headerText: __('Select host group'),
children: [],
};

export default HostGroupSelect;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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 (
<BulkReassignHostgroupModal
key="bulk-reassign-hg-modal"
selectedCount={selectedCount}
fetchBulkParams={fetchBulkParams}
isOpen={modalOpen}
closeModal={setModalClosed}
orgId={org?.id}
/>
);
};

export default BulkReassignHostgroupModalScene;
Loading

0 comments on commit 9ea0843

Please sign in to comment.