foreman_rh_cloud 12.1.1 → 12.1.3

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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/app/views/api/v2/hosts/insights/single.rabl +5 -0
  3. data/config/package-lock.json.plugin +4137 -3036
  4. data/config/routes.rb +1 -0
  5. data/db/seeds.d/200_features.rb +4 -0
  6. data/lib/foreman_inventory_upload/generators/tags.rb +4 -1
  7. data/lib/foreman_rh_cloud/engine.rb +39 -4
  8. data/lib/foreman_rh_cloud/version.rb +1 -1
  9. data/lib/insights_vulnerability.rb +2 -0
  10. data/lib/tasks/hybrid_cloud.rake +105 -37
  11. data/package.json +1 -1
  12. data/test/controllers/insights_cloud/api/cloud_request_controller_test.rb +1 -2
  13. data/test/unit/rh_cloud_http_proxy_test.rb +8 -0
  14. data/test/unit/tags_generator_test.rb +1 -0
  15. data/webpack/CVEsHostDetailsTab/CVEsHostDetailsTab.js +17 -0
  16. data/webpack/CVEsHostDetailsTab/__tests__/CVEsHostDetailsTab.test.js +18 -0
  17. data/webpack/CVEsHostDetailsTab/index.js +3 -0
  18. data/webpack/ForemanInventoryUpload/Components/InventorySettings/MinimalInventoryDropdown.js +1 -1
  19. data/webpack/ForemanRhCloudFills.js +11 -1
  20. data/webpack/ForemanRhCloudHelpers.js +3 -0
  21. data/webpack/ForemanRhCloudPages.js +7 -0
  22. data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsLabel.js +40 -0
  23. data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsSection.js +30 -0
  24. data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsSection.scss +25 -0
  25. data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsTableConstants.js +4 -6
  26. data/webpack/InsightsVulnerability/InsightsVulnerability.js +13 -0
  27. data/webpack/InsightsVulnerability/InsightsVulnerability.test.js +18 -0
  28. data/webpack/__tests__/ForemanRhCloudHelpers.test.js +30 -1
  29. data/webpack/__tests__/__snapshots__/ForemanRhCloudHelpers.test.js.snap +10 -0
  30. metadata +12 -1
data/config/routes.rb CHANGED
@@ -37,6 +37,7 @@ Rails.application.routes.draw do
37
37
  get 'inventory_upload', to: '/react#index'
38
38
  end
39
39
  get 'insights_cloud', to: '/react#index' # Uses foreman's react controller
40
+ get 'insights_vulnerability', to: '/react#index'
40
41
  end
41
42
 
42
43
  scope :module => :'insights_cloud/api', :path => :redhat_access do
@@ -0,0 +1,4 @@
1
+ ForemanRhCloud.on_prem_smart_proxy_features.each do |feature_name|
2
+ f = Feature.where(:name => feature_name).first_or_create
3
+ raise "Unable to create proxy feature: #{SeedHelper.format_errors f}" if f.nil? || f.errors.any?
4
+ end
@@ -41,7 +41,10 @@ module ForemanInventoryUpload
41
41
  end
42
42
 
43
43
  def organizations
44
- [['organization', @host.organization.name]]
44
+ [
45
+ ['organization', @host.organization.name],
46
+ ['organization_label', @host.organization.label],
47
+ ]
45
48
  end
46
49
 
47
50
  def content_data
@@ -44,12 +44,12 @@ module ForemanRhCloud
44
44
  setting('allow_auto_inventory_upload', type: :boolean, description: N_('Enable automatic upload of your host inventory to the Red Hat cloud'), default: true, full_name: N_('Automatic inventory upload'))
45
45
  setting('allow_auto_insights_sync', type: :boolean, description: N_('Enable automatic synchronization of Insights recommendations from the Red Hat cloud'), default: true, full_name: N_('Synchronize recommendations Automatically'))
46
46
  setting('allow_auto_insights_mismatch_delete', type: :boolean, description: N_('Enable automatic deletion of mismatched host records from the Red Hat cloud'), default: false, full_name: N_('Automatic mismatch deletion'))
47
- setting('obfuscate_inventory_hostnames', type: :boolean, description: N_('Obfuscate host names sent to the Red Hat cloud. (If insights_minimal_data_collection is set to true, this setting is ignored and host names are always obfuscated.)'), default: false, full_name: N_('Obfuscate host names'))
48
- setting('obfuscate_inventory_ips', type: :boolean, description: N_('Obfuscate ipv4 addresses sent to the Red Hat cloud. (If insights_minimal_data_collection is set to true, this setting is ignored and host ipv4 addresses are always obfuscated.)'), default: false, full_name: N_('Obfuscate host ipv4 addresses.'))
47
+ setting('obfuscate_inventory_hostnames', type: :boolean, description: N_('Obfuscate host names sent to the Red Hat cloud. (If insights_minimal_data_collection is set to true, this setting is ignored because host names are not included in the report.)'), default: false, full_name: N_('Obfuscate host names'))
48
+ setting('obfuscate_inventory_ips', type: :boolean, description: N_('Obfuscate ipv4 addresses sent to the Red Hat cloud. (If insights_minimal_data_collection is set to true, this setting is ignored because host IPv4 addresses are not included in the report.)'), default: false, full_name: N_('Obfuscate host ipv4 addresses.'))
49
49
  setting('exclude_installed_packages', type: :boolean, description: N_('Exclude installed packages from being uploaded to the Red Hat cloud. (If insights_minimal_data_collection is set to true, this setting is ignored and installed packages are always excluded.)'), default: false, full_name: N_("Exclude installed packages"))
50
50
  setting('include_parameter_tags', type: :boolean, description: N_('Should import include parameter tags from Foreman?'), default: false, full_name: N_('Include parameters in insights-client reports'))
51
51
  setting('rhc_instance_id', type: :string, description: N_('RHC daemon id'), default: nil, full_name: N_('ID of the RHC(Yggdrasil) daemon'))
52
- setting('insights_minimal_data_collection', type: :boolean, default: false, full_name: N_('Minimal data collection'), description: N_('Only send the minimum required data to Red Hat cloud, and obfuscate wherever possible. When this is true, hostnames and IPv4 addresses are always obfuscated and installed packages are excluded, regardless of those settings.'))
52
+ setting('insights_minimal_data_collection', type: :boolean, default: false, full_name: N_('Minimal data collection'), description: N_('Only include the minimum required data in inventory reports for uploading to Red Hat cloud. When this is true, installed packages are excluded from the report regardless of the exclude_installed_packages setting, and host names and IPv4 addresses are excluded from the report regardless of obfuscation settings.'))
53
53
  end
54
54
  end
55
55
 
@@ -117,11 +117,18 @@ module ForemanRhCloud
117
117
  parent: :insights_menu,
118
118
  if: -> { !ForemanRhCloud.with_local_advisor_engine? }
119
119
  menu :top_menu, :insights_hits, caption: N_('Recommendations'), url: '/foreman_rh_cloud/insights_cloud', url_hash: { controller: :react, action: :index }, parent: :insights_menu
120
+ menu :top_menu,
121
+ :insights_vulnerability,
122
+ caption: N_('Vulnerability'),
123
+ url: '/foreman_rh_cloud/insights_vulnerability',
124
+ url_hash: { controller: :react, action: :index },
125
+ parent: :insights_menu,
126
+ if: -> { ForemanRhCloud.with_local_advisor_engine? }
120
127
  end
121
128
 
122
129
  register_facet InsightsFacet, :insights do
123
130
  configure_host do
124
- api_view :list => 'api/v2/hosts/insights/insights'
131
+ api_view :list => 'api/v2/hosts/insights/insights', :single => 'api/v2/hosts/insights/single'
125
132
  set_dependent_action :destroy
126
133
  end
127
134
  end
@@ -203,6 +210,30 @@ module ForemanRhCloud
203
210
  )
204
211
  end
205
212
 
213
+ # Ideally this code belongs to an initializer. The problem is that Katello controllers are not initialized completely until after the end of the to_prepare blocks
214
+ # This means I can patch the controller only in the after_initialize block that is promised to run after the to_prepare
215
+ # initializer 'foreman_rh_cloud.allow_smart_proxy_actions', :before => :finisher_hook, :after => 'katello.register_plugin' do |_app|
216
+ # end
217
+ config.after_initialize do
218
+ # skip overrides in migrations, since the controller initialization depends on tables existense
219
+ if defined?(Katello) && !Foreman.in_setup_db_rake?
220
+ Katello::Api::V2::OrganizationsController.include Foreman::Controller::SmartProxyAuth
221
+ # patch the callbacks order for :download_debug_certificate, since local_find_taxonomy has to run after the user is already initialized
222
+ Katello::Api::V2::OrganizationsController.skip_before_action(:local_find_taxonomy, only: :download_debug_certificate)
223
+ Katello::Api::V2::OrganizationsController.add_smart_proxy_filters(
224
+ [:index, :download_debug_certificate],
225
+ features: ForemanRhCloud.on_prem_smart_proxy_features
226
+ )
227
+ Katello::Api::V2::OrganizationsController.before_action(:local_find_taxonomy, only: :download_debug_certificate)
228
+
229
+ Katello::Api::V2::RepositoriesController.include Foreman::Controller::SmartProxyAuth
230
+ Katello::Api::V2::RepositoriesController.add_smart_proxy_filters(
231
+ :index,
232
+ features: ForemanRhCloud.on_prem_smart_proxy_features
233
+ )
234
+ end
235
+ end
236
+
206
237
  rake_tasks do
207
238
  Rake::Task['db:seed'].enhance do
208
239
  ForemanRhCloud::Engine.load_seed
@@ -224,4 +255,8 @@ module ForemanRhCloud
224
255
  ::SETTINGS[:ssl_ca_file]
225
256
  end
226
257
  end
258
+
259
+ def self.on_prem_smart_proxy_features
260
+ ['Insights']
261
+ end
227
262
  end
@@ -1,3 +1,3 @@
1
1
  module ForemanRhCloud
2
- VERSION = '12.1.1'.freeze
2
+ VERSION = '12.1.3'.freeze
3
3
  end
@@ -0,0 +1,2 @@
1
+ module InsightsVulnerability
2
+ end
@@ -1,64 +1,132 @@
1
1
  require 'io/console'
2
+ require 'uri'
2
3
 
3
- namespace :rh_cloud do |args|
4
- desc 'Register Satellite Organization with Hybrid Cloud API. \
5
- Specify org_id=x replace your organization ID with x. \
6
- Specify SATELLITE_RH_CLOUD_URL=https://x with the Hybrid Cloud endpoint you are connecting to.'
4
+ def logger
5
+ @logger ||= Logger.new(STDOUT)
6
+ end
7
+
8
+ namespace :rh_cloud do
9
+ desc 'Register Satellite Organization with Hybrid Cloud API.'
10
+ # This task registers the Satellite Organization with the Hybrid Cloud API.
11
+ # It requires the user to input their organization ID, Insights URL, and token.
12
+ # The task will then send a POST request to the Hybrid Cloud API to register the organization.
13
+ # The response will be logged, and any errors will be caught and logged as well.
14
+ # The task will exit with an error message if the organization does not have a manifest imported or if the token is not entered.
15
+ # The task will also log a warning if the custom URL is not set and the default one is used.
7
16
  task hybridcloud_register: [:environment] do
8
17
  include ::ForemanRhCloud::CertAuth
9
18
  include ::InsightsCloud::CandlepinCache
10
19
 
11
- def logger
12
- @logger ||= Logger.new(STDOUT)
20
+ def default_registrations_url
21
+ URI.join(ForemanRhCloud.base_url, '/api/identity/certificate/registrations').to_s
13
22
  end
14
23
 
15
- def registrations_url
16
- logger.warn("Custom url is not set, using the default one: #{ForemanRhCloud.base_url}") if ENV['SATELLITE_RH_CLOUD_URL'].empty?
17
- ForemanRhCloud.base_url + '/api/identity/certificate/registrations'
24
+ # Helper method to get the registrations URL, with a warning for default usage
25
+ def registrations_url(custom_url)
26
+ if custom_url.empty?
27
+ logger.warn("Custom url is not set, using the default one: #{default_registrations_url}")
28
+ default_registrations_url
29
+ else
30
+ if URI(custom_url).scheme.nil?
31
+ logger.warn("Custom URL lacks a scheme; prepending https:// prefix.")
32
+ custom_url = "https://" + custom_url
33
+ end
34
+ custom_url
35
+ end
18
36
  end
19
37
 
20
- if ENV['org_id'].nil?
21
- logger.error('ERROR: org_id needs to be specified.')
22
- exit(1)
38
+ def get_organization(user_org_id)
39
+ maybe_organization = Organization.find_by(id: user_org_id)
40
+ if maybe_organization.nil?
41
+ logger.error("Organization with ID '#{user_org_id}' not found.")
42
+ exit(1)
43
+ end
44
+ maybe_organization
23
45
  end
24
46
 
25
- @organization = Organization.find_by(id: ENV['org_id'].to_i) # saw this coming in as a string, so making sure it gets passed as an integer.
26
- @uid = cp_owner_id(@organization)
27
- @hostname = ForemanRhCloud.foreman_host_name
28
- logger.error('Organization provided does not have a manifest imported.') + exit(1) if @uid.nil?
29
-
30
- puts 'Paste your token, output will be hidden.'
31
- @token = STDIN.noecho(&:gets).chomp
32
- logger.error('Token was not entered.') + exit(1) if @token.empty?
33
-
34
- def headers
35
- {
36
- Authorization: "Bearer #{@token}",
37
- }
47
+ def get_uid(organization)
48
+ maybe_uid = cp_owner_id(organization)
49
+ if maybe_uid.nil?
50
+ logger.error("Organization '#{organization}' does not have a manifest imported.")
51
+ exit(1)
52
+ end
53
+ maybe_uid
38
54
  end
39
55
 
40
- def payload
41
- {
42
- "uid": @uid,
43
- "display_name": "#{@hostname}+#{@organization.label}",
44
- }
56
+ # --- Input Collection ---
57
+ puts "Paste in your organization ID, this can be retrieved with the command: hammer organization list"
58
+ loop do
59
+ input = STDIN.gets.chomp
60
+ if input.match?(/^\d+$/) # Checks if input consists only of digits
61
+ @user_org_id = input.to_i
62
+ break
63
+ else
64
+ puts "Invalid input. Please enter a numeric organization ID."
65
+ end
45
66
  end
46
67
 
47
- def method
48
- :post
68
+ puts "\n" + "-" * 50 + "\n\n"
69
+ puts "Paste in your custom Insights URL. If nothing is entered, the default will be used (#{default_registrations_url})."
70
+ insights_user_input = STDIN.gets.chomp
71
+
72
+ puts "\n" + "-" * 50 + "\n\n"
73
+ puts 'Paste in your Hybrid Cloud API token, output will be hidden.'
74
+ puts 'This token can be retrieved from the Hybrid Cloud console.'
75
+ token = STDIN.noecho(&:gets).chomp
76
+ if token.empty?
77
+ logger.error('Token was not entered.')
78
+ exit(1)
49
79
  end
50
80
 
81
+ # --- Data Preparation ---
82
+
83
+ organization = get_organization(@user_org_id)
84
+ uid = get_uid(organization)
85
+ hostname = ForemanRhCloud.foreman_host_name
86
+ insights_url = registrations_url(insights_user_input)
87
+
88
+ # --- API Request ---
89
+
90
+ headers = {
91
+ Authorization: "Bearer #{token}",
92
+ }
93
+
94
+ payload = {
95
+ 'uid': uid,
96
+ "display_name": "#{hostname}+#{organization.label}",
97
+ }
98
+
51
99
  begin
52
100
  response = execute_cloud_request(
53
- organization: @organization,
54
- method: method,
55
- url: registrations_url,
101
+ organization: organization,
102
+ method: :post,
103
+ url: insights_url,
56
104
  headers: headers,
57
105
  payload: payload.to_json
58
106
  )
59
- logger.debug(response)
107
+ logger.debug("Cloud request completed: status=#{response.code}, body_preview=#{response.body&.slice(0, 200)}")
108
+ rescue RestClient::Unauthorized => _ex
109
+ # Add a more specific rescue for 401 Unauthorized errors
110
+ logger.error('Registration failed: Your token is invalid or unauthorized. Please check your token and try again.')
111
+ # Optionally, you can still log the full debug info if helpful for advanced troubleshooting
112
+ # logger.debug(ex.backtrace.join("\n"))
113
+ exit(1)
114
+ rescue RestClient::ExceptionWithResponse => ex
115
+ # This catches any RestClient exception that has a response (like 400, 403, 404, 500, etc.)
116
+ status_code = begin
117
+ ex.response.code
118
+ rescue StandardError
119
+ "unknown"
120
+ end
121
+ logger.error("Registration failed with HTTP status #{status_code}: #{ex.message}")
122
+ logger.debug("Response body (if available): #{ex.response.body}")
123
+ exit(1)
60
124
  rescue StandardError => ex
61
- logger.error(ex)
125
+ # This is the catch-all for any other unexpected errors
126
+ logger.error("An unexpected error occurred during registration: #{ex.message}")
127
+ exit(1)
62
128
  end
129
+
130
+ logger.info("Satellite Organization '#{organization.label}' (ID: #{@user_org_id}) successfully registered.")
63
131
  end
64
132
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foreman_rh_cloud",
3
- "version": "12.1.1",
3
+ "version": "12.1.3",
4
4
  "description": "Inventory Upload =============",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -51,8 +51,7 @@ module InsightsCloud::Api
51
51
  mock_composer = mock('composer')
52
52
  ::JobInvocationComposer.expects(:for_feature).with do |feature, host_ids, params|
53
53
  feature == :rh_cloud_connector_run_playbook &&
54
- host_ids.first == host1.id &&
55
- host_ids.last == host2.id
54
+ host_ids.sort == [host1.id, host2.id].sort
56
55
  end.returns(mock_composer)
57
56
  mock_composer.expects(:trigger!)
58
57
  mock_composer.expects(:job_invocation)
@@ -4,6 +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
8
  end
8
9
 
9
10
  test 'selects global content proxy' do
@@ -18,6 +19,13 @@ class RhCloudHttpProxyTest < ActiveSupport::TestCase
18
19
  assert_equal @global_foreman_proxy_mock, ForemanRhCloud.proxy_setting
19
20
  end
20
21
 
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)
25
+
26
+ assert_empty ForemanRhCloud.proxy_setting
27
+ end
28
+
21
29
  def setup_global_content_proxy
22
30
  http_proxy = FactoryBot.create(:http_proxy, url: @global_content_proxy_mock)
23
31
  HttpProxy.stubs(:default_global_content_proxy).returns(http_proxy)
@@ -61,6 +61,7 @@ class TagsGeneratorTest < ActiveSupport::TestCase
61
61
  assert_equal @host.content_views.pluck(:name).max, actual['content_view'].map(&:second).max
62
62
  assert_equal Foreman.instance_id, actual['satellite_instance_id'].first.last
63
63
  assert_equal @host.organization_id.to_s, actual['organization_id'].first.last
64
+ assert_equal @host.organization.label, actual['organization_label'].first.last
64
65
  end
65
66
 
66
67
  test 'filters tags with empty values' do
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+
5
+ const CVEsHostDetailsTab = ({ hostName }) => (
6
+ <div>
7
+ <h1>
8
+ {__('CVEs tab for host:')} {hostName}
9
+ </h1>
10
+ </div>
11
+ );
12
+
13
+ CVEsHostDetailsTab.propTypes = {
14
+ hostName: PropTypes.string.isRequired,
15
+ };
16
+
17
+ export default CVEsHostDetailsTab;
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import CVEsHostDetailsTab from '../CVEsHostDetailsTab';
4
+
5
+ describe('CVEsHostDetailsTab', () => {
6
+ it('renders without crashing', () => {
7
+ render(<CVEsHostDetailsTab hostName="test-host.example.com" />);
8
+ expect(
9
+ screen.getByText('CVEs tab for host: test-host.example.com')
10
+ ).toBeTruthy();
11
+ });
12
+
13
+ it('renders the host name', () => {
14
+ const hostName = 'test-host.example.com';
15
+ render(<CVEsHostDetailsTab hostName={hostName} />);
16
+ expect(screen.getByText(`CVEs tab for host: ${hostName}`)).toBeTruthy();
17
+ });
18
+ });
@@ -0,0 +1,3 @@
1
+ import CVEsHostDetailsTab from './CVEsHostDetailsTab';
2
+
3
+ export default CVEsHostDetailsTab;
@@ -25,7 +25,7 @@ const MinimalInventoryDropdown = ({ setChosenValue }) => {
25
25
  ),
26
26
  },
27
27
  optional: {
28
- title: __('Optional data collection'),
28
+ title: __('Analytics data collection'),
29
29
  description: __(
30
30
  'Send additional data to enhance Insights services, as per the settings'
31
31
  ),
@@ -3,7 +3,8 @@ import { addGlobalFill } from 'foremanReact/components/common/Fill/GlobalFill';
3
3
  import InventoryAutoUploadSwitcher from './ForemanInventoryUpload/SubscriptionsPageExtension/InventoryAutoUpload';
4
4
  import NewHostDetailsTab from './InsightsHostDetailsTab/NewHostDetailsTab';
5
5
  import { InsightsTotalRiskChartWrapper } from './InsightsHostDetailsTab/InsightsTotalRiskChartWrapper';
6
- import { isNotRhelHost } from './ForemanRhCloudHelpers';
6
+ import { isNotRhelHost, vulnerabilityDisabled } from './ForemanRhCloudHelpers';
7
+ import CVEsHostDetailsTab from './CVEsHostDetailsTab/CVEsHostDetailsTab';
7
8
 
8
9
  const fills = [
9
10
  {
@@ -27,6 +28,15 @@ const fills = [
27
28
  component: props => <InsightsTotalRiskChartWrapper {...props} />,
28
29
  weight: 2800,
29
30
  },
31
+ {
32
+ slot: 'host-details-page-tabs',
33
+ name: 'CVEs',
34
+ component: props => <CVEsHostDetailsTab {...props} />,
35
+ weight: 300,
36
+ metadata: {
37
+ hideTab: vulnerabilityDisabled,
38
+ },
39
+ },
30
40
  ];
31
41
 
32
42
  export const registerFills = () => {
@@ -11,3 +11,6 @@ export const isNotRhelHost = ({ hostDetails }) =>
11
11
  // eslint-disable-next-line camelcase
12
12
  hostDetails?.operatingsystem_name
13
13
  );
14
+
15
+ export const vulnerabilityDisabled = ({ hostDetails }) =>
16
+ isNotRhelHost({ hostDetails }) || !hostDetails?.vulnerability?.enabled;
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import componentRegistry from 'foremanReact/components/componentRegistry';
3
3
  import { registerRoutes as foremanRegisterRoutes } from 'foremanReact/routes/RoutingService';
4
4
  import ForemanInventoryUpload from './ForemanInventoryUpload';
5
+ import InsightsVulnerability from './InsightsVulnerability/InsightsVulnerability';
5
6
  import InsightsCloudSync from './InsightsCloudSync';
6
7
  import InsightsHostDetailsTab from './InsightsHostDetailsTab';
7
8
 
@@ -9,6 +10,7 @@ const pages = [
9
10
  { name: 'ForemanInventoryUpload', type: ForemanInventoryUpload },
10
11
  { name: 'InsightsCloudSync', type: InsightsCloudSync },
11
12
  { name: 'InsightsHostDetailsTab', type: InsightsHostDetailsTab },
13
+ { name: 'InsightsVulnerability', type: InsightsVulnerability },
12
14
  ];
13
15
 
14
16
  export const registerPages = () => {
@@ -26,6 +28,11 @@ export const routes = [
26
28
  exact: true,
27
29
  render: props => <ForemanInventoryUpload {...props} />,
28
30
  },
31
+ {
32
+ path: '/foreman_rh_cloud/insights_vulnerability',
33
+ exact: true,
34
+ render: props => <InsightsVulnerability {...props} />,
35
+ },
29
36
  ];
30
37
 
31
38
  export const registerRoutes = () => {
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import {
3
+ AngleDoubleDownIcon,
4
+ AngleDoubleUpIcon,
5
+ CriticalRiskIcon,
6
+ EqualsIcon,
7
+ } from '@patternfly/react-icons';
8
+ import { Label } from '@patternfly/react-core';
9
+ import PropTypes from 'prop-types';
10
+
11
+ const VALUE_TO_STATE = {
12
+ 1: { icon: <AngleDoubleDownIcon />, text: 'Low', color: 'blue' },
13
+ 2: { icon: <EqualsIcon />, text: 'Moderate', color: 'yellow' },
14
+ 3: { icon: <AngleDoubleUpIcon />, text: 'Important', color: 'orange' },
15
+ 4: { icon: <CriticalRiskIcon />, text: 'Critical', color: 'red' },
16
+ };
17
+
18
+ const InsightsLabel = ({ value = 1, text, hideIcon, ...props }) => (
19
+ <Label
20
+ {...props}
21
+ color={VALUE_TO_STATE[value].color}
22
+ icon={!hideIcon && VALUE_TO_STATE[value].icon}
23
+ >
24
+ {text || VALUE_TO_STATE[value].text}
25
+ </Label>
26
+ );
27
+
28
+ InsightsLabel.propTypes = {
29
+ value: PropTypes.oneOf([1, 2, 3, 4]),
30
+ text: PropTypes.string,
31
+ hideIcon: PropTypes.bool,
32
+ };
33
+
34
+ InsightsLabel.defaultProps = {
35
+ value: 1,
36
+ text: '',
37
+ hideIcon: false,
38
+ };
39
+
40
+ export default InsightsLabel;
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ import './InsightsSection.scss';
5
+
6
+ const InsightsSection = ({ type, children, className, ...props }) => {
7
+ let sectionClasses = className || '';
8
+ if (type !== undefined) {
9
+ sectionClasses = `${sectionClasses} ins-l-${type}`.trim();
10
+ }
11
+
12
+ return (
13
+ <section {...props} className={sectionClasses}>
14
+ {children}
15
+ </section>
16
+ );
17
+ };
18
+
19
+ InsightsSection.propTypes = {
20
+ type: PropTypes.string,
21
+ children: PropTypes.node,
22
+ className: PropTypes.string,
23
+ };
24
+ InsightsSection.defaultProps = {
25
+ type: undefined,
26
+ children: null,
27
+ className: '',
28
+ };
29
+
30
+ export default InsightsSection;
@@ -0,0 +1,25 @@
1
+ section.ins-l-content {
2
+ padding: var(--pf-t--global--spacer--lg);
3
+ }
4
+
5
+ section.ins-l-button-group {
6
+ > * { display: inline; }
7
+ * + * { margin-left: 0.3125rem; } // 5px = 0.3125rem
8
+ margin: 1.5rem 0; // 24px = 1.5rem
9
+ }
10
+
11
+ section.ins-l-icon-group {
12
+ * + * { margin-left: 0.625rem; } // 10px = 0.625rem
13
+ }
14
+
15
+ section.ins-l-icon-group__with-major {
16
+ * + * { margin-left: 0.46875rem; } // 7.5px = 0.46875rem
17
+ .ins-battery:last-of-type {
18
+ padding-left: 0.9375rem; // 15px = 0.9375rem
19
+ border-left: 2px solid #eaeaea;
20
+ span.label{
21
+ font-weight: 500;
22
+ margin: 0 0.625rem; // 10px = 0.625rem
23
+ }
24
+ }
25
+ }
@@ -1,21 +1,19 @@
1
1
  /* eslint-disable camelcase */
2
2
  import React from 'react';
3
- import {
4
- InsightsLabel,
5
- Section,
6
- } from '@redhat-cloud-services/frontend-components';
7
3
  import { DropdownItem } from '@patternfly/react-core/deprecated';
8
4
  import { sortable, cellWidth } from '@patternfly/react-table';
9
5
  import { AnsibeTowerIcon, ExternalLinkAltIcon } from '@patternfly/react-icons';
10
6
  import { translate as __ } from 'foremanReact/common/I18n';
11
7
  import { foremanUrl } from '../../../ForemanRhCloudHelpers';
12
8
  import DropdownToggle from '../../../common/DropdownToggle';
9
+ import InsightsSection from './InsightsSection';
10
+ import InsightsLabel from './InsightsLabel';
13
11
 
14
12
  export const totalRiskFormatter = ({ title: totalRisk }) => ({
15
13
  children: (
16
- <Section className="insights-total-risk" type="icon-group">
14
+ <InsightsSection className="insights-total-risk" type="icon-group">
17
15
  <InsightsLabel value={totalRisk} />
18
- </Section>
16
+ </InsightsSection>
19
17
  ),
20
18
  });
21
19
 
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+
5
+ const InsightsVulnerability = () => (
6
+ <PageLayout searchable={false} header={__('Vulnerability')}>
7
+ <div className="insights-vulnerability">
8
+ <p>This page is under development. Please check back soon for updates.</p>
9
+ </div>
10
+ </PageLayout>
11
+ );
12
+
13
+ export default InsightsVulnerability;
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import InsightsVulnerability from './InsightsVulnerability';
5
+
6
+ describe('InsightsVulnerability component', () => {
7
+ it('renders the "under development" message', () => {
8
+ render(<InsightsVulnerability />);
9
+ expect(
10
+ screen.getByText(/this page is under development/i)
11
+ ).toBeInTheDocument();
12
+ });
13
+
14
+ it('renders the container with correct class', () => {
15
+ const { container } = render(<InsightsVulnerability />);
16
+ expect(container.querySelector('.insights-vulnerability')).toBeTruthy();
17
+ });
18
+ });
@@ -1,10 +1,39 @@
1
1
  import { testSelectorsSnapshotWithFixtures } from '@theforeman/test';
2
- import { foremanUrl } from '../ForemanRhCloudHelpers';
2
+ import { foremanUrl, vulnerabilityDisabled } from '../ForemanRhCloudHelpers';
3
3
 
4
4
  global.URL_PREFIX = 'MY_TEST_URL_PREFIX.example.com';
5
5
 
6
6
  const fixtures = {
7
7
  'should return foreman Url': () => foremanUrl('/test_path'),
8
+ 'vulnerabilityDisabled returns false for RHEL host with vulnerability enabled': () =>
9
+ vulnerabilityDisabled({
10
+ hostDetails: {
11
+ operatingsystem_name: 'Red Hat Enterprise Linux',
12
+ vulnerability: { enabled: true },
13
+ },
14
+ }),
15
+ 'vulnerabilityDisabled returns true for non-RHEL host': () =>
16
+ vulnerabilityDisabled({
17
+ hostDetails: {
18
+ operatingsystem_name: 'Ubuntu',
19
+ vulnerability: { enabled: true },
20
+ },
21
+ }),
22
+ 'vulnerabilityDisabled returns true for RHEL host with vulnerability disabled': () =>
23
+ vulnerabilityDisabled({
24
+ hostDetails: {
25
+ operatingsystem_name: 'Red Hat Enterprise Linux',
26
+ vulnerability: { enabled: false },
27
+ },
28
+ }),
29
+ 'vulnerabilityDisabled returns true for missing vulnerability object': () =>
30
+ vulnerabilityDisabled({
31
+ hostDetails: {
32
+ operatingsystem_name: 'Red Hat Enterprise Linux',
33
+ },
34
+ }),
35
+ 'vulnerabilityDisabled returns true for missing hostDetails': () =>
36
+ vulnerabilityDisabled({}),
8
37
  };
9
38
 
10
39
  describe('ForemanRhCloud helpers', () =>