diff --git a/app/controllers/concerns/foreman_openscap/api/v2/hosts_bulk_actions_controller_extensions.rb b/app/controllers/concerns/foreman_openscap/api/v2/hosts_bulk_actions_controller_extensions.rb new file mode 100644 index 000000000..d543d4bc9 --- /dev/null +++ b/app/controllers/concerns/foreman_openscap/api/v2/hosts_bulk_actions_controller_extensions.rb @@ -0,0 +1,78 @@ +module ForemanOpenscap + module Api + module V2 + module HostsBulkActionsControllerExtensions + extend ActiveSupport::Concern + + included do + before_action :find_editable_hosts, only: [:change_openscap_proxy] + end + + def change_openscap_proxy + openscap_proxy_id = params[:openscap_proxy_id] + + if openscap_proxy_id.blank? + return render json: { + error: { + message: _("No OpenSCAP Proxy selected."), + }, + }, status: :unprocessable_entity + end + + smart_proxy = begin + ::SmartProxy.find(openscap_proxy_id) + rescue ActiveRecord::RecordNotFound + nil + end + + if smart_proxy.nil? + return render json: { + error: { + message: _("OpenSCAP proxy with id %s not found") % openscap_proxy_id, + }, + }, status: :unprocessable_entity + end + + unless smart_proxy.has_feature?('Openscap') + return render json: { + error: { + message: _("The selected proxy does not have the OpenSCAP feature enabled."), + }, + }, status: :unprocessable_entity + end + + failed_hosts = [] + @hosts.each do |host| + host.openscap_proxy = smart_proxy + failed_hosts << host unless host.save + end + + if failed_hosts.empty? + process_response(true, { + message: _("OpenSCAP Proxy set to %s") % smart_proxy.name, + }) + else + render_error(:bulk_hosts_error, status: :unprocessable_entity, + locals: { + message: n_("Failed to assign OpenSCAP Proxy to %s host", + "Failed to assign OpenSCAP Proxy to %s hosts", + failed_hosts.count) % failed_hosts.count, + failed_host_ids: failed_hosts.map(&:id), + }) + end + end + + private + + def action_permission + case params[:action] + when 'change_openscap_proxy' + 'edit' + else + super + end + end + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index e9f5ca4f5..dee9d38fa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -103,6 +103,8 @@ end end end + + match 'hosts/bulk/change_openscap_proxy', :to => 'hosts_bulk_actions#change_openscap_proxy', :via => [:put] end end end diff --git a/lib/foreman_openscap/engine.rb b/lib/foreman_openscap/engine.rb index 7ade8dd68..dc070c240 100644 --- a/lib/foreman_openscap/engine.rb +++ b/lib/foreman_openscap/engine.rb @@ -93,7 +93,8 @@ class Engine < ::Rails::Engine :resource_type => 'ForemanOpenscap::ScapContent' permission :edit_hosts, { :hosts => %i[openscap_proxy_changed select_multiple_openscap_proxy - update_multiple_openscap_proxy] }, + update_multiple_openscap_proxy], + 'api/v2/hosts_bulk_actions' => [:change_openscap_proxy] }, :resource_type => "Host" permission :view_hosts, { 'api/v2/hosts' => [:policies_enc] }, :resource_type => 'Host' permission :edit_hostgroups, { :hostgroups => [:openscap_proxy_changed] }, :resource_type => "Hostgroup" @@ -195,6 +196,7 @@ class Engine < ::Rails::Engine # Include concerns in this config.to_prepare block config.to_prepare do ::Api::V2::HostsController.send(:include, ForemanOpenscap::Api::V2::HostsControllerExtensions) + ::Api::V2::HostsBulkActionsController.send(:include, ForemanOpenscap::Api::V2::HostsBulkActionsControllerExtensions) ::Host::Managed.send(:include, ForemanOpenscap::OpenscapProxyExtensions) ::Host::Managed.send(:include, ForemanOpenscap::OpenscapProxyCoreExtensions) ::Host::Managed.send(:prepend, ForemanOpenscap::HostExtensions) diff --git a/test/functional/api/v2/hosts_bulk_actions_controller_test.rb b/test/functional/api/v2/hosts_bulk_actions_controller_test.rb new file mode 100644 index 000000000..280fd9804 --- /dev/null +++ b/test/functional/api/v2/hosts_bulk_actions_controller_test.rb @@ -0,0 +1,98 @@ +require 'test_plugin_helper' + +class Api::V2::HostsBulkActionsControllerTest < ActionController::TestCase + def setup + as_admin do + @organization = FactoryBot.create(:organization) + @location = FactoryBot.create(:location) + @proxy = FactoryBot.create(:openscap_proxy, + :organizations => [@organization], + :locations => [@location]) + @host1 = FactoryBot.create(:host, :managed, + :organization => @organization, + :location => @location) + @host2 = FactoryBot.create(:host, :managed, + :organization => @organization, + :location => @location) + @host_ids = [@host1.id, @host2.id] + end + end + + def valid_bulk_params(host_ids = @host_ids) + { + :organization_id => @organization.id, + :location_id => @location.id, + :included => { + :ids => host_ids, + }, + :excluded => { + :ids => [], + }, + } + end + + test "should assign openscap proxy to selected hosts" do + put :change_openscap_proxy, + params: valid_bulk_params.merge(:openscap_proxy_id => @proxy.id), + session: set_session_user + + assert_response :success + response = ActiveSupport::JSON.decode(@response.body) + assert_match(/OpenSCAP Proxy set to/, response['message']) + assert_includes response['message'], @proxy.name + + [@host1, @host2].each do |host| + host.reload + assert_equal @proxy.id, host.openscap_proxy_id + end + end + + test "should require openscap_proxy_id" do + put :change_openscap_proxy, + params: valid_bulk_params, + session: set_session_user + + assert_response :unprocessable_entity + response = ActiveSupport::JSON.decode(@response.body) + assert_match(/No OpenSCAP Proxy selected/, response['error']['message']) + end + + test "should return error when proxy is not found" do + put :change_openscap_proxy, + params: valid_bulk_params.merge(:openscap_proxy_id => 0), + session: set_session_user + + assert_response :unprocessable_entity + response = ActiveSupport::JSON.decode(@response.body) + assert_match(/not found/, response['error']['message']) + end + + test "should return error when proxy lacks Openscap feature" do + other_proxy = FactoryBot.create(:smart_proxy, + :organizations => [@organization], + :locations => [@location]) + openscap_feature = Feature.find_by(:name => 'Openscap') + other_proxy.features.delete(openscap_feature) if openscap_feature + refute other_proxy.reload.has_feature?('Openscap') + + put :change_openscap_proxy, + params: valid_bulk_params.merge(:openscap_proxy_id => other_proxy.id), + session: set_session_user + + assert_response :unprocessable_entity + response = ActiveSupport::JSON.decode(@response.body) + assert_match(/OpenSCAP feature/, response['error']['message']) + end + + test "should assign openscap proxy for a single host" do + put :change_openscap_proxy, + params: valid_bulk_params([@host1.id]).merge(:openscap_proxy_id => @proxy.id), + session: set_session_user + + assert_response :success + @host1.reload + assert_equal @proxy.id, @host1.openscap_proxy_id + @host2.reload + assert_nil @host2.openscap_proxy_id + end +end diff --git a/webpack/components/HostsIndex/BulkActions/changeOpenscapProxy/BulkChangeOpenscapProxyModal.js b/webpack/components/HostsIndex/BulkActions/changeOpenscapProxy/BulkChangeOpenscapProxyModal.js new file mode 100644 index 000000000..1d02856c4 --- /dev/null +++ b/webpack/components/HostsIndex/BulkActions/changeOpenscapProxy/BulkChangeOpenscapProxyModal.js @@ -0,0 +1,229 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; +import { + Modal, + Button, + Grid, + GridItem, + Form, + FormGroup, + Stack, + StackItem, +} from '@patternfly/react-core'; +// eslint-disable-next-line import/no-unresolved +import { SimpleDropdown } from '@patternfly/react-templates'; +import { foremanUrl } from 'foremanReact/common/helpers'; +import { APIActions } from 'foremanReact/redux/API'; +import { sprintf, translate as __ } from 'foremanReact/common/I18n'; +import { STATUS } from 'foremanReact/constants'; +import { + selectAPIStatus, + selectAPIResponse, +} from 'foremanReact/redux/API/APISelectors'; +import { buildBulkRequestBody } from 'foremanReact/components/HostsIndex/BulkActions/helpers'; +import { + BULK_CHANGE_OPENSCAP_PROXY_KEY, + HOSTS_API_PATH, + HOSTS_API_REQUEST_KEY, + OPENSCAP_PROXIES_KEY, +} from '../../../OpenscapRemediationWizard/constants'; + +const fetchOpenscapProxies = () => + APIActions.get({ + key: OPENSCAP_PROXIES_KEY, + url: foremanUrl( + '/api/smart_proxies?search=feature%3DOpenscap&per_page=all' + ), + }); + +const BulkChangeOpenscapProxyModal = ({ + isOpen, + closeModal, + selectAllHostsMode, + selectedCount, + fetchBulkParams, + organizationId, + locationId, +}) => { + const dispatch = useDispatch(); + const [proxyId, setProxyId] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + if (isOpen) { + dispatch(fetchOpenscapProxies()); + } else { + setIsSubmitting(false); + } + }, [dispatch, isOpen]); + + const proxies = useSelector(state => + selectAPIResponse(state, OPENSCAP_PROXIES_KEY) + ); + const proxyStatus = useSelector(state => + selectAPIStatus(state, OPENSCAP_PROXIES_KEY) + ); + + const handleModalClose = () => { + setProxyId(''); + setIsSubmitting(false); + closeModal(); + }; + + const handleSuccess = () => { + dispatch( + APIActions.get({ + key: HOSTS_API_REQUEST_KEY, + url: foremanUrl(HOSTS_API_PATH), + }) + ); + handleModalClose(); + }; + + const handleError = () => { + setIsSubmitting(false); + handleModalClose(); + }; + + const handleConfirm = () => { + const requestBody = buildBulkRequestBody({ + fetchBulkParams, + organizationId, + locationId, + openscap_proxy_id: proxyId, + }); + + setIsSubmitting(true); + dispatch( + APIActions.put({ + key: BULK_CHANGE_OPENSCAP_PROXY_KEY, + url: foremanUrl('/api/v2/hosts/bulk/change_openscap_proxy'), + handleSuccess, + successToast: response => response.data.message, + handleError, + errorToast: error => + error?.response?.data?.error?.message || __('Error'), + params: requestBody, + }) + ); + }; + + const descriptionText = selectAllHostsMode ? ( + <> + {__('Assign OpenSCAP proxy for ')} + {__('ALL selected hosts.')} +
+ {__('This will change previous proxy assignments on the selected hosts.')} + + ) : ( + <> + {__('Assign OpenSCAP proxy for ')} + {sprintf(__('%s selected hosts.'), selectedCount)} +
+ {__('This will change previous proxy assignments on the selected hosts.')} + + ); + + const getProxyLabel = id => { + const proxy = proxies?.results?.find( + p => p.id.toString() === id.toString() + ); + return proxy?.name || id; + }; + + const proxyItems = + proxies?.results?.map(proxy => ({ + value: proxy.id.toString(), + content: proxy.name, + onClick: () => setProxyId(proxy.id.toString()), + })) || []; + + const modalActions = [ + , + , + ]; + + return ( + + + {descriptionText} + {proxyStatus === STATUS.RESOLVED && proxies?.results?.length > 0 && ( + + + +
+ + + +
+
+
+
+ )} + {proxyStatus === STATUS.RESOLVED && + (!proxies?.results || proxies.results.length === 0) && + __( + 'No OpenSCAP Proxies available. Please configure a Smart Proxy with the OpenSCAP feature.' + )} +
+
+ ); +}; + +BulkChangeOpenscapProxyModal.propTypes = { + isOpen: PropTypes.bool, + closeModal: PropTypes.func, + fetchBulkParams: PropTypes.func.isRequired, + selectedCount: PropTypes.number.isRequired, + selectAllHostsMode: PropTypes.bool.isRequired, + organizationId: PropTypes.number, + locationId: PropTypes.number, +}; + +BulkChangeOpenscapProxyModal.defaultProps = { + isOpen: false, + closeModal: () => {}, + organizationId: undefined, + locationId: undefined, +}; + +export default BulkChangeOpenscapProxyModal; diff --git a/webpack/components/HostsIndex/BulkActions/changeOpenscapProxy/__tests__/BulkChangeOpenscapProxyModal.test.js b/webpack/components/HostsIndex/BulkActions/changeOpenscapProxy/__tests__/BulkChangeOpenscapProxyModal.test.js new file mode 100644 index 000000000..25c5b8b46 --- /dev/null +++ b/webpack/components/HostsIndex/BulkActions/changeOpenscapProxy/__tests__/BulkChangeOpenscapProxyModal.test.js @@ -0,0 +1,160 @@ +import React from 'react'; +import { screen, fireEvent, waitFor, within } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { rtlHelpers, initMockStore } from 'foremanReact/common/testHelpers'; +import { STATUS } from 'foremanReact/constants'; +import { APIActions } from 'foremanReact/redux/API'; +import { OPENSCAP_PROXIES_KEY } from '../../../../OpenscapRemediationWizard/constants'; +import BulkChangeOpenscapProxyModal from '../BulkChangeOpenscapProxyModal'; + +const { renderWithStore } = rtlHelpers; + +jest.mock('foremanReact/common/I18n'); + +jest.spyOn(APIActions, 'get'); +jest.spyOn(APIActions, 'put'); + +const proxies = { + results: [ + { id: 1, name: 'openscap-proxy-1.example.com' }, + { id: 2, name: 'openscap-proxy-2.example.com' }, + ], +}; + +const defaultProps = { + selectedCount: 3, + selectAllHostsMode: false, + fetchBulkParams: jest.fn(() => 'id ^ (1,2,3)'), + isOpen: true, + closeModal: jest.fn(), + organizationId: 1, + locationId: 2, +}; + +const proxiesResolvedState = { + API: { + [OPENSCAP_PROXIES_KEY]: { + status: STATUS.RESOLVED, + response: proxies, + }, + }, +}; + +const noProxiesState = { + API: { + [OPENSCAP_PROXIES_KEY]: { + status: STATUS.RESOLVED, + response: { results: [] }, + }, + }, +}; + +const renderModal = (props = {}, initialState = proxiesResolvedState) => + renderWithStore( + , + initialState + ); + +describe('BulkChangeOpenscapProxyModal', () => { + beforeEach(() => { + jest.clearAllMocks(); + // renderWithStore lodash-merges into shared initMockStore; clear prior API fixtures + delete initMockStore.API[OPENSCAP_PROXIES_KEY]; + APIActions.get.mockImplementation(payload => ({ + type: 'TEST_API_GET', + payload, + })); + APIActions.put.mockImplementation(payload => ({ + type: 'TEST_API_PUT', + payload, + })); + }); + + it('renders modal title and description with selected host count', () => { + renderModal(); + expect(screen.getByText('Assign OpenSCAP Proxy')).toBeInTheDocument(); + expect(screen.getByText('3 selected hosts.')).toBeInTheDocument(); + }); + + it('renders all-hosts mode description', () => { + renderModal({ selectAllHostsMode: true }); + expect(screen.getByText('ALL selected hosts.')).toBeInTheDocument(); + }); + + it('disables Assign until a proxy is selected', () => { + renderModal(); + expect(screen.getByRole('button', { name: 'Assign' })).toBeDisabled(); + }); + + it('lists OpenSCAP proxies in the dropdown', async () => { + renderModal(); + fireEvent.click( + screen.getByRole('button', { name: 'Select OpenSCAP Proxy' }) + ); + + const menu = await screen.findByRole('menu'); + expect( + within(menu).getByRole('menuitem', { + name: 'openscap-proxy-1.example.com', + }) + ).toBeInTheDocument(); + expect( + within(menu).getByRole('menuitem', { + name: 'openscap-proxy-2.example.com', + }) + ).toBeInTheDocument(); + }); + + it('enables Assign and dispatches bulk PUT after selecting a proxy', async () => { + renderModal(); + fireEvent.click( + screen.getByRole('button', { name: 'Select OpenSCAP Proxy' }) + ); + + const menu = await screen.findByRole('menu'); + fireEvent.click( + within(menu).getByRole('menuitem', { + name: 'openscap-proxy-1.example.com', + }) + ); + + const assignBtn = screen.getByRole('button', { name: 'Assign' }); + await waitFor(() => { + expect(assignBtn).not.toBeDisabled(); + }); + + fireEvent.click(assignBtn); + expect(APIActions.put).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.stringContaining('/hosts/bulk/change_openscap_proxy'), + params: expect.objectContaining({ + included: { search: 'id ^ (1,2,3)' }, + organization_id: 1, + location_id: 2, + openscap_proxy_id: '1', + }), + }) + ); + }); + + it('calls closeModal on Cancel', () => { + const closeModal = jest.fn(); + renderModal({ closeModal }); + fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(closeModal).toHaveBeenCalled(); + }); + + it('shows empty state when no proxies are available', () => { + renderModal({}, noProxiesState); + expect( + screen.getByText( + 'No OpenSCAP Proxies available. Please configure a Smart Proxy with the OpenSCAP feature.' + ) + ).toBeInTheDocument(); + }); + + it('does not render when isOpen is false', () => { + renderModal({ isOpen: false }); + expect(screen.queryByText('Assign OpenSCAP Proxy')).not.toBeInTheDocument(); + }); +}); diff --git a/webpack/components/HostsIndex/ChangeOpenscapProxyAction.js b/webpack/components/HostsIndex/ChangeOpenscapProxyAction.js new file mode 100644 index 000000000..9396807bc --- /dev/null +++ b/webpack/components/HostsIndex/ChangeOpenscapProxyAction.js @@ -0,0 +1,63 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import { MenuItem } from '@patternfly/react-core'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { + openBulkModal, + useBulkModalOpen, +} from 'foremanReact/common/BulkModalStateHelper'; +import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar'; +import BulkChangeOpenscapProxyModal from './BulkActions/changeOpenscapProxy/BulkChangeOpenscapProxyModal'; +import { CHANGE_OPENSCAP_MODAL_ID } from '../OpenscapRemediationWizard/constants'; + +export const ChangeOpenscapProxyMenuItem = ({ selectedCount }) => { + const openModal = () => openBulkModal(CHANGE_OPENSCAP_MODAL_ID, true); + + return ( + + {__('OpenSCAP Capsule')} + + ); +}; + +ChangeOpenscapProxyMenuItem.propTypes = { + selectedCount: PropTypes.number, +}; + +ChangeOpenscapProxyMenuItem.defaultProps = { + selectedCount: 0, +}; + +const BulkChangeOpenscapProxyModalScene = () => { + const { + selectAllHostsMode = false, + selectedCount = 0, + fetchBulkParams, + organizationId, + locationId, + } = useContext(ForemanActionsBarContext) || {}; + + const { isOpen, close: closeModal } = useBulkModalOpen( + CHANGE_OPENSCAP_MODAL_ID + ); + + return ( + + ); +}; + +export default BulkChangeOpenscapProxyModalScene; diff --git a/webpack/components/HostsIndex/__tests__/ChangeOpenscapProxyAction.test.js b/webpack/components/HostsIndex/__tests__/ChangeOpenscapProxyAction.test.js new file mode 100644 index 000000000..1e2d27167 --- /dev/null +++ b/webpack/components/HostsIndex/__tests__/ChangeOpenscapProxyAction.test.js @@ -0,0 +1,174 @@ +import React from 'react'; +import { screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { Menu, MenuContent, MenuList } from '@patternfly/react-core'; +import { rtlHelpers, initMockStore } from 'foremanReact/common/testHelpers'; +import { openBulkModal } from 'foremanReact/common/BulkModalStateHelper'; +import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar'; +import { STATUS } from 'foremanReact/constants'; +import { APIActions } from 'foremanReact/redux/API'; +import { + CHANGE_OPENSCAP_MODAL_ID, + OPENSCAP_PROXIES_KEY, +} from '../../OpenscapRemediationWizard/constants'; +import BulkChangeOpenscapProxyModalScene, { + ChangeOpenscapProxyMenuItem, +} from '../ChangeOpenscapProxyAction'; + +const { renderWithStore } = rtlHelpers; + +jest.mock('foremanReact/common/I18n'); + +jest.spyOn(APIActions, 'get'); +jest.spyOn(APIActions, 'put'); + +const proxiesResolvedState = { + API: { + [OPENSCAP_PROXIES_KEY]: { + status: STATUS.RESOLVED, + response: { + results: [ + { id: 1, name: 'openscap-proxy-1.example.com' }, + { id: 2, name: 'openscap-proxy-2.example.com' }, + ], + }, + }, + }, +}; + +const actionsBarValue = { + selectAllHostsMode: false, + selectedCount: 3, + fetchBulkParams: jest.fn(() => 'id ^ (1,2,3)'), + organizationId: 1, + locationId: 2, +}; + +const renderMenuItem = (props = {}) => + renderWithStore( + // eslint-disable-next-line @theforeman/rules/require-ouiaid + + + + + + + + ); + +const renderScene = (contextValue = {}, initialState = proxiesResolvedState) => + renderWithStore( + + + , + initialState + ); + +const renderMenuItemWithScene = ( + menuProps = {}, + contextValue = {}, + initialState = proxiesResolvedState +) => + renderWithStore( + + {/* eslint-disable-next-line @theforeman/rules/require-ouiaid */} + + + + + + + + + , + initialState + ); + +describe('ChangeOpenscapProxyMenuItem', () => { + beforeEach(() => { + jest.clearAllMocks(); + delete initMockStore.API[OPENSCAP_PROXIES_KEY]; + openBulkModal(CHANGE_OPENSCAP_MODAL_ID, false); + APIActions.get.mockImplementation(payload => ({ + type: 'TEST_API_GET', + payload, + })); + APIActions.put.mockImplementation(payload => ({ + type: 'TEST_API_PUT', + payload, + })); + }); + + it('renders the OpenSCAP Capsule menu item', () => { + renderMenuItem(); + expect(screen.getByText('OpenSCAP Capsule')).toBeInTheDocument(); + }); + + it('is disabled when no hosts are selected', () => { + renderMenuItem({ selectedCount: 0 }); + expect( + screen.getByRole('menuitem', { name: 'OpenSCAP Capsule' }) + ).toBeDisabled(); + }); + + it('opens the bulk modal when clicked', () => { + renderMenuItemWithScene({ selectedCount: 2 }); + + expect(screen.queryByText('Assign OpenSCAP Proxy')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByText('OpenSCAP Capsule')); + + expect(screen.getByText('Assign OpenSCAP Proxy')).toBeInTheDocument(); + }); +}); + +describe('BulkChangeOpenscapProxyModalScene', () => { + beforeEach(() => { + jest.clearAllMocks(); + delete initMockStore.API[OPENSCAP_PROXIES_KEY]; + openBulkModal(CHANGE_OPENSCAP_MODAL_ID, false); + APIActions.get.mockImplementation(payload => ({ + type: 'TEST_API_GET', + payload, + })); + APIActions.put.mockImplementation(payload => ({ + type: 'TEST_API_PUT', + payload, + })); + }); + + it('does not render the modal when closed', () => { + renderScene(); + expect(screen.queryByText('Assign OpenSCAP Proxy')).not.toBeInTheDocument(); + }); + + it('passes actions-bar context props when the modal is open', () => { + openBulkModal(CHANGE_OPENSCAP_MODAL_ID, true); + renderScene({ + selectAllHostsMode: true, + selectedCount: 5, + organizationId: 10, + locationId: 20, + }); + + expect(screen.getByText('Assign OpenSCAP Proxy')).toBeInTheDocument(); + expect(screen.getByText('ALL selected hosts.')).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Select OpenSCAP Proxy' }) + ).toBeInTheDocument(); + }); + + it('closes the modal via Cancel', () => { + openBulkModal(CHANGE_OPENSCAP_MODAL_ID, true); + renderScene(); + + expect(screen.getByText('Assign OpenSCAP Proxy')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(screen.queryByText('Assign OpenSCAP Proxy')).not.toBeInTheDocument(); + }); +}); diff --git a/webpack/components/OpenscapRemediationWizard/constants.js b/webpack/components/OpenscapRemediationWizard/constants.js index b8f745bde..7e6f34d33 100644 --- a/webpack/components/OpenscapRemediationWizard/constants.js +++ b/webpack/components/OpenscapRemediationWizard/constants.js @@ -1,7 +1,5 @@ import { translate as __ } from 'foremanReact/common/I18n'; -export const OPENSCAP_REMEDIATION_MODAL_ID = 'openscapRemediationModal'; -export const HOSTS_PATH = '/hosts'; export const FAIL_RULE_SEARCH = 'fails_xccdf_rule'; export const HOSTS_API_PATH = '/api/hosts'; @@ -21,3 +19,8 @@ export const WIZARD_TITLES = { reviewRemediation: __('Review remediation'), finish: __('Done'), }; + +export const BULK_CHANGE_OPENSCAP_PROXY_KEY = 'BULK_CHANGE_OPENSCAP_PROXY'; +export const OPENSCAP_PROXIES_KEY = 'OPENSCAP_PROXIES_KEY'; + +export const CHANGE_OPENSCAP_MODAL_ID = 'BULK_CHANGE_OPENSCAP_PROXY_MODAL'; diff --git a/webpack/global_index.js b/webpack/global_index.js index d62e51fa3..35979f093 100644 --- a/webpack/global_index.js +++ b/webpack/global_index.js @@ -1,10 +1,31 @@ import React from 'react'; import { addGlobalFill } from 'foremanReact/components/common/Fill/GlobalFill'; import HostKebabItems from './components/HostExtentions/HostKebabItems'; +import BulkChangeOpenscapProxyModalScene, { + ChangeOpenscapProxyMenuItem, +} from './components/HostsIndex/ChangeOpenscapProxyAction'; + +const HOST_DETAILS_KEBAB_WEIGHT = 400; +const HOST_ASSOCIATIONS_WEIGHT = 1212; +const BULK_MODAL_WEIGHT = 100; addGlobalFill( 'host-details-kebab', `openscap-kebab-items`, , - 400 + HOST_DETAILS_KEBAB_WEIGHT +); + +addGlobalFill( + '_host-associations', + 'openscap-change-proxy-menu-item', + , + HOST_ASSOCIATIONS_WEIGHT +); + +addGlobalFill( + '_all-hosts-modals', + 'BulkChangeOpenscapProxyModal', + , + BULK_MODAL_WEIGHT );