foreman_openscap 13.0.1 → 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/OpenscapRemediationWizard/constants.js +5 -2
- data/webpack/components/RuleSeverity/RuleSeverity.test.js +35 -8
- data/webpack/components/RuleSeverity/index.js +5 -5
- data/webpack/global_index.js +20 -0
- metadata +8 -2
- data/webpack/components/RuleSeverity/__snapshots__/RuleSeverity.test.js.snap +0 -41
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
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { MenuItem } from '@patternfly/react-core';
|
|
4
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
|
+
import {
|
|
6
|
+
openBulkModal,
|
|
7
|
+
useBulkModalOpen,
|
|
8
|
+
} from 'foremanReact/common/BulkModalStateHelper';
|
|
9
|
+
import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar';
|
|
10
|
+
import BulkChangeOpenscapProxyModal from './BulkActions/changeOpenscapProxy/BulkChangeOpenscapProxyModal';
|
|
11
|
+
import { CHANGE_OPENSCAP_MODAL_ID } from '../OpenscapRemediationWizard/constants';
|
|
12
|
+
|
|
13
|
+
export const ChangeOpenscapProxyMenuItem = ({ selectedCount }) => {
|
|
14
|
+
const openModal = () => openBulkModal(CHANGE_OPENSCAP_MODAL_ID, true);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<MenuItem
|
|
18
|
+
itemId="change-openscap-proxy-dropdown-item"
|
|
19
|
+
key="change-openscap-proxy-dropdown-item"
|
|
20
|
+
onClick={openModal}
|
|
21
|
+
isDisabled={selectedCount === 0}
|
|
22
|
+
>
|
|
23
|
+
{__('OpenSCAP Proxy')}
|
|
24
|
+
</MenuItem>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
ChangeOpenscapProxyMenuItem.propTypes = {
|
|
29
|
+
selectedCount: PropTypes.number,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
ChangeOpenscapProxyMenuItem.defaultProps = {
|
|
33
|
+
selectedCount: 0,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const BulkChangeOpenscapProxyModalScene = () => {
|
|
37
|
+
const {
|
|
38
|
+
selectAllHostsMode = false,
|
|
39
|
+
selectedCount = 0,
|
|
40
|
+
fetchBulkParams,
|
|
41
|
+
organizationId,
|
|
42
|
+
locationId,
|
|
43
|
+
refreshTableData,
|
|
44
|
+
} = useContext(ForemanActionsBarContext) || {};
|
|
45
|
+
|
|
46
|
+
const { isOpen, close: closeModal } = useBulkModalOpen(
|
|
47
|
+
CHANGE_OPENSCAP_MODAL_ID
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<BulkChangeOpenscapProxyModal
|
|
52
|
+
key="bulk-change-openscap-proxy-modal"
|
|
53
|
+
selectAllHostsMode={selectAllHostsMode}
|
|
54
|
+
selectedCount={selectedCount}
|
|
55
|
+
fetchBulkParams={fetchBulkParams}
|
|
56
|
+
organizationId={organizationId}
|
|
57
|
+
locationId={locationId}
|
|
58
|
+
isOpen={isOpen}
|
|
59
|
+
closeModal={closeModal}
|
|
60
|
+
onSuccess={refreshTableData}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default BulkChangeOpenscapProxyModalScene;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import { Menu, MenuContent, MenuList } from '@patternfly/react-core';
|
|
5
|
+
import { rtlHelpers, initMockStore } from 'foremanReact/common/testHelpers';
|
|
6
|
+
import { openBulkModal } from 'foremanReact/common/BulkModalStateHelper';
|
|
7
|
+
import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar';
|
|
8
|
+
import { STATUS } from 'foremanReact/constants';
|
|
9
|
+
import { APIActions } from 'foremanReact/redux/API';
|
|
10
|
+
import {
|
|
11
|
+
CHANGE_OPENSCAP_MODAL_ID,
|
|
12
|
+
OPENSCAP_PROXIES_KEY,
|
|
13
|
+
} from '../../OpenscapRemediationWizard/constants';
|
|
14
|
+
import BulkChangeOpenscapProxyModalScene, {
|
|
15
|
+
ChangeOpenscapProxyMenuItem,
|
|
16
|
+
} from '../ChangeOpenscapProxyAction';
|
|
17
|
+
|
|
18
|
+
const { renderWithStore } = rtlHelpers;
|
|
19
|
+
|
|
20
|
+
jest.spyOn(APIActions, 'get');
|
|
21
|
+
jest.spyOn(APIActions, 'put');
|
|
22
|
+
|
|
23
|
+
const proxiesResolvedState = {
|
|
24
|
+
API: {
|
|
25
|
+
[OPENSCAP_PROXIES_KEY]: {
|
|
26
|
+
status: STATUS.RESOLVED,
|
|
27
|
+
response: {
|
|
28
|
+
results: [
|
|
29
|
+
{ id: 1, name: 'openscap-proxy-1.example.com' },
|
|
30
|
+
{ id: 2, name: 'openscap-proxy-2.example.com' },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const actionsBarValue = {
|
|
38
|
+
selectAllHostsMode: false,
|
|
39
|
+
selectedCount: 3,
|
|
40
|
+
fetchBulkParams: jest.fn(() => 'id ^ (1,2,3)'),
|
|
41
|
+
organizationId: 1,
|
|
42
|
+
locationId: 2,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const renderMenuItem = (props = {}) =>
|
|
46
|
+
renderWithStore(
|
|
47
|
+
// eslint-disable-next-line @theforeman/rules/require-ouiaid
|
|
48
|
+
<Menu activeItemId={null}>
|
|
49
|
+
<MenuContent>
|
|
50
|
+
<MenuList>
|
|
51
|
+
<ChangeOpenscapProxyMenuItem selectedCount={3} {...props} />
|
|
52
|
+
</MenuList>
|
|
53
|
+
</MenuContent>
|
|
54
|
+
</Menu>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const renderScene = (contextValue = {}, initialState = proxiesResolvedState) =>
|
|
58
|
+
renderWithStore(
|
|
59
|
+
<ForemanActionsBarContext.Provider
|
|
60
|
+
value={{ ...actionsBarValue, ...contextValue }}
|
|
61
|
+
>
|
|
62
|
+
<BulkChangeOpenscapProxyModalScene />
|
|
63
|
+
</ForemanActionsBarContext.Provider>,
|
|
64
|
+
initialState
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const renderMenuItemWithScene = (
|
|
68
|
+
menuProps = {},
|
|
69
|
+
contextValue = {},
|
|
70
|
+
initialState = proxiesResolvedState
|
|
71
|
+
) =>
|
|
72
|
+
renderWithStore(
|
|
73
|
+
<ForemanActionsBarContext.Provider
|
|
74
|
+
value={{ ...actionsBarValue, ...contextValue }}
|
|
75
|
+
>
|
|
76
|
+
{/* eslint-disable-next-line @theforeman/rules/require-ouiaid */}
|
|
77
|
+
<Menu activeItemId={null}>
|
|
78
|
+
<MenuContent>
|
|
79
|
+
<MenuList>
|
|
80
|
+
<ChangeOpenscapProxyMenuItem selectedCount={3} {...menuProps} />
|
|
81
|
+
</MenuList>
|
|
82
|
+
</MenuContent>
|
|
83
|
+
</Menu>
|
|
84
|
+
<BulkChangeOpenscapProxyModalScene />
|
|
85
|
+
</ForemanActionsBarContext.Provider>,
|
|
86
|
+
initialState
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
describe('ChangeOpenscapProxyMenuItem', () => {
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
jest.clearAllMocks();
|
|
92
|
+
delete initMockStore.API[OPENSCAP_PROXIES_KEY];
|
|
93
|
+
openBulkModal(CHANGE_OPENSCAP_MODAL_ID, false);
|
|
94
|
+
APIActions.get.mockImplementation(payload => ({
|
|
95
|
+
type: 'TEST_API_GET',
|
|
96
|
+
payload,
|
|
97
|
+
}));
|
|
98
|
+
APIActions.put.mockImplementation(payload => ({
|
|
99
|
+
type: 'TEST_API_PUT',
|
|
100
|
+
payload,
|
|
101
|
+
}));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('renders the OpenSCAP Proxy menu item', () => {
|
|
105
|
+
renderMenuItem();
|
|
106
|
+
expect(screen.getByText('OpenSCAP Proxy')).toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('is disabled when no hosts are selected', () => {
|
|
110
|
+
renderMenuItem({ selectedCount: 0 });
|
|
111
|
+
expect(
|
|
112
|
+
screen.getByRole('menuitem', { name: 'OpenSCAP Proxy' })
|
|
113
|
+
).toBeDisabled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('opens the bulk modal when clicked', () => {
|
|
117
|
+
renderMenuItemWithScene({ selectedCount: 2 });
|
|
118
|
+
|
|
119
|
+
expect(screen.queryByText('Assign OpenSCAP Proxy')).not.toBeInTheDocument();
|
|
120
|
+
|
|
121
|
+
fireEvent.click(screen.getByText('OpenSCAP Proxy'));
|
|
122
|
+
|
|
123
|
+
expect(screen.getByText('Assign OpenSCAP Proxy')).toBeInTheDocument();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('BulkChangeOpenscapProxyModalScene', () => {
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
jest.clearAllMocks();
|
|
130
|
+
delete initMockStore.API[OPENSCAP_PROXIES_KEY];
|
|
131
|
+
openBulkModal(CHANGE_OPENSCAP_MODAL_ID, false);
|
|
132
|
+
APIActions.get.mockImplementation(payload => ({
|
|
133
|
+
type: 'TEST_API_GET',
|
|
134
|
+
payload,
|
|
135
|
+
}));
|
|
136
|
+
APIActions.put.mockImplementation(payload => ({
|
|
137
|
+
type: 'TEST_API_PUT',
|
|
138
|
+
payload,
|
|
139
|
+
}));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('does not render the modal when closed', () => {
|
|
143
|
+
renderScene();
|
|
144
|
+
expect(screen.queryByText('Assign OpenSCAP Proxy')).not.toBeInTheDocument();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('passes actions-bar context props when the modal is open', () => {
|
|
148
|
+
openBulkModal(CHANGE_OPENSCAP_MODAL_ID, true);
|
|
149
|
+
renderScene({
|
|
150
|
+
selectAllHostsMode: true,
|
|
151
|
+
selectedCount: 5,
|
|
152
|
+
organizationId: 10,
|
|
153
|
+
locationId: 20,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(screen.getByText('Assign OpenSCAP Proxy')).toBeInTheDocument();
|
|
157
|
+
expect(screen.getByText('ALL selected hosts.')).toBeInTheDocument();
|
|
158
|
+
expect(
|
|
159
|
+
screen.getByRole('button', { name: 'Select OpenSCAP Proxy' })
|
|
160
|
+
).toBeInTheDocument();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('closes the modal via Cancel', () => {
|
|
164
|
+
openBulkModal(CHANGE_OPENSCAP_MODAL_ID, true);
|
|
165
|
+
renderScene();
|
|
166
|
+
|
|
167
|
+
expect(screen.getByText('Assign OpenSCAP Proxy')).toBeInTheDocument();
|
|
168
|
+
|
|
169
|
+
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
170
|
+
expect(screen.queryByText('Assign OpenSCAP Proxy')).not.toBeInTheDocument();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
2
2
|
|
|
3
|
-
export const OPENSCAP_REMEDIATION_MODAL_ID = 'openscapRemediationModal';
|
|
4
|
-
export const HOSTS_PATH = '/hosts';
|
|
5
3
|
export const FAIL_RULE_SEARCH = 'fails_xccdf_rule';
|
|
6
4
|
|
|
7
5
|
export const HOSTS_API_PATH = '/api/hosts';
|
|
@@ -24,3 +22,8 @@ export const WIZARD_TITLES = {
|
|
|
24
22
|
reviewRemediation: __('Review remediation'),
|
|
25
23
|
finish: __('Done'),
|
|
26
24
|
};
|
|
25
|
+
|
|
26
|
+
export const BULK_CHANGE_OPENSCAP_PROXY_KEY = 'BULK_CHANGE_OPENSCAP_PROXY';
|
|
27
|
+
export const OPENSCAP_PROXIES_KEY = 'OPENSCAP_PROXIES_KEY';
|
|
28
|
+
|
|
29
|
+
export const CHANGE_OPENSCAP_MODAL_ID = 'BULK_CHANGE_OPENSCAP_PROXY_MODAL';
|
|
@@ -1,13 +1,40 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
2
4
|
|
|
3
5
|
import RuleSeverity from './index';
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
jest.mock('./i_severity-critical.svg', () => 'critical.svg');
|
|
8
|
+
jest.mock('./i_severity-high.svg', () => 'high.svg');
|
|
9
|
+
jest.mock('./i_severity-med.svg', () => 'med.svg');
|
|
10
|
+
jest.mock('./i_severity-low.svg', () => 'low.svg');
|
|
11
|
+
jest.mock('./i_unknown.svg', () => 'unknown.svg');
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
describe('RuleSeverity', () => {
|
|
14
|
+
it.each([
|
|
15
|
+
['low', 'Low Severity', 'low.svg'],
|
|
16
|
+
['medium', 'Medium Severity', 'med.svg'],
|
|
17
|
+
['high', 'High Severity', 'high.svg'],
|
|
18
|
+
['critical', 'Critical Severity', 'critical.svg'],
|
|
19
|
+
['unknown', 'Unknown Severity', 'unknown.svg'],
|
|
20
|
+
])('renders the %s severity icon', (severity, altText, iconSrc) => {
|
|
21
|
+
render(<RuleSeverity severity={severity} />);
|
|
11
22
|
|
|
12
|
-
|
|
13
|
-
|
|
23
|
+
const icon = screen.getByRole('img', { name: altText });
|
|
24
|
+
|
|
25
|
+
expect(icon).toBeInTheDocument();
|
|
26
|
+
expect(icon).toHaveAttribute('src', iconSrc);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it.each(['foo', 'Low', 'Medium', 'High', 'Critical'])(
|
|
30
|
+
'renders the unknown severity icon for unrecognized severity %s',
|
|
31
|
+
severity => {
|
|
32
|
+
render(<RuleSeverity severity={severity} />);
|
|
33
|
+
|
|
34
|
+
const icon = screen.getByRole('img', { name: 'Unknown Severity' });
|
|
35
|
+
|
|
36
|
+
expect(icon).toBeInTheDocument();
|
|
37
|
+
expect(icon).toHaveAttribute('src', 'unknown.svg');
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
});
|
|
@@ -11,14 +11,14 @@ import './RuleSeverity.scss';
|
|
|
11
11
|
|
|
12
12
|
const RuleSeverity = props => {
|
|
13
13
|
const propsMapping = {
|
|
14
|
-
low: { alt: 'Low
|
|
15
|
-
medium: { alt: 'Medium
|
|
16
|
-
high: { alt: 'High
|
|
14
|
+
low: { alt: 'Low Severity', src: SeverityLow },
|
|
15
|
+
medium: { alt: 'Medium Severity', src: SeverityMedium },
|
|
16
|
+
high: { alt: 'High Severity', src: SeverityHigh },
|
|
17
17
|
critical: {
|
|
18
|
-
alt: 'Critical
|
|
18
|
+
alt: 'Critical Severity',
|
|
19
19
|
src: SeverityCritical,
|
|
20
20
|
},
|
|
21
|
-
unknown: { alt: 'Unknown
|
|
21
|
+
unknown: { alt: 'Unknown Severity', src: SeverityUnknown },
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
const imgProps = propsMapping[props.severity] || propsMapping.unknown;
|
data/webpack/global_index.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { addGlobalFill } from 'foremanReact/components/common/Fill/GlobalFill';
|
|
3
3
|
import HostKebabItems from './components/HostExtentions/HostKebabItems';
|
|
4
|
+
import BulkChangeOpenscapProxyModalScene, {
|
|
5
|
+
ChangeOpenscapProxyMenuItem,
|
|
6
|
+
} from './components/HostsIndex/ChangeOpenscapProxyAction';
|
|
7
|
+
|
|
8
|
+
const HOST_ASSOCIATIONS_WEIGHT = 1212;
|
|
9
|
+
const BULK_MODAL_WEIGHT = 100;
|
|
4
10
|
|
|
5
11
|
const OPENSCAP_KEBAB_WEIGHT = 400;
|
|
6
12
|
|
|
@@ -10,3 +16,17 @@ addGlobalFill(
|
|
|
10
16
|
<HostKebabItems key="openscap-host-kebab" />,
|
|
11
17
|
OPENSCAP_KEBAB_WEIGHT
|
|
12
18
|
);
|
|
19
|
+
|
|
20
|
+
addGlobalFill(
|
|
21
|
+
'_host-associations',
|
|
22
|
+
'openscap-change-proxy-menu-item',
|
|
23
|
+
<ChangeOpenscapProxyMenuItem key="openscap-change-proxy-menu-item" />,
|
|
24
|
+
HOST_ASSOCIATIONS_WEIGHT
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
addGlobalFill(
|
|
28
|
+
'_all-hosts-modals',
|
|
29
|
+
'BulkChangeOpenscapProxyModal',
|
|
30
|
+
<BulkChangeOpenscapProxyModalScene key="bulk-change-openscap-proxy-modal" />,
|
|
31
|
+
BULK_MODAL_WEIGHT
|
|
32
|
+
);
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreman_openscap
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 13.0
|
|
4
|
+
version: 13.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- slukasik@redhat.com
|
|
@@ -45,6 +45,7 @@ files:
|
|
|
45
45
|
- app/assets/stylesheets/foreman_openscap/reports.css
|
|
46
46
|
- app/assets/stylesheets/foreman_openscap/scap_breakdown_chart.css
|
|
47
47
|
- app/controllers/api/v2/compliance/arf_reports_controller.rb
|
|
48
|
+
- app/controllers/api/v2/compliance/hosts_bulk_actions_controller.rb
|
|
48
49
|
- app/controllers/api/v2/compliance/policies_controller.rb
|
|
49
50
|
- app/controllers/api/v2/compliance/scap_content_profiles_controller.rb
|
|
50
51
|
- app/controllers/api/v2/compliance/scap_contents_controller.rb
|
|
@@ -333,6 +334,7 @@ files:
|
|
|
333
334
|
- test/files/tailoring_files/ssg-firefox-ds-tailoring-2.xml
|
|
334
335
|
- test/files/tailoring_files/ssg-firefox-ds-tailoring.xml
|
|
335
336
|
- test/functional/api/v2/compliance/arf_reports_controller_test.rb
|
|
337
|
+
- test/functional/api/v2/compliance/hosts_bulk_actions_controller_test.rb
|
|
336
338
|
- test/functional/api/v2/compliance/policies_controller_test.rb
|
|
337
339
|
- test/functional/api/v2/compliance/scap_content_profiles_controller_test.rb
|
|
338
340
|
- test/functional/api/v2/compliance/scap_contents_controller_test.rb
|
|
@@ -364,6 +366,10 @@ files:
|
|
|
364
366
|
- test/unit/tailoring_file_test.rb
|
|
365
367
|
- webpack/components/EmptyState.js
|
|
366
368
|
- webpack/components/HostExtentions/HostKebabItems.js
|
|
369
|
+
- webpack/components/HostsIndex/BulkActions/changeOpenscapProxy/BulkChangeOpenscapProxyModal.js
|
|
370
|
+
- webpack/components/HostsIndex/BulkActions/changeOpenscapProxy/__tests__/BulkChangeOpenscapProxyModal.test.js
|
|
371
|
+
- webpack/components/HostsIndex/ChangeOpenscapProxyAction.js
|
|
372
|
+
- webpack/components/HostsIndex/__tests__/ChangeOpenscapProxyAction.test.js
|
|
367
373
|
- webpack/components/IndexLayout.scss
|
|
368
374
|
- webpack/components/LineChart/LineChart.fixtures.js
|
|
369
375
|
- webpack/components/LineChart/LineChart.scss
|
|
@@ -386,7 +392,6 @@ files:
|
|
|
386
392
|
- webpack/components/OpenscapRemediationWizard/steps/index.js
|
|
387
393
|
- webpack/components/RuleSeverity/RuleSeverity.scss
|
|
388
394
|
- webpack/components/RuleSeverity/RuleSeverity.test.js
|
|
389
|
-
- webpack/components/RuleSeverity/__snapshots__/RuleSeverity.test.js.snap
|
|
390
395
|
- webpack/components/RuleSeverity/i_severity-critical.svg
|
|
391
396
|
- webpack/components/RuleSeverity/i_severity-high.svg
|
|
392
397
|
- webpack/components/RuleSeverity/i_severity-low.svg
|
|
@@ -437,6 +442,7 @@ test_files:
|
|
|
437
442
|
- test/files/tailoring_files/ssg-firefox-ds-tailoring-2.xml
|
|
438
443
|
- test/files/tailoring_files/ssg-firefox-ds-tailoring.xml
|
|
439
444
|
- test/functional/api/v2/compliance/arf_reports_controller_test.rb
|
|
445
|
+
- test/functional/api/v2/compliance/hosts_bulk_actions_controller_test.rb
|
|
440
446
|
- test/functional/api/v2/compliance/policies_controller_test.rb
|
|
441
447
|
- test/functional/api/v2/compliance/scap_content_profiles_controller_test.rb
|
|
442
448
|
- test/functional/api/v2/compliance/scap_contents_controller_test.rb
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
-
|
|
3
|
-
exports[`RuleSeverity should render for Critical severity 1`] = `
|
|
4
|
-
<img
|
|
5
|
-
alt="Unknown Serverity"
|
|
6
|
-
className="severity-img"
|
|
7
|
-
src={[Function]}
|
|
8
|
-
/>
|
|
9
|
-
`;
|
|
10
|
-
|
|
11
|
-
exports[`RuleSeverity should render for High severity 1`] = `
|
|
12
|
-
<img
|
|
13
|
-
alt="Unknown Serverity"
|
|
14
|
-
className="severity-img"
|
|
15
|
-
src={[Function]}
|
|
16
|
-
/>
|
|
17
|
-
`;
|
|
18
|
-
|
|
19
|
-
exports[`RuleSeverity should render for Low severity 1`] = `
|
|
20
|
-
<img
|
|
21
|
-
alt="Unknown Serverity"
|
|
22
|
-
className="severity-img"
|
|
23
|
-
src={[Function]}
|
|
24
|
-
/>
|
|
25
|
-
`;
|
|
26
|
-
|
|
27
|
-
exports[`RuleSeverity should render for Medium severity 1`] = `
|
|
28
|
-
<img
|
|
29
|
-
alt="Unknown Serverity"
|
|
30
|
-
className="severity-img"
|
|
31
|
-
src={[Function]}
|
|
32
|
-
/>
|
|
33
|
-
`;
|
|
34
|
-
|
|
35
|
-
exports[`RuleSeverity should render for foo severity 1`] = `
|
|
36
|
-
<img
|
|
37
|
-
alt="Unknown Serverity"
|
|
38
|
-
className="severity-img"
|
|
39
|
-
src={[Function]}
|
|
40
|
-
/>
|
|
41
|
-
`;
|