foreman_rh_cloud 12.2.1 → 12.2.2

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/rh_cloud/advisor_engine_config_controller.rb +1 -1
  3. data/app/controllers/api/v2/rh_cloud/cloud_request_controller.rb +3 -0
  4. data/app/controllers/api/v2/rh_cloud/inventory_controller.rb +3 -0
  5. data/app/controllers/concerns/foreman_rh_cloud/iop_smart_proxy_access.rb +28 -0
  6. data/app/controllers/concerns/insights_cloud/package_profile_upload_extensions.rb +1 -1
  7. data/app/controllers/foreman_inventory_upload/uploads_controller.rb +3 -0
  8. data/app/controllers/foreman_rh_cloud/foreman_rh_cloud_controller.rb +22 -0
  9. data/app/controllers/insights_cloud/api/machine_telemetries_controller.rb +19 -5
  10. data/app/services/foreman_rh_cloud/cert_auth.rb +1 -1
  11. data/app/services/foreman_rh_cloud/cloud_request.rb +1 -1
  12. data/app/services/foreman_rh_cloud/cloud_request_forwarder.rb +1 -1
  13. data/app/services/foreman_rh_cloud/hit_remediations_retriever.rb +27 -10
  14. data/app/views/api/v2/hosts/insights/base.rabl +2 -2
  15. data/app/views/api/v2/hosts/insights/single.rabl +1 -1
  16. data/config/routes.rb +6 -14
  17. data/lib/foreman_inventory_upload/async/generate_all_reports_job.rb +2 -2
  18. data/lib/foreman_inventory_upload/async/upload_report_job.rb +1 -1
  19. data/lib/foreman_inventory_upload/generators/slice.rb +24 -0
  20. data/lib/foreman_rh_cloud/engine.rb +6 -2
  21. data/lib/foreman_rh_cloud/plugin.rb +14 -2
  22. data/lib/foreman_rh_cloud/version.rb +1 -1
  23. data/lib/foreman_rh_cloud.rb +5 -6
  24. data/lib/insights_cloud/async/insights_scheduled_sync.rb +2 -2
  25. data/lib/inventory_sync/async/inventory_hosts_sync.rb +1 -1
  26. data/lib/inventory_sync/async/inventory_scheduled_sync.rb +2 -2
  27. data/lib/tasks/insights.rake +1 -1
  28. data/lib/tasks/rh_cloud_inventory.rake +20 -2
  29. data/package.json +1 -1
  30. data/test/controllers/insights_cloud/api/machine_telemetries_controller_test.rb +20 -3
  31. data/test/controllers/insights_sync/settings_controller_test.rb +1 -1
  32. data/test/factories/inventory_upload_factories.rb +4 -112
  33. data/test/jobs/inventory_scheduled_sync_test.rb +3 -3
  34. data/test/test_plugin_helper.rb +8 -2
  35. data/test/unit/rh_cloud_http_proxy_test.rb +3 -3
  36. data/test/unit/services/foreman_rh_cloud/cloud_request_forwarder_test.rb +4 -1
  37. data/test/unit/slice_generator_test.rb +33 -0
  38. data/test/unit/tags_generator_test.rb +4 -1
  39. data/webpack/CVEsHostDetailsTab/CVEsHostDetailsTab.js +1 -1
  40. data/webpack/CveDetailsPage/CveDetailsPage.js +1 -1
  41. data/webpack/CveDetailsPage/CveDetailsPage.test.js +1 -3
  42. data/webpack/ForemanRhCloudPages.js +1 -0
  43. data/webpack/InsightsCloudSync/Components/RemediationModal/RemediationHelpers.js +26 -4
  44. data/webpack/InsightsCloudSync/Components/RemediationModal/RemediationModal.js +85 -11
  45. data/webpack/InsightsCloudSync/Components/RemediationModal/RemediationModalFooter.js +39 -5
  46. data/webpack/InsightsCloudSync/Components/RemediationModal/Resolutions.js +13 -0
  47. data/webpack/InsightsCloudSync/InsightsCloudSync.js +9 -7
  48. data/webpack/InsightsHostDetailsTab/NewHostDetailsTab.js +12 -10
  49. data/webpack/InsightsVulnerability/InsightsVulnerabilityListPage.js +1 -1
  50. data/webpack/IopRecommendationDetails/IopRecommendationDetails.js +1 -1
  51. data/webpack/common/Hooks/ConfigHooks.js +1 -2
  52. data/webpack/common/styles.scss +7 -0
  53. metadata +4 -1
@@ -1,87 +1,3 @@
1
- # redefine katello factories, as long as katello is not compatible with dynamic properties
2
- FactoryBot.define do
3
- factory :katello_organization, :class => "Organization" do
4
- type { "Organization" }
5
- sequence(:name) { |n| "Organization#{n}" }
6
- sequence(:label) { |n| "org#{n}" }
7
- sequence(:id) { |n| n }
8
-
9
- trait :acme_corporation do
10
- name { "ACME_Corporation" }
11
- type { "Organization" }
12
- description { "This is the first Organization." }
13
- label { "acme_corporation_label" }
14
- end
15
-
16
- trait :with_library do
17
- association :library, :factory => :katello_library
18
- end
19
-
20
- factory :acme_corporation, :traits => [:acme_corporation]
21
- end
22
- end
23
-
24
- FactoryBot.define do
25
- factory :katello_content_view, :class => Katello::ContentView do
26
- sequence(:name) { |n| "Database#{n}" }
27
- description { "This content view is for database content" }
28
- association :organization, :factory => :katello_organization
29
-
30
- trait :composite do
31
- composite { true }
32
- end
33
- end
34
- end
35
-
36
- FactoryBot.define do
37
- factory :katello_content_view_environment, :class => Katello::ContentViewEnvironment do
38
- sequence(:name) { |n| "name#{n}" }
39
- sequence(:label) { |n| "label#{n}" }
40
- end
41
- end
42
-
43
- FactoryBot.define do
44
- factory :katello_k_t_environment, :aliases => [:katello_environment], :class => Katello::KTEnvironment do
45
- sequence(:name) { |n| "Environment#{n}" }
46
- sequence(:label) { |n| "environment#{n}" }
47
- association :organization, :factory => :katello_organization
48
- end
49
- end
50
-
51
- FactoryBot.define do
52
- factory :katello_content_facets, :aliases => [:content_facet], :class => ::Katello::Host::ContentFacet do
53
- sequence(:uuid) { |n| "uuid-#{n}" }
54
- end
55
- end
56
-
57
- FactoryBot.define do
58
- factory :katello_subscription_facets, :aliases => [:subscription_facet], :class => ::Katello::Host::SubscriptionFacet do
59
- sequence(:uuid) { |n| "00000000-%<n>04d-%<r>04d-0000-000000000000" % { n: n, r: rand(500) } }
60
- facts { { 'memory.memtotal' => "12 GB" } }
61
- end
62
- end
63
-
64
- FactoryBot.define do
65
- factory :katello_subscription, :class => Katello::Subscription do
66
- end
67
- end
68
-
69
- FactoryBot.define do
70
- factory :katello_pool, :class => Katello::Pool do
71
- active { true }
72
- end_date { Date.today + 1.year }
73
- cp_id { 1 }
74
-
75
- association :organization, :factory => :katello_organization
76
-
77
- after(:build) do |pool, _evaluator|
78
- pool.subscription.organization = pool.organization
79
- end
80
-
81
- association :subscription, :factory => :katello_subscription
82
- end
83
- end
84
-
85
1
  FactoryBot.define do
86
2
  factory :katello_host_collection_host, :class => Katello::HostCollectionHosts do
87
3
  host_id { nil }
@@ -94,34 +10,10 @@ FactoryBot.define do
94
10
  end
95
11
  end
96
12
 
13
+ # Fix Katello factories to return a valid UUID
97
14
  FactoryBot.modify do
98
- factory :host do
99
- transient do
100
- content_view { nil }
101
- lifecycle_environment { nil }
102
- content_source { nil }
103
- content_view_environments { [] }
104
- end
105
-
106
- trait :with_content do
107
- association :content_facet, :factory => :content_facet, :strategy => :build
108
-
109
- after(:build) do |host, evaluator|
110
- if host.content_facet
111
- if evaluator.content_view && evaluator.lifecycle_environment
112
- host.content_facet.assign_single_environment(
113
- content_view_id: evaluator.content_view.id,
114
- lifecycle_environment_id: evaluator.lifecycle_environment.id
115
- )
116
- end
117
- host.content_facet.content_source = evaluator.content_source if evaluator.content_source
118
- host.content_facet.content_view_environments = evaluator.content_view_environments unless evaluator.content_view_environments.empty?
119
- end
120
- end
121
- end
122
-
123
- trait :with_subscription do
124
- association :subscription_facet, :factory => :subscription_facet, :strategy => :build
125
- end
15
+ factory :katello_subscription_facets, :aliases => [:subscription_facet], :class => ::Katello::Host::SubscriptionFacet do
16
+ sequence(:uuid) { |n| "00000000-%<n>04d-%<r>04d-0000-000000000000" % { n: n, r: rand(500) } }
17
+ facts { { 'memory.memtotal' => "12 GB" } }
126
18
  end
127
19
  end
@@ -14,14 +14,14 @@ class InventoryScheduledSyncTest < ActiveSupport::TestCase
14
14
  ForemanTasks.sync_task(InventorySync::Async::InventoryScheduledSync)
15
15
  end
16
16
 
17
- test 'Skips execution if with_local_advisor_engine? is true' do
18
- ForemanRhCloud.stubs(:with_local_advisor_engine?).returns(true)
17
+ test 'Skips execution if with_iop_smart_proxy? is true' do
18
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
19
19
 
20
20
  InventorySync::Async::InventoryScheduledSync.any_instance.expects(:plan_org_sync).never
21
21
 
22
22
  task = ForemanTasks.sync_task(InventorySync::Async::InventoryScheduledSync)
23
23
  status = task.output[:status].to_s
24
- assert_match(/Foreman is configured with the use_local_advisor_engine option/, status)
24
+ assert_match(/Foreman is configured with a local IoP Smart Proxy/, status)
25
25
  end
26
26
 
27
27
  test 'Skips execution if auto upload is disabled' do
@@ -2,10 +2,16 @@
2
2
  require 'test_helper'
3
3
 
4
4
  # Add plugin to FactoryBot's paths
5
- FactoryBot.definition_file_paths << "#{ForemanTasks::Engine.root}/test/factories"
5
+ katello_files = Dir.glob("#{Katello::Engine.root}/test/factories/**/*.rb")
6
+ FactoryBot.definition_file_paths +=
7
+ katello_files
8
+ # skip foreman_task factories, since we already have those definitions in ForemanTasks
9
+ .reject { |f| f =~ /foreman_task|recurring_logic/ }
10
+ .map { |f| f.gsub('.rb', '') } # .rb extension is will be appended by factorybot
6
11
  FactoryBot.definition_file_paths << "#{ForemanRemoteExecution::Engine.root}/test/factories"
12
+ FactoryBot.definition_file_paths << "#{ForemanTasks::Engine.root}/test/factories"
13
+
7
14
  FactoryBot.definition_file_paths << File.join(File.dirname(__FILE__), 'factories')
8
- # FactoryBot.definition_file_paths << "#{Katello::Engine.root}/test/factories"
9
15
  FactoryBot.reload
10
16
 
11
17
  begin
@@ -4,7 +4,7 @@ class RhCloudHttpProxyTest < ActiveSupport::TestCase
4
4
  setup do
5
5
  @global_content_proxy_mock = 'http://global:content@localhost:80'
6
6
  @global_foreman_proxy_mock = 'http://global:foreman@localhost:80'
7
- ForemanRhCloud.stubs(:with_local_advisor_engine?).returns(false)
7
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
8
8
  end
9
9
 
10
10
  test 'selects global content proxy' do
@@ -20,8 +20,8 @@ class RhCloudHttpProxyTest < ActiveSupport::TestCase
20
20
  end
21
21
 
22
22
  test 'returns empty string in on-prem setup' do
23
- ForemanRhCloud.unstub(:with_local_advisor_engine?)
24
- ForemanRhCloud.stubs(:with_local_advisor_engine?).returns(true)
23
+ ForemanRhCloud.unstub(:with_iop_smart_proxy?)
24
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
25
25
 
26
26
  assert_empty ForemanRhCloud.proxy_setting
27
27
  end
@@ -22,7 +22,10 @@ class CloudRequestForwarderTest < ActiveSupport::TestCase
22
22
  :with_content,
23
23
  :with_hostgroup,
24
24
  :with_parameter,
25
- content_view_environments: [make_cve(lifecycle_environment: env), make_cve(lifecycle_environment: env2)],
25
+ content_facet: FactoryBot.build(
26
+ :content_facet,
27
+ content_view_environments: [make_cve(lifecycle_environment: env), make_cve(lifecycle_environment: env2)]
28
+ ),
26
29
  organization: env.organization
27
30
  )
28
31
 
@@ -998,6 +998,39 @@ class SliceGeneratorTest < ActiveSupport::TestCase
998
998
  assert_not_nil actual_host['insights_id']
999
999
  end
1000
1000
 
1001
+ test 'reports yum repos' do
1002
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
1003
+ FactoryBot.create(:katello_content, cp_content_id: '1', organization: @host.organization, name: 'Test Content', label: 'test-content')
1004
+ repo = FactoryBot.build(
1005
+ :katello_repository,
1006
+ :fedora_17_x86_64_dev,
1007
+ :with_content_view,
1008
+ product: FactoryBot.create(:katello_product, :redhat, :with_provider, organization: @host.organization)
1009
+ )
1010
+ # force-save the root repo to avoid validation errors
1011
+ repo.root.save(validate: false)
1012
+ repo.save(validate: false)
1013
+ @host.content_facet.bound_repositories << repo
1014
+ @host.content_facet.content_source = FactoryBot.create(:smart_proxy, :with_pulp3)
1015
+ @host.save(validate: false)
1016
+
1017
+ batch = Host.where(id: @host.id).in_batches.first
1018
+ generator = create_generator(batch)
1019
+ json_str = generator.render
1020
+ actual = JSON.parse(json_str.join("\n"))
1021
+
1022
+ assert_not_nil(actual_host = actual['hosts'].first)
1023
+ assert_not_nil(actual_system_profile = actual_host['system_profile'])
1024
+ assert_not_nil(actual_yum_repos = actual_system_profile['yum_repos'])
1025
+ assert_equal 1, actual_yum_repos.count
1026
+ assert_not_nil(actual_yum_repo = actual_yum_repos.first)
1027
+ assert_equal 'Test Content', actual_yum_repo['name']
1028
+ assert_equal 'test-content', actual_yum_repo['id']
1029
+ assert_match(/fedora_17/, actual_yum_repo['base_url'])
1030
+ assert_equal true, actual_yum_repo['enabled']
1031
+ assert_equal true, actual_yum_repo['gpgcheck']
1032
+ end
1033
+
1001
1034
  private
1002
1035
 
1003
1036
  def create_generator(batch, name = '00000000-0000-0000-0000-000000000000')
@@ -26,7 +26,10 @@ class TagsGeneratorTest < ActiveSupport::TestCase
26
26
  organization: env.organization,
27
27
  location: @location2,
28
28
  hostgroup: @hostgroup2,
29
- content_view_environments: [make_cve(lifecycle_environment: env), make_cve(lifecycle_environment: env2)]
29
+ content_facet: FactoryBot.build(
30
+ :content_facet,
31
+ content_view_environments: [make_cve(lifecycle_environment: env), make_cve(lifecycle_environment: env2)]
32
+ )
30
33
  )
31
34
 
32
35
  @host.organization.pools << FactoryBot.create(:katello_pool, account_number: '1234', cp_id: 1)
@@ -8,7 +8,7 @@ const CVEsHostDetailsTab = ({ systemId }) => {
8
8
  const scope = 'vulnerability';
9
9
  const module = './SystemDetailTable';
10
10
  return (
11
- <div className="rh-cloud-insights-vulnerability-host-details-component">
11
+ <div className="rh-cloud-insights-vulnerability-host-details-component vulnerability">
12
12
  <ScalprumComponent scope={scope} module={module} systemId={systemId} />
13
13
  </div>
14
14
  );
@@ -10,7 +10,7 @@ const CveDetailsPage = () => {
10
10
 
11
11
  return (
12
12
  <ScalprumProvider {...providerOptions}>
13
- <div className="rh-cloud-cve-details-page">
13
+ <div className="rh-cloud-cve-details-page vulnerability">
14
14
  <ScalprumComponent scope={scope} module={module} cveId={cveId} />
15
15
  </div>
16
16
  </ScalprumProvider>
@@ -18,9 +18,7 @@ jest.mock('@scalprum/react-core', () => ({
18
18
  describe('CveDetailsPage component', () => {
19
19
  it('renders the container with correct class', () => {
20
20
  const { container } = render(<CveDetailsPage />);
21
- expect(
22
- container.querySelector('.rh-cloud-cve-details-page')
23
- ).toBeTruthy();
21
+ expect(container.querySelector('.rh-cloud-cve-details-page')).toBeTruthy();
24
22
  });
25
23
 
26
24
  it('passes cveId from URL params to ScalprumComponent', () => {
@@ -8,6 +8,7 @@ import InsightsCloudSync from './InsightsCloudSync';
8
8
  import IopRecommendationDetails from './IopRecommendationDetails/IopRecommendationDetails';
9
9
  import InsightsHostDetailsTab from './InsightsHostDetailsTab';
10
10
  import CveDetailsPage from './CveDetailsPage';
11
+ import './common/styles.scss';
11
12
 
12
13
  const pages = [
13
14
  { name: 'ForemanInventoryUpload', type: ForemanInventoryUpload },
@@ -3,7 +3,15 @@ import React from 'react';
3
3
  import { orderBy } from 'lodash';
4
4
  import Resolutions from './Resolutions';
5
5
 
6
- export const modifyRows = (remediations, setResolutions, setHostsIds) => {
6
+ export const getResolutionId = (selectedResolution, id) =>
7
+ `${id}_${selectedResolution}`;
8
+
9
+ export const modifyRows = (
10
+ remediations,
11
+ setResolutions,
12
+ setHostsIds,
13
+ isIop
14
+ ) => {
7
15
  if (remediations.length === 0) return [];
8
16
 
9
17
  const resolutionToSubmit = [];
@@ -15,9 +23,22 @@ export const modifyRows = (remediations, setResolutions, setHostsIds) => {
15
23
  ).map(({ id, host_id, hostname, title, resolutions, reboot }) => {
16
24
  hostsIdsToSubmit.add(host_id);
17
25
  const selectedResolution = resolutions[0]?.id;
26
+ /* eslint-disable spellcheck/spell-checker */
27
+
28
+ // For IoP: {
29
+ // hit_id: "c7c6727e-2966-4f7c-87f1-20ef14db7a2d",
30
+ // rule_id: "hardening_ssh_client_alive|OPENSSH_HARDENING_CLIENT_ALIVE",
31
+ // resolution_type: "less_secure",
32
+ // resolution_id:"hardening_ssh_client_alive|OPENSSH_HARDENING_CLIENT_ALIVE_less_secure",
33
+ // }
34
+ // for Hosted, hit_id and rule_id will be Foreman database IDs
35
+
36
+ /* eslint-enable spellcheck/spell-checker */
18
37
  resolutionToSubmit.push({
19
- hit_id: id,
20
- resolution_id: selectedResolution /** defaults to the first resolution if many */,
38
+ hit_id: isIop ? host_id : id,
39
+ rule_id: id,
40
+ resolution_type: selectedResolution /** defaults to the first resolution if many */,
41
+ resolution_id: getResolutionId(selectedResolution, id),
21
42
  });
22
43
  return {
23
44
  cells: [
@@ -25,10 +46,11 @@ export const modifyRows = (remediations, setResolutions, setHostsIds) => {
25
46
  title,
26
47
  <div>
27
48
  <Resolutions
28
- hit_id={id}
49
+ hit_id={isIop ? host_id : id}
29
50
  resolutions={resolutions}
30
51
  setResolutions={setResolutions}
31
52
  selectedResolution={selectedResolution}
53
+ isIop={isIop}
32
54
  />
33
55
  </div>,
34
56
  reboot,
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable react-hooks/exhaustive-deps */
2
2
  import React, { useEffect } from 'react';
3
+ import Immutable from 'seamless-immutable';
3
4
  import PropTypes from 'prop-types';
4
5
  import {
5
6
  Table,
@@ -7,7 +8,7 @@ import {
7
8
  TableBody,
8
9
  } from '@patternfly/react-table/deprecated';
9
10
  import { Modal, ModalVariant, Button } from '@patternfly/react-core';
10
- import { isEmpty } from 'lodash';
11
+ import { isEmpty, noop } from 'lodash';
11
12
  import { STATUS } from 'foremanReact/constants';
12
13
  import { translate as __ } from 'foremanReact/common/I18n';
13
14
  import { columns } from './RemediationTableConstants';
@@ -15,8 +16,48 @@ import { modifyRows } from './RemediationHelpers';
15
16
  import ModalFooter from './RemediationModalFooter';
16
17
  import TableEmptyState from '../../../common/table/EmptyState';
17
18
  import './RemediationModal.scss';
19
+ import { useAdvisorEngineConfig } from '../../../common/Hooks/ConfigHooks';
20
+
21
+ /* eslint-disable spellcheck/spell-checker */
22
+
23
+ // Sample iopData:
24
+ // const iopTestData = Immutable([
25
+ // {
26
+ // hostid: 'c7c6727e-2966-4f7c-87f1-20ef14db7a2d',
27
+ // host_name: 'advisor-test.local',
28
+ // rulename: 'hardening_cryptopol_krb5|NO_CPOL_KRB5',
29
+ // resolutions: [
30
+ // {
31
+ // description: 'Remove manual crypto-policies',
32
+ // id: 'fix',
33
+ // needs_reboot: true,
34
+ // resolution_risk: 1,
35
+ // },
36
+ // ],
37
+ // rebootable: true,
38
+ // description: 'Decreased security: krb5 crypto-policies overridden',
39
+ // },
40
+ // {
41
+ // hostid: 'c7c6727e-2966-4f7c-87f1-20ef14db7a2d',
42
+ // host_name: 'advisor-test.local',
43
+ // rulename: 'hardening_logging_auditd|HARDENING_LOGGING_5_AUDITD',
44
+ // resolutions: [
45
+ // {
46
+ // description: 'Install and enable auditd',
47
+ // id: 'fix',
48
+ // needs_reboot: false,
49
+ // resolution_risk: 1,
50
+ // },
51
+ // ],
52
+ // rebootable: false,
53
+ // description: 'Decreased security: auditd not running',
54
+ // },
55
+ // ]);
56
+
57
+ /* eslint-enable spellcheck/spell-checker */
18
58
 
19
59
  const RemediationModal = ({
60
+ iopData,
20
61
  selectedIds,
21
62
  fetchRemediations,
22
63
  remediations,
@@ -24,31 +65,49 @@ const RemediationModal = ({
24
65
  error,
25
66
  isAllSelected,
26
67
  query,
68
+ isDisabled,
27
69
  }) => {
28
- const [rows, setRows] = React.useState([]);
70
+ const iopRows = Immutable(iopData ?? []).map(recommendation => ({
71
+ id: recommendation.rulename,
72
+ host_id: recommendation.hostid,
73
+ hostname: recommendation.host_name,
74
+ title: recommendation.description,
75
+ resolutions: recommendation.resolutions ?? [],
76
+ reboot: recommendation.rebootable,
77
+ }));
78
+
29
79
  const [open, setOpen] = React.useState(false);
30
80
  const [resolutions, setResolutions] = React.useState([]);
31
81
  const [hostsIds, setHostsIds] = React.useState([]);
82
+ const [rows, setRows] = React.useState([]);
32
83
  const toggleModal = () => setOpen(prevValue => !prevValue);
33
84
 
85
+ const isIop = useAdvisorEngineConfig();
34
86
  useEffect(() => {
35
- if (open) fetchRemediations({ selectedIds, isAllSelected, query });
87
+ // only fetch for Hosted. IoP provides via props.
88
+ if (!isIop && open)
89
+ fetchRemediations({ selectedIds, isAllSelected, query });
36
90
  }, [open]);
37
91
 
38
92
  useEffect(() => {
39
- const modifiedRows =
40
- status === STATUS.PENDING
41
- ? []
42
- : modifyRows(remediations, setResolutions, setHostsIds);
93
+ let modifiedRows;
94
+ if (isIop) {
95
+ modifiedRows = modifyRows(iopRows, setResolutions, setHostsIds, true);
96
+ } else {
97
+ modifiedRows =
98
+ status === STATUS.PENDING
99
+ ? []
100
+ : modifyRows(remediations, setResolutions, setHostsIds, false);
101
+ }
43
102
  setRows(modifiedRows);
44
- }, [remediations, status]);
103
+ }, [remediations, status, iopData, isIop]);
45
104
 
46
105
  return (
47
106
  <React.Fragment>
48
107
  <Button
49
108
  ouiaId="button-remediate"
50
109
  variant="primary"
51
- isDisabled={isEmpty(selectedIds)}
110
+ isDisabled={isDisabled || isEmpty(selectedIds)}
52
111
  onClick={() => {
53
112
  toggleModal();
54
113
  }}
@@ -68,6 +127,7 @@ const RemediationModal = ({
68
127
  toggleModal={toggleModal}
69
128
  resolutions={resolutions}
70
129
  hostsIds={hostsIds}
130
+ isIop={isIop}
71
131
  />
72
132
  }
73
133
  >
@@ -92,22 +152,36 @@ const RemediationModal = ({
92
152
  };
93
153
 
94
154
  RemediationModal.propTypes = {
95
- selectedIds: PropTypes.object,
96
- fetchRemediations: PropTypes.func.isRequired,
155
+ iopData: PropTypes.arrayOf(
156
+ PropTypes.shape({
157
+ hostid: PropTypes.string,
158
+ host_name: PropTypes.string,
159
+ rulename: PropTypes.string,
160
+ resolutions: PropTypes.array,
161
+ rebootable: PropTypes.bool,
162
+ description: PropTypes.string,
163
+ })
164
+ ),
165
+ selectedIds: PropTypes.shape({}),
166
+ fetchRemediations: PropTypes.func,
97
167
  remediations: PropTypes.array,
98
168
  status: PropTypes.string,
99
169
  error: PropTypes.string,
100
170
  isAllSelected: PropTypes.bool,
101
171
  query: PropTypes.string,
172
+ isDisabled: PropTypes.bool,
102
173
  };
103
174
 
104
175
  RemediationModal.defaultProps = {
176
+ iopData: null,
105
177
  selectedIds: {},
178
+ fetchRemediations: noop,
106
179
  remediations: [],
107
180
  status: null,
108
181
  error: null,
109
182
  isAllSelected: false,
110
183
  query: null,
184
+ isDisabled: false,
111
185
  };
112
186
 
113
187
  export default RemediationModal;
@@ -2,18 +2,44 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { Button } from '@patternfly/react-core';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { useBulkSelect } from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks';
5
6
  import { JOB_INVOCATION_PATH } from './RemediationTableConstants';
6
7
 
7
- const ModalFooter = ({ toggleModal, resolutions, hostsIds }) => {
8
+ const ModalFooter = ({ toggleModal, resolutions, hostsIds, isIop }) => {
8
9
  let token = document.querySelector('meta[name="csrf-token"]');
9
10
  token = token?.content || '';
11
+
12
+ const [jobInProgress, setJobInProgress] = React.useState(false);
13
+ const formRef = React.useRef(null);
14
+
15
+ const { fetchBulkParams } = useBulkSelect({
16
+ initialArry: hostsIds,
17
+ idColumn: 'insights_uuid',
18
+ });
19
+
20
+ const handleSubmit = e => {
21
+ e.preventDefault();
22
+ setJobInProgress(true);
23
+
24
+ setTimeout(() => {
25
+ // eslint-disable-next-line no-unused-expressions
26
+ formRef.current?.submit?.();
27
+ }, 100);
28
+ };
10
29
  return (
11
- <form action={JOB_INVOCATION_PATH} method="post">
30
+ <form
31
+ action={JOB_INVOCATION_PATH}
32
+ method="post"
33
+ ref={formRef}
34
+ onSubmit={handleSubmit}
35
+ >
12
36
  <Button
13
37
  type="submit"
14
38
  ouiaId="button-confirm"
15
39
  key="confirm"
16
40
  variant="primary"
41
+ isDisabled={jobInProgress}
42
+ isLoading={jobInProgress}
17
43
  >
18
44
  {__('Remediate')}
19
45
  </Button>
@@ -32,9 +58,15 @@ const ModalFooter = ({ toggleModal, resolutions, hostsIds }) => {
32
58
  name="inputs[hit_remediation_pairs]"
33
59
  value={JSON.stringify(resolutions)}
34
60
  />
35
- {hostsIds.map(id => (
36
- <input type="hidden" name="host_ids[]" key={id} value={id} />
37
- ))}
61
+ {!isIop &&
62
+ hostsIds.map(id => (
63
+ <input type="hidden" name="host_ids[]" key={id} value={id} />
64
+ ))}
65
+ {isIop && (
66
+ <>
67
+ <input type="hidden" name="search" value={fetchBulkParams()} />
68
+ </>
69
+ )}
38
70
  </form>
39
71
  );
40
72
  };
@@ -43,11 +75,13 @@ ModalFooter.propTypes = {
43
75
  toggleModal: PropTypes.func.isRequired,
44
76
  resolutions: PropTypes.array,
45
77
  hostsIds: PropTypes.array,
78
+ isIop: PropTypes.bool,
46
79
  };
47
80
 
48
81
  ModalFooter.defaultProps = {
49
82
  resolutions: [],
50
83
  hostsIds: [],
84
+ isIop: false,
51
85
  };
52
86
 
53
87
  export default ModalFooter;
@@ -2,12 +2,14 @@
2
2
  import React from 'react';
3
3
  import PropTypes from 'prop-types';
4
4
  import { Radio } from '@patternfly/react-core';
5
+ import { getResolutionId } from './RemediationHelpers';
5
6
 
6
7
  const Resolutions = ({
7
8
  resolutions,
8
9
  setResolutions,
9
10
  selectedResolution,
10
11
  hit_id,
12
+ isIop,
11
13
  }) => {
12
14
  const [checkedID, setCheckedID] = React.useState(selectedResolution);
13
15
 
@@ -27,6 +29,15 @@ const Resolutions = ({
27
29
  stateRes.map(res => {
28
30
  if (hit_id === res.hit_id) {
29
31
  setCheckedID(resolution_id);
32
+ if (isIop)
33
+ return {
34
+ ...res,
35
+ resolution_id: getResolutionId(
36
+ resolution_id,
37
+ res.rule_id
38
+ ),
39
+ resolution_type: resolution_id,
40
+ };
30
41
  return { ...res, resolution_id };
31
42
  }
32
43
  return res;
@@ -45,12 +56,14 @@ Resolutions.propTypes = {
45
56
  resolutions: PropTypes.array,
46
57
  hit_id: PropTypes.number,
47
58
  selectedResolution: PropTypes.number,
59
+ isIop: PropTypes.bool,
48
60
  };
49
61
 
50
62
  Resolutions.defaultProps = {
51
63
  resolutions: [],
52
64
  hit_id: null,
53
65
  selectedResolution: null,
66
+ isIop: false,
54
67
  };
55
68
 
56
69
  export default Resolutions;
@@ -67,13 +67,15 @@ export const generateRuleUrl = ruleId =>
67
67
  foremanUrl(`/foreman_rh_cloud/recommendations/${ruleId}`);
68
68
 
69
69
  const IopRecommendationsPage = props => (
70
- <ScalprumComponent
71
- scope={scope}
72
- module={module}
73
- IopRemediationModal={RemediationModal}
74
- generateRuleUrl={generateRuleUrl}
75
- {...props}
76
- />
70
+ <div className="advisor">
71
+ <ScalprumComponent
72
+ scope={scope}
73
+ module={module}
74
+ IopRemediationModal={RemediationModal}
75
+ generateRuleUrl={generateRuleUrl}
76
+ {...props}
77
+ />
78
+ </div>
77
79
  );
78
80
 
79
81
  const IopRecommendationsPageWrapped = props => (