foreman_openscap 13.1.2 → 13.2.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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/foreman_openscap/locale/de/foreman_openscap.js +5 -5
  3. data/app/assets/javascripts/foreman_openscap/locale/en/foreman_openscap.js +1 -1
  4. data/app/assets/javascripts/foreman_openscap/locale/en_GB/foreman_openscap.js +1 -1
  5. data/app/assets/javascripts/foreman_openscap/locale/es/foreman_openscap.js +1 -1
  6. data/app/assets/javascripts/foreman_openscap/locale/fr/foreman_openscap.js +26 -24
  7. data/app/assets/javascripts/foreman_openscap/locale/gl/foreman_openscap.js +1 -1
  8. data/app/assets/javascripts/foreman_openscap/locale/it/foreman_openscap.js +1 -1
  9. data/app/assets/javascripts/foreman_openscap/locale/ja/foreman_openscap.js +26 -28
  10. data/app/assets/javascripts/foreman_openscap/locale/ka/foreman_openscap.js +5 -5
  11. data/app/assets/javascripts/foreman_openscap/locale/ko/foreman_openscap.js +26 -28
  12. data/app/assets/javascripts/foreman_openscap/locale/pt_BR/foreman_openscap.js +1 -1
  13. data/app/assets/javascripts/foreman_openscap/locale/ru/foreman_openscap.js +1 -1
  14. data/app/assets/javascripts/foreman_openscap/locale/sv_SE/foreman_openscap.js +1 -1
  15. data/app/assets/javascripts/foreman_openscap/locale/zh_CN/foreman_openscap.js +22 -24
  16. data/app/assets/javascripts/foreman_openscap/locale/zh_TW/foreman_openscap.js +1 -1
  17. data/app/controllers/api/v2/compliance/hosts_bulk_actions_controller.rb +38 -1
  18. data/app/controllers/policies_controller.rb +1 -1
  19. data/app/models/concerns/foreman_openscap/host_extensions.rb +10 -4
  20. data/app/models/concerns/foreman_openscap/hostgroup_extensions.rb +7 -0
  21. data/app/models/foreman_openscap/asset.rb +21 -1
  22. data/app/models/foreman_openscap/compliance_status.rb +19 -7
  23. data/app/models/foreman_openscap/policy.rb +43 -3
  24. data/app/services/foreman_openscap/compliance_status_resetter.rb +26 -0
  25. data/config/routes.rb +2 -0
  26. data/lib/foreman_openscap/engine.rb +3 -1
  27. data/lib/foreman_openscap/version.rb +1 -1
  28. data/locale/de/LC_MESSAGES/foreman_openscap.mo +0 -0
  29. data/locale/de/foreman_openscap.po +5 -5
  30. data/locale/en/LC_MESSAGES/foreman_openscap.mo +0 -0
  31. data/locale/en/foreman_openscap.po +1 -1
  32. data/locale/en_GB/LC_MESSAGES/foreman_openscap.mo +0 -0
  33. data/locale/en_GB/foreman_openscap.po +1 -1
  34. data/locale/es/LC_MESSAGES/foreman_openscap.mo +0 -0
  35. data/locale/es/foreman_openscap.po +1 -1
  36. data/locale/foreman_openscap.pot +2 -2
  37. data/locale/fr/LC_MESSAGES/foreman_openscap.mo +0 -0
  38. data/locale/fr/foreman_openscap.po +26 -24
  39. data/locale/gl/LC_MESSAGES/foreman_openscap.mo +0 -0
  40. data/locale/gl/foreman_openscap.po +1 -1
  41. data/locale/it/LC_MESSAGES/foreman_openscap.mo +0 -0
  42. data/locale/it/foreman_openscap.po +1 -1
  43. data/locale/ja/LC_MESSAGES/foreman_openscap.mo +0 -0
  44. data/locale/ja/foreman_openscap.po +26 -28
  45. data/locale/ka/LC_MESSAGES/foreman_openscap.mo +0 -0
  46. data/locale/ka/foreman_openscap.po +5 -5
  47. data/locale/ko/LC_MESSAGES/foreman_openscap.mo +0 -0
  48. data/locale/ko/foreman_openscap.po +26 -28
  49. data/locale/pt_BR/LC_MESSAGES/foreman_openscap.mo +0 -0
  50. data/locale/pt_BR/foreman_openscap.po +1 -1
  51. data/locale/ru/LC_MESSAGES/foreman_openscap.mo +0 -0
  52. data/locale/ru/foreman_openscap.po +1 -1
  53. data/locale/sv_SE/LC_MESSAGES/foreman_openscap.mo +0 -0
  54. data/locale/sv_SE/foreman_openscap.po +1 -1
  55. data/locale/zh_CN/LC_MESSAGES/foreman_openscap.mo +0 -0
  56. data/locale/zh_CN/foreman_openscap.po +22 -24
  57. data/locale/zh_TW/LC_MESSAGES/foreman_openscap.mo +0 -0
  58. data/locale/zh_TW/foreman_openscap.po +1 -1
  59. data/test/functional/api/v2/compliance/hosts_bulk_actions_controller_test.rb +175 -1
  60. data/test/test_plugin_helper.rb +7 -0
  61. data/test/unit/compliance_status_resetter_test.rb +48 -0
  62. data/test/unit/compliance_status_test.rb +53 -4
  63. data/test/unit/concerns/host_extensions_test.rb +55 -0
  64. data/test/unit/concerns/hostgroup_extensions_test.rb +64 -0
  65. data/test/unit/policy_test.rb +90 -0
  66. data/webpack/components/BulkActions/compliancePolicy/BulkCompliancePolicyMenuItems.js +49 -0
  67. data/webpack/components/BulkActions/compliancePolicy/BulkCompliancePolicyModal.js +304 -0
  68. data/webpack/components/BulkActions/compliancePolicy/BulkCompliancePolicyModalScene.js +65 -0
  69. data/webpack/components/BulkActions/compliancePolicy/__tests__/actions.test.js +121 -0
  70. data/webpack/components/BulkActions/compliancePolicy/actions.js +32 -0
  71. data/webpack/components/BulkActions/compliancePolicy/constants.js +8 -0
  72. data/webpack/global_index.js +27 -1
  73. metadata +10 -1
@@ -74,12 +74,48 @@ class ComplianceStatusTest < ActiveSupport::TestCase
74
74
  status = ForemanOpenscap::ComplianceStatus.new
75
75
  host = FactoryBot.create(:compliance_host, :policies => [@policy_a])
76
76
 
77
+ host.expects(:combined_policies).never
77
78
  status.host = host
78
79
 
79
80
  assert status.relevant?
80
81
  end
81
82
 
82
- test 'relevant? should be true for hostgroup policies and to_status should use them' do
83
+ test 'relevant? should be false for host without policies' do
84
+ status = ForemanOpenscap::ComplianceStatus.new
85
+ host = FactoryBot.create(:host)
86
+
87
+ status.host = host
88
+
89
+ refute status.relevant?
90
+ end
91
+
92
+ test 'relevant? should be false for host with hostgroup but no policies' do
93
+ status = ForemanOpenscap::ComplianceStatus.new
94
+ host = FactoryBot.create(:host, :with_hostgroup)
95
+
96
+ status.host = host
97
+
98
+ refute status.relevant?
99
+ end
100
+
101
+ test 'relevant? should be true for hostgroup policies without loading combined_policies' do
102
+ status = ForemanOpenscap::ComplianceStatus.new
103
+ host = FactoryBot.create(:host, :with_hostgroup)
104
+
105
+ assert_empty host.policies
106
+ @policy_a.hostgroup_ids = [host.hostgroup.id]
107
+ assert @policy_a.save
108
+
109
+ host.reload
110
+ assert_includes host.combined_policies, @policy_a
111
+
112
+ status.host = host
113
+ host.expects(:combined_policies).never
114
+
115
+ assert status.relevant?
116
+ end
117
+
118
+ test 'to_status should use hostgroup policies' do
83
119
  status = ForemanOpenscap::ComplianceStatus.new
84
120
  host = FactoryBot.create(:host, :with_hostgroup)
85
121
  othered_status = { :passed => 0, :failed => 0, :othered => 1 }.with_indifferent_access
@@ -95,12 +131,10 @@ class ComplianceStatusTest < ActiveSupport::TestCase
95
131
  FactoryBot.create(:policy_arf_report, :policy_id => @policy_a.id, :arf_report_id => report.id)
96
132
 
97
133
  status.host = host
98
-
99
- assert status.relevant?
100
134
  assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, status.to_status
101
135
  end
102
136
 
103
- test 'relevant? should be true for inherited parent hostgroup policies' do
137
+ test 'relevant? should be true for inherited parent hostgroup policies without loading combined_policies' do
104
138
  status = ForemanOpenscap::ComplianceStatus.new
105
139
  host = FactoryBot.create(:host, :with_hostgroup)
106
140
  hostgroup = host.hostgroup
@@ -116,7 +150,22 @@ class ComplianceStatusTest < ActiveSupport::TestCase
116
150
  assert_includes host.combined_policies, @policy_a
117
151
 
118
152
  status.host = host
153
+ host.expects(:combined_policies).never
119
154
 
120
155
  assert status.relevant?
121
156
  end
157
+
158
+ test 'to_label returns label based on stored status' do
159
+ status = ForemanOpenscap::ComplianceStatus.new
160
+ status.status = ForemanOpenscap::ComplianceStatus::COMPLIANT
161
+ status.expects(:to_status).never
162
+ assert_equal 'Compliant', status.to_label
163
+ end
164
+
165
+ test 'to_global returns global status based on stored status' do
166
+ status = ForemanOpenscap::ComplianceStatus.new
167
+ status.status = ForemanOpenscap::ComplianceStatus::COMPLIANT
168
+ status.expects(:to_status).never
169
+ assert_equal ::HostStatus::Global::OK, status.to_global
170
+ end
122
171
  end
@@ -162,6 +162,61 @@ class HostExtensionsTest < ActiveSupport::TestCase
162
162
  assert_empty ForemanOpenscap::Asset.where(:assetable_id => host.id, :assetable_type => 'Host::Base')
163
163
  end
164
164
 
165
+ test 'should reset compliance status when assigning policy through host policies' do
166
+ host = FactoryBot.create(:compliance_host)
167
+ set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
168
+
169
+ host.policies = [@policy]
170
+
171
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, host.reload.compliance_status
172
+ end
173
+
174
+ test 'should reset compliance status when removing policy through host policies' do
175
+ host = FactoryBot.create(:compliance_host, :policies => [@policy])
176
+ set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
177
+
178
+ host.policies = []
179
+
180
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, host.reload.compliance_status
181
+ end
182
+
183
+ test 'should reset compliance status when host is moved to a different hostgroup' do
184
+ old_hostgroup = FactoryBot.create(:hostgroup)
185
+ new_hostgroup = FactoryBot.create(:hostgroup)
186
+ host = FactoryBot.create(:compliance_host, :hostgroup_id => old_hostgroup.id)
187
+ set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
188
+
189
+ host.update!(:hostgroup_id => new_hostgroup.id)
190
+
191
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, host.reload.compliance_status
192
+ end
193
+
194
+ test 'should not reset compliance status when host is saved without hostgroup change' do
195
+ hostgroup = FactoryBot.create(:hostgroup)
196
+ host = FactoryBot.create(:compliance_host, :hostgroup_id => hostgroup.id)
197
+ set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
198
+
199
+ host.update!(:comment => 'unrelated change')
200
+
201
+ assert_equal ForemanOpenscap::ComplianceStatus::COMPLIANT, host.reload.compliance_status
202
+ end
203
+
204
+ test 'compliance_status returns stored persisted status without recalculation' do
205
+ set_compliance_status(@host, ForemanOpenscap::ComplianceStatus::INCONCLUSIVE)
206
+ status = @host.reload.get_status(ForemanOpenscap::ComplianceStatus)
207
+ status.expects(:to_status).never
208
+
209
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, @host.compliance_status
210
+ end
211
+
212
+ test 'compliance_status_label returns label from stored persisted status without recalculation' do
213
+ set_compliance_status(@host, ForemanOpenscap::ComplianceStatus::INCOMPLIANT)
214
+ status = @host.reload.get_status(ForemanOpenscap::ComplianceStatus)
215
+ status.expects(:to_status).never
216
+
217
+ assert_equal 'Incompliant', @host.compliance_status_label
218
+ end
219
+
165
220
  private
166
221
 
167
222
  def setup_hosts_with_policy
@@ -7,4 +7,68 @@ class HostgroupExtensionsTest < ActiveSupport::TestCase
7
7
  asset_scope = ::ForemanOpenscap::Asset.where(:assetable_id => hostgroup.id, :assetable_type => 'Hostgroup')
8
8
  assert_difference("asset_scope.count", -3) { hostgroup.destroy }
9
9
  end
10
+
11
+ test "should reset compliance status for subtree when assigning policy through hostgroup asset policies" do
12
+ parent = FactoryBot.create(:hostgroup)
13
+ child = FactoryBot.create(:hostgroup, :ancestry => parent.id.to_s)
14
+ parent_host = FactoryBot.create(:compliance_host, :hostgroup_id => parent.id)
15
+ child_host = FactoryBot.create(:compliance_host, :hostgroup_id => child.id)
16
+ policy = FactoryBot.create(:policy)
17
+ asset = FactoryBot.create(:asset, :assetable_id => parent.id, :assetable_type => 'Hostgroup')
18
+
19
+ set_compliance_status(parent_host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
20
+ set_compliance_status(child_host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
21
+
22
+ asset.policies = [policy]
23
+
24
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, parent_host.reload.compliance_status
25
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, child_host.reload.compliance_status
26
+ end
27
+
28
+ test "should reset compliance status for subtree when removing policy through hostgroup asset policies" do
29
+ parent = FactoryBot.create(:hostgroup)
30
+ child = FactoryBot.create(:hostgroup, :ancestry => parent.id.to_s)
31
+ parent_host = FactoryBot.create(:compliance_host, :hostgroup_id => parent.id)
32
+ child_host = FactoryBot.create(:compliance_host, :hostgroup_id => child.id)
33
+ policy = FactoryBot.create(:policy)
34
+ asset = FactoryBot.create(:asset, :assetable_id => parent.id, :assetable_type => 'Hostgroup')
35
+ asset.policies = [policy]
36
+
37
+ set_compliance_status(parent_host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
38
+ set_compliance_status(child_host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
39
+
40
+ asset.policies = []
41
+
42
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, parent_host.reload.compliance_status
43
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, child_host.reload.compliance_status
44
+ end
45
+
46
+ test "should reset compliance status for subtree when hostgroup is reparented" do
47
+ old_parent = FactoryBot.create(:hostgroup)
48
+ new_parent = FactoryBot.create(:hostgroup)
49
+ moved = FactoryBot.create(:hostgroup, :ancestry => old_parent.id.to_s)
50
+ child = FactoryBot.create(:hostgroup, :ancestry => "#{old_parent.id}/#{moved.id}")
51
+ moved_host = FactoryBot.create(:compliance_host, :hostgroup_id => moved.id)
52
+ child_host = FactoryBot.create(:compliance_host, :hostgroup_id => child.id)
53
+ unrelated_host = FactoryBot.create(:compliance_host, :hostgroup_id => old_parent.id)
54
+ [moved_host, child_host, unrelated_host].each do |host|
55
+ set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
56
+ end
57
+
58
+ moved.update!(:parent_id => new_parent.id)
59
+
60
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, moved_host.reload.compliance_status
61
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, child_host.reload.compliance_status
62
+ assert_equal ForemanOpenscap::ComplianceStatus::COMPLIANT, unrelated_host.reload.compliance_status
63
+ end
64
+
65
+ test "should not reset compliance status for hostgroup save without ancestry change" do
66
+ hostgroup = FactoryBot.create(:hostgroup)
67
+ host = FactoryBot.create(:compliance_host, :hostgroup_id => hostgroup.id)
68
+ set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
69
+
70
+ hostgroup.update!(:name => 'renamed')
71
+
72
+ assert_equal ForemanOpenscap::ComplianceStatus::COMPLIANT, host.reload.compliance_status
73
+ end
10
74
  end
@@ -46,6 +46,90 @@ class PolicyTest < ActiveSupport::TestCase
46
46
  assert_equal 1, policy.hosts.count
47
47
  end
48
48
 
49
+ test "should reset compliance status when assigning policy to host" do
50
+ host = FactoryBot.create(:compliance_host)
51
+ unrelated_host = FactoryBot.create(:compliance_host)
52
+ set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
53
+ set_compliance_status(unrelated_host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
54
+ policy = FactoryBot.create(:policy, :scap_content => @scap_content, :scap_content_profile => @scap_profile)
55
+
56
+ policy.host_ids = [host.id]
57
+ policy.save!
58
+
59
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, compliance_status_for(host)
60
+ assert_equal ForemanOpenscap::ComplianceStatus::COMPLIANT, compliance_status_for(unrelated_host)
61
+ end
62
+
63
+ test "should create inconclusive compliance status when assigning policy to host without status" do
64
+ host = FactoryBot.create(:compliance_host)
65
+ policy = FactoryBot.create(:policy, :scap_content => @scap_content, :scap_content_profile => @scap_profile)
66
+
67
+ policy.host_ids = [host.id]
68
+ policy.save!
69
+
70
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, compliance_status_for(host)
71
+ end
72
+
73
+ test "should reset compliance status when removing policy from host assignment" do
74
+ host = FactoryBot.create(:compliance_host)
75
+ asset = FactoryBot.create(:asset, :assetable_id => host.id, :assetable_type => 'Host::Base')
76
+ policy = FactoryBot.create(:policy, :assets => [asset], :scap_content => @scap_content, :scap_content_profile => @scap_profile)
77
+ set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
78
+
79
+ policy.host_ids = []
80
+ policy.save!
81
+
82
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, compliance_status_for(host)
83
+ end
84
+
85
+ test "should reset compliance status when unassigning host from policy" do
86
+ host = FactoryBot.create(:compliance_host)
87
+ asset = FactoryBot.create(:asset, :assetable_id => host.id, :assetable_type => 'Host::Base')
88
+ policy = FactoryBot.create(:policy, :assets => [asset], :scap_content => @scap_content, :scap_content_profile => @scap_profile)
89
+ set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
90
+
91
+ policy.unassign_hosts([host])
92
+
93
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, compliance_status_for(host)
94
+ end
95
+
96
+ test "should reset compliance status for hostgroup subtree when assigning policy to hostgroup" do
97
+ parent = FactoryBot.create(:hostgroup)
98
+ child = FactoryBot.create(:hostgroup, :ancestry => parent.id.to_s)
99
+ parent_host = FactoryBot.create(:compliance_host, :hostgroup_id => parent.id)
100
+ child_host = FactoryBot.create(:compliance_host, :hostgroup_id => child.id)
101
+ unrelated_host = FactoryBot.create(:compliance_host)
102
+ [parent_host, child_host, unrelated_host].each do |host|
103
+ set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
104
+ end
105
+ policy = FactoryBot.create(:policy, :scap_content => @scap_content, :scap_content_profile => @scap_profile)
106
+
107
+ policy.hostgroup_ids = [parent.id]
108
+ policy.save!
109
+
110
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, compliance_status_for(parent_host)
111
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, compliance_status_for(child_host)
112
+ assert_equal ForemanOpenscap::ComplianceStatus::COMPLIANT, compliance_status_for(unrelated_host)
113
+ end
114
+
115
+ test "should reset compliance status for hostgroup subtree when removing policy from hostgroup" do
116
+ parent = FactoryBot.create(:hostgroup)
117
+ child = FactoryBot.create(:hostgroup, :ancestry => parent.id.to_s)
118
+ parent_host = FactoryBot.create(:compliance_host, :hostgroup_id => parent.id)
119
+ child_host = FactoryBot.create(:compliance_host, :hostgroup_id => child.id)
120
+ asset = FactoryBot.create(:asset, :assetable_id => parent.id, :assetable_type => 'Hostgroup')
121
+ policy = FactoryBot.create(:policy, :assets => [asset], :scap_content => @scap_content, :scap_content_profile => @scap_profile)
122
+ [parent_host, child_host].each do |host|
123
+ set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
124
+ end
125
+
126
+ policy.hostgroup_ids = []
127
+ policy.save!
128
+
129
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, compliance_status_for(parent_host)
130
+ assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, compliance_status_for(child_host)
131
+ end
132
+
49
133
  test "should delete assets when unassigning hosts" do
50
134
  host1 = FactoryBot.create(:compliance_host)
51
135
  host2 = FactoryBot.create(:compliance_host)
@@ -331,4 +415,10 @@ class PolicyTest < ActiveSupport::TestCase
331
415
  assert_equal policy.deploy_by, 'manual'
332
416
  assert_equal policy.name, name
333
417
  end
418
+
419
+ private
420
+
421
+ def compliance_status_for(host)
422
+ ForemanOpenscap::ComplianceStatus.find_by(:host_id => host.id).try(:status)
423
+ end
334
424
  end
@@ -0,0 +1,49 @@
1
+ import React, { useContext } from 'react';
2
+ import { MenuItem } from '@patternfly/react-core';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { openBulkModal } from 'foremanReact/common/BulkModalStateHelper';
5
+ import { usePermissions } from 'foremanReact/common/hooks/Permissions/permissionHooks';
6
+ import { EDIT_HOSTS } from 'foremanReact/permissions';
7
+ import { ForemanHostsIndexActionsBarContext } from 'foremanReact/components/HostsIndex';
8
+ import {
9
+ ASSIGN_POLICY_MODAL_ID,
10
+ UNASSIGN_POLICY_MODAL_ID,
11
+ ASSIGN_POLICIES,
12
+ } from './constants';
13
+
14
+ const BulkCompliancePolicyMenuItems = () => {
15
+ const { selectedCount } = useContext(ForemanHostsIndexActionsBarContext);
16
+
17
+ const hasPermission = usePermissions([ASSIGN_POLICIES, EDIT_HOSTS]);
18
+
19
+ const handleAssignClick = () => openBulkModal(ASSIGN_POLICY_MODAL_ID, true);
20
+ const handleUnassignClick = () =>
21
+ openBulkModal(UNASSIGN_POLICY_MODAL_ID, true);
22
+
23
+ if (!hasPermission) {
24
+ return null;
25
+ }
26
+
27
+ return (
28
+ <>
29
+ <MenuItem
30
+ itemId="assign-compliance-policy-dropdown-item"
31
+ key="assign-compliance-policy-dropdown-item"
32
+ onClick={handleAssignClick}
33
+ isDisabled={selectedCount === 0}
34
+ >
35
+ {__('Assign compliance policy')}
36
+ </MenuItem>
37
+ <MenuItem
38
+ itemId="unassign-compliance-policy-dropdown-item"
39
+ key="unassign-compliance-policy-dropdown-item"
40
+ onClick={handleUnassignClick}
41
+ isDisabled={selectedCount === 0}
42
+ >
43
+ {__('Unassign compliance policy')}
44
+ </MenuItem>
45
+ </>
46
+ );
47
+ };
48
+
49
+ export default BulkCompliancePolicyMenuItems;
@@ -0,0 +1,304 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormattedMessage } from 'react-intl';
4
+ import { useDispatch, useSelector } from 'react-redux';
5
+ import {
6
+ Modal,
7
+ Button,
8
+ TextContent,
9
+ Text,
10
+ Select,
11
+ SelectOption,
12
+ SelectList,
13
+ MenuToggle,
14
+ Spinner,
15
+ EmptyState,
16
+ EmptyStateHeader,
17
+ EmptyStateIcon,
18
+ EmptyStateBody,
19
+ Flex,
20
+ FlexItem,
21
+ } from '@patternfly/react-core';
22
+ import { ExclamationCircleIcon, SearchIcon } from '@patternfly/react-icons';
23
+ import { STATUS } from 'foremanReact/constants';
24
+ import { addToast } from 'foremanReact/components/ToastsList/slice';
25
+ import { translate as __ } from 'foremanReact/common/I18n';
26
+ import {
27
+ selectAPIStatus,
28
+ selectAPIResponse,
29
+ } from 'foremanReact/redux/API/APISelectors';
30
+ import {
31
+ buildBulkRequestBody,
32
+ bulkErrorToastParams,
33
+ } from 'foremanReact/components/HostsIndex/BulkActions/helpers';
34
+ import { fetchPolicies, bulkAssignPolicy, bulkUnassignPolicy } from './actions';
35
+ import {
36
+ POLICIES_KEY,
37
+ BULK_ASSIGN_POLICY_KEY,
38
+ BULK_UNASSIGN_POLICY_KEY,
39
+ } from './constants';
40
+
41
+ const BulkCompliancePolicyModal = ({
42
+ isOpen,
43
+ closeModal,
44
+ mode,
45
+ selectAllHostsMode,
46
+ selectedCount,
47
+ fetchBulkParams,
48
+ organizationId,
49
+ locationId,
50
+ onSuccess: onSuccessCallback,
51
+ }) => {
52
+ const dispatch = useDispatch();
53
+ const [policyId, setPolicyId] = useState('');
54
+ const [policySelectOpen, setPolicySelectOpen] = useState(false);
55
+
56
+ useEffect(() => {
57
+ if (isOpen) {
58
+ dispatch(fetchPolicies());
59
+ }
60
+ }, [dispatch, isOpen]);
61
+
62
+ const isAssignMode = mode === 'assign';
63
+ const actionKey = isAssignMode
64
+ ? BULK_ASSIGN_POLICY_KEY
65
+ : BULK_UNASSIGN_POLICY_KEY;
66
+
67
+ const policies = useSelector(state => selectAPIResponse(state, POLICIES_KEY));
68
+ const policiesStatus = useSelector(state =>
69
+ selectAPIStatus(state, POLICIES_KEY)
70
+ );
71
+ const policiesError = useSelector(
72
+ state => state.API?.[POLICIES_KEY]?.error?.message
73
+ );
74
+ const hostUpdateStatus = useSelector(state =>
75
+ selectAPIStatus(state, actionKey)
76
+ );
77
+
78
+ const onToggleClick = () => {
79
+ setPolicySelectOpen(!policySelectOpen);
80
+ };
81
+
82
+ const handlePolicySelect = (event, selection) => {
83
+ setPolicyId(selection);
84
+ setPolicySelectOpen(false);
85
+ };
86
+
87
+ const getPolicyLabel = id =>
88
+ policies?.results?.find(p => p.id.toString() === id)?.name ||
89
+ __('Select a policy');
90
+
91
+ const toggle = toggleRef => (
92
+ <MenuToggle
93
+ ref={toggleRef}
94
+ ouiaId="bulk-compliance-policy-toggle"
95
+ onClick={onToggleClick}
96
+ isExpanded={policySelectOpen}
97
+ style={{ width: '100%' }}
98
+ >
99
+ {policyId ? getPolicyLabel(policyId) : __('Select a policy')}
100
+ </MenuToggle>
101
+ );
102
+
103
+ const handleModalClose = () => {
104
+ setPolicyId('');
105
+ setPolicySelectOpen(false);
106
+ closeModal();
107
+ };
108
+
109
+ const handleError = error => {
110
+ handleModalClose();
111
+ dispatch(addToast(bulkErrorToastParams(error, actionKey)));
112
+ };
113
+
114
+ const handleSuccess = response => {
115
+ dispatch(
116
+ addToast({
117
+ type: 'success',
118
+ message: response.data.message,
119
+ })
120
+ );
121
+ if (onSuccessCallback) onSuccessCallback();
122
+ handleModalClose();
123
+ };
124
+
125
+ const handleConfirm = () => {
126
+ const requestBody = buildBulkRequestBody({
127
+ fetchBulkParams,
128
+ organizationId,
129
+ locationId,
130
+ policy_id: policyId,
131
+ });
132
+
133
+ if (mode === 'assign') {
134
+ dispatch(bulkAssignPolicy(requestBody, handleSuccess, handleError));
135
+ } else {
136
+ dispatch(bulkUnassignPolicy(requestBody, handleSuccess, handleError));
137
+ }
138
+ };
139
+
140
+ const title = isAssignMode
141
+ ? __('Assign Compliance Policy')
142
+ : __('Unassign Compliance Policy');
143
+ const confirmButtonText = isAssignMode
144
+ ? __('Assign policy')
145
+ : __('Unassign policy');
146
+
147
+ const modalActions = [
148
+ <Button
149
+ key="confirm"
150
+ ouiaId="bulk-compliance-policy-modal-confirm-button"
151
+ variant="primary"
152
+ onClick={handleConfirm}
153
+ isDisabled={hostUpdateStatus === STATUS.PENDING || policyId === ''}
154
+ isLoading={hostUpdateStatus === STATUS.PENDING}
155
+ >
156
+ {confirmButtonText}
157
+ </Button>,
158
+ <Button
159
+ key="cancel"
160
+ ouiaId="bulk-compliance-policy-modal-cancel-button"
161
+ variant="link"
162
+ onClick={handleModalClose}
163
+ >
164
+ {__('Cancel')}
165
+ </Button>,
166
+ ];
167
+
168
+ const warningMessage = isAssignMode
169
+ ? __(
170
+ 'The selected policy will be assigned to the selected hosts. Some hosts may already be assigned to this policy.'
171
+ )
172
+ : __(
173
+ 'The selected policy will be unassigned from the selected hosts. Some hosts may not be assigned to this policy.'
174
+ );
175
+
176
+ return (
177
+ <Modal
178
+ isOpen={isOpen}
179
+ onClose={handleModalClose}
180
+ onEscapePress={handleModalClose}
181
+ title={title}
182
+ variant="medium"
183
+ position="top"
184
+ actions={modalActions}
185
+ id="bulk-compliance-policy-modal"
186
+ key="bulk-compliance-policy-modal"
187
+ ouiaId="bulk-compliance-policy-modal"
188
+ >
189
+ <TextContent>
190
+ <Text ouiaId="bulk-compliance-policy-count">
191
+ {selectAllHostsMode ? (
192
+ <FormattedMessage
193
+ id="bulk-compliance-policy-all-hosts"
194
+ defaultMessage="{action} will affect {boldCount} selected hosts."
195
+ values={{
196
+ action: isAssignMode ? __('Assigning') : __('Unassigning'),
197
+ boldCount: <strong>{__('All')}</strong>,
198
+ }}
199
+ />
200
+ ) : (
201
+ <FormattedMessage
202
+ id="bulk-compliance-policy-selected-hosts"
203
+ defaultMessage="{action} will affect {boldCount} selected {count, plural, one {host} other {hosts}}."
204
+ values={{
205
+ action: isAssignMode ? __('Assigning') : __('Unassigning'),
206
+ count: selectedCount,
207
+ boldCount: <strong>{selectedCount}</strong>,
208
+ }}
209
+ />
210
+ )}
211
+ </Text>
212
+ <Text ouiaId="bulk-compliance-policy-warning">{warningMessage}</Text>
213
+ </TextContent>
214
+
215
+ {policiesStatus === STATUS.PENDING && (
216
+ <Flex
217
+ direction={{ default: 'column' }}
218
+ alignItems={{ default: 'alignItemsCenter' }}
219
+ spaceItems={{ default: 'spaceItemsSm' }}
220
+ >
221
+ <FlexItem>
222
+ <Spinner size="lg" aria-label={__('Loading policies')} />
223
+ </FlexItem>
224
+ <FlexItem>
225
+ <Text ouiaId="loading-policies-text">
226
+ {__('Loading policies...')}
227
+ </Text>
228
+ </FlexItem>
229
+ </Flex>
230
+ )}
231
+
232
+ {policiesStatus === STATUS.ERROR && (
233
+ <EmptyState>
234
+ <EmptyStateHeader
235
+ titleText={__('Failed to load policies')}
236
+ icon={<EmptyStateIcon icon={ExclamationCircleIcon} />}
237
+ headingLevel="h4"
238
+ />
239
+ <EmptyStateBody>
240
+ {policiesError || __('An error occurred while fetching policies.')}
241
+ </EmptyStateBody>
242
+ </EmptyState>
243
+ )}
244
+
245
+ {policiesStatus === STATUS.RESOLVED && policies?.results?.length === 0 && (
246
+ <EmptyState>
247
+ <EmptyStateHeader
248
+ titleText={__('No policies available')}
249
+ icon={<EmptyStateIcon icon={SearchIcon} />}
250
+ headingLevel="h4"
251
+ />
252
+ <EmptyStateBody>
253
+ {__(
254
+ 'There are no compliance policies available in your current organization and location.'
255
+ )}
256
+ </EmptyStateBody>
257
+ </EmptyState>
258
+ )}
259
+
260
+ {policiesStatus === STATUS.RESOLVED && policies?.results?.length > 0 && (
261
+ <Select
262
+ id="compliance-policy-select"
263
+ isOpen={policySelectOpen}
264
+ selected={policyId}
265
+ onSelect={handlePolicySelect}
266
+ onOpenChange={isSelectOpen => setPolicySelectOpen(isSelectOpen)}
267
+ toggle={toggle}
268
+ shouldFocusToggleOnSelect
269
+ ouiaId="bulk-compliance-policy-select"
270
+ >
271
+ <SelectList>
272
+ {policies.results.map(policy => (
273
+ <SelectOption key={policy.id} value={policy.id.toString()}>
274
+ {policy.name}
275
+ </SelectOption>
276
+ ))}
277
+ </SelectList>
278
+ </Select>
279
+ )}
280
+ </Modal>
281
+ );
282
+ };
283
+
284
+ BulkCompliancePolicyModal.propTypes = {
285
+ isOpen: PropTypes.bool,
286
+ closeModal: PropTypes.func,
287
+ mode: PropTypes.oneOf(['assign', 'unassign']).isRequired,
288
+ fetchBulkParams: PropTypes.func.isRequired,
289
+ selectedCount: PropTypes.number.isRequired,
290
+ selectAllHostsMode: PropTypes.bool.isRequired,
291
+ organizationId: PropTypes.number,
292
+ locationId: PropTypes.number,
293
+ onSuccess: PropTypes.func,
294
+ };
295
+
296
+ BulkCompliancePolicyModal.defaultProps = {
297
+ isOpen: false,
298
+ closeModal: () => {},
299
+ organizationId: undefined,
300
+ locationId: undefined,
301
+ onSuccess: undefined,
302
+ };
303
+
304
+ export default BulkCompliancePolicyModal;