Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hostgroup bulk actions : Reassign hostgroups #10202

Merged
merged 1 commit into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REST API wise I think POST /hostgroups/:id/hosts would be cleaner but I see the bulk pattern is already established so I'm not going to block on this.

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
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
sjha4 marked this conversation as resolved.
Show resolved Hide resolved
sjha4 marked this conversation as resolved.
Show resolved Hide resolved
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,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 = [
<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,29 @@
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,
handleSuccess,
handleError,
params,
});
};

export const HOSTGROUP_KEY = 'HOSTGROUP_KEY';

export const fetchHostgroups = () => {
sjha4 marked this conversation as resolved.
Show resolved Hide resolved
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,24 @@
import React, { useContext } from 'react';
import { ForemanActionsBarContext } from '../../../../components/HostDetails/ActionsBar';
import { useForemanModal } from '../../../../components/ForemanModal/ForemanModalHooks';
import BulkReassignHostgroupModal from './BulkReassignHostgroupModal';

const BulkReassignHostgroupModalScene = () => {
const { selectedCount, fetchBulkParams } = useContext(
ForemanActionsBarContext
);
const { modalOpen, setModalClosed } = useForemanModal({
id: 'bulk-reassign-hg-modal',
});
return (
<BulkReassignHostgroupModal
key="bulk-reassign-hg-modal"
selectedCount={selectedCount}
fetchBulkParams={fetchBulkParams}
isOpen={modalOpen}
closeModal={setModalClosed}
/>
);
};

export default BulkReassignHostgroupModalScene;
Loading
Loading