foreman_rh_cloud 14.3.0 → 14.5.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/controllers/api/v2/rh_cloud/cloud_request_controller.rb +1 -0
- data/app/controllers/api/v2/rh_cloud/inventory_controller.rb +11 -5
- data/app/controllers/foreman_inventory_upload/accounts_controller.rb +0 -1
- data/app/controllers/foreman_inventory_upload/uploads_controller.rb +0 -11
- data/app/controllers/foreman_inventory_upload/uploads_settings_controller.rb +0 -8
- data/app/services/foreman_rh_cloud/cloud_presence.rb +23 -0
- data/config/routes.rb +1 -3
- data/lib/foreman_rh_cloud/engine.rb +1 -6
- data/lib/foreman_rh_cloud/plugin.rb +3 -2
- data/lib/foreman_rh_cloud/version.rb +1 -1
- data/lib/insights_cloud/async/cloud_connector_announce_task.rb +76 -19
- data/package.json +1 -1
- data/test/controllers/inventory_upload/api/inventory_controller_test.rb +10 -8
- data/test/jobs/cloud_connector_announce_task_test.rb +116 -106
- data/webpack/ForemanInventoryUpload/Components/InventorySettings/MinimalInventoryDropdown.js +1 -0
- data/webpack/ForemanInventoryUpload/Components/PageHeader/PageTitle.js +1 -0
- data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/integrations.test.js +4 -12
- 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/InsightsTable/__tests__/InsightsTableActions.test.js +2 -2
- data/webpack/InsightsCloudSync/Components/ToolbarDropdown.js +1 -0
- data/webpack/InsightsCloudSync/__tests__/InsightsCloudSyncActions.test.js +7 -5
- 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/InsightsVulnerabilityActionsBar/__tests__/InsightsVulnerabilityActionsBar.test.js +1 -5
- data/webpack/IopPathwayDetails/IopPathwayDetails.js +40 -0
- data/webpack/IopRecommendationDetails/IopRecommendationDetails.js +0 -18
- data/webpack/common/DropdownToggle.js +1 -0
- data/webpack/common/Switcher/HelpLabel.js +7 -5
- data/webpack/test_setup.js +1 -0
- metadata +4 -8
- data/app/services/foreman_rh_cloud/cloud_connector.rb +0 -74
- data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext.js +0 -6
- data/webpack/__mocks__/foremanReact/common/I18n.js +0 -6
- data/webpack/__mocks__/foremanReact/common/helpers.js +0 -14
- data/webpack/__mocks__/foremanReact/constants.js +0 -24
- data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +0 -24
- data/webpack/__mocks__/foremanReact/redux/API/index.js +0 -12
|
@@ -3,24 +3,16 @@ import { render, screen, fireEvent } from '@testing-library/react';
|
|
|
3
3
|
import { Provider } from 'react-redux';
|
|
4
4
|
import configureMockStore from 'redux-mock-store';
|
|
5
5
|
import thunk from 'redux-thunk';
|
|
6
|
-
import * as API from 'foremanReact/redux/API';
|
|
7
6
|
import ConnectedSyncButton from '../index';
|
|
8
|
-
import { successResponse } from './SyncButtonFixtures';
|
|
9
|
-
import { INVENTORY_SYNC } from '../SyncButtonConstants';
|
|
10
|
-
|
|
11
|
-
jest.spyOn(API, 'post');
|
|
12
7
|
|
|
13
8
|
const mockStore = configureMockStore([thunk]);
|
|
14
9
|
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
15
14
|
describe('SyncButton integration test', () => {
|
|
16
15
|
it('dispatches sync action when button is clicked', () => {
|
|
17
|
-
API.post.mockImplementation(({ handleSuccess, key, ...action }) => {
|
|
18
|
-
if (key === INVENTORY_SYNC && handleSuccess) {
|
|
19
|
-
handleSuccess(successResponse);
|
|
20
|
-
}
|
|
21
|
-
return { type: 'API_POST', ...action };
|
|
22
|
-
});
|
|
23
|
-
|
|
24
16
|
const store = mockStore({
|
|
25
17
|
API: {},
|
|
26
18
|
});
|
|
@@ -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>
|
data/webpack/InsightsCloudSync/Components/InsightsTable/__tests__/InsightsTableActions.test.js
CHANGED
|
@@ -117,10 +117,10 @@ describe('InsightsTable actions', () => {
|
|
|
117
117
|
expect(push).toHaveBeenCalled();
|
|
118
118
|
|
|
119
119
|
const apiAction = dispatch.mock.calls.find(
|
|
120
|
-
call => call[0] && call[0].key === INSIGHTS_HITS_API_KEY
|
|
120
|
+
call => call[0] && call[0].payload?.key === INSIGHTS_HITS_API_KEY
|
|
121
121
|
);
|
|
122
122
|
expect(apiAction).toBeTruthy();
|
|
123
|
-
const getArg = apiAction[0];
|
|
123
|
+
const getArg = apiAction[0].payload;
|
|
124
124
|
expect(getArg.url).toBe(INSIGHTS_HITS_PATH);
|
|
125
125
|
expect(getArg.params.page).toBe(2);
|
|
126
126
|
expect(getArg.params.per_page).toBe(7);
|
|
@@ -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,3 +1,4 @@
|
|
|
1
|
+
import { API_OPERATIONS } from 'foremanReact/redux/API';
|
|
1
2
|
import { syncInsights } from '../InsightsCloudSyncActions';
|
|
2
3
|
import { INSIGHTS_CLOUD_SYNC } from '../InsightsCloudSyncConstants';
|
|
3
4
|
|
|
@@ -17,17 +18,18 @@ describe('InsightsCloudSync actions', () => {
|
|
|
17
18
|
|
|
18
19
|
expect(dispatch).toHaveBeenCalledTimes(1);
|
|
19
20
|
const dispatched = dispatch.mock.calls[0][0];
|
|
20
|
-
expect(dispatched.
|
|
21
|
-
expect(dispatched.
|
|
22
|
-
expect(
|
|
23
|
-
expect(typeof dispatched.
|
|
21
|
+
expect(dispatched.type).toBe(API_OPERATIONS.POST);
|
|
22
|
+
expect(dispatched.payload.key).toBe(INSIGHTS_CLOUD_SYNC);
|
|
23
|
+
expect(dispatched.payload.url).toBe('/insights_cloud/tasks');
|
|
24
|
+
expect(typeof dispatched.payload.handleSuccess).toBe('function');
|
|
25
|
+
expect(typeof dispatched.payload.errorToast).toBe('function');
|
|
24
26
|
});
|
|
25
27
|
|
|
26
28
|
it('errorToast returns failure message with error details', () => {
|
|
27
29
|
syncInsights(jest.fn(), '')(dispatch);
|
|
28
30
|
const dispatched = dispatch.mock.calls[0][0];
|
|
29
31
|
|
|
30
|
-
const result = dispatched.errorToast('some error');
|
|
32
|
+
const result = dispatched.payload.errorToast('some error');
|
|
31
33
|
expect(result).toContain('some error');
|
|
32
34
|
});
|
|
33
35
|
});
|
|
@@ -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
|
|