foreman_rh_cloud 14.2.1 → 14.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/models/concerns/rh_cloud_host.rb +36 -1
- data/config/routes.rb +1 -0
- data/lib/foreman_inventory_upload/async/upload_report_direct_job.rb +2 -0
- data/lib/foreman_rh_cloud/version.rb +1 -1
- data/package.json +1 -1
- data/test/jobs/upload_report_direct_job_test.rb +29 -0
- data/test/unit/insights_facet_test.rb +5 -0
- data/test/unit/rh_cloud_host_test.rb +68 -0
- data/webpack/ForemanInventoryUpload/Components/AccountList/AccountList.js +5 -1
- data/webpack/ForemanInventoryUpload/Components/AccountList/AccountListConstants.js +5 -0
- data/webpack/ForemanInventoryUpload/Components/InventorySettings/MinimalInventoryDropdown.js +1 -0
- data/webpack/ForemanInventoryUpload/Components/PageHeader/PageTitle.js +1 -0
- data/webpack/ForemanInventoryUpload/Components/TaskConstants.js +6 -0
- data/webpack/ForemanInventoryUpload/Components/TaskHistory/TaskHistory.js +3 -2
- data/webpack/ForemanInventoryUpload/Components/TaskProgress/TaskProgress.js +4 -3
- data/webpack/ForemanInventoryUpload/ForemanInventoryHelpers.js +1 -1
- data/webpack/ForemanInventoryUpload/__tests__/ForemanInventoryHelpers.test.js +3 -3
- data/webpack/ForemanRhCloudPages.js +6 -0
- data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsLabel.js +8 -7
- data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsTableConstants.js +2 -2
- data/webpack/InsightsCloudSync/Components/InsightsTable/Pagination.js +1 -0
- data/webpack/InsightsCloudSync/Components/RemediationModal/RemediationModalFooter.js +5 -2
- data/webpack/InsightsCloudSync/Components/RemediationModal/RemediationTableConstants.js +4 -0
- data/webpack/InsightsCloudSync/Components/ToolbarDropdown.js +1 -0
- data/webpack/InsightsHostDetailsTab/InsightsTab.js +53 -39
- data/webpack/InsightsHostDetailsTab/InsightsTab.scss +7 -73
- data/webpack/InsightsHostDetailsTab/__tests__/InsightsTab.fixtures.js +7 -0
- data/webpack/InsightsHostDetailsTab/__tests__/InsightsTab.test.js +52 -30
- data/webpack/InsightsHostDetailsTab/components/ListItem/ListItem.js +43 -35
- data/webpack/InsightsHostDetailsTab/components/ListItem/__tests__/ListItem.test.js +111 -0
- data/webpack/IopPathwayDetails/IopPathwayDetails.js +40 -0
- data/webpack/IopRecommendationDetails/IopRecommendationDetails.js +1 -0
- data/webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware.js +5 -1
- data/webpack/common/DropdownToggle.js +1 -0
- data/webpack/common/Switcher/HelpLabel.js +7 -5
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '03458c064241e69e2f5384094f124fd0911a9bb2b08a0e299581c8d5c22ef5da'
|
|
4
|
+
data.tar.gz: 4a14089d8e6410a5389473824d6206637370a34c552dcd178fe843b2eea3307c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5daadeadfe2543a242aea723e832b4fcc5f7ac31a598fc05f34792e1c58042ec993718e07b96bff3841b35f366438b8ed7e52166fe94ea118f635f9caadfa16
|
|
7
|
+
data.tar.gz: c6008984ca0d88a5bcc51a1d6fd288e031b51238a0c561fcdb414cc967b7814750a529c5f5c0916866401dd922b71efd9fc20ad0b927fb2faec36ee21d5ae859
|
|
@@ -10,7 +10,8 @@ module RhCloudHost
|
|
|
10
10
|
)
|
|
11
11
|
|
|
12
12
|
has_many :insights_hits, through: :insights, source: :hits
|
|
13
|
-
scoped_search :
|
|
13
|
+
scoped_search :on => :id, :rename => :insights_recommendations_count, :only_explicit => true,
|
|
14
|
+
:ext_method => :search_by_insights_recommendations_count, :complete_value => false
|
|
14
15
|
|
|
15
16
|
has_one :insights_client_report_status_object, :class_name => '::InsightsClientReportStatus', :foreign_key => 'host_id'
|
|
16
17
|
scoped_search :relation => :insights_client_report_status_object, :on => :status, :rename => :insights_client_report_status,
|
|
@@ -46,6 +47,40 @@ module RhCloudHost
|
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
module ClassMethods
|
|
50
|
+
def search_by_insights_recommendations_count(_key, operator, value)
|
|
51
|
+
if ForemanRhCloud.with_iop_smart_proxy?
|
|
52
|
+
raise ScopedSearch::QueryNotSupported.new(
|
|
53
|
+
_('Searching by recommendations count is not available in IoP mode.')
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
unless ScopedSearch::QueryBuilder::SQL_OPERATORS.value?(operator)
|
|
58
|
+
raise ScopedSearch::QueryNotSupported.new(
|
|
59
|
+
_('Unsupported operator for recommendations count search: %s') % operator
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
facet_table = InsightsFacet.table_name
|
|
64
|
+
hits_count_col = "COALESCE(#{facet_table}.hits_count, 0)"
|
|
65
|
+
|
|
66
|
+
if ['IN', 'NOT IN'].include?(operator)
|
|
67
|
+
values = value.is_a?(Array) ? value : value.to_s.split(',').map(&:strip)
|
|
68
|
+
placeholders = (['?'] * values.size).join(',')
|
|
69
|
+
condition = sanitize_sql_for_conditions(
|
|
70
|
+
["#{hits_count_col} #{operator} (#{placeholders})", *values]
|
|
71
|
+
)
|
|
72
|
+
else
|
|
73
|
+
condition = sanitize_sql_for_conditions(
|
|
74
|
+
["#{hits_count_col} #{operator} ?", value_to_sql(operator, value)]
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
joins: "LEFT JOIN #{facet_table} ON #{facet_table}.host_id = #{Host::Managed.table_name}.id",
|
|
80
|
+
conditions: condition,
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
49
84
|
def search_by_insights_uuid(_key, operator, value)
|
|
50
85
|
# Determine which facet table to search based on IoP mode
|
|
51
86
|
facet_table = ForemanRhCloud.with_iop_smart_proxy? ? Katello::Host::SubscriptionFacet.table_name : InsightsFacet.table_name
|
data/config/routes.rb
CHANGED
|
@@ -33,6 +33,7 @@ Rails.application.routes.draw do
|
|
|
33
33
|
namespace :foreman_rh_cloud do
|
|
34
34
|
get 'inventory_upload', to: 'foreman_rh_cloud#inventory_upload'
|
|
35
35
|
get 'recommendations', to: 'foreman_rh_cloud#recommendations'
|
|
36
|
+
get 'recommendations/pathways/:slug', to: 'foreman_rh_cloud#recommendations'
|
|
36
37
|
get 'recommendations/:rule_id', to: 'foreman_rh_cloud#recommendations'
|
|
37
38
|
get 'insights_cloud', to: '/react#index' # Uses foreman's react controller
|
|
38
39
|
get 'insights_vulnerability', to: '/react#index'
|
|
@@ -59,11 +59,13 @@ module ForemanInventoryUpload
|
|
|
59
59
|
def try_execute
|
|
60
60
|
if content_disconnected?
|
|
61
61
|
logger.info("Upload canceled: connection to Insights is not enabled. Report location: #{filename}")
|
|
62
|
+
done!
|
|
62
63
|
return
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
unless organization.owner_details&.dig('upstreamConsumer', 'idCert')
|
|
66
67
|
logger.info("Skipping organization '#{organization}', no candlepin certificate defined.")
|
|
68
|
+
done!
|
|
67
69
|
return
|
|
68
70
|
end
|
|
69
71
|
|
data/package.json
CHANGED
|
@@ -401,4 +401,33 @@ class UploadReportDirectJobTest < ActiveSupport::TestCase
|
|
|
401
401
|
file.close
|
|
402
402
|
end
|
|
403
403
|
end
|
|
404
|
+
|
|
405
|
+
test 'mark as done when upload aborted due to missing certificate' do
|
|
406
|
+
# Remove certificate from organization
|
|
407
|
+
Organization.any_instance.stubs(:owner_details).returns({})
|
|
408
|
+
|
|
409
|
+
action = create_action(ForemanInventoryUpload::Async::UploadReportDirectJob)
|
|
410
|
+
action.expects(:action_subject).with(@organization)
|
|
411
|
+
plan_action(action, nil, @organization.id)
|
|
412
|
+
|
|
413
|
+
# Execute the action
|
|
414
|
+
action.send(:try_execute)
|
|
415
|
+
|
|
416
|
+
# Verify it is marked as done
|
|
417
|
+
assert action.done?
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
test 'mark as done when upload aborted due to disconnected mode' do
|
|
421
|
+
Setting.stubs(:[]).with(:subscription_connection_enabled).returns(false)
|
|
422
|
+
|
|
423
|
+
action = create_action(ForemanInventoryUpload::Async::UploadReportDirectJob)
|
|
424
|
+
action.expects(:action_subject).with(@organization)
|
|
425
|
+
plan_action(action, nil, @organization.id)
|
|
426
|
+
|
|
427
|
+
# Execute the action
|
|
428
|
+
action.send(:try_execute)
|
|
429
|
+
|
|
430
|
+
# Verify it is marked as done
|
|
431
|
+
assert action.done?
|
|
432
|
+
end
|
|
404
433
|
end
|
|
@@ -6,6 +6,10 @@ class InsightsFacetTest < ActiveSupport::TestCase
|
|
|
6
6
|
InsightsFacet.reset_counters(@host.insights.id, :hits_count)
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
+
teardown do
|
|
10
|
+
ForemanRhCloud.unstub(:with_iop_smart_proxy?)
|
|
11
|
+
end
|
|
12
|
+
|
|
9
13
|
test 'host with hits can be deleted' do
|
|
10
14
|
assert_equal 1, @host.insights.hits.count
|
|
11
15
|
|
|
@@ -16,6 +20,7 @@ class InsightsFacetTest < ActiveSupport::TestCase
|
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
test 'search host by recommendations_count' do
|
|
23
|
+
ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
|
|
19
24
|
FactoryBot.create(:host) # create another host with no recommendations
|
|
20
25
|
|
|
21
26
|
assert_equal 1, Host.search_for('insights_recommendations_count = 1').count
|
|
@@ -343,6 +343,74 @@ class RhCloudHostTest < ActiveSupport::TestCase
|
|
|
343
343
|
end
|
|
344
344
|
end
|
|
345
345
|
|
|
346
|
+
context 'scoped search on insights_recommendations_count' do
|
|
347
|
+
setup do
|
|
348
|
+
@org = FactoryBot.create(:organization)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
teardown do
|
|
352
|
+
ForemanRhCloud.unstub(:with_iop_smart_proxy?)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
test 'searches insights_facets.hits_count in non-IoP mode' do
|
|
356
|
+
ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
|
|
357
|
+
host_with_hits = FactoryBot.create(:host, :with_insights_hits, organization: @org)
|
|
358
|
+
InsightsFacet.reset_counters(host_with_hits.insights.id, :hits_count)
|
|
359
|
+
host_without_hits = FactoryBot.create(:host, :managed, organization: @org)
|
|
360
|
+
|
|
361
|
+
results = Host::Managed.search_for('insights_recommendations_count = 1')
|
|
362
|
+
|
|
363
|
+
assert_includes results, host_with_hits
|
|
364
|
+
assert_not_includes results, host_without_hits
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
test 'supports greater-than operator in non-IoP mode' do
|
|
368
|
+
ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
|
|
369
|
+
host_with_hits = FactoryBot.create(:host, :with_insights_hits, organization: @org)
|
|
370
|
+
InsightsFacet.reset_counters(host_with_hits.insights.id, :hits_count)
|
|
371
|
+
host_without_hits = FactoryBot.create(:host, :managed, organization: @org)
|
|
372
|
+
|
|
373
|
+
results = Host::Managed.search_for('insights_recommendations_count > 0')
|
|
374
|
+
|
|
375
|
+
assert_includes results, host_with_hits
|
|
376
|
+
assert_not_includes results, host_without_hits
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
test 'raises QueryNotSupported in IoP mode' do
|
|
380
|
+
ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
|
|
381
|
+
|
|
382
|
+
error = assert_raises(ScopedSearch::QueryNotSupported) do
|
|
383
|
+
Host::Managed.search_for('insights_recommendations_count = 1')
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
assert_match(/not available in IoP mode/, error.message)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
test 'supports IN operator in non-IoP mode' do
|
|
390
|
+
ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
|
|
391
|
+
host_with_hits = FactoryBot.create(:host, :with_insights_hits, organization: @org)
|
|
392
|
+
InsightsFacet.reset_counters(host_with_hits.insights.id, :hits_count)
|
|
393
|
+
host_without_hits = FactoryBot.create(:host, :managed, organization: @org)
|
|
394
|
+
|
|
395
|
+
results = Host::Managed.search_for('insights_recommendations_count ^ (1,2,3)')
|
|
396
|
+
|
|
397
|
+
assert_includes results, host_with_hits
|
|
398
|
+
assert_not_includes results, host_without_hits
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
test 'handles hosts without insights facets in non-IoP mode' do
|
|
402
|
+
ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
|
|
403
|
+
host_without_facet = FactoryBot.create(:host, :managed, organization: @org)
|
|
404
|
+
host_with_facet = FactoryBot.create(:host, :with_insights_hits, organization: @org)
|
|
405
|
+
InsightsFacet.reset_counters(host_with_facet.insights.id, :hits_count)
|
|
406
|
+
|
|
407
|
+
results = Host::Managed.search_for('insights_recommendations_count = 1')
|
|
408
|
+
|
|
409
|
+
assert_includes results, host_with_facet
|
|
410
|
+
assert_not_includes results, host_without_facet
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
346
414
|
test 'scoped search for user_omitted inventory status works' do
|
|
347
415
|
host1 = FactoryBot.create(:host, :managed)
|
|
348
416
|
host2 = FactoryBot.create(:host, :managed)
|
|
@@ -8,6 +8,7 @@ import EmptyState from './Components/EmptyState';
|
|
|
8
8
|
import ErrorState from './Components/ErrorState';
|
|
9
9
|
import EmptyResults from './Components/EmptyResults';
|
|
10
10
|
import { filterAccounts } from './AccountListHelper';
|
|
11
|
+
import { ACCOUNT_STATUS_POLLING_INTERVAL_MS } from './AccountListConstants';
|
|
11
12
|
import './accountList.scss';
|
|
12
13
|
|
|
13
14
|
const AccountList = ({
|
|
@@ -21,7 +22,10 @@ const AccountList = ({
|
|
|
21
22
|
}) => {
|
|
22
23
|
useEffect(() => {
|
|
23
24
|
fetchAccountsStatus();
|
|
24
|
-
const pollingID = setInterval(
|
|
25
|
+
const pollingID = setInterval(
|
|
26
|
+
fetchAccountsStatus,
|
|
27
|
+
ACCOUNT_STATUS_POLLING_INTERVAL_MS
|
|
28
|
+
);
|
|
25
29
|
startAccountStatusPolling(pollingID);
|
|
26
30
|
|
|
27
31
|
return () => {
|
|
@@ -7,3 +7,8 @@ export const INVENTORY_ACCOUNT_STATUS_POLLING_START =
|
|
|
7
7
|
export const INVENTORY_ACCOUNT_STATUS_POLLING_STOP =
|
|
8
8
|
'INVENTORY_ACCOUNT_STATUS_POLLING_STOP';
|
|
9
9
|
export const INVENTORY_PROCESS_RESTART = 'INVENTORY_PROCESS_RESTART';
|
|
10
|
+
|
|
11
|
+
// How often to re-fetch account statuses while generation/upload is in
|
|
12
|
+
// progress. Frequent enough to reflect status changes promptly, without
|
|
13
|
+
// hammering the status endpoint.
|
|
14
|
+
export const ACCOUNT_STATUS_POLLING_INTERVAL_MS = 2000;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Task durations reported by the API are in seconds; used to derive
|
|
2
|
+
// minutes/seconds for display in TaskHistory and TaskProgress.
|
|
3
|
+
export const SECONDS_PER_MINUTE = 60;
|
|
4
|
+
|
|
5
|
+
// Progress percentage to show once a task has finished running.
|
|
6
|
+
export const PERCENT_COMPLETE = 100;
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from '@patternfly/react-icons';
|
|
19
19
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
20
20
|
import RelativeDateTime from 'foremanReact/components/common/dates/RelativeDateTime';
|
|
21
|
+
import { SECONDS_PER_MINUTE } from '../TaskConstants';
|
|
21
22
|
import './taskHistory.scss';
|
|
22
23
|
|
|
23
24
|
const TaskHistory = ({ tasks, title }) => {
|
|
@@ -52,8 +53,8 @@ const TaskHistory = ({ tasks, title }) => {
|
|
|
52
53
|
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
53
54
|
return __('N/A');
|
|
54
55
|
}
|
|
55
|
-
const mins = Math.floor(seconds /
|
|
56
|
-
const secs = Math.floor(seconds %
|
|
56
|
+
const mins = Math.floor(seconds / SECONDS_PER_MINUTE);
|
|
57
|
+
const secs = Math.floor(seconds % SECONDS_PER_MINUTE);
|
|
57
58
|
if (mins > 0) {
|
|
58
59
|
return `${mins}m ${secs}s`;
|
|
59
60
|
}
|
|
@@ -29,6 +29,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
|
|
29
29
|
import { inventoryUrl } from '../../ForemanInventoryHelpers';
|
|
30
30
|
import { selectSubscriptionConnectionEnabled } from '../InventorySettings/InventorySettingsSelectors';
|
|
31
31
|
import { useIopConfig } from '../../../common/Hooks/ConfigHooks';
|
|
32
|
+
import { SECONDS_PER_MINUTE, PERCENT_COMPLETE } from '../TaskConstants';
|
|
32
33
|
import './taskProgress.scss';
|
|
33
34
|
|
|
34
35
|
const TaskProgress = ({
|
|
@@ -213,8 +214,8 @@ const TaskProgress = ({
|
|
|
213
214
|
) {
|
|
214
215
|
return '';
|
|
215
216
|
}
|
|
216
|
-
const mins = Math.floor(seconds /
|
|
217
|
-
const secs = Math.floor(seconds %
|
|
217
|
+
const mins = Math.floor(seconds / SECONDS_PER_MINUTE);
|
|
218
|
+
const secs = Math.floor(seconds % SECONDS_PER_MINUTE);
|
|
218
219
|
if (mins > 0) {
|
|
219
220
|
return `${mins}m ${secs}s`;
|
|
220
221
|
}
|
|
@@ -232,7 +233,7 @@ const TaskProgress = ({
|
|
|
232
233
|
if (isStartingNewTask) {
|
|
233
234
|
progressValue = 0;
|
|
234
235
|
} else if (task.state === 'stopped') {
|
|
235
|
-
progressValue =
|
|
236
|
+
progressValue = PERCENT_COMPLETE;
|
|
236
237
|
} else {
|
|
237
238
|
progressValue = task.progress || 0;
|
|
238
239
|
}
|
|
@@ -10,11 +10,11 @@ jest.mock('foremanReact/common/helpers', () => ({
|
|
|
10
10
|
|
|
11
11
|
describe('ForemanInventoryUpload helpers', () => {
|
|
12
12
|
describe('getInventoryDocsUrl', () => {
|
|
13
|
-
it('requests the
|
|
13
|
+
it('requests the admin guide cloud connection chapter', () => {
|
|
14
14
|
getInventoryDocsUrl();
|
|
15
15
|
expect(getDocsURL).toHaveBeenCalledWith(
|
|
16
|
-
'
|
|
17
|
-
'configuring-foreman-server-for-cloud-connection'
|
|
16
|
+
'Administering_Project',
|
|
17
|
+
'configuring-foreman-server-for-cloud-connection'
|
|
18
18
|
);
|
|
19
19
|
});
|
|
20
20
|
});
|
|
@@ -5,6 +5,7 @@ import ForemanInventoryUpload from './ForemanInventoryUpload';
|
|
|
5
5
|
import InsightsVulnerabilityListPage from './InsightsVulnerability/InsightsVulnerabilityListPage';
|
|
6
6
|
import InsightsCloudSync from './InsightsCloudSync';
|
|
7
7
|
import IopRecommendationDetails from './IopRecommendationDetails/IopRecommendationDetails';
|
|
8
|
+
import IopPathwayDetails from './IopPathwayDetails/IopPathwayDetails';
|
|
8
9
|
import InsightsHostDetailsTab from './InsightsHostDetailsTab';
|
|
9
10
|
import CveDetailsPage from './CveDetailsPage';
|
|
10
11
|
import './common/styles.scss';
|
|
@@ -31,6 +32,11 @@ export const routes = [
|
|
|
31
32
|
exact: true,
|
|
32
33
|
render: props => <InsightsCloudSync {...props} />,
|
|
33
34
|
},
|
|
35
|
+
{
|
|
36
|
+
path: '/foreman_rh_cloud/recommendations/pathways/:slug',
|
|
37
|
+
exact: false,
|
|
38
|
+
render: props => <IopPathwayDetails {...props} />,
|
|
39
|
+
},
|
|
34
40
|
{
|
|
35
41
|
path: '/foreman_rh_cloud/recommendations',
|
|
36
42
|
exact: false,
|
|
@@ -7,21 +7,22 @@ import {
|
|
|
7
7
|
} from '@patternfly/react-icons';
|
|
8
8
|
import { Label } from '@patternfly/react-core';
|
|
9
9
|
import PropTypes from 'prop-types';
|
|
10
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
10
11
|
|
|
11
12
|
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' },
|
|
13
|
+
1: { icon: <AngleDoubleDownIcon />, text: __('Low'), color: 'blue' },
|
|
14
|
+
2: { icon: <EqualsIcon />, text: __('Moderate'), color: 'yellow' },
|
|
15
|
+
3: { icon: <AngleDoubleUpIcon />, text: __('Important'), color: 'orange' },
|
|
16
|
+
4: { icon: <CriticalRiskIcon />, text: __('Critical'), color: 'red' },
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
const InsightsLabel = ({ value = 1, text, hideIcon, ...props }) => (
|
|
19
20
|
<Label
|
|
20
21
|
{...props}
|
|
21
|
-
color={VALUE_TO_STATE[value]
|
|
22
|
-
icon={!hideIcon && VALUE_TO_STATE[value]
|
|
22
|
+
color={VALUE_TO_STATE[value]?.color}
|
|
23
|
+
icon={!hideIcon && VALUE_TO_STATE[value]?.icon}
|
|
23
24
|
>
|
|
24
|
-
{text || VALUE_TO_STATE[value]
|
|
25
|
+
{text || VALUE_TO_STATE[value]?.text}
|
|
25
26
|
</Label>
|
|
26
27
|
);
|
|
27
28
|
|
|
@@ -31,7 +31,7 @@ export const actionsFormatter = rowData => {
|
|
|
31
31
|
recommendationUrl &&
|
|
32
32
|
!isLocalAdvisorEngine &&
|
|
33
33
|
dropdownItems.push(
|
|
34
|
-
<DropdownItem key="recommendation-url">
|
|
34
|
+
<DropdownItem key="recommendation-url" ouiaId="insights-dropdown-item-recommendation-url">
|
|
35
35
|
<a href={recommendationUrl} target="_blank" rel="noopener noreferrer">
|
|
36
36
|
{__('View in Red Hat Insights')} <ExternalLinkAltIcon />
|
|
37
37
|
</a>
|
|
@@ -40,7 +40,7 @@ export const actionsFormatter = rowData => {
|
|
|
40
40
|
|
|
41
41
|
accessRHUrl &&
|
|
42
42
|
dropdownItems.push(
|
|
43
|
-
<DropdownItem key="access-url">
|
|
43
|
+
<DropdownItem key="access-url" ouiaId="insights-dropdown-item-knowledgebase">
|
|
44
44
|
<a href={accessRHUrl} target="_blank" rel="noopener noreferrer">
|
|
45
45
|
{__('Knowledgebase article')} <ExternalLinkAltIcon />
|
|
46
46
|
</a>
|
|
@@ -3,7 +3,10 @@ import PropTypes from 'prop-types';
|
|
|
3
3
|
import { Button } from '@patternfly/react-core';
|
|
4
4
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
5
|
import { useBulkSelect } from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
JOB_INVOCATION_PATH,
|
|
8
|
+
FORM_SUBMIT_DELAY_MS,
|
|
9
|
+
} from './RemediationTableConstants';
|
|
7
10
|
|
|
8
11
|
const ModalFooter = ({ toggleModal, resolutions, hostsIds, isIop }) => {
|
|
9
12
|
let token = document.querySelector('meta[name="csrf-token"]');
|
|
@@ -24,7 +27,7 @@ const ModalFooter = ({ toggleModal, resolutions, hostsIds, isIop }) => {
|
|
|
24
27
|
setTimeout(() => {
|
|
25
28
|
// eslint-disable-next-line no-unused-expressions
|
|
26
29
|
formRef.current?.submit?.();
|
|
27
|
-
},
|
|
30
|
+
}, FORM_SUBMIT_DELAY_MS);
|
|
28
31
|
};
|
|
29
32
|
return (
|
|
30
33
|
<form
|
|
@@ -45,3 +45,7 @@ export const JOB_INVOCATION_PATH = foremanUrl('/job_invocations/new');
|
|
|
45
45
|
export const REMEDIATIONS_API_KEY = 'INSIGHTS_REMEDIATIONS';
|
|
46
46
|
|
|
47
47
|
export const SUBMIT_RESOLUTIONS = 'SUBMIT_INSIGHTS_RESOLUTIONS';
|
|
48
|
+
|
|
49
|
+
// Brief delay before the (non-React) form submit/navigation fires, so the
|
|
50
|
+
// button's isLoading spinner has a chance to render first.
|
|
51
|
+
export const FORM_SUBMIT_DELAY_MS = 100;
|
|
@@ -53,6 +53,7 @@ const ToolbarDropdown = ({ onRecommendationSync }) => {
|
|
|
53
53
|
aria-label={__('Recommendations actions')}
|
|
54
54
|
onClick={() => setIsDropdownOpen(prev => !prev)}
|
|
55
55
|
isExpanded={isDropdownOpen}
|
|
56
|
+
ouiaId="recommendations-dropdown-toggle"
|
|
56
57
|
>
|
|
57
58
|
<EllipsisVIcon />
|
|
58
59
|
</MenuToggle>
|
|
@@ -1,56 +1,70 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { orderBy } from 'lodash';
|
|
4
|
-
import { Grid,
|
|
4
|
+
import { Accordion, Grid, GridItem, Title } from '@patternfly/react-core';
|
|
5
5
|
import { noop } from 'foremanReact/common/helpers';
|
|
6
6
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
7
7
|
import ListItem from './components/ListItem';
|
|
8
8
|
import './InsightsTab.scss';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const { fetchHits, hostID } = this.props;
|
|
10
|
+
const InsightsHostDetailsTab = ({ fetchHits, hostID, hits }) => {
|
|
11
|
+
useEffect(() => {
|
|
13
12
|
fetchHits(hostID);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
render() {
|
|
17
|
-
const { hits } = this.props;
|
|
13
|
+
}, [hostID]);
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
return <h2>{__('No recommendations were found for this host!')}</h2>;
|
|
21
|
-
}
|
|
22
|
-
const hitsSorted = orderBy(hits, ['total_risk'], ['desc']);
|
|
23
|
-
const items = hitsSorted.map(
|
|
24
|
-
(
|
|
25
|
-
{
|
|
26
|
-
title,
|
|
27
|
-
total_risk: totalRisk,
|
|
28
|
-
results_url: resultsUrl,
|
|
29
|
-
solution_url: solutionUrl,
|
|
30
|
-
},
|
|
31
|
-
index
|
|
32
|
-
) => (
|
|
33
|
-
<ListItem
|
|
34
|
-
key={index}
|
|
35
|
-
title={title}
|
|
36
|
-
totalRisk={totalRisk}
|
|
37
|
-
resultsUrl={resultsUrl}
|
|
38
|
-
solutionUrl={solutionUrl}
|
|
39
|
-
/>
|
|
40
|
-
)
|
|
41
|
-
);
|
|
15
|
+
if (!hits.length) {
|
|
42
16
|
return (
|
|
43
17
|
<div id="host_details_insights_tab">
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
18
|
+
<Title
|
|
19
|
+
headingLevel="h2"
|
|
20
|
+
size="md"
|
|
21
|
+
ouiaId="insights-recommendations-empty-title"
|
|
22
|
+
>
|
|
23
|
+
{__('No recommendations were found for this host!')}
|
|
24
|
+
</Title>
|
|
50
25
|
</div>
|
|
51
26
|
);
|
|
52
27
|
}
|
|
53
|
-
|
|
28
|
+
|
|
29
|
+
const hitsSorted = orderBy(hits, ['total_risk'], ['desc']);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div id="host_details_insights_tab">
|
|
33
|
+
<Grid hasGutter>
|
|
34
|
+
<GridItem span={12}>
|
|
35
|
+
<Title
|
|
36
|
+
headingLevel="h2"
|
|
37
|
+
size="md"
|
|
38
|
+
ouiaId="insights-recommendations-title"
|
|
39
|
+
>
|
|
40
|
+
{__('Recommendations')}
|
|
41
|
+
</Title>
|
|
42
|
+
<div id="hits_list" data-testid="hits-list">
|
|
43
|
+
<Accordion>
|
|
44
|
+
{hitsSorted.map(
|
|
45
|
+
({
|
|
46
|
+
id,
|
|
47
|
+
title,
|
|
48
|
+
total_risk: totalRisk,
|
|
49
|
+
results_url: resultsUrl,
|
|
50
|
+
solution_url: solutionUrl,
|
|
51
|
+
}) => (
|
|
52
|
+
<ListItem
|
|
53
|
+
key={id}
|
|
54
|
+
title={title}
|
|
55
|
+
totalRisk={totalRisk}
|
|
56
|
+
resultsUrl={resultsUrl}
|
|
57
|
+
solutionUrl={solutionUrl}
|
|
58
|
+
/>
|
|
59
|
+
)
|
|
60
|
+
)}
|
|
61
|
+
</Accordion>
|
|
62
|
+
</div>
|
|
63
|
+
</GridItem>
|
|
64
|
+
</Grid>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
54
68
|
|
|
55
69
|
InsightsHostDetailsTab.propTypes = {
|
|
56
70
|
hostID: PropTypes.number.isRequired,
|
|
@@ -1,84 +1,18 @@
|
|
|
1
1
|
#host_details_insights_tab {
|
|
2
|
-
#btn_toolbar {
|
|
3
|
-
float: right;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
#btn_fullscreen {
|
|
7
|
-
margin-left: 5px;
|
|
8
|
-
|
|
9
|
-
.fa-arrows-alt {
|
|
10
|
-
margin-left: 5px;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
h2 {
|
|
15
|
-
margin-top: 5px;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
2
|
#hits_list {
|
|
19
3
|
max-height: 650px;
|
|
20
4
|
overflow-y: scroll;
|
|
21
5
|
overflow-x: hidden;
|
|
22
|
-
margin-top:
|
|
23
|
-
|
|
24
|
-
.list-view-pf-expand {
|
|
25
|
-
padding: 0;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.list-group-item-header {
|
|
29
|
-
.list-view-pf-expand .fa-angle-right {
|
|
30
|
-
margin-top: 6px;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.list-view-pf-main-info .list-view-pf-description {
|
|
34
|
-
width: 83%;
|
|
35
|
-
|
|
36
|
-
.list-group-item-heading {
|
|
37
|
-
width: 375px;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
p {
|
|
41
|
-
margin: 0;
|
|
42
|
-
font-size: 14px;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.list-view-pf-additional-info {
|
|
47
|
-
.risk-label {
|
|
48
|
-
padding: 5px 8px;
|
|
49
|
-
border-radius: 12px;
|
|
50
|
-
|
|
51
|
-
&.risk-1 {
|
|
52
|
-
background-color: #e7f1fa;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
&.risk-2 {
|
|
56
|
-
background-color: #fdf7e7;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
&.risk-3 {
|
|
60
|
-
background-color: #f9dddd;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
&.risk-4 {
|
|
64
|
-
background-color: #ffecec;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
p {
|
|
68
|
-
margin: 0;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
6
|
+
margin-top: var(--pf-v5-global--spacer--md);
|
|
73
7
|
|
|
74
8
|
&::-webkit-scrollbar {
|
|
75
|
-
width:
|
|
76
|
-
height:
|
|
9
|
+
width: var(--pf-v5-global--spacer--sm);
|
|
10
|
+
height: var(--pf-v5-global--spacer--sm);
|
|
77
11
|
}
|
|
78
12
|
|
|
79
13
|
&::-webkit-scrollbar-thumb {
|
|
80
|
-
background:
|
|
81
|
-
border-radius:
|
|
14
|
+
background: var(--pf-v5-global--palette--black-900);
|
|
15
|
+
border-radius: var(--pf-v5-global--BorderRadius--sm);
|
|
82
16
|
border: 3px solid transparent;
|
|
83
17
|
background-clip: content-box;
|
|
84
18
|
}
|
|
@@ -86,9 +20,9 @@
|
|
|
86
20
|
}
|
|
87
21
|
|
|
88
22
|
#new_host_details_insights_tab {
|
|
89
|
-
padding:
|
|
23
|
+
padding: var(--pf-v5-global--spacer--lg);
|
|
90
24
|
}
|
|
91
25
|
|
|
92
26
|
#total-risks-dropdown-container ul.pf-v5-c-menu__list {
|
|
93
|
-
margin-bottom:
|
|
27
|
+
margin-bottom: 0;
|
|
94
28
|
}
|
|
@@ -2,6 +2,7 @@ export const hostID = 1234;
|
|
|
2
2
|
|
|
3
3
|
export const hits = [
|
|
4
4
|
{
|
|
5
|
+
id: 1,
|
|
5
6
|
hostname: 'my-host.example.com',
|
|
6
7
|
rhel_version: '7.8',
|
|
7
8
|
uuid: '4739b323-a343-4e89-b71b-81991b8dc656',
|
|
@@ -17,6 +18,12 @@ export const hits = [
|
|
|
17
18
|
},
|
|
18
19
|
];
|
|
19
20
|
|
|
21
|
+
export const multipleHits = [
|
|
22
|
+
{ ...hits[0], id: 1, title: 'Low risk', total_risk: 1 },
|
|
23
|
+
{ ...hits[0], id: 2, title: 'High risk', total_risk: 4 },
|
|
24
|
+
{ ...hits[0], id: 3, title: 'Medium risk', total_risk: 2 },
|
|
25
|
+
];
|
|
26
|
+
|
|
20
27
|
export const props = {
|
|
21
28
|
hostID,
|
|
22
29
|
hits,
|
|
@@ -1,46 +1,68 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { screen, waitFor, within } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import { rtlHelpers } from 'foremanReact/common/rtlTestHelpers';
|
|
3
5
|
import InsightsTab from '../InsightsTab';
|
|
4
|
-
import { hits, hostID } from './InsightsTab.fixtures';
|
|
6
|
+
import { hits, hostID, multipleHits } from './InsightsTab.fixtures';
|
|
7
|
+
|
|
8
|
+
const { renderWithI18n } = rtlHelpers;
|
|
9
|
+
|
|
10
|
+
const renderInsightsTab = (props = {}) =>
|
|
11
|
+
renderWithI18n(
|
|
12
|
+
<InsightsTab hostID={hostID} hits={[]} fetchHits={jest.fn()} {...props} />
|
|
13
|
+
);
|
|
5
14
|
|
|
6
15
|
describe('InsightsTab', () => {
|
|
7
|
-
it('renders
|
|
8
|
-
|
|
16
|
+
it('renders empty state heading when there are no recommendations', async () => {
|
|
17
|
+
renderInsightsTab({ hits: [] });
|
|
18
|
+
|
|
19
|
+
expect(
|
|
20
|
+
await screen.findByRole('heading', {
|
|
21
|
+
name: 'No recommendations were found for this host!',
|
|
22
|
+
})
|
|
23
|
+
).toBeInTheDocument();
|
|
24
|
+
|
|
9
25
|
expect(
|
|
10
|
-
screen.
|
|
11
|
-
).
|
|
26
|
+
screen.queryByRole('heading', { name: 'Recommendations' })
|
|
27
|
+
).not.toBeInTheDocument();
|
|
12
28
|
});
|
|
13
29
|
|
|
14
|
-
it('renders
|
|
15
|
-
|
|
16
|
-
|
|
30
|
+
it('renders recommendations heading when hits exist', async () => {
|
|
31
|
+
renderInsightsTab({ hits });
|
|
32
|
+
|
|
33
|
+
expect(
|
|
34
|
+
await screen.findByRole('heading', { name: 'Recommendations' })
|
|
35
|
+
).toBeInTheDocument();
|
|
17
36
|
});
|
|
18
37
|
|
|
19
|
-
it('displays
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
38
|
+
it('displays recommendation titles in the list', async () => {
|
|
39
|
+
renderInsightsTab({ hits });
|
|
40
|
+
|
|
41
|
+
expect(
|
|
42
|
+
await screen.findByText(
|
|
43
|
+
'New Ansible Engine packages are inaccessible when dedicated Ansible repo is not enabled'
|
|
44
|
+
)
|
|
45
|
+
).toBeInTheDocument();
|
|
24
46
|
});
|
|
25
47
|
|
|
26
|
-
it('
|
|
48
|
+
it('requests recommendations for the host on mount', async () => {
|
|
27
49
|
const fetchHits = jest.fn();
|
|
28
|
-
|
|
29
|
-
|
|
50
|
+
|
|
51
|
+
renderInsightsTab({ hits: [], fetchHits });
|
|
52
|
+
|
|
53
|
+
await waitFor(() => {
|
|
54
|
+
expect(fetchHits).toHaveBeenCalledWith(hostID);
|
|
55
|
+
});
|
|
30
56
|
});
|
|
31
57
|
|
|
32
|
-
it('sorts
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const mediumRiskIndex = titles.findIndex(t => t === 'Medium risk');
|
|
42
|
-
const lowRiskIndex = titles.findIndex(t => t === 'Low risk');
|
|
43
|
-
expect(highRiskIndex).toBeLessThan(mediumRiskIndex);
|
|
44
|
-
expect(mediumRiskIndex).toBeLessThan(lowRiskIndex);
|
|
58
|
+
it('sorts recommendations by total risk descending', async () => {
|
|
59
|
+
renderInsightsTab({ hits: multipleHits });
|
|
60
|
+
|
|
61
|
+
const hitsList = await screen.findByTestId('hits-list');
|
|
62
|
+
const toggles = within(hitsList).getAllByRole('button');
|
|
63
|
+
|
|
64
|
+
expect(toggles[0]).toHaveTextContent('High risk');
|
|
65
|
+
expect(toggles[1]).toHaveTextContent('Medium risk');
|
|
66
|
+
expect(toggles[2]).toHaveTextContent('Low risk');
|
|
45
67
|
});
|
|
46
68
|
});
|
|
@@ -1,33 +1,24 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
AccordionContent,
|
|
4
|
+
AccordionItem,
|
|
5
|
+
AccordionToggle,
|
|
6
|
+
Flex,
|
|
7
|
+
FlexItem,
|
|
8
|
+
Truncate,
|
|
9
|
+
} from '@patternfly/react-core';
|
|
10
|
+
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
|
3
11
|
import PropTypes from 'prop-types';
|
|
4
12
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
|
-
|
|
6
|
-
const labelMapper = {
|
|
7
|
-
1: __('Low'),
|
|
8
|
-
2: __('Moderate'),
|
|
9
|
-
3: __('Important'),
|
|
10
|
-
4: __('Critical'),
|
|
11
|
-
};
|
|
13
|
+
import InsightsLabel from '../../../InsightsCloudSync/Components/InsightsTable/InsightsLabel';
|
|
12
14
|
|
|
13
15
|
const ListItem = ({ title, totalRisk, resultsUrl, solutionUrl }) => {
|
|
14
|
-
const
|
|
15
|
-
<p className="ellipsis list-item-heading" title={title}>
|
|
16
|
-
{title}
|
|
17
|
-
</p>
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
const riskLabel = labelMapper[totalRisk];
|
|
21
|
-
const additionalInfo = [
|
|
22
|
-
<span key={`risk-info-${title}`} className={`risk-label risk-${totalRisk}`}>
|
|
23
|
-
<p>{riskLabel}</p>
|
|
24
|
-
</span>,
|
|
25
|
-
];
|
|
16
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
26
17
|
|
|
27
18
|
const knowledgebaseLink = solutionUrl && (
|
|
28
19
|
<p>
|
|
29
20
|
<a href={solutionUrl} target="_blank" rel="noopener noreferrer">
|
|
30
|
-
{__('Knowledgebase article')} <
|
|
21
|
+
{__('Knowledgebase article')} <ExternalLinkAltIcon />
|
|
31
22
|
</a>
|
|
32
23
|
</p>
|
|
33
24
|
);
|
|
@@ -35,24 +26,41 @@ const ListItem = ({ title, totalRisk, resultsUrl, solutionUrl }) => {
|
|
|
35
26
|
const insightsCloudLink = resultsUrl && (
|
|
36
27
|
<p>
|
|
37
28
|
<a href={resultsUrl} target="_blank" rel="noopener noreferrer">
|
|
38
|
-
{__('Read more about it in RH cloud insights')}
|
|
39
|
-
<
|
|
29
|
+
{__('Read more about it in RH cloud insights')}
|
|
30
|
+
<ExternalLinkAltIcon />
|
|
40
31
|
</a>
|
|
41
32
|
</p>
|
|
42
33
|
);
|
|
43
34
|
|
|
44
35
|
return (
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
36
|
+
<AccordionItem>
|
|
37
|
+
<AccordionToggle
|
|
38
|
+
onClick={() => setIsExpanded(currentValue => !currentValue)}
|
|
39
|
+
isExpanded={isExpanded}
|
|
40
|
+
>
|
|
41
|
+
<Flex
|
|
42
|
+
alignItems={{ default: 'alignItemsCenter' }}
|
|
43
|
+
justifyContent={{ default: 'justifyContentSpaceBetween' }}
|
|
44
|
+
flexWrap={{ default: 'nowrap' }}
|
|
45
|
+
>
|
|
46
|
+
<FlexItem>
|
|
47
|
+
<Truncate content={title} />
|
|
48
|
+
</FlexItem>
|
|
49
|
+
<FlexItem>
|
|
50
|
+
<InsightsLabel value={totalRisk} />
|
|
51
|
+
</FlexItem>
|
|
52
|
+
</Flex>
|
|
53
|
+
</AccordionToggle>
|
|
54
|
+
<AccordionContent isHidden={!isExpanded}>
|
|
55
|
+
{isExpanded && (
|
|
56
|
+
<>
|
|
57
|
+
<p>{title}</p>
|
|
58
|
+
{knowledgebaseLink}
|
|
59
|
+
{insightsCloudLink}
|
|
60
|
+
</>
|
|
61
|
+
)}
|
|
62
|
+
</AccordionContent>
|
|
63
|
+
</AccordionItem>
|
|
56
64
|
);
|
|
57
65
|
};
|
|
58
66
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import { Accordion } from '@patternfly/react-core';
|
|
6
|
+
import { rtlHelpers } from 'foremanReact/common/rtlTestHelpers';
|
|
7
|
+
import ListItem from '../ListItem';
|
|
8
|
+
|
|
9
|
+
const { renderWithI18n } = rtlHelpers;
|
|
10
|
+
|
|
11
|
+
const defaultProps = {
|
|
12
|
+
title: 'Test recommendation title',
|
|
13
|
+
totalRisk: 2,
|
|
14
|
+
resultsUrl:
|
|
15
|
+
'https://cloud.redhat.com/insights/advisor/recommendations/example',
|
|
16
|
+
solutionUrl: 'https://access.redhat.com/solutions/123456',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const renderListItem = (props = {}) =>
|
|
20
|
+
renderWithI18n(
|
|
21
|
+
<Accordion>
|
|
22
|
+
<ListItem {...defaultProps} {...props} />
|
|
23
|
+
</Accordion>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
describe('ListItem', () => {
|
|
27
|
+
it('renders the recommendation title when collapsed', async () => {
|
|
28
|
+
renderListItem();
|
|
29
|
+
|
|
30
|
+
expect(
|
|
31
|
+
await screen.findByText('Test recommendation title')
|
|
32
|
+
).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('renders the total risk label when collapsed', async () => {
|
|
36
|
+
renderListItem({ totalRisk: 2 });
|
|
37
|
+
|
|
38
|
+
expect(await screen.findByText('Moderate')).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('does not show detail links before expanding', async () => {
|
|
42
|
+
renderListItem();
|
|
43
|
+
|
|
44
|
+
await screen.findByText('Test recommendation title');
|
|
45
|
+
|
|
46
|
+
expect(
|
|
47
|
+
screen.queryByRole('link', { name: /Knowledgebase article/i })
|
|
48
|
+
).not.toBeInTheDocument();
|
|
49
|
+
expect(
|
|
50
|
+
screen.queryByRole('link', {
|
|
51
|
+
name: /Read more about it in RH cloud insights/i,
|
|
52
|
+
})
|
|
53
|
+
).not.toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('shows detail links after expanding the recommendation', async () => {
|
|
57
|
+
renderListItem();
|
|
58
|
+
|
|
59
|
+
await userEvent.click(await screen.findByRole('button'));
|
|
60
|
+
|
|
61
|
+
const knowledgebaseLink = screen.getByRole('link', {
|
|
62
|
+
name: /Knowledgebase article/i,
|
|
63
|
+
});
|
|
64
|
+
expect(knowledgebaseLink).toHaveAttribute('href', defaultProps.solutionUrl);
|
|
65
|
+
expect(knowledgebaseLink).toHaveAttribute('target', '_blank');
|
|
66
|
+
expect(knowledgebaseLink).toHaveAttribute('rel', 'noopener noreferrer');
|
|
67
|
+
|
|
68
|
+
const insightsCloudLink = screen.getByRole('link', {
|
|
69
|
+
name: /Read more about it in RH cloud insights/i,
|
|
70
|
+
});
|
|
71
|
+
expect(insightsCloudLink).toHaveAttribute('href', defaultProps.resultsUrl);
|
|
72
|
+
expect(insightsCloudLink).toHaveAttribute('target', '_blank');
|
|
73
|
+
expect(insightsCloudLink).toHaveAttribute('rel', 'noopener noreferrer');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('omits the knowledgebase link when no solution URL is provided', async () => {
|
|
77
|
+
renderListItem({ solutionUrl: '' });
|
|
78
|
+
|
|
79
|
+
await userEvent.click(await screen.findByRole('button'));
|
|
80
|
+
|
|
81
|
+
expect(
|
|
82
|
+
screen.queryByRole('link', { name: /Knowledgebase article/i })
|
|
83
|
+
).not.toBeInTheDocument();
|
|
84
|
+
|
|
85
|
+
const insightsCloudLink = screen.getByRole('link', {
|
|
86
|
+
name: /Read more about it in RH cloud insights/i,
|
|
87
|
+
});
|
|
88
|
+
expect(insightsCloudLink).toBeInTheDocument();
|
|
89
|
+
expect(insightsCloudLink).toHaveAttribute('target', '_blank');
|
|
90
|
+
expect(insightsCloudLink).toHaveAttribute('rel', 'noopener noreferrer');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('omits the insights cloud link when no results URL is provided', async () => {
|
|
94
|
+
renderListItem({ resultsUrl: '' });
|
|
95
|
+
|
|
96
|
+
await userEvent.click(await screen.findByRole('button'));
|
|
97
|
+
|
|
98
|
+
const knowledgebaseLink = screen.getByRole('link', {
|
|
99
|
+
name: /Knowledgebase article/i,
|
|
100
|
+
});
|
|
101
|
+
expect(knowledgebaseLink).toBeInTheDocument();
|
|
102
|
+
expect(knowledgebaseLink).toHaveAttribute('target', '_blank');
|
|
103
|
+
expect(knowledgebaseLink).toHaveAttribute('rel', 'noopener noreferrer');
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
screen.queryByRole('link', {
|
|
107
|
+
name: /Read more about it in RH cloud insights/i,
|
|
108
|
+
})
|
|
109
|
+
).not.toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useRouteMatch } from 'react-router-dom';
|
|
3
|
+
import { ScalprumComponent, ScalprumProvider } from '@scalprum/react-core';
|
|
4
|
+
|
|
5
|
+
import RemediationModal from '../InsightsCloudSync/Components/RemediationModal';
|
|
6
|
+
import { createProviderOptions } from '../common/ScalprumModule/ScalprumContext';
|
|
7
|
+
import { useInsightsPermissions } from '../common/Hooks/PermissionsHooks';
|
|
8
|
+
|
|
9
|
+
const scope = 'advisor';
|
|
10
|
+
const pathwayModule = './PathwayDetailsWrapped';
|
|
11
|
+
|
|
12
|
+
const IopPathwayDetails = props => {
|
|
13
|
+
const pathwayMatch = useRouteMatch(
|
|
14
|
+
'/foreman_rh_cloud/recommendations/pathways/:slug'
|
|
15
|
+
);
|
|
16
|
+
const slug = pathwayMatch?.params?.slug;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="iop-pathway-details-scalprum advisor">
|
|
20
|
+
<ScalprumComponent
|
|
21
|
+
scope={scope}
|
|
22
|
+
module={pathwayModule}
|
|
23
|
+
pathwayId={slug}
|
|
24
|
+
IopRemediationModal={RemediationModal}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const IopPathwayDetailsWrapped = props => {
|
|
32
|
+
const permissions = useInsightsPermissions();
|
|
33
|
+
return (
|
|
34
|
+
<ScalprumProvider {...createProviderOptions(permissions)}>
|
|
35
|
+
<IopPathwayDetails {...props} />
|
|
36
|
+
</ScalprumProvider>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default IopPathwayDetailsWrapped;
|
|
@@ -13,6 +13,7 @@ const IopRecommendationDetails = props => {
|
|
|
13
13
|
const urlParams = useRouteMatch('/foreman_rh_cloud/recommendations/:rule_id');
|
|
14
14
|
// eslint-disable-next-line camelcase
|
|
15
15
|
const ruleId = urlParams?.params?.rule_id;
|
|
16
|
+
|
|
16
17
|
return (
|
|
17
18
|
<div className="iop-recommendation-details-scalprum advisor">
|
|
18
19
|
<ScalprumComponent
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
// Arbitrary fallback for this test double only; the real foremanReact
|
|
2
|
+
// default lives in IntervalMiddleware/IntervalConstants.js.
|
|
3
|
+
const DEFAULT_INTERVAL_MS = 3000;
|
|
4
|
+
|
|
5
|
+
export const withInterval = (action, interval = DEFAULT_INTERVAL_MS) => ({
|
|
2
6
|
...action,
|
|
3
7
|
interval,
|
|
4
8
|
});
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
import { Popover } from '@patternfly/react-core';
|
|
3
|
+
import { Button, Popover } from '@patternfly/react-core';
|
|
4
4
|
import { HelpIcon } from '@patternfly/react-icons';
|
|
5
5
|
|
|
6
6
|
export const HelpLabel = ({ text, id, className }) => {
|
|
7
7
|
if (!text) return null;
|
|
8
|
+
|
|
8
9
|
return (
|
|
9
10
|
<Popover id={`${id}-help`} bodyContent={text} aria-label="help-text">
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
<Button
|
|
12
|
+
variant="plain"
|
|
13
|
+
ouiaId={`help-button-${id}`}
|
|
14
|
+
className={className}
|
|
13
15
|
>
|
|
14
16
|
<HelpIcon />
|
|
15
|
-
</
|
|
17
|
+
</Button>
|
|
16
18
|
</Popover>
|
|
17
19
|
);
|
|
18
20
|
};
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreman_rh_cloud
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 14.
|
|
4
|
+
version: 14.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foreman Red Hat Cloud team
|
|
@@ -381,6 +381,7 @@ files:
|
|
|
381
381
|
- webpack/ForemanInventoryUpload/Components/PageHeader/components/ToolbarButtons/index.js
|
|
382
382
|
- webpack/ForemanInventoryUpload/Components/PageHeader/components/ToolbarButtons/toolbarButtons.scss
|
|
383
383
|
- webpack/ForemanInventoryUpload/Components/PageHeader/index.js
|
|
384
|
+
- webpack/ForemanInventoryUpload/Components/TaskConstants.js
|
|
384
385
|
- webpack/ForemanInventoryUpload/Components/TaskHistory/TaskHistory.js
|
|
385
386
|
- webpack/ForemanInventoryUpload/Components/TaskHistory/index.js
|
|
386
387
|
- webpack/ForemanInventoryUpload/Components/TaskHistory/taskHistory.scss
|
|
@@ -463,6 +464,7 @@ files:
|
|
|
463
464
|
- webpack/InsightsHostDetailsTab/__tests__/InsightsTotalRiskChart.test.js
|
|
464
465
|
- webpack/InsightsHostDetailsTab/__tests__/NewHostDetailsTab.test.js
|
|
465
466
|
- webpack/InsightsHostDetailsTab/components/ListItem/ListItem.js
|
|
467
|
+
- webpack/InsightsHostDetailsTab/components/ListItem/__tests__/ListItem.test.js
|
|
466
468
|
- webpack/InsightsHostDetailsTab/components/ListItem/index.js
|
|
467
469
|
- webpack/InsightsHostDetailsTab/index.js
|
|
468
470
|
- webpack/InsightsVulnerability/InsightsVulnerabilityListPage.js
|
|
@@ -474,6 +476,7 @@ files:
|
|
|
474
476
|
- webpack/InsightsVulnerabilityActionsBar/index.js
|
|
475
477
|
- webpack/InsightsVulnerabilityHostIndexExtensions/CVECountCell.js
|
|
476
478
|
- webpack/InsightsVulnerabilityHostIndexExtensions/__tests__/CVECountCell.test.js
|
|
479
|
+
- webpack/IopPathwayDetails/IopPathwayDetails.js
|
|
477
480
|
- webpack/IopRecommendationDetails/IopRecommendationDetails.js
|
|
478
481
|
- webpack/__mocks__/foremanReact/Root/Context/ForemanContext.js
|
|
479
482
|
- webpack/__mocks__/foremanReact/common/I18n.js
|
|
@@ -532,7 +535,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
532
535
|
- !ruby/object:Gem::Version
|
|
533
536
|
version: '0'
|
|
534
537
|
requirements: []
|
|
535
|
-
rubygems_version: 4.0.
|
|
538
|
+
rubygems_version: 4.0.16
|
|
536
539
|
specification_version: 4
|
|
537
540
|
summary: Summary of ForemanRhCloud.
|
|
538
541
|
test_files:
|