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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c2ad7078753ff687e7dfa1cf410efbfbc3f1ad9b8dc74500f9e77bf0c9a7fc9
4
- data.tar.gz: 9eb2679eb38c91fa3c1163915328d91959f7e6408e73bd4fc0c4d7fe98b9a0cf
3
+ metadata.gz: '03458c064241e69e2f5384094f124fd0911a9bb2b08a0e299581c8d5c22ef5da'
4
+ data.tar.gz: 4a14089d8e6410a5389473824d6206637370a34c552dcd178fe843b2eea3307c
5
5
  SHA512:
6
- metadata.gz: 0e4cb92c3a41b257ebf018a988ced92b086aa5cec373214888bb22a3dc43f765a007f42182b1da6a058c97b8633d59e1239efef658c3e0d53744bb9b8fc41125
7
- data.tar.gz: 780195de89dcac76c3774a8b48ae67c7c52ad8b2b4f1029020e1c1e42ec701092f2122ff4caec64de083cf01094ebac40b42cf1d0985d06fdbbc46838331d541
6
+ metadata.gz: c5daadeadfe2543a242aea723e832b4fcc5f7ac31a598fc05f34792e1c58042ec993718e07b96bff3841b35f366438b8ed7e52166fe94ea118f635f9caadfa16
7
+ data.tar.gz: c6008984ca0d88a5bcc51a1d6fd288e031b51238a0c561fcdb414cc967b7814750a529c5f5c0916866401dd922b71efd9fc20ad0b927fb2faec36ee21d5ae859
@@ -1,3 +1,3 @@
1
1
  module ForemanRhCloud
2
- VERSION = '14.3.0'.freeze
2
+ VERSION = '14.4.0'.freeze
3
3
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foreman_rh_cloud",
3
- "version": "14.3.0",
3
+ "version": "14.4.0",
4
4
  "description": "Inventory Upload =============",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -79,6 +79,7 @@ const MinimalInventoryDropdown = ({ setChosenValue }) => {
79
79
  isFullWidth
80
80
  onClick={onToggleClick}
81
81
  isExpanded={isOpen}
82
+ ouiaId="inventory-dropdown-toggle"
82
83
  >
83
84
  {dropdownValues[currentDropdownValue].title}
84
85
  </MenuToggle>
@@ -59,6 +59,7 @@ const PageTitle = () => {
59
59
  variant="plain"
60
60
  onClick={() => setIsDropdownOpen(prev => !prev)}
61
61
  isExpanded={isDropdownOpen}
62
+ ouiaId="title-dropdown-toggle"
62
63
  >
63
64
  <EllipsisVIcon />
64
65
  </MenuToggle>
@@ -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].color}
22
- icon={!hideIcon && VALUE_TO_STATE[value].icon}
22
+ color={VALUE_TO_STATE[value]?.color}
23
+ icon={!hideIcon && VALUE_TO_STATE[value]?.icon}
23
24
  >
24
- {text || VALUE_TO_STATE[value].text}
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>
@@ -42,6 +42,7 @@ const Pagination = ({ variant, ...props }) => {
42
42
 
43
43
  return (
44
44
  <PfPagination
45
+ ouiaId={`recommendations-pagination-${variant}`}
45
46
  itemCount={itemCount}
46
47
  widgetId={`recommendation-pagination-${variant}`}
47
48
  perPage={perPage}
@@ -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, ListView } from 'patternfly-react';
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
- class InsightsHostDetailsTab extends React.Component {
11
- componentDidMount() {
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
- if (!hits.length) {
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
- <Grid.Row>
45
- <Grid.Col xs={12}>
46
- <h2>{__('Recommendations')}</h2>
47
- <ListView id="hits_list">{items}</ListView>
48
- </Grid.Col>
49
- </Grid.Row>
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: 15px;
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: 12px;
76
- height: 12px;
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: #222;
81
- border-radius: 6px;
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: 24px;
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: 0px;
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 { render, screen } from '@testing-library/react';
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 "No recommendations" message when hits is empty', () => {
8
- render(<InsightsTab hostID={hostID} hits={[]} />);
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.getByText('No recommendations were found for this host!')
11
- ).toBeTruthy();
26
+ screen.queryByRole('heading', { name: 'Recommendations' })
27
+ ).not.toBeInTheDocument();
12
28
  });
13
29
 
14
- it('renders Recommendations heading when hits exist', () => {
15
- render(<InsightsTab hostID={hostID} hits={hits} />);
16
- expect(screen.getByText('Recommendations')).toBeTruthy();
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 hit titles', () => {
20
- render(<InsightsTab hostID={hostID} hits={hits} />);
21
- hits.forEach(hit => {
22
- expect(screen.getByText(hit.title)).toBeTruthy();
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('calls fetchHits on mount', () => {
48
+ it('requests recommendations for the host on mount', async () => {
27
49
  const fetchHits = jest.fn();
28
- render(<InsightsTab hostID={hostID} hits={[]} fetchHits={fetchHits} />);
29
- expect(fetchHits).toHaveBeenCalledWith(hostID);
50
+
51
+ renderInsightsTab({ hits: [], fetchHits });
52
+
53
+ await waitFor(() => {
54
+ expect(fetchHits).toHaveBeenCalledWith(hostID);
55
+ });
30
56
  });
31
57
 
32
- it('sorts hits by total_risk descending', () => {
33
- const multipleHits = [
34
- { ...hits[0], title: 'Low risk', total_risk: 1 },
35
- { ...hits[0], title: 'High risk', total_risk: 4 },
36
- { ...hits[0], title: 'Medium risk', total_risk: 2 },
37
- ];
38
- render(<InsightsTab hostID={hostID} hits={multipleHits} />);
39
- const titles = screen.getAllByText(/risk/i).map(el => el.textContent);
40
- const highRiskIndex = titles.findIndex(t => t === 'High risk');
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, { Fragment } from 'react';
2
- import { ListView, Icon } from 'patternfly-react';
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 heading = (
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')} <Icon name="external-link" />
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
- <Icon name="external-link" />
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
- <ListView.Item
46
- heading={heading}
47
- additionalInfo={additionalInfo}
48
- hideCloseIcon
49
- >
50
- <Fragment>
51
- <p>{title}</p>
52
- {knowledgebaseLink}
53
- {insightsCloudLink}
54
- </Fragment>
55
- </ListView.Item>
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
@@ -18,6 +18,7 @@ const DropdownToggle = ({ items, ...props }) => {
18
18
  aria-label={__('Table actions')}
19
19
  onClick={() => setOpen(prev => !prev)}
20
20
  isExpanded={isOpen}
21
+ ouiaId="toggle-dropdown-toggle"
21
22
  >
22
23
  <EllipsisVIcon />
23
24
  </MenuToggle>
@@ -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
- <button
11
- onClick={e => e.preventDefault()}
12
- className={`pf-v5-c-form__group-label-help ${className}`}
11
+ <Button
12
+ variant="plain"
13
+ ouiaId={`help-button-${id}`}
14
+ className={className}
13
15
  >
14
16
  <HelpIcon />
15
- </button>
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.3.0
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