From 70afff58e129738993c7e129416ce5ea431a0b2b Mon Sep 17 00:00:00 2001 From: Nadja Heitmann Date: Mon, 22 Sep 2025 19:29:47 +0000 Subject: [PATCH] Add Puppet proxy (CA) bulk actions Assisted-by: OpenAI Codex --- .../api/v2/hosts_bulk_actions_controller.rb | 102 +++++++ .../extensions/bulk_hosts_manager.rb | 24 ++ config/api_routes.rb | 11 + lib/foreman_puppet/engine.rb | 1 + lib/foreman_puppet/register.rb | 2 + .../v2/hosts_bulk_actions_controller_test.rb | 171 ++++++++++++ .../foreman_puppet/bulk_hosts_manager_test.rb | 33 +++ webpack/global_index.js | 42 +++ .../Extends/Hosts/ActionsBar/ActionsBar.scss | 14 + webpack/src/Extends/Hosts/ActionsBar/index.js | 73 +++++ .../__tests__/actions.test.js | 78 ++++++ .../BulkChangeProxyCommon/actions.js | 36 +++ .../BulkChangeProxyCommon/index.js | 252 ++++++++++++++++++ .../__tests__/index.test.js | 66 +++++ .../BulkChangePuppetCAProxy/index.js | 40 +++ .../__tests__/index.test.js | 66 +++++ .../BulkChangePuppetProxy/index.js | 40 +++ .../__tests__/actions.test.js | 64 +++++ .../BulkRemoveProxyCommon/actions.js | 24 ++ .../BulkRemoveProxyCommon/index.js | 164 ++++++++++++ .../__tests__/index.test.js | 65 +++++ .../BulkRemovePuppetCAProxy/index.js | 39 +++ .../__tests__/index.test.js | 61 +++++ .../BulkRemovePuppetProxy/index.js | 39 +++ .../__tests__/toastHelpers.test.js | 55 ++++ .../Extends/Hosts/BulkActions/toastHelpers.js | 17 ++ webpack/src/foreman_puppet_host_form.test.js | 8 +- 27 files changed, 1583 insertions(+), 4 deletions(-) create mode 100644 app/controllers/foreman_puppet/api/v2/hosts_bulk_actions_controller.rb create mode 100644 app/services/concerns/foreman_puppet/extensions/bulk_hosts_manager.rb create mode 100644 test/controllers/foreman_puppet/api/v2/hosts_bulk_actions_controller_test.rb create mode 100644 test/services/foreman_puppet/bulk_hosts_manager_test.rb create mode 100644 webpack/src/Extends/Hosts/ActionsBar/ActionsBar.scss create mode 100644 webpack/src/Extends/Hosts/ActionsBar/index.js create mode 100644 webpack/src/Extends/Hosts/BulkActions/BulkChangeProxyCommon/__tests__/actions.test.js create mode 100644 webpack/src/Extends/Hosts/BulkActions/BulkChangeProxyCommon/actions.js create mode 100644 webpack/src/Extends/Hosts/BulkActions/BulkChangeProxyCommon/index.js create mode 100644 webpack/src/Extends/Hosts/BulkActions/BulkChangePuppetCAProxy/__tests__/index.test.js create mode 100644 webpack/src/Extends/Hosts/BulkActions/BulkChangePuppetCAProxy/index.js create mode 100644 webpack/src/Extends/Hosts/BulkActions/BulkChangePuppetProxy/__tests__/index.test.js create mode 100644 webpack/src/Extends/Hosts/BulkActions/BulkChangePuppetProxy/index.js create mode 100644 webpack/src/Extends/Hosts/BulkActions/BulkRemoveProxyCommon/__tests__/actions.test.js create mode 100644 webpack/src/Extends/Hosts/BulkActions/BulkRemoveProxyCommon/actions.js create mode 100644 webpack/src/Extends/Hosts/BulkActions/BulkRemoveProxyCommon/index.js create mode 100644 webpack/src/Extends/Hosts/BulkActions/BulkRemovePuppetCAProxy/__tests__/index.test.js create mode 100644 webpack/src/Extends/Hosts/BulkActions/BulkRemovePuppetCAProxy/index.js create mode 100644 webpack/src/Extends/Hosts/BulkActions/BulkRemovePuppetProxy/__tests__/index.test.js create mode 100644 webpack/src/Extends/Hosts/BulkActions/BulkRemovePuppetProxy/index.js create mode 100644 webpack/src/Extends/Hosts/BulkActions/__tests__/toastHelpers.test.js create mode 100644 webpack/src/Extends/Hosts/BulkActions/toastHelpers.js diff --git a/app/controllers/foreman_puppet/api/v2/hosts_bulk_actions_controller.rb b/app/controllers/foreman_puppet/api/v2/hosts_bulk_actions_controller.rb new file mode 100644 index 00000000..cf7ab289 --- /dev/null +++ b/app/controllers/foreman_puppet/api/v2/hosts_bulk_actions_controller.rb @@ -0,0 +1,102 @@ +module ForemanPuppet + module Api + module V2 + class HostsBulkActionsController < ::ForemanPuppet::Api::V2::PuppetBaseController + include ::Api::V2::BulkHostsExtension + before_action :find_editable_hosts, only: %i[change_puppet_proxy remove_puppet_proxy] + before_action :find_smart_proxy, only: %i[change_puppet_proxy] + + def_param_group :bulk_params do + param :organization_id, :number, required: true, desc: N_('ID of the organization') + param :included, Hash, required: true, action_aware: true do + param :search, String, required: false, desc: N_('Search string for hosts to perform an action on') + param :ids, Array, required: false, desc: N_('List of host ids to perform an action on') + end + param :excluded, Hash, required: true, action_aware: true do + param :ids, Array, required: false, desc: N_('List of host ids to exclude and not run an action on') + end + end + + api :PUT, '/hosts/bulk/change_puppet_proxy', N_('Change Puppet (CA) Proxy') + param_group :bulk_params + param :proxy_id, :number, required: true, desc: N_('ID of the Puppet proxy to reassign the hosts to') + param :ca_proxy, :bool, required: true, desc: N_('True, if Puppet CA proxy should be changed instead of the Puppet proxy') + def change_puppet_proxy + error_hosts = ::BulkHostsManager.new(hosts: @hosts).change_puppet_proxy(@proxy, ca_proxy?) + process_bulk_puppet_proxy_response( + error_hosts, + success_message: format(n_( + 'Updated host: changed %{proxy_type}', + 'Updated hosts: changed %{proxy_type}', + @hosts.count + ), proxy_type: proxy_type), + error_message: format(n_( + 'Failed to change %{proxy_type} for %{count} host', + 'Failed to change %{proxy_type} for %{count} hosts', + error_hosts.count + ), proxy_type: proxy_type, count: error_hosts.count) + ) + end + + api :PUT, '/hosts/bulk/remove_puppet_proxy', N_('Remove Puppet (CA) Proxy') + param_group :bulk_params + param :ca_proxy, :bool, required: true, desc: N_('True, if Puppet CA proxy should be removed instead of the Puppet proxy') + def remove_puppet_proxy + error_hosts = ::BulkHostsManager.new(hosts: @hosts).change_puppet_proxy(nil, ca_proxy?) + process_bulk_puppet_proxy_response( + error_hosts, + success_message: format(n_( + 'Updated host: removed %{proxy_type}', + 'Updated hosts: removed %{proxy_type}', + @hosts.count + ), proxy_type: proxy_type), + error_message: format(n_( + 'Failed to remove %{proxy_type} for %{count} host', + 'Failed to remove %{proxy_type} for %{count} hosts', + error_hosts.count + ), proxy_type: proxy_type, count: error_hosts.count) + ) + end + + private + + def find_editable_hosts + find_bulk_hosts(:edit_hosts, params) + end + + def process_bulk_puppet_proxy_response(error_hosts, success_message:, error_message:) + if error_hosts.empty? + process_response(true, { message: success_message }) + else + render_error(:bulk_hosts_error, status: :unprocessable_entity, + locals: { message: error_message, failed_host_ids: error_hosts }) + end + end + + def find_smart_proxy + feature = ca_proxy? ? 'Puppet CA' : 'Puppet' + @proxy = SmartProxy.with_features(feature).find_by(id: params[:proxy_id]) + + if @proxy.nil? + render json: { + error: { + message: format(_('A Smart Proxy with id %{id} and the %{proxy_type} feature could not be found.'), id: params[:proxy_id], proxy_type: proxy_type), + }, + }, status: :unprocessable_entity + false + else + true + end + end + + def ca_proxy? + Foreman::Cast.to_bool(params[:ca_proxy]) + end + + def proxy_type + ca_proxy? ? _('Puppet CA proxy') : _('Puppet proxy') + end + end + end + end +end diff --git a/app/services/concerns/foreman_puppet/extensions/bulk_hosts_manager.rb b/app/services/concerns/foreman_puppet/extensions/bulk_hosts_manager.rb new file mode 100644 index 00000000..dd9b07f3 --- /dev/null +++ b/app/services/concerns/foreman_puppet/extensions/bulk_hosts_manager.rb @@ -0,0 +1,24 @@ +module ForemanPuppet + module Extensions + module BulkHostsManager + extend ActiveSupport::Concern + + def change_puppet_proxy(proxy, is_ca_proxy) + error_hosts = [] + @hosts.each do |host| + if is_ca_proxy + host.puppet_ca_proxy = proxy + else + host.puppet_proxy = proxy + end + host.save(validate: false) + rescue StandardError => e + message = format(_('Failed to set proxy for %{host}.'), host: host) + Foreman::Logging.exception(message, e) + error_hosts << host.id + end + error_hosts + end + end + end +end diff --git a/config/api_routes.rb b/config/api_routes.rb index cda02fed..1d1b1eca 100644 --- a/config/api_routes.rb +++ b/config/api_routes.rb @@ -1,3 +1,14 @@ +Foreman::Application.routes.draw do + scope module: 'foreman_puppet' do + namespace :api, defaults: { format: 'json' } do + scope '(:apiv)', module: :v2, defaults: { apiv: 'v2' }, apiv: /v1|v2/, constraints: ApiConstraints.new(version: 2, default: true) do + match 'hosts/bulk/change_puppet_proxy', to: 'hosts_bulk_actions#change_puppet_proxy', via: [:put] + match 'hosts/bulk/remove_puppet_proxy', to: 'hosts_bulk_actions#remove_puppet_proxy', via: [:put] + end + end + end +end + ForemanPuppet::Engine.routes.draw do namespace :api, defaults: { format: 'json' } do scope '(:apiv)', module: :v2, defaults: { apiv: 'v2' }, apiv: /v1|v2/, constraints: ApiConstraints.new(version: 2, default: true) do diff --git a/lib/foreman_puppet/engine.rb b/lib/foreman_puppet/engine.rb index 63424e87..8f7d7396 100644 --- a/lib/foreman_puppet/engine.rb +++ b/lib/foreman_puppet/engine.rb @@ -49,6 +49,7 @@ class Engine < ::Rails::Engine ::ProvisioningTemplate.include ForemanPuppet::Extensions::ProvisioningTemplate ::HostCounter.prepend ForemanPuppet::Extensions::HostCounter + ::BulkHostsManager.include ForemanPuppet::Extensions::BulkHostsManager ::Api::V2::BaseController.include ForemanPuppet::Extensions::ApiBaseController ::Api::V2::HostsController.include ForemanPuppet::Extensions::ApiV2HostsController diff --git a/lib/foreman_puppet/register.rb b/lib/foreman_puppet/register.rb index 0dec86ea..c5293c41 100644 --- a/lib/foreman_puppet/register.rb +++ b/lib/foreman_puppet/register.rb @@ -62,6 +62,8 @@ p.actions << 'hosts/update_multiple_environment' p.actions << 'hosts/select_multiple_puppet_proxy' p.actions << 'hosts/update_multiple_puppet_proxy' + p.actions << 'foreman_puppet/api/v2/hosts_bulk_actions/change_puppet_proxy' + p.actions << 'foreman_puppet/api/v2/hosts_bulk_actions/remove_puppet_proxy' end p.actions << 'foreman_puppet/puppetclasses/parameters' end diff --git a/test/controllers/foreman_puppet/api/v2/hosts_bulk_actions_controller_test.rb b/test/controllers/foreman_puppet/api/v2/hosts_bulk_actions_controller_test.rb new file mode 100644 index 00000000..485bbdad --- /dev/null +++ b/test/controllers/foreman_puppet/api/v2/hosts_bulk_actions_controller_test.rb @@ -0,0 +1,171 @@ +require 'test_puppet_helper' + +module ForemanPuppet + module Api + module V2 + class HostsBulkActionsControllerTest < ActionController::TestCase + tests ::ForemanPuppet::Api::V2::HostsBulkActionsController + + setup do + @routes = ::Foreman::Application.routes + end + + let(:host) { FactoryBot.create(:host, :with_puppet_enc) } + let(:host2) do + FactoryBot.create(:host, :with_puppet_enc, + organization: host.organization, + location: host.location) + end + let(:proxy) { FactoryBot.create(:puppet_and_ca_smart_proxy, organizations: [host.organization], locations: [host.location]) } + + test 'changes puppet proxy for selected hosts' do + put :change_puppet_proxy, + params: bulk_params.merge(proxy_id: proxy.id, ca_proxy: false) + + assert_response :success + assert_equal proxy.id, host2.reload.puppet_proxy_id + assert_equal proxy.id, host.reload.puppet_proxy_id + end + + test 'changes puppet ca proxy for selected hosts' do + put :change_puppet_proxy, + params: bulk_params.merge(proxy_id: proxy.id, ca_proxy: true) + + assert_response :success + assert_equal proxy.id, host2.reload.puppet_ca_proxy_id + assert_equal proxy.id, host.reload.puppet_ca_proxy_id + end + + test 'removes puppet proxy for selected hosts' do + host.update!(puppet_proxy: proxy) + host2.update!(puppet_proxy: proxy) + + assert_equal proxy, host.reload.puppet_proxy + + put :remove_puppet_proxy, + params: bulk_params.merge(ca_proxy: false), + session: set_session_user + + assert_response :success + assert_nil host.reload.puppet_proxy + assert_nil host2.reload.puppet_proxy + end + + test 'returns error when puppet proxy is missing' do + missing_proxy_id = 999_999 + + put :change_puppet_proxy, + params: bulk_params.merge(proxy_id: missing_proxy_id, ca_proxy: false), + session: set_session_user + + assert_response :unprocessable_entity + response = JSON.parse(@response.body) + assert_equal "A Smart Proxy with id #{missing_proxy_id} and the Puppet proxy feature could not be found.", + response.dig('error', 'message') + end + + test 'returns error when puppet ca proxy is missing' do + missing_proxy_id = 999_999 + + put :change_puppet_proxy, + params: bulk_params.merge(proxy_id: missing_proxy_id, ca_proxy: true), + session: set_session_user + + assert_response :unprocessable_entity + response = JSON.parse(@response.body) + assert_equal "A Smart Proxy with id #{missing_proxy_id} and the Puppet CA proxy feature could not be found.", + response.dig('error', 'message') + end + + test 'returns error when smart proxy is missing puppet feature' do + invalid_proxy = FactoryBot.create(:smart_proxy, organizations: [host.organization], locations: [host.location]) + invalid_proxy.smart_proxy_feature_by_name('Puppet')&.destroy! + + put :change_puppet_proxy, + params: bulk_params.merge(proxy_id: invalid_proxy.id, ca_proxy: false), + session: set_session_user + + assert_response :unprocessable_entity + response = JSON.parse(@response.body) + assert_equal "A Smart Proxy with id #{invalid_proxy.id} and the Puppet proxy feature could not be found.", + response.dig('error', 'message') + end + + test 'returns error when smart proxy is missing puppet ca feature' do + invalid_proxy = FactoryBot.create(:puppet_smart_proxy, organizations: [host.organization], locations: [host.location]) + + put :change_puppet_proxy, + params: bulk_params.merge(proxy_id: invalid_proxy.id, ca_proxy: true), + session: set_session_user + + assert_response :unprocessable_entity + response = JSON.parse(@response.body) + assert_equal "A Smart Proxy with id #{invalid_proxy.id} and the Puppet CA proxy feature could not be found.", + response.dig('error', 'message') + end + + test 'returns error when changing puppet proxy fails for some hosts' do + ::BulkHostsManager.any_instance.expects(:change_puppet_proxy) + .with(proxy, false) + .returns([host2.id]) + + put :change_puppet_proxy, + params: bulk_params.merge(proxy_id: proxy.id, ca_proxy: false), + session: set_session_user + + assert_response :unprocessable_entity + response = JSON.parse(@response.body) + assert_equal 'Failed to change Puppet proxy for 1 host', + response.dig('error', 'message') + assert_equal [host2.id], response.dig('error', 'failed_host_ids') + end + + test 'returns error when removing puppet proxy fails for some hosts' do + host.update!(puppet_proxy: proxy) + host2.update!(puppet_proxy: proxy) + ::BulkHostsManager.any_instance.expects(:change_puppet_proxy) + .with(nil, false) + .returns([host.id]) + + put :remove_puppet_proxy, + params: bulk_params.merge(ca_proxy: false), + session: set_session_user + + assert_response :unprocessable_entity + response = JSON.parse(@response.body) + assert_equal 'Failed to remove Puppet proxy for 1 host', + response.dig('error', 'message') + assert_equal [host.id], response.dig('error', 'failed_host_ids') + assert_equal proxy.id, host.reload.puppet_proxy_id + end + + test 'returns error when removing puppet ca proxy fails for some hosts' do + host.update!(puppet_ca_proxy: proxy) + host2.update!(puppet_ca_proxy: proxy) + ::BulkHostsManager.any_instance.expects(:change_puppet_proxy) + .with(nil, true) + .returns([host.id]) + + put :remove_puppet_proxy, + params: bulk_params.merge(ca_proxy: true), + session: set_session_user + + assert_response :unprocessable_entity + response = JSON.parse(@response.body) + assert_equal 'Failed to remove Puppet CA proxy for 1 host', + response.dig('error', 'message') + assert_equal [host.id], response.dig('error', 'failed_host_ids') + assert_equal proxy.id, host.reload.puppet_ca_proxy_id + end + + def bulk_params + { + organization_id: host.organization_id, + included: { ids: [host.id, host2.id] }, + excluded: { ids: [] }, + } + end + end + end + end +end diff --git a/test/services/foreman_puppet/bulk_hosts_manager_test.rb b/test/services/foreman_puppet/bulk_hosts_manager_test.rb new file mode 100644 index 00000000..6aee2364 --- /dev/null +++ b/test/services/foreman_puppet/bulk_hosts_manager_test.rb @@ -0,0 +1,33 @@ +require 'test_puppet_helper' + +class BulkHostsManagerTest < ActiveSupport::TestCase + let(:hosts) { FactoryBot.create_list(:host, 2, :with_puppet_enc) } + let(:manager) { ::BulkHostsManager.new(hosts: hosts) } + let(:proxy) { FactoryBot.create(:puppet_smart_proxy) } + + test 'changes puppet proxy for hosts' do + manager.change_puppet_proxy(proxy, false) + + hosts.each do |host| + assert_equal proxy.id, host.reload.puppet_proxy_id + end + end + + test 'changes puppet ca proxy for hosts' do + manager.change_puppet_proxy(proxy, true) + + hosts.each do |host| + assert_equal proxy.id, host.reload.puppet_ca_proxy_id + end + end + + test 'clears puppet proxy when proxy is nil' do + hosts.each { |host| host.update!(puppet_proxy: proxy) } + + manager.change_puppet_proxy(nil, false) + + hosts.each do |host| + assert_nil host.reload.puppet_proxy + end + end +end diff --git a/webpack/global_index.js b/webpack/global_index.js index 2ee8f404..51e88141 100644 --- a/webpack/global_index.js +++ b/webpack/global_index.js @@ -1,9 +1,16 @@ +import React from 'react'; +import { addGlobalFill } from 'foremanReact/components/common/Fill/GlobalFill'; import { registerReducer } from 'foremanReact/common/MountingService'; import { registerColumns } from 'foremanReact/components/HostsIndex/Columns/core'; import { translate as __ } from 'foremanReact/common/I18n'; import reducers from './src/reducers'; import { registerFills } from './src/Extends/Fills'; import { registerLegacy } from './legacy'; +import HostsIndexActionsBar from './src/Extends/Hosts/ActionsBar'; +import BulkChangePuppetProxy from './src/Extends/Hosts/BulkActions/BulkChangePuppetProxy'; +import BulkChangePuppetCAProxy from './src/Extends/Hosts/BulkActions/BulkChangePuppetCAProxy'; +import BulkRemovePuppetProxy from './src/Extends/Hosts/BulkActions/BulkRemovePuppetProxy'; +import BulkRemovePuppetCAProxy from './src/Extends/Hosts/BulkActions/BulkRemovePuppetCAProxy'; // register reducers registerReducer('puppet', reducers); @@ -29,4 +36,39 @@ puppetHostsIndexColumns.forEach(column => { column.categoryKey = 'puppet'; }); +addGlobalFill( + 'hosts-index-kebab', + 'puppet-hosts-index-kebab', + , + 100 +); + +addGlobalFill( + '_all-hosts-modals', + 'BulkChangePuppetProxy', + , + 100 +); + +addGlobalFill( + '_all-hosts-modals', + 'BulkChangePuppetCAProxy', + , + 100 +); + +addGlobalFill( + '_all-hosts-modals', + 'BulkRemovePuppetCAProxy', + , + 100 +); + +addGlobalFill( + '_all-hosts-modals', + 'BulkRemovePuppetProxy', + , + 100 +); + registerColumns(puppetHostsIndexColumns); diff --git a/webpack/src/Extends/Hosts/ActionsBar/ActionsBar.scss b/webpack/src/Extends/Hosts/ActionsBar/ActionsBar.scss new file mode 100644 index 00000000..926931cc --- /dev/null +++ b/webpack/src/Extends/Hosts/ActionsBar/ActionsBar.scss @@ -0,0 +1,14 @@ +.disabled-menu-item-span { + width: 25em; + display: flex; + flex-direction: row; +} + +.disabled-menu-item-p { + margin-left: 0.6em; + word-break: normal; +} + +.disabled-menu-item-icon { + font-size: small; +} diff --git a/webpack/src/Extends/Hosts/ActionsBar/index.js b/webpack/src/Extends/Hosts/ActionsBar/index.js new file mode 100644 index 00000000..e32cedf0 --- /dev/null +++ b/webpack/src/Extends/Hosts/ActionsBar/index.js @@ -0,0 +1,73 @@ +import React, { useContext } from 'react'; +import { Menu, MenuItem, MenuContent, MenuList } from '@patternfly/react-core'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { ForemanHostsIndexActionsBarContext } from 'foremanReact/components/HostsIndex'; +import { openBulkModal } from 'foremanReact/common/BulkModalStateHelper'; +import './ActionsBar.scss'; + +const HostActionsBar = () => { + const { selectedCount, setMenuOpen } = useContext( + ForemanHostsIndexActionsBarContext + ); + + const handleOpenBulkModal = modalId => { + setMenuOpen(false); + setTimeout(() => openBulkModal(modalId, true), 0); + }; + + return ( + setMenuOpen(false)}> + + + handleOpenBulkModal('bulk-change-puppet-proxy')} + isDisabled={selectedCount === 0} + > + {__('Change Puppet proxy')} + + handleOpenBulkModal('bulk-remove-puppet-proxy')} + isDisabled={selectedCount === 0} + > + {__('Remove Puppet proxy')} + + + handleOpenBulkModal('bulk-change-puppet-ca-proxy') + } + isDisabled={selectedCount === 0} + > + {__('Change Puppet CA proxy')} + + + handleOpenBulkModal('bulk-remove-puppet-ca-proxy') + } + isDisabled={selectedCount === 0} + > + {__('Remove Puppet CA proxy')} + + + + + } + > + {__('Puppet')} + + ); +}; + +export default HostActionsBar; diff --git a/webpack/src/Extends/Hosts/BulkActions/BulkChangeProxyCommon/__tests__/actions.test.js b/webpack/src/Extends/Hosts/BulkActions/BulkChangeProxyCommon/__tests__/actions.test.js new file mode 100644 index 00000000..9439e82f --- /dev/null +++ b/webpack/src/Extends/Hosts/BulkActions/BulkChangeProxyCommon/__tests__/actions.test.js @@ -0,0 +1,78 @@ +import { APIActions } from 'foremanReact/redux/API'; +import { foremanUrl } from 'foremanReact/common/helpers'; + +import { + fetchSmartProxies, + SMART_PROXY_KEY, + bulkChangePuppetProxy, + BULK_CHANGE_PUPPET_CA_PROXY_KEY, + BULK_CHANGE_PUPPET_PROXY_KEY, +} from '../actions'; + +jest.mock('foremanReact/redux/API', () => ({ + APIActions: { + get: jest.fn(), + put: jest.fn(), + }, +})); + +describe('BulkChangeProxyCommon actions', () => { + const smartProxiesUrl = foremanUrl('/api/smart_proxies'); + const bulkChangeUrl = foremanUrl('/api/v2/hosts/bulk/change_puppet_proxy'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('fetches smart proxies list filtered by feature search', () => { + fetchSmartProxies('Puppet'); + + expect(APIActions.get).toHaveBeenCalledWith({ + key: SMART_PROXY_KEY, + url: smartProxiesUrl, + params: { per_page: 'all', search: 'feature = "Puppet"' }, + }); + }); + + it('calls bulk change puppet proxy endpoint for Puppet Proxy changes', () => { + const params = { included: { ids: [1] }, proxy_id: '1', ca_proxy: false }; + const handleSuccess = jest.fn(); + const handleError = jest.fn(); + + bulkChangePuppetProxy( + params, + handleSuccess, + handleError, + BULK_CHANGE_PUPPET_PROXY_KEY + ); + + expect(APIActions.put).toHaveBeenCalledWith({ + key: BULK_CHANGE_PUPPET_PROXY_KEY, + url: bulkChangeUrl, + handleSuccess, + handleError, + params, + }); + }); + + it('calls bulk change puppet proxy endpoint for Puppet CA Proxy changes', () => { + const params = { included: { ids: [1] }, proxy_id: '1', ca_proxy: true }; + const handleSuccess = jest.fn(); + const handleError = jest.fn(); + + bulkChangePuppetProxy( + params, + handleSuccess, + handleError, + BULK_CHANGE_PUPPET_CA_PROXY_KEY + ); + + expect(APIActions.put).toHaveBeenCalledWith({ + key: BULK_CHANGE_PUPPET_CA_PROXY_KEY, + url: bulkChangeUrl, + handleSuccess, + handleError, + params, + }); + }); +}); diff --git a/webpack/src/Extends/Hosts/BulkActions/BulkChangeProxyCommon/actions.js b/webpack/src/Extends/Hosts/BulkActions/BulkChangeProxyCommon/actions.js new file mode 100644 index 00000000..05350a0d --- /dev/null +++ b/webpack/src/Extends/Hosts/BulkActions/BulkChangeProxyCommon/actions.js @@ -0,0 +1,36 @@ +import { APIActions } from 'foremanReact/redux/API'; +import { foremanUrl } from 'foremanReact/common/helpers'; + +export const SMART_PROXY_KEY = 'SMART_PROXY_KEY'; +export const BULK_CHANGE_PUPPET_CA_PROXY_KEY = 'BULK_CHANGE_PUPPET_CA_PROXY'; +export const BULK_CHANGE_PUPPET_PROXY_KEY = 'BULK_CHANGE_PUPPET_PROXY'; + +export const fetchSmartProxies = feature => { + const url = foremanUrl('/api/smart_proxies'); + return APIActions.get({ + key: SMART_PROXY_KEY, + url, + params: { + per_page: 'all', + ...(feature ? { search: `feature = "${feature}"` } : {}), + }, + }); +}; + +export const bulkChangePuppetProxy = ( + params, + handleSuccess, + handleError, + key +) => { + const url = foremanUrl(`/api/v2/hosts/bulk/change_puppet_proxy`); + return APIActions.put({ + key, + url, + handleSuccess, + handleError, + params, + }); +}; + +export default fetchSmartProxies; diff --git a/webpack/src/Extends/Hosts/BulkActions/BulkChangeProxyCommon/index.js b/webpack/src/Extends/Hosts/BulkActions/BulkChangeProxyCommon/index.js new file mode 100644 index 00000000..68db6785 --- /dev/null +++ b/webpack/src/Extends/Hosts/BulkActions/BulkChangeProxyCommon/index.js @@ -0,0 +1,252 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { useDispatch, useSelector } from 'react-redux'; +import { + Alert, + Modal, + Button, + TextContent, + Text, + Select, + SelectOption, + SelectList, + MenuToggle, +} from '@patternfly/react-core'; +import { addToast } from 'foremanReact/components/ToastsList/slice'; +import { sprintf, translate as __ } from 'foremanReact/common/I18n'; +import { foremanUrl } from 'foremanReact/common/helpers'; +import { APIActions } from 'foremanReact/redux/API'; +import { STATUS } from 'foremanReact/constants'; +import { + selectAPIStatus, + selectAPIResponse, +} from 'foremanReact/redux/API/APISelectors'; +import { + HOSTS_API_PATH, + API_REQUEST_KEY, +} from 'foremanReact/routes/Hosts/constants'; +import { + fetchSmartProxies, + SMART_PROXY_KEY, + bulkChangePuppetProxy, + BULK_CHANGE_PUPPET_PROXY_KEY, + BULK_CHANGE_PUPPET_CA_PROXY_KEY, +} from './actions'; +import { bulkActionErrorToastParams } from '../toastHelpers'; + +const BulkChangeProxyCommon = ({ + isOpen, + closeModal, + selectAllHostsMode, + selectedCount, + fetchBulkParams, + selectMessage, + handleErrorMessage, + changeMessage, + allHostsMessage, + someHostsMessage, + isCAProxy, +}) => { + const dispatch = useDispatch(); + const [smartProxyId, setSmartProxyId] = useState(''); + const [smartProxySelectOpen, setSmartProxySelectOpen] = useState(false); + + const actionKey = isCAProxy + ? BULK_CHANGE_PUPPET_CA_PROXY_KEY + : BULK_CHANGE_PUPPET_PROXY_KEY; + const smartProxyFeature = isCAProxy ? 'Puppet CA' : 'Puppet'; + + useEffect(() => { + dispatch(fetchSmartProxies(smartProxyFeature)); + }, [dispatch, smartProxyFeature]); + + const smartProxies = useSelector(state => + selectAPIResponse(state, SMART_PROXY_KEY) + ); + const smartProxyStatus = useSelector(state => + selectAPIStatus(state, SMART_PROXY_KEY) + ); + const hasSmartProxies = smartProxies?.results?.length > 0; + + const onToggleClick = () => { + setSmartProxySelectOpen(!smartProxySelectOpen); + }; + + const handleSmartProxySelect = (event, selection) => { + setSmartProxyId(selection); + setSmartProxySelectOpen(false); + }; + + const getSmartProxyLabel = id => id.substring(id.indexOf('-') + 1); + + const toggle = toggleRef => ( + + {smartProxyId ? getSmartProxyLabel(smartProxyId) : selectMessage} + + ); + + const handleModalClose = () => { + setSmartProxyId(''); + closeModal(); + }; + + const handleError = response => { + handleModalClose(); + dispatch( + addToast( + bulkActionErrorToastParams(response, handleErrorMessage, actionKey) + ) + ); + }; + + 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 handleConfirm = () => { + const requestBody = { + included: { + search: fetchBulkParams(), + }, + proxy_id: smartProxyId.split('-')[0], + ca_proxy: isCAProxy, + }; + + dispatch( + bulkChangePuppetProxy(requestBody, handleSuccess, handleError, actionKey) + ); + }; + + const modalActions = [ + , + , + ]; + + return ( + + {(smartProxyStatus !== STATUS.RESOLVED || hasSmartProxies) && ( + + + {selectAllHostsMode ? ( + {__('All')}, + }} + /> + ) : ( + {selectedCount}, + }} + /> + )} + + + )} + {smartProxyStatus === STATUS.RESOLVED && hasSmartProxies && ( + + )} + {smartProxyStatus === STATUS.RESOLVED && !hasSmartProxies && ( + + )} + + ); +}; + +BulkChangeProxyCommon.propTypes = { + isOpen: PropTypes.bool, + closeModal: PropTypes.func, + fetchBulkParams: PropTypes.func.isRequired, + selectedCount: PropTypes.number.isRequired, + selectAllHostsMode: PropTypes.bool.isRequired, + selectMessage: PropTypes.string.isRequired, + handleErrorMessage: PropTypes.string.isRequired, + changeMessage: PropTypes.string.isRequired, + allHostsMessage: PropTypes.string.isRequired, + someHostsMessage: PropTypes.string.isRequired, + isCAProxy: PropTypes.bool.isRequired, +}; + +BulkChangeProxyCommon.defaultProps = { + isOpen: false, + closeModal: () => {}, +}; + +export default BulkChangeProxyCommon; diff --git a/webpack/src/Extends/Hosts/BulkActions/BulkChangePuppetCAProxy/__tests__/index.test.js b/webpack/src/Extends/Hosts/BulkActions/BulkChangePuppetCAProxy/__tests__/index.test.js new file mode 100644 index 00000000..be2959ec --- /dev/null +++ b/webpack/src/Extends/Hosts/BulkActions/BulkChangePuppetCAProxy/__tests__/index.test.js @@ -0,0 +1,66 @@ +import React from 'react'; +import { mount } from '@theforeman/test'; + +import { openBulkModal } from 'foremanReact/common/BulkModalStateHelper'; +import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar'; + +import BulkChangePuppetCAProxyScene from '../index'; +import BulkChangeProxyCommon from '../../BulkChangeProxyCommon'; + +jest.mock('foremanReact/components/HostDetails/ActionsBar', () => ({ + ForemanActionsBarContext: jest.requireActual('react').createContext(), +})); + +jest.mock('../../BulkChangeProxyCommon', () => ({ + __esModule: true, + default: jest.fn(() => null), +})); + +describe('BulkChangePuppetCAProxyScene', () => { + const fetchBulkParams = jest.fn(); + const contextValue = { + selectAllHostsMode: false, + selectedCount: 2, + selectedResults: [1, 2], + fetchBulkParams, + }; + + beforeEach(() => { + jest.clearAllMocks(); + openBulkModal('bulk-change-puppet-ca-proxy', false); + }); + + it('opens with bulk modal state and passes expected CA proxy props', () => { + openBulkModal('bulk-change-puppet-ca-proxy', true); + const wrapper = mount( + + + + ); + + const componentType = + BulkChangeProxyCommon.default || BulkChangeProxyCommon; + const props = wrapper.find(componentType).props(); + + expect(props).toEqual( + expect.objectContaining({ + isCAProxy: true, + fetchBulkParams, + selectedCount: 2, + selectedResults: [1, 2], + selectAllHostsMode: false, + isOpen: true, + closeModal: expect.any(Function), + selectMessage: 'Select a Puppet CA Proxy', + handleErrorMessage: 'Failed to change Puppet CA Proxy', + changeMessage: 'Change Puppet CA Proxy', + allHostsMessage: + 'Changing the Puppet CA proxy will affect {boldCount} selected hosts. Some hosts may already have been associated with the selected Puppet CA proxy.', + someHostsMessage: + 'Changing the Puppet CA proxy will affect {boldCount} selected {count, plural, one {host} other {hosts}}. Some hosts may already have been associated with the selected Puppet CA proxy.', + }) + ); + + wrapper.unmount(); + }); +}); diff --git a/webpack/src/Extends/Hosts/BulkActions/BulkChangePuppetCAProxy/index.js b/webpack/src/Extends/Hosts/BulkActions/BulkChangePuppetCAProxy/index.js new file mode 100644 index 00000000..7f655e3f --- /dev/null +++ b/webpack/src/Extends/Hosts/BulkActions/BulkChangePuppetCAProxy/index.js @@ -0,0 +1,40 @@ +import React, { useContext } from 'react'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar'; +import { useBulkModalOpen } from 'foremanReact/common/BulkModalStateHelper'; + +import BulkChangeProxyCommon from '../BulkChangeProxyCommon'; + +const BulkChangePuppetCAProxyScene = () => { + const { + selectAllHostsMode, + selectedCount, + selectedResults, + fetchBulkParams, + } = useContext(ForemanActionsBarContext); + const { isOpen, close: closeModal } = useBulkModalOpen( + 'bulk-change-puppet-ca-proxy' + ); + return ( + + ); +}; + +export default BulkChangePuppetCAProxyScene; diff --git a/webpack/src/Extends/Hosts/BulkActions/BulkChangePuppetProxy/__tests__/index.test.js b/webpack/src/Extends/Hosts/BulkActions/BulkChangePuppetProxy/__tests__/index.test.js new file mode 100644 index 00000000..00e0d8c1 --- /dev/null +++ b/webpack/src/Extends/Hosts/BulkActions/BulkChangePuppetProxy/__tests__/index.test.js @@ -0,0 +1,66 @@ +import React from 'react'; +import { mount } from '@theforeman/test'; + +import { openBulkModal } from 'foremanReact/common/BulkModalStateHelper'; +import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar'; + +import BulkChangePuppetProxyScene from '../index'; +import BulkChangeProxyCommon from '../../BulkChangeProxyCommon'; + +jest.mock('foremanReact/components/HostDetails/ActionsBar', () => ({ + ForemanActionsBarContext: jest.requireActual('react').createContext(), +})); + +jest.mock('../../BulkChangeProxyCommon', () => ({ + __esModule: true, + default: jest.fn(() => null), +})); + +describe('BulkChangePuppetProxyScene', () => { + const fetchBulkParams = jest.fn(); + const contextValue = { + selectAllHostsMode: false, + selectedCount: 2, + selectedResults: [1, 2], + fetchBulkParams, + }; + + beforeEach(() => { + jest.clearAllMocks(); + openBulkModal('bulk-change-puppet-proxy', false); + }); + + it('opens with bulk modal state and passes expected props', () => { + openBulkModal('bulk-change-puppet-proxy', true); + const wrapper = mount( + + + + ); + + const componentType = + BulkChangeProxyCommon.default || BulkChangeProxyCommon; + const props = wrapper.find(componentType).props(); + + expect(props).toEqual( + expect.objectContaining({ + isCAProxy: false, + fetchBulkParams, + selectedCount: 2, + selectedResults: [1, 2], + selectAllHostsMode: false, + isOpen: true, + closeModal: expect.any(Function), + selectMessage: 'Select a Puppet Proxy', + handleErrorMessage: 'Failed to change Puppet Proxy', + changeMessage: 'Change Puppet Proxy', + allHostsMessage: + 'Changing the Puppet proxy will affect {boldCount} selected hosts. Some hosts may already have been associated with the selected Puppet proxy.', + someHostsMessage: + 'Changing the Puppet proxy will affect {boldCount} selected {count, plural, one {host} other {hosts}}. Some hosts may already have been associated with the selected Puppet proxy.', + }) + ); + + wrapper.unmount(); + }); +}); diff --git a/webpack/src/Extends/Hosts/BulkActions/BulkChangePuppetProxy/index.js b/webpack/src/Extends/Hosts/BulkActions/BulkChangePuppetProxy/index.js new file mode 100644 index 00000000..fba748b2 --- /dev/null +++ b/webpack/src/Extends/Hosts/BulkActions/BulkChangePuppetProxy/index.js @@ -0,0 +1,40 @@ +import React, { useContext } from 'react'; +import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar'; +import { useBulkModalOpen } from 'foremanReact/common/BulkModalStateHelper'; +import { translate as __ } from 'foremanReact/common/I18n'; + +import BulkChangeProxyCommon from '../BulkChangeProxyCommon'; + +const BulkChangePuppetProxyScene = () => { + const { + selectAllHostsMode, + selectedCount, + selectedResults, + fetchBulkParams, + } = useContext(ForemanActionsBarContext); + const { isOpen, close: closeModal } = useBulkModalOpen( + 'bulk-change-puppet-proxy' + ); + return ( + + ); +}; + +export default BulkChangePuppetProxyScene; diff --git a/webpack/src/Extends/Hosts/BulkActions/BulkRemoveProxyCommon/__tests__/actions.test.js b/webpack/src/Extends/Hosts/BulkActions/BulkRemoveProxyCommon/__tests__/actions.test.js new file mode 100644 index 00000000..72d04696 --- /dev/null +++ b/webpack/src/Extends/Hosts/BulkActions/BulkRemoveProxyCommon/__tests__/actions.test.js @@ -0,0 +1,64 @@ +import { APIActions } from 'foremanReact/redux/API'; +import { foremanUrl } from 'foremanReact/common/helpers'; + +import { + bulkRemovePuppetProxyAction, + BULK_REMOVE_PUPPET_PROXY_KEY, + BULK_REMOVE_PUPPET_CA_PROXY_KEY, +} from '../actions'; + +jest.mock('foremanReact/redux/API', () => ({ + APIActions: { + put: jest.fn(), + }, +})); + +describe('BulkRemoveProxyCommon actions', () => { + const url = foremanUrl('/api/v2/hosts/bulk/remove_puppet_proxy'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls bulk remove puppet proxy endpoint for Puppet Proxy removal', () => { + const params = { included: { ids: [1] }, ca_proxy: false }; + const handleSuccess = jest.fn(); + const handleError = jest.fn(); + + bulkRemovePuppetProxyAction( + BULK_REMOVE_PUPPET_PROXY_KEY, + params, + handleSuccess, + handleError + ); + + expect(APIActions.put).toHaveBeenCalledWith({ + key: BULK_REMOVE_PUPPET_PROXY_KEY, + url, + handleSuccess, + handleError, + params, + }); + }); + + it('calls bulk remove puppet proxy endpoint for Puppet CA Proxy removal', () => { + const params = { included: { ids: [1] }, ca_proxy: true }; + const handleSuccess = jest.fn(); + const handleError = jest.fn(); + + bulkRemovePuppetProxyAction( + BULK_REMOVE_PUPPET_CA_PROXY_KEY, + params, + handleSuccess, + handleError + ); + + expect(APIActions.put).toHaveBeenCalledWith({ + key: BULK_REMOVE_PUPPET_CA_PROXY_KEY, + url, + handleSuccess, + handleError, + params, + }); + }); +}); diff --git a/webpack/src/Extends/Hosts/BulkActions/BulkRemoveProxyCommon/actions.js b/webpack/src/Extends/Hosts/BulkActions/BulkRemoveProxyCommon/actions.js new file mode 100644 index 00000000..97acce11 --- /dev/null +++ b/webpack/src/Extends/Hosts/BulkActions/BulkRemoveProxyCommon/actions.js @@ -0,0 +1,24 @@ +import { APIActions } from 'foremanReact/redux/API'; +import { foremanUrl } from 'foremanReact/common/helpers'; + +export const BULK_REMOVE_PUPPET_PROXY_KEY = 'BULK_REMOVE_PUPPET_PROXY_KEY'; +export const BULK_REMOVE_PUPPET_CA_PROXY_KEY = + 'BULK_REMOVE_PUPPET_CA_PROXY_KEY'; + +export const bulkRemovePuppetProxyAction = ( + key, + params, + handleSuccess, + handleError +) => { + const url = foremanUrl(`/api/v2/hosts/bulk/remove_puppet_proxy`); + return APIActions.put({ + key, + url, + handleSuccess, + handleError, + params, + }); +}; + +export default bulkRemovePuppetProxyAction; diff --git a/webpack/src/Extends/Hosts/BulkActions/BulkRemoveProxyCommon/index.js b/webpack/src/Extends/Hosts/BulkActions/BulkRemoveProxyCommon/index.js new file mode 100644 index 00000000..92111739 --- /dev/null +++ b/webpack/src/Extends/Hosts/BulkActions/BulkRemoveProxyCommon/index.js @@ -0,0 +1,164 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { useDispatch } from 'react-redux'; +import { Modal, Button, TextContent, Text } from '@patternfly/react-core'; +import { addToast } from 'foremanReact/components/ToastsList/slice'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { foremanUrl } from 'foremanReact/common/helpers'; +import { APIActions } from 'foremanReact/redux/API'; +import { + HOSTS_API_PATH, + API_REQUEST_KEY, +} from 'foremanReact/routes/Hosts/constants'; + +import { + BULK_REMOVE_PUPPET_PROXY_KEY, + BULK_REMOVE_PUPPET_CA_PROXY_KEY, + bulkRemovePuppetProxyAction, +} from './actions'; +import { bulkActionErrorToastParams } from '../toastHelpers'; + +const BulkRemoveProxyCommon = ({ + isCAProxy, + isOpen, + closeModal, + selectAllHostsMode, + selectedCount, + fetchBulkParams, + handleErrorMessage, + removeMessage, + allHostsMessage, + someHostsMessage, +}) => { + const actionKey = isCAProxy + ? BULK_REMOVE_PUPPET_CA_PROXY_KEY + : BULK_REMOVE_PUPPET_PROXY_KEY; + + const handleModalClose = () => { + closeModal(); + }; + + const dispatch = useDispatch(); + + const handleError = response => { + handleModalClose(); + dispatch( + addToast( + bulkActionErrorToastParams(response, handleErrorMessage, actionKey) + ) + ); + }; + + 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 handleConfirm = () => { + const requestBody = { + included: { + search: fetchBulkParams(), + }, + ca_proxy: isCAProxy, + }; + + dispatch( + bulkRemovePuppetProxyAction( + actionKey, + requestBody, + handleSuccess, + handleError + ) + ); + }; + + const modalActions = [ + , + , + ]; + + return ( + + + + {selectAllHostsMode ? ( + {__('All')}, + }} + /> + ) : ( + {selectedCount}, + }} + /> + )} + + + + ); +}; + +BulkRemoveProxyCommon.propTypes = { + isCAProxy: PropTypes.bool.isRequired, + isOpen: PropTypes.bool, + closeModal: PropTypes.func, + fetchBulkParams: PropTypes.func.isRequired, + selectedCount: PropTypes.number.isRequired, + selectAllHostsMode: PropTypes.bool.isRequired, + handleErrorMessage: PropTypes.string.isRequired, + removeMessage: PropTypes.string, + allHostsMessage: PropTypes.string.isRequired, + someHostsMessage: PropTypes.string.isRequired, +}; + +BulkRemoveProxyCommon.defaultProps = { + isOpen: false, + closeModal: () => {}, + removeMessage: 'Remove Puppet (CA) Proxy', +}; + +export default BulkRemoveProxyCommon; diff --git a/webpack/src/Extends/Hosts/BulkActions/BulkRemovePuppetCAProxy/__tests__/index.test.js b/webpack/src/Extends/Hosts/BulkActions/BulkRemovePuppetCAProxy/__tests__/index.test.js new file mode 100644 index 00000000..16c19157 --- /dev/null +++ b/webpack/src/Extends/Hosts/BulkActions/BulkRemovePuppetCAProxy/__tests__/index.test.js @@ -0,0 +1,65 @@ +import React from 'react'; +import { mount } from '@theforeman/test'; + +import { openBulkModal } from 'foremanReact/common/BulkModalStateHelper'; +import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar'; + +import BulkRemovePuppetCAProxyScene from '../index'; +import BulkRemoveProxyCommon from '../../BulkRemoveProxyCommon'; + +jest.mock('foremanReact/components/HostDetails/ActionsBar', () => ({ + ForemanActionsBarContext: jest.requireActual('react').createContext(), +})); + +jest.mock('../../BulkRemoveProxyCommon', () => ({ + __esModule: true, + default: jest.fn(() => null), +})); + +describe('BulkRemovePuppetCAProxyScene', () => { + const fetchBulkParams = jest.fn(); + const contextValue = { + selectAllHostsMode: false, + selectedCount: 2, + selectedResults: [1, 2], + fetchBulkParams, + }; + + beforeEach(() => { + jest.clearAllMocks(); + openBulkModal('bulk-remove-puppet-ca-proxy', false); + }); + + it('opens with bulk modal state and passes expected CA proxy props', () => { + openBulkModal('bulk-remove-puppet-ca-proxy', true); + const wrapper = mount( + + + + ); + + const componentType = + BulkRemoveProxyCommon.default || BulkRemoveProxyCommon; + const props = wrapper.find(componentType).props(); + + expect(props).toEqual( + expect.objectContaining({ + isCAProxy: true, + selectAllHostsMode: false, + selectedCount: 2, + selectedResults: [1, 2], + fetchBulkParams, + isOpen: true, + closeModal: expect.any(Function), + handleErrorMessage: 'Failed to remove Puppet CA Proxy', + allHostsMessage: + 'Removing the Puppet CA proxy will affect {boldCount} selected hosts. Warning: If a Puppet Proxy is still set, the Puppet CA Proxy will fall back to that value after removal!', + someHostsMessage: + 'Removing the Puppet CA proxy will affect {boldCount} selected {count, plural, one {host} other {hosts}}. Warning: If a Puppet Proxy is still set, the Puppet CA Proxy will fall back to that value after removal!', + removeMessage: 'Remove Puppet CA Proxy', + }) + ); + + wrapper.unmount(); + }); +}); diff --git a/webpack/src/Extends/Hosts/BulkActions/BulkRemovePuppetCAProxy/index.js b/webpack/src/Extends/Hosts/BulkActions/BulkRemovePuppetCAProxy/index.js new file mode 100644 index 00000000..091cfdb7 --- /dev/null +++ b/webpack/src/Extends/Hosts/BulkActions/BulkRemovePuppetCAProxy/index.js @@ -0,0 +1,39 @@ +import React, { useContext } from 'react'; +import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar'; +import { useBulkModalOpen } from 'foremanReact/common/BulkModalStateHelper'; +import { translate as __ } from 'foremanReact/common/I18n'; +import BulkRemoveProxyCommon from '../BulkRemoveProxyCommon'; + +const BulkRemovePuppetCAProxyScene = () => { + const { + selectAllHostsMode, + selectedCount, + selectedResults, + fetchBulkParams, + } = useContext(ForemanActionsBarContext); + const { isOpen, close: closeModal } = useBulkModalOpen( + 'bulk-remove-puppet-ca-proxy' + ); + return ( + + ); +}; + +export default BulkRemovePuppetCAProxyScene; diff --git a/webpack/src/Extends/Hosts/BulkActions/BulkRemovePuppetProxy/__tests__/index.test.js b/webpack/src/Extends/Hosts/BulkActions/BulkRemovePuppetProxy/__tests__/index.test.js new file mode 100644 index 00000000..9f0b0429 --- /dev/null +++ b/webpack/src/Extends/Hosts/BulkActions/BulkRemovePuppetProxy/__tests__/index.test.js @@ -0,0 +1,61 @@ +import React from 'react'; +import { mount } from '@theforeman/test'; + +import { openBulkModal } from 'foremanReact/common/BulkModalStateHelper'; +import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar'; + +import BulkRemovePuppetProxyScene from '../index'; +import BulkRemoveProxyCommon from '../../BulkRemoveProxyCommon'; + +jest.mock('foremanReact/components/HostDetails/ActionsBar', () => ({ + ForemanActionsBarContext: jest.requireActual('react').createContext(), +})); + +jest.mock('../../BulkRemoveProxyCommon', () => ({ + __esModule: true, + default: jest.fn(() => null), +})); + +describe('BulkRemovePuppetProxyScene', () => { + const fetchBulkParams = jest.fn(); + const contextValue = { + selectAllHostsMode: false, + selectedCount: 2, + selectedResults: [1, 2], + fetchBulkParams, + }; + + beforeEach(() => { + jest.clearAllMocks(); + openBulkModal('bulk-remove-puppet-proxy', false); + }); + + it('opens with bulk modal state and passes expected props', () => { + openBulkModal('bulk-remove-puppet-proxy', true); + const wrapper = mount( + + + + ); + + const componentType = + BulkRemoveProxyCommon.default || BulkRemoveProxyCommon; + const props = wrapper.find(componentType).props(); + + expect(props).toEqual( + expect.objectContaining({ + isCAProxy: false, + selectAllHostsMode: false, + selectedCount: 2, + selectedResults: [1, 2], + fetchBulkParams, + isOpen: true, + closeModal: expect.any(Function), + handleErrorMessage: 'Failed to remove Puppet Proxy', + removeMessage: 'Remove Puppet Proxy', + }) + ); + + wrapper.unmount(); + }); +}); diff --git a/webpack/src/Extends/Hosts/BulkActions/BulkRemovePuppetProxy/index.js b/webpack/src/Extends/Hosts/BulkActions/BulkRemovePuppetProxy/index.js new file mode 100644 index 00000000..4618123b --- /dev/null +++ b/webpack/src/Extends/Hosts/BulkActions/BulkRemovePuppetProxy/index.js @@ -0,0 +1,39 @@ +import React, { useContext } from 'react'; +import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar'; +import { useBulkModalOpen } from 'foremanReact/common/BulkModalStateHelper'; +import { translate as __ } from 'foremanReact/common/I18n'; +import BulkRemoveProxyCommon from '../BulkRemoveProxyCommon'; + +const BulkRemovePuppetProxyScene = () => { + const { + selectAllHostsMode, + selectedCount, + selectedResults, + fetchBulkParams, + } = useContext(ForemanActionsBarContext); + const { isOpen, close: closeModal } = useBulkModalOpen( + 'bulk-remove-puppet-proxy' + ); + return ( + + ); +}; + +export default BulkRemovePuppetProxyScene; diff --git a/webpack/src/Extends/Hosts/BulkActions/__tests__/toastHelpers.test.js b/webpack/src/Extends/Hosts/BulkActions/__tests__/toastHelpers.test.js new file mode 100644 index 00000000..56e2286c --- /dev/null +++ b/webpack/src/Extends/Hosts/BulkActions/__tests__/toastHelpers.test.js @@ -0,0 +1,55 @@ +import { bulkActionErrorToastParams } from '../toastHelpers'; + +jest.mock('foremanReact/components/HostsIndex/BulkActions/helpers', () => ({ + failedHostsToastParams: jest.fn(params => params), +})); + +describe('bulkActionErrorToastParams', () => { + const actionKey = 'BULK_ACTION_KEY'; + const fallbackMessage = 'Fallback error'; + + it('uses the API error payload message when present', () => { + const response = { + data: { + error: { + message: 'API error', + failed_host_ids: [1, 2], + }, + }, + }; + + expect( + bulkActionErrorToastParams(response, fallbackMessage, actionKey) + ).toEqual({ + message: 'API error', + failed_host_ids: [1, 2], + key: actionKey, + }); + }); + + it('supports axios-style wrapped error responses', () => { + const response = { + response: { + data: { + error: { + message: 'Wrapped API error', + }, + }, + }, + }; + + expect( + bulkActionErrorToastParams(response, fallbackMessage, actionKey) + ).toEqual({ + message: 'Wrapped API error', + key: actionKey, + }); + }); + + it('falls back to the generic message when the API message is missing', () => { + expect(bulkActionErrorToastParams({}, fallbackMessage, actionKey)).toEqual({ + message: fallbackMessage, + key: actionKey, + }); + }); +}); diff --git a/webpack/src/Extends/Hosts/BulkActions/toastHelpers.js b/webpack/src/Extends/Hosts/BulkActions/toastHelpers.js new file mode 100644 index 00000000..8e2518f4 --- /dev/null +++ b/webpack/src/Extends/Hosts/BulkActions/toastHelpers.js @@ -0,0 +1,17 @@ +import { failedHostsToastParams } from 'foremanReact/components/HostsIndex/BulkActions/helpers'; + +export const bulkActionErrorToastParams = ( + response, + fallbackMessage, + actionKey +) => { + const error = response?.data?.error || response?.response?.data?.error || {}; + + return failedHostsToastParams({ + ...error, + message: error.message || fallbackMessage, + key: actionKey, + }); +}; + +export default bulkActionErrorToastParams; diff --git a/webpack/src/foreman_puppet_host_form.test.js b/webpack/src/foreman_puppet_host_form.test.js index f7415fe4..240e0bf3 100644 --- a/webpack/src/foreman_puppet_host_form.test.js +++ b/webpack/src/foreman_puppet_host_form.test.js @@ -37,7 +37,7 @@ describe('checkForUnavailablePuppetclasses', () => { ); checkForUnavailablePuppetclasses(); - expect($('#puppetclasses_unavailable_warning').length).toBe(1); + expect($('#puppetclasses_unavailable_warning')).toHaveLength(1); }); it('does not add a warning if no unavailable classes are found', () => { @@ -48,8 +48,8 @@ describe('checkForUnavailablePuppetclasses', () => { expect( $('#hostgroup .help-block') .first() - .children().length - ).toBe(0); + .children() + ).toHaveLength(0); }); it('adds a warning sign to the tab if unavailable classes are found', () => { @@ -58,7 +58,7 @@ describe('checkForUnavailablePuppetclasses', () => { ); checkForUnavailablePuppetclasses(); setTimeout(() => { - expect($('a .pficon').length).toBe(1); + expect($('a .pficon')).toHaveLength(1); }, 100); }); });