foreman_openscap 13.0.0 → 13.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/api/v2/compliance/hosts_bulk_actions_controller.rb +89 -0
- data/config/routes.rb +2 -0
- data/lib/foreman_openscap/engine.rb +2 -1
- data/lib/foreman_openscap/version.rb +1 -1
- data/test/functional/api/v2/compliance/hosts_bulk_actions_controller_test.rb +132 -0
- data/webpack/components/HostsIndex/BulkActions/changeOpenscapProxy/BulkChangeOpenscapProxyModal.js +227 -0
- data/webpack/components/HostsIndex/BulkActions/changeOpenscapProxy/__tests__/BulkChangeOpenscapProxyModal.test.js +198 -0
- data/webpack/components/HostsIndex/ChangeOpenscapProxyAction.js +65 -0
- data/webpack/components/HostsIndex/__tests__/ChangeOpenscapProxyAction.test.js +172 -0
- data/webpack/components/LineChart/LineChartHelpers.js +23 -7
- data/webpack/components/OpenscapRemediationWizard/constants.js +8 -2
- data/webpack/components/OpenscapRemediationWizard/steps/Finish.js +2 -2
- data/webpack/components/OpenscapRemediationWizard/steps/ReviewRemediation.js +10 -2
- data/webpack/components/OpenscapRemediationWizard/steps/SnippetSelect.js +3 -1
- data/webpack/components/RuleSeverity/RuleSeverity.test.js +35 -8
- data/webpack/components/RuleSeverity/index.js +5 -5
- data/webpack/global_index.js +23 -1
- data/webpack/test_setup.js +1 -0
- metadata +10 -19
- data/webpack/components/ConfirmModal.js +0 -66
- data/webpack/components/ConfirmModal.scss +0 -3
- data/webpack/components/IndexLayout.js +0 -44
- data/webpack/components/IndexTable/IndexTableHelper.js +0 -6
- data/webpack/components/IndexTable/index.js +0 -73
- data/webpack/components/LinkButton.js +0 -42
- data/webpack/components/RuleSeverity/__snapshots__/RuleSeverity.test.js.snap +0 -41
- data/webpack/components/withDeleteModal.js +0 -51
- data/webpack/components/withLoading.js +0 -107
- data/webpack/helpers/commonHelper.js +0 -1
- data/webpack/helpers/globalIdHelper.js +0 -15
- data/webpack/helpers/mutationHelper.js +0 -68
- data/webpack/helpers/pageParamsHelper.js +0 -31
- data/webpack/helpers/permissionsHelper.js +0 -42
- data/webpack/helpers/tableHelper.js +0 -9
- data/webpack/helpers/toastHelper.js +0 -3
- data/webpack/testHelper.js +0 -127
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 18cf4b07fd89183f5ced6a907f35a88be64e9a19461dfdfc68511d826e42797e
|
|
4
|
+
data.tar.gz: f581fc51af310ca96c388121914040d7885d2b81d42c14e1982a8efa6501c336
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c1c1b44af883638d4bd5c3991dbd40e6ac18d49d85089726d43eca66802727ee91716e2abe3161b5348452e52e8ee97dbd404508381c05482d57dbf10be141d1
|
|
7
|
+
data.tar.gz: 8c3296b8ac590c9da6323a9fc7cff8d4f73a2b0a258bb5bcb3cda6efc6cedc416c66494590987e2893a499fb64be8d44a1c79b214a581fb907758e4751f69971
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module Api::V2
|
|
2
|
+
module Compliance
|
|
3
|
+
class HostsBulkActionsController < ::Api::V2::BaseController
|
|
4
|
+
include Api::V2::BulkHostsExtension
|
|
5
|
+
|
|
6
|
+
rescue_from ActionController::ParameterMissing do |exception|
|
|
7
|
+
render_error(:custom_error, :status => :unprocessable_entity, :locals => { :message => exception.message })
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
before_action :find_editable_hosts, only: [:change_openscap_proxy]
|
|
11
|
+
before_action :find_openscap_proxy, only: [:change_openscap_proxy]
|
|
12
|
+
before_action :validate_openscap_proxy_feature, only: [:change_openscap_proxy]
|
|
13
|
+
|
|
14
|
+
def_param_group :bulk_host_ids do
|
|
15
|
+
param :included, Hash, :desc => N_("Hosts to include in the action"), :required => true, :action_aware => true do
|
|
16
|
+
param :search, String, :required => false, :desc => N_("Search string describing which hosts to perform the action on")
|
|
17
|
+
param :ids, Array, :required => false, :desc => N_("List of host ids to perform the action on")
|
|
18
|
+
end
|
|
19
|
+
param :excluded, Hash, :desc => N_("Hosts to explicitly exclude in the action."\
|
|
20
|
+
" All other hosts will be included in the action,"\
|
|
21
|
+
" unless an included parameter is passed as well."), :required => true, :action_aware => true do
|
|
22
|
+
param :ids, Array, :required => false, :desc => N_("List of host ids to exclude and not perform the action on")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
api :PUT, "/hosts/bulk/change_openscap_proxy", N_("Assign OpenSCAP Proxy to multiple hosts")
|
|
27
|
+
param_group :bulk_host_ids
|
|
28
|
+
param :openscap_proxy_id, :number, :required => true, :desc => N_("ID of the OpenSCAP Proxy to assign to the hosts")
|
|
29
|
+
def change_openscap_proxy
|
|
30
|
+
failed_host_ids = []
|
|
31
|
+
host_count = @hosts.count
|
|
32
|
+
|
|
33
|
+
@hosts.find_each do |host|
|
|
34
|
+
host.openscap_proxy = @smart_proxy
|
|
35
|
+
failed_host_ids << host.id unless host.save
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if failed_host_ids.empty?
|
|
39
|
+
message = _("OpenSCAP Proxy is set to %s") % @smart_proxy.name
|
|
40
|
+
process_response(true, {
|
|
41
|
+
:message => n_("Updated host: #{message}", "Updated hosts: #{message}", host_count),
|
|
42
|
+
})
|
|
43
|
+
else
|
|
44
|
+
failed_count = failed_host_ids.size
|
|
45
|
+
success_count = host_count - failed_count
|
|
46
|
+
|
|
47
|
+
parts = [
|
|
48
|
+
n_("Failed to assign OpenSCAP Proxy to %{failed} of %{total} host.",
|
|
49
|
+
"Failed to assign OpenSCAP Proxy to %{failed} of %{total} hosts.",
|
|
50
|
+
host_count) % { failed: failed_count, total: host_count },
|
|
51
|
+
]
|
|
52
|
+
if success_count > 0
|
|
53
|
+
parts << n_("Successfully updated %{success} host.",
|
|
54
|
+
"Successfully updated %{success} hosts.",
|
|
55
|
+
success_count) % { success: success_count }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
render_error(:bulk_hosts_error, :status => :unprocessable_entity,
|
|
59
|
+
:locals => {
|
|
60
|
+
:message => parts.join(' '),
|
|
61
|
+
:failed_host_ids => failed_host_ids,
|
|
62
|
+
})
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def find_editable_hosts
|
|
69
|
+
find_bulk_hosts(:edit_hosts, params)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def find_openscap_proxy
|
|
73
|
+
@smart_proxy = ::SmartProxy.authorized(:view_smart_proxies)
|
|
74
|
+
.find_by(:id => params.require(:openscap_proxy_id))
|
|
75
|
+
return if @smart_proxy
|
|
76
|
+
|
|
77
|
+
render_error(:custom_error, :status => :unprocessable_entity,
|
|
78
|
+
:locals => { :message => _("OpenSCAP Proxy with id %s not found") % params[:openscap_proxy_id] })
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def validate_openscap_proxy_feature
|
|
82
|
+
return if @smart_proxy.has_feature?('Openscap')
|
|
83
|
+
|
|
84
|
+
render_error(:custom_error, :status => :unprocessable_entity,
|
|
85
|
+
:locals => { :message => _("The selected OpenSCAP Proxy does not have the OpenSCAP feature enabled.") })
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
data/config/routes.rb
CHANGED
|
@@ -93,7 +93,8 @@ module ForemanOpenscap
|
|
|
93
93
|
:resource_type => 'ForemanOpenscap::ScapContent'
|
|
94
94
|
permission :edit_hosts, { :hosts => %i[openscap_proxy_changed
|
|
95
95
|
select_multiple_openscap_proxy
|
|
96
|
-
update_multiple_openscap_proxy]
|
|
96
|
+
update_multiple_openscap_proxy],
|
|
97
|
+
'api/v2/compliance/hosts_bulk_actions' => [:change_openscap_proxy] },
|
|
97
98
|
:resource_type => "Host"
|
|
98
99
|
permission :view_hosts, { 'api/v2/hosts' => [:policies_enc] }, :resource_type => 'Host'
|
|
99
100
|
permission :edit_hostgroups, { :hostgroups => [:openscap_proxy_changed] }, :resource_type => "Hostgroup"
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require 'test_plugin_helper'
|
|
2
|
+
|
|
3
|
+
class Api::V2::Compliance::HostsBulkActionsControllerTest < ActionController::TestCase
|
|
4
|
+
tests Api::V2::Compliance::HostsBulkActionsController
|
|
5
|
+
|
|
6
|
+
def setup
|
|
7
|
+
as_admin do
|
|
8
|
+
@organization = FactoryBot.create(:organization)
|
|
9
|
+
@location = FactoryBot.create(:location)
|
|
10
|
+
@proxy = FactoryBot.create(:openscap_proxy,
|
|
11
|
+
:organizations => [@organization],
|
|
12
|
+
:locations => [@location])
|
|
13
|
+
@host1 = FactoryBot.create(:host, :managed,
|
|
14
|
+
:organization => @organization,
|
|
15
|
+
:location => @location)
|
|
16
|
+
@host2 = FactoryBot.create(:host, :managed,
|
|
17
|
+
:organization => @organization,
|
|
18
|
+
:location => @location)
|
|
19
|
+
@host_ids = [@host1.id, @host2.id]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def valid_bulk_params(host_ids = @host_ids)
|
|
24
|
+
{
|
|
25
|
+
:organization_id => @organization.id,
|
|
26
|
+
:location_id => @location.id,
|
|
27
|
+
:included => {
|
|
28
|
+
:ids => host_ids,
|
|
29
|
+
},
|
|
30
|
+
:excluded => {
|
|
31
|
+
:ids => [],
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
test "should assign openscap proxy to selected hosts" do
|
|
37
|
+
put :change_openscap_proxy,
|
|
38
|
+
params: valid_bulk_params.merge(:openscap_proxy_id => @proxy.id),
|
|
39
|
+
session: set_session_user
|
|
40
|
+
|
|
41
|
+
assert_response :success
|
|
42
|
+
response = ActiveSupport::JSON.decode(@response.body)
|
|
43
|
+
assert_match(/Updated hosts: OpenSCAP Proxy is set to/, response['message'])
|
|
44
|
+
assert_includes response['message'], @proxy.name
|
|
45
|
+
|
|
46
|
+
[@host1, @host2].each do |host|
|
|
47
|
+
host.reload
|
|
48
|
+
assert_equal @proxy.id, host.openscap_proxy_id
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
test "should require openscap_proxy_id" do
|
|
53
|
+
put :change_openscap_proxy,
|
|
54
|
+
params: valid_bulk_params,
|
|
55
|
+
session: set_session_user
|
|
56
|
+
|
|
57
|
+
assert_response :unprocessable_entity
|
|
58
|
+
response = ActiveSupport::JSON.decode(@response.body)
|
|
59
|
+
assert_match(/openscap_proxy_id/, response['error']['message'])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
test "should return error when proxy is not found" do
|
|
63
|
+
put :change_openscap_proxy,
|
|
64
|
+
params: valid_bulk_params.merge(:openscap_proxy_id => 0),
|
|
65
|
+
session: set_session_user
|
|
66
|
+
|
|
67
|
+
assert_response :unprocessable_entity
|
|
68
|
+
response = ActiveSupport::JSON.decode(@response.body)
|
|
69
|
+
assert_match(/OpenSCAP Proxy with id .* not found/, response['error']['message'])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
test "should return error when proxy lacks Openscap feature" do
|
|
73
|
+
other_proxy = FactoryBot.create(:smart_proxy,
|
|
74
|
+
:organizations => [@organization],
|
|
75
|
+
:locations => [@location])
|
|
76
|
+
openscap_feature = Feature.find_by(:name => 'Openscap')
|
|
77
|
+
other_proxy.features.delete(openscap_feature) if openscap_feature
|
|
78
|
+
refute other_proxy.reload.has_feature?('Openscap')
|
|
79
|
+
|
|
80
|
+
put :change_openscap_proxy,
|
|
81
|
+
params: valid_bulk_params.merge(:openscap_proxy_id => other_proxy.id),
|
|
82
|
+
session: set_session_user
|
|
83
|
+
|
|
84
|
+
assert_response :unprocessable_entity
|
|
85
|
+
response = ActiveSupport::JSON.decode(@response.body)
|
|
86
|
+
assert_match(/OpenSCAP Proxy does not have the OpenSCAP feature/, response['error']['message'])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
test "should assign openscap proxy for a single host" do
|
|
90
|
+
put :change_openscap_proxy,
|
|
91
|
+
params: valid_bulk_params([@host1.id]).merge(:openscap_proxy_id => @proxy.id),
|
|
92
|
+
session: set_session_user
|
|
93
|
+
|
|
94
|
+
assert_response :success
|
|
95
|
+
response = ActiveSupport::JSON.decode(@response.body)
|
|
96
|
+
assert_match(/Updated host: OpenSCAP Proxy is set to/, response['message'])
|
|
97
|
+
|
|
98
|
+
@host1.reload
|
|
99
|
+
assert_equal @proxy.id, @host1.openscap_proxy_id
|
|
100
|
+
@host2.reload
|
|
101
|
+
assert_nil @host2.openscap_proxy_id
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
test "should report failed and successful counts on partial failure" do
|
|
105
|
+
Host.any_instance.stubs(:save).returns(false).then.returns(true)
|
|
106
|
+
|
|
107
|
+
put :change_openscap_proxy,
|
|
108
|
+
params: valid_bulk_params.merge(:openscap_proxy_id => @proxy.id),
|
|
109
|
+
session: set_session_user
|
|
110
|
+
|
|
111
|
+
assert_response :unprocessable_entity
|
|
112
|
+
response = ActiveSupport::JSON.decode(@response.body)
|
|
113
|
+
assert_match(/Failed to assign OpenSCAP Proxy to 1 of 2 hosts/, response['error']['message'])
|
|
114
|
+
assert_match(/Successfully updated 1 host/, response['error']['message'])
|
|
115
|
+
assert_equal 1, response['error']['failed_host_ids'].size
|
|
116
|
+
assert_includes @host_ids, response['error']['failed_host_ids'].first
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
test "should report only failures when all hosts fail" do
|
|
120
|
+
Host.any_instance.stubs(:save).returns(false)
|
|
121
|
+
|
|
122
|
+
put :change_openscap_proxy,
|
|
123
|
+
params: valid_bulk_params.merge(:openscap_proxy_id => @proxy.id),
|
|
124
|
+
session: set_session_user
|
|
125
|
+
|
|
126
|
+
assert_response :unprocessable_entity
|
|
127
|
+
response = ActiveSupport::JSON.decode(@response.body)
|
|
128
|
+
assert_match(/Failed to assign OpenSCAP Proxy to 2 of 2 hosts/, response['error']['message'])
|
|
129
|
+
refute_match(/Successfully updated/, response['error']['message'])
|
|
130
|
+
assert_equal @host_ids.sort, response['error']['failed_host_ids'].sort
|
|
131
|
+
end
|
|
132
|
+
end
|
data/webpack/components/HostsIndex/BulkActions/changeOpenscapProxy/BulkChangeOpenscapProxyModal.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
4
|
+
import {
|
|
5
|
+
Modal,
|
|
6
|
+
Button,
|
|
7
|
+
Grid,
|
|
8
|
+
GridItem,
|
|
9
|
+
Form,
|
|
10
|
+
FormGroup,
|
|
11
|
+
Stack,
|
|
12
|
+
StackItem,
|
|
13
|
+
Skeleton,
|
|
14
|
+
} from '@patternfly/react-core';
|
|
15
|
+
import { SimpleDropdown } from '@patternfly/react-templates';
|
|
16
|
+
import { foremanUrl } from 'foremanReact/common/helpers';
|
|
17
|
+
import { APIActions } from 'foremanReact/redux/API';
|
|
18
|
+
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
|
19
|
+
import { STATUS } from 'foremanReact/constants';
|
|
20
|
+
import {
|
|
21
|
+
selectAPIStatus,
|
|
22
|
+
selectAPIResponse,
|
|
23
|
+
} from 'foremanReact/redux/API/APISelectors';
|
|
24
|
+
import { buildBulkRequestBody } from 'foremanReact/components/HostsIndex/BulkActions/helpers';
|
|
25
|
+
import {
|
|
26
|
+
BULK_CHANGE_OPENSCAP_PROXY_KEY,
|
|
27
|
+
OPENSCAP_PROXIES_KEY,
|
|
28
|
+
} from '../../../OpenscapRemediationWizard/constants';
|
|
29
|
+
|
|
30
|
+
const fetchOpenscapProxies = () =>
|
|
31
|
+
APIActions.get({
|
|
32
|
+
key: OPENSCAP_PROXIES_KEY,
|
|
33
|
+
url: foremanUrl(
|
|
34
|
+
'/api/smart_proxies?search=feature%3DOpenscap&per_page=all'
|
|
35
|
+
),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const BulkChangeOpenscapProxyModal = ({
|
|
39
|
+
isOpen,
|
|
40
|
+
closeModal,
|
|
41
|
+
selectAllHostsMode,
|
|
42
|
+
selectedCount,
|
|
43
|
+
fetchBulkParams,
|
|
44
|
+
organizationId,
|
|
45
|
+
locationId,
|
|
46
|
+
onSuccess: onSuccessCallback,
|
|
47
|
+
}) => {
|
|
48
|
+
const dispatch = useDispatch();
|
|
49
|
+
const [proxyId, setProxyId] = useState('');
|
|
50
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (isOpen) {
|
|
54
|
+
dispatch(fetchOpenscapProxies());
|
|
55
|
+
} else {
|
|
56
|
+
setIsSubmitting(false);
|
|
57
|
+
}
|
|
58
|
+
}, [dispatch, isOpen]);
|
|
59
|
+
|
|
60
|
+
const proxies = useSelector(state =>
|
|
61
|
+
selectAPIResponse(state, OPENSCAP_PROXIES_KEY)
|
|
62
|
+
);
|
|
63
|
+
const proxyStatus = useSelector(state =>
|
|
64
|
+
selectAPIStatus(state, OPENSCAP_PROXIES_KEY)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const handleModalClose = () => {
|
|
68
|
+
setProxyId('');
|
|
69
|
+
setIsSubmitting(false);
|
|
70
|
+
closeModal();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handleSuccess = () => {
|
|
74
|
+
handleModalClose();
|
|
75
|
+
if (onSuccessCallback) onSuccessCallback();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const handleError = () => {
|
|
79
|
+
setIsSubmitting(false);
|
|
80
|
+
handleModalClose();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleConfirm = () => {
|
|
84
|
+
const requestBody = buildBulkRequestBody({
|
|
85
|
+
fetchBulkParams,
|
|
86
|
+
organizationId,
|
|
87
|
+
locationId,
|
|
88
|
+
openscap_proxy_id: proxyId,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
setIsSubmitting(true);
|
|
92
|
+
dispatch(
|
|
93
|
+
APIActions.put({
|
|
94
|
+
key: BULK_CHANGE_OPENSCAP_PROXY_KEY,
|
|
95
|
+
url: foremanUrl('/api/v2/hosts/bulk/change_openscap_proxy'),
|
|
96
|
+
handleSuccess,
|
|
97
|
+
successToast: response => response.data.message,
|
|
98
|
+
handleError,
|
|
99
|
+
errorToast: error => error?.response?.data?.error?.message,
|
|
100
|
+
params: requestBody,
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const descriptionText = selectAllHostsMode ? (
|
|
106
|
+
<>
|
|
107
|
+
{__('Assign OpenSCAP Proxy for ')}
|
|
108
|
+
<strong>{__('ALL selected hosts.')}</strong>
|
|
109
|
+
<br />
|
|
110
|
+
{__('This will change previous proxy assignments on the selected hosts.')}
|
|
111
|
+
</>
|
|
112
|
+
) : (
|
|
113
|
+
<>
|
|
114
|
+
{__('Assign OpenSCAP Proxy for ')}
|
|
115
|
+
<strong>{sprintf(__('%s selected hosts.'), selectedCount)}</strong>
|
|
116
|
+
<br />
|
|
117
|
+
{__('This will change previous proxy assignments on the selected hosts.')}
|
|
118
|
+
</>
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const getProxyLabel = id => {
|
|
122
|
+
const proxy = proxies?.results?.find(
|
|
123
|
+
p => p.id.toString() === id.toString()
|
|
124
|
+
);
|
|
125
|
+
return proxy?.name || id;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const proxyItems =
|
|
129
|
+
proxies?.results?.map(proxy => ({
|
|
130
|
+
value: proxy.id.toString(),
|
|
131
|
+
content: proxy.name,
|
|
132
|
+
onClick: () => setProxyId(proxy.id.toString()),
|
|
133
|
+
})) || [];
|
|
134
|
+
|
|
135
|
+
const modalActions = [
|
|
136
|
+
<Button
|
|
137
|
+
key="confirm"
|
|
138
|
+
ouiaId="bulk-change-openscap-proxy-modal-confirm-button"
|
|
139
|
+
variant="primary"
|
|
140
|
+
onClick={handleConfirm}
|
|
141
|
+
isDisabled={proxyId === '' || isSubmitting}
|
|
142
|
+
isLoading={isSubmitting}
|
|
143
|
+
spinnerAriaLabel={__('Loading')}
|
|
144
|
+
>
|
|
145
|
+
{__('Assign')}
|
|
146
|
+
</Button>,
|
|
147
|
+
<Button
|
|
148
|
+
key="cancel"
|
|
149
|
+
ouiaId="bulk-change-openscap-proxy-modal-cancel-button"
|
|
150
|
+
variant="link"
|
|
151
|
+
onClick={handleModalClose}
|
|
152
|
+
isDisabled={isSubmitting}
|
|
153
|
+
>
|
|
154
|
+
{__('Cancel')}
|
|
155
|
+
</Button>,
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<Modal
|
|
160
|
+
isOpen={isOpen}
|
|
161
|
+
onClose={handleModalClose}
|
|
162
|
+
onEscapePress={handleModalClose}
|
|
163
|
+
title={__('Assign OpenSCAP Proxy')}
|
|
164
|
+
variant="small"
|
|
165
|
+
position="top"
|
|
166
|
+
actions={modalActions}
|
|
167
|
+
id="bulk-change-openscap-proxy-modal"
|
|
168
|
+
key="bulk-change-openscap-proxy-modal"
|
|
169
|
+
ouiaId="bulk-change-openscap-proxy-modal"
|
|
170
|
+
>
|
|
171
|
+
<Stack hasGutter>
|
|
172
|
+
<StackItem>{descriptionText}</StackItem>
|
|
173
|
+
{proxyStatus === STATUS.RESOLVED && proxies?.results?.length > 0 && (
|
|
174
|
+
<StackItem>
|
|
175
|
+
<Grid>
|
|
176
|
+
<GridItem span={8}>
|
|
177
|
+
<Form>
|
|
178
|
+
<FormGroup label={__('Select OpenSCAP Proxy')}>
|
|
179
|
+
<SimpleDropdown
|
|
180
|
+
id="openscap-proxy-select"
|
|
181
|
+
ouiaId="bulk-change-openscap-proxy-select"
|
|
182
|
+
toggleContent={
|
|
183
|
+
proxyId
|
|
184
|
+
? getProxyLabel(proxyId)
|
|
185
|
+
: __('Select OpenSCAP Proxy')
|
|
186
|
+
}
|
|
187
|
+
initialItems={proxyItems}
|
|
188
|
+
/>
|
|
189
|
+
</FormGroup>
|
|
190
|
+
</Form>
|
|
191
|
+
</GridItem>
|
|
192
|
+
</Grid>
|
|
193
|
+
</StackItem>
|
|
194
|
+
)}
|
|
195
|
+
{proxyStatus === STATUS.RESOLVED &&
|
|
196
|
+
(!proxies?.results || proxies.results.length === 0) &&
|
|
197
|
+
__(
|
|
198
|
+
'No OpenSCAP Proxies available. Please configure a Smart Proxy with the OpenSCAP feature.'
|
|
199
|
+
)}
|
|
200
|
+
{proxyStatus === STATUS.PENDING && (
|
|
201
|
+
<Skeleton screenreaderText="Loading contents" />
|
|
202
|
+
)}
|
|
203
|
+
</Stack>
|
|
204
|
+
</Modal>
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
BulkChangeOpenscapProxyModal.propTypes = {
|
|
209
|
+
isOpen: PropTypes.bool,
|
|
210
|
+
closeModal: PropTypes.func,
|
|
211
|
+
fetchBulkParams: PropTypes.func.isRequired,
|
|
212
|
+
selectedCount: PropTypes.number.isRequired,
|
|
213
|
+
selectAllHostsMode: PropTypes.bool.isRequired,
|
|
214
|
+
organizationId: PropTypes.number,
|
|
215
|
+
locationId: PropTypes.number,
|
|
216
|
+
onSuccess: PropTypes.func,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
BulkChangeOpenscapProxyModal.defaultProps = {
|
|
220
|
+
isOpen: false,
|
|
221
|
+
closeModal: () => {},
|
|
222
|
+
organizationId: undefined,
|
|
223
|
+
locationId: undefined,
|
|
224
|
+
onSuccess: undefined,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export default BulkChangeOpenscapProxyModal;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
screen,
|
|
4
|
+
fireEvent,
|
|
5
|
+
waitFor,
|
|
6
|
+
within,
|
|
7
|
+
act,
|
|
8
|
+
} from '@testing-library/react';
|
|
9
|
+
import '@testing-library/jest-dom';
|
|
10
|
+
import { rtlHelpers, initMockStore } from 'foremanReact/common/testHelpers';
|
|
11
|
+
import { STATUS } from 'foremanReact/constants';
|
|
12
|
+
import { APIActions } from 'foremanReact/redux/API';
|
|
13
|
+
import { OPENSCAP_PROXIES_KEY } from '../../../../OpenscapRemediationWizard/constants';
|
|
14
|
+
import BulkChangeOpenscapProxyModal from '../BulkChangeOpenscapProxyModal';
|
|
15
|
+
|
|
16
|
+
const { renderWithStore } = rtlHelpers;
|
|
17
|
+
|
|
18
|
+
jest.mock('foremanReact/common/I18n');
|
|
19
|
+
|
|
20
|
+
jest.spyOn(APIActions, 'get');
|
|
21
|
+
jest.spyOn(APIActions, 'put');
|
|
22
|
+
|
|
23
|
+
const proxies = {
|
|
24
|
+
results: [
|
|
25
|
+
{ id: 1, name: 'openscap-proxy-1.example.com' },
|
|
26
|
+
{ id: 2, name: 'openscap-proxy-2.example.com' },
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const defaultProps = {
|
|
31
|
+
selectedCount: 3,
|
|
32
|
+
selectAllHostsMode: false,
|
|
33
|
+
fetchBulkParams: jest.fn(() => 'id ^ (1,2,3)'),
|
|
34
|
+
isOpen: true,
|
|
35
|
+
closeModal: jest.fn(),
|
|
36
|
+
organizationId: 1,
|
|
37
|
+
locationId: 2,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const proxiesResolvedState = {
|
|
41
|
+
API: {
|
|
42
|
+
[OPENSCAP_PROXIES_KEY]: {
|
|
43
|
+
status: STATUS.RESOLVED,
|
|
44
|
+
response: proxies,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const noProxiesState = {
|
|
50
|
+
API: {
|
|
51
|
+
[OPENSCAP_PROXIES_KEY]: {
|
|
52
|
+
status: STATUS.RESOLVED,
|
|
53
|
+
response: { results: [] },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const renderModal = (props = {}, initialState = proxiesResolvedState) =>
|
|
59
|
+
renderWithStore(
|
|
60
|
+
<BulkChangeOpenscapProxyModal {...defaultProps} {...props} />,
|
|
61
|
+
initialState
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
describe('BulkChangeOpenscapProxyModal', () => {
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
jest.clearAllMocks();
|
|
67
|
+
// renderWithStore lodash-merges into shared initMockStore; clear prior API fixtures
|
|
68
|
+
delete initMockStore.API[OPENSCAP_PROXIES_KEY];
|
|
69
|
+
APIActions.get.mockImplementation(payload => ({
|
|
70
|
+
type: 'TEST_API_GET',
|
|
71
|
+
payload,
|
|
72
|
+
}));
|
|
73
|
+
APIActions.put.mockImplementation(payload => ({
|
|
74
|
+
type: 'TEST_API_PUT',
|
|
75
|
+
payload,
|
|
76
|
+
}));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('renders modal title and description with selected host count', () => {
|
|
80
|
+
renderModal();
|
|
81
|
+
expect(screen.getByText('Assign OpenSCAP Proxy')).toBeInTheDocument();
|
|
82
|
+
expect(screen.getByText('3 selected hosts.')).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('renders all-hosts mode description', () => {
|
|
86
|
+
renderModal({ selectAllHostsMode: true });
|
|
87
|
+
expect(screen.getByText('ALL selected hosts.')).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('disables Assign until a proxy is selected', () => {
|
|
91
|
+
renderModal();
|
|
92
|
+
expect(screen.getByRole('button', { name: 'Assign' })).toBeDisabled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('lists OpenSCAP proxies in the dropdown', async () => {
|
|
96
|
+
renderModal();
|
|
97
|
+
fireEvent.click(
|
|
98
|
+
screen.getByRole('button', { name: 'Select OpenSCAP Proxy' })
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const menu = await screen.findByRole('menu');
|
|
102
|
+
expect(
|
|
103
|
+
within(menu).getByRole('menuitem', {
|
|
104
|
+
name: 'openscap-proxy-1.example.com',
|
|
105
|
+
})
|
|
106
|
+
).toBeInTheDocument();
|
|
107
|
+
expect(
|
|
108
|
+
within(menu).getByRole('menuitem', {
|
|
109
|
+
name: 'openscap-proxy-2.example.com',
|
|
110
|
+
})
|
|
111
|
+
).toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('enables Assign and dispatches bulk PUT after selecting a proxy', async () => {
|
|
115
|
+
renderModal();
|
|
116
|
+
fireEvent.click(
|
|
117
|
+
screen.getByRole('button', { name: 'Select OpenSCAP Proxy' })
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const menu = await screen.findByRole('menu');
|
|
121
|
+
fireEvent.click(
|
|
122
|
+
within(menu).getByRole('menuitem', {
|
|
123
|
+
name: 'openscap-proxy-1.example.com',
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const assignBtn = screen.getByRole('button', { name: 'Assign' });
|
|
128
|
+
await waitFor(() => {
|
|
129
|
+
expect(assignBtn).not.toBeDisabled();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
fireEvent.click(assignBtn);
|
|
133
|
+
expect(APIActions.put).toHaveBeenCalledWith(
|
|
134
|
+
expect.objectContaining({
|
|
135
|
+
url: expect.stringContaining('/hosts/bulk/change_openscap_proxy'),
|
|
136
|
+
params: expect.objectContaining({
|
|
137
|
+
included: { search: 'id ^ (1,2,3)' },
|
|
138
|
+
organization_id: 1,
|
|
139
|
+
location_id: 2,
|
|
140
|
+
openscap_proxy_id: '1',
|
|
141
|
+
}),
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('calls onSuccess and closeModal after a successful bulk PUT', async () => {
|
|
147
|
+
const onSuccess = jest.fn();
|
|
148
|
+
const closeModal = jest.fn();
|
|
149
|
+
renderModal({ onSuccess, closeModal });
|
|
150
|
+
|
|
151
|
+
fireEvent.click(
|
|
152
|
+
screen.getByRole('button', { name: 'Select OpenSCAP Proxy' })
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const menu = await screen.findByRole('menu');
|
|
156
|
+
fireEvent.click(
|
|
157
|
+
within(menu).getByRole('menuitem', {
|
|
158
|
+
name: 'openscap-proxy-1.example.com',
|
|
159
|
+
})
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const assignBtn = screen.getByRole('button', { name: 'Assign' });
|
|
163
|
+
await waitFor(() => {
|
|
164
|
+
expect(assignBtn).not.toBeDisabled();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
fireEvent.click(assignBtn);
|
|
168
|
+
|
|
169
|
+
const { handleSuccess } = APIActions.put.mock.calls[0][0];
|
|
170
|
+
act(() => {
|
|
171
|
+
handleSuccess();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(onSuccess).toHaveBeenCalled();
|
|
175
|
+
expect(closeModal).toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('calls closeModal on Cancel', () => {
|
|
179
|
+
const closeModal = jest.fn();
|
|
180
|
+
renderModal({ closeModal });
|
|
181
|
+
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
182
|
+
expect(closeModal).toHaveBeenCalled();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('shows empty state when no proxies are available', () => {
|
|
186
|
+
renderModal({}, noProxiesState);
|
|
187
|
+
expect(
|
|
188
|
+
screen.getByText(
|
|
189
|
+
'No OpenSCAP Proxies available. Please configure a Smart Proxy with the OpenSCAP feature.'
|
|
190
|
+
)
|
|
191
|
+
).toBeInTheDocument();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('does not render when isOpen is false', () => {
|
|
195
|
+
renderModal({ isOpen: false });
|
|
196
|
+
expect(screen.queryByText('Assign OpenSCAP Proxy')).not.toBeInTheDocument();
|
|
197
|
+
});
|
|
198
|
+
});
|