foreman_rh_cloud 14.3.0 → 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/lib/foreman_rh_cloud/version.rb +1 -1
- data/package.json +1 -1
- data/webpack/ForemanInventoryUpload/Components/InventorySettings/MinimalInventoryDropdown.js +1 -0
- data/webpack/ForemanInventoryUpload/Components/PageHeader/PageTitle.js +1 -0
- 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/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 +0 -18
- data/webpack/common/DropdownToggle.js +1 -0
- data/webpack/common/Switcher/HelpLabel.js +7 -5
- metadata +3 -1
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
|
data/package.json
CHANGED
|
@@ -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>
|
|
@@ -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;
|
|
@@ -8,30 +8,12 @@ import { useInsightsPermissions } from '../common/Hooks/PermissionsHooks';
|
|
|
8
8
|
|
|
9
9
|
const scope = 'advisor';
|
|
10
10
|
const module = './RecommendationDetailsWrapped';
|
|
11
|
-
const pathwayModule = './PathwayDetailsWrapped';
|
|
12
11
|
|
|
13
12
|
const IopRecommendationDetails = props => {
|
|
14
|
-
const pathwayMatch = useRouteMatch(
|
|
15
|
-
'/foreman_rh_cloud/recommendations/pathways/:slug'
|
|
16
|
-
);
|
|
17
13
|
const urlParams = useRouteMatch('/foreman_rh_cloud/recommendations/:rule_id');
|
|
18
14
|
// eslint-disable-next-line camelcase
|
|
19
15
|
const ruleId = urlParams?.params?.rule_id;
|
|
20
16
|
|
|
21
|
-
if (pathwayMatch) {
|
|
22
|
-
return (
|
|
23
|
-
<div className="iop-pathway-details-scalprum advisor">
|
|
24
|
-
<ScalprumComponent
|
|
25
|
-
scope={scope}
|
|
26
|
-
module={pathwayModule}
|
|
27
|
-
pathwayId={pathwayMatch.params.slug}
|
|
28
|
-
IopRemediationModal={RemediationModal}
|
|
29
|
-
{...props}
|
|
30
|
-
/>
|
|
31
|
-
</div>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
17
|
return (
|
|
36
18
|
<div className="iop-recommendation-details-scalprum advisor">
|
|
37
19
|
<ScalprumComponent
|
|
@@ -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
|
|
@@ -464,6 +464,7 @@ files:
|
|
|
464
464
|
- webpack/InsightsHostDetailsTab/__tests__/InsightsTotalRiskChart.test.js
|
|
465
465
|
- webpack/InsightsHostDetailsTab/__tests__/NewHostDetailsTab.test.js
|
|
466
466
|
- webpack/InsightsHostDetailsTab/components/ListItem/ListItem.js
|
|
467
|
+
- webpack/InsightsHostDetailsTab/components/ListItem/__tests__/ListItem.test.js
|
|
467
468
|
- webpack/InsightsHostDetailsTab/components/ListItem/index.js
|
|
468
469
|
- webpack/InsightsHostDetailsTab/index.js
|
|
469
470
|
- webpack/InsightsVulnerability/InsightsVulnerabilityListPage.js
|
|
@@ -475,6 +476,7 @@ files:
|
|
|
475
476
|
- webpack/InsightsVulnerabilityActionsBar/index.js
|
|
476
477
|
- webpack/InsightsVulnerabilityHostIndexExtensions/CVECountCell.js
|
|
477
478
|
- webpack/InsightsVulnerabilityHostIndexExtensions/__tests__/CVECountCell.test.js
|
|
479
|
+
- webpack/IopPathwayDetails/IopPathwayDetails.js
|
|
478
480
|
- webpack/IopRecommendationDetails/IopRecommendationDetails.js
|
|
479
481
|
- webpack/__mocks__/foremanReact/Root/Context/ForemanContext.js
|
|
480
482
|
- webpack/__mocks__/foremanReact/common/I18n.js
|