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.
- checksums.yaml +4 -4
- data/app/views/api/v2/hosts/insights/single.rabl +5 -0
- data/config/package-lock.json.plugin +4137 -3036
- data/config/routes.rb +1 -0
- data/db/seeds.d/200_features.rb +4 -0
- data/lib/foreman_inventory_upload/generators/tags.rb +4 -1
- data/lib/foreman_rh_cloud/engine.rb +39 -4
- data/lib/foreman_rh_cloud/version.rb +1 -1
- data/lib/insights_vulnerability.rb +2 -0
- data/lib/tasks/hybrid_cloud.rake +105 -37
- data/package.json +1 -1
- data/test/controllers/insights_cloud/api/cloud_request_controller_test.rb +1 -2
- data/test/unit/rh_cloud_http_proxy_test.rb +8 -0
- data/test/unit/tags_generator_test.rb +1 -0
- data/webpack/CVEsHostDetailsTab/CVEsHostDetailsTab.js +17 -0
- data/webpack/CVEsHostDetailsTab/__tests__/CVEsHostDetailsTab.test.js +18 -0
- data/webpack/CVEsHostDetailsTab/index.js +3 -0
- data/webpack/ForemanInventoryUpload/Components/InventorySettings/MinimalInventoryDropdown.js +1 -1
- data/webpack/ForemanRhCloudFills.js +11 -1
- data/webpack/ForemanRhCloudHelpers.js +3 -0
- data/webpack/ForemanRhCloudPages.js +7 -0
- data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsLabel.js +40 -0
- data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsSection.js +30 -0
- data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsSection.scss +25 -0
- data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsTableConstants.js +4 -6
- data/webpack/InsightsVulnerability/InsightsVulnerability.js +13 -0
- data/webpack/InsightsVulnerability/InsightsVulnerability.test.js +18 -0
- data/webpack/__tests__/ForemanRhCloudHelpers.test.js +30 -1
- data/webpack/__tests__/__snapshots__/ForemanRhCloudHelpers.test.js.snap +10 -0
- 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
|
@@ -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
|
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
|
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
|
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
|
data/lib/tasks/hybrid_cloud.rake
CHANGED
@@ -1,64 +1,132 @@
|
|
1
1
|
require 'io/console'
|
2
|
+
require 'uri'
|
2
3
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
12
|
-
|
20
|
+
def default_registrations_url
|
21
|
+
URI.join(ForemanRhCloud.base_url, '/api/identity/certificate/registrations').to_s
|
13
22
|
end
|
14
23
|
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
48
|
-
|
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:
|
54
|
-
method:
|
55
|
-
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
|
-
|
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
@@ -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.
|
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
|
+
});
|
data/webpack/ForemanInventoryUpload/Components/InventorySettings/MinimalInventoryDropdown.js
CHANGED
@@ -25,7 +25,7 @@ const MinimalInventoryDropdown = ({ setChosenValue }) => {
|
|
25
25
|
),
|
26
26
|
},
|
27
27
|
optional: {
|
28
|
-
title: __('
|
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
|
-
<
|
14
|
+
<InsightsSection className="insights-total-risk" type="icon-group">
|
17
15
|
<InsightsLabel value={totalRisk} />
|
18
|
-
</
|
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', () =>
|