foreman_rh_cloud 14.6.2 → 15.0.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/app/assets/javascripts/foreman_rh_cloud/locale/fr/foreman_rh_cloud.js +76 -88
  4. data/app/assets/javascripts/foreman_rh_cloud/locale/ja/foreman_rh_cloud.js +87 -99
  5. data/app/assets/javascripts/foreman_rh_cloud/locale/ka/foreman_rh_cloud.js +139 -151
  6. data/app/assets/javascripts/foreman_rh_cloud/locale/ko/foreman_rh_cloud.js +71 -83
  7. data/app/assets/javascripts/foreman_rh_cloud/locale/zh_CN/foreman_rh_cloud.js +70 -82
  8. data/app/services/foreman_rh_cloud/insights_api_forwarder.rb +16 -0
  9. data/app/services/foreman_rh_cloud/tags_auth.rb +28 -13
  10. data/config/routes.rb +10 -0
  11. data/lib/foreman_inventory_upload/generators/queries.rb +1 -1
  12. data/lib/foreman_inventory_upload/generators/slice.rb +14 -2
  13. data/lib/foreman_inventory_upload.rb +1 -7
  14. data/lib/foreman_rh_cloud/plugin.rb +14 -0
  15. data/lib/foreman_rh_cloud/version.rb +1 -1
  16. data/locale/foreman_rh_cloud.pot +246 -268
  17. data/locale/fr/LC_MESSAGES/foreman_rh_cloud.mo +0 -0
  18. data/locale/fr/foreman_rh_cloud.po +78 -91
  19. data/locale/ja/LC_MESSAGES/foreman_rh_cloud.mo +0 -0
  20. data/locale/ja/foreman_rh_cloud.po +87 -100
  21. data/locale/ka/LC_MESSAGES/foreman_rh_cloud.mo +0 -0
  22. data/locale/ka/foreman_rh_cloud.po +139 -152
  23. data/locale/ko/LC_MESSAGES/foreman_rh_cloud.mo +0 -0
  24. data/locale/ko/foreman_rh_cloud.po +71 -84
  25. data/locale/zh_CN/LC_MESSAGES/foreman_rh_cloud.mo +0 -0
  26. data/locale/zh_CN/foreman_rh_cloud.po +71 -84
  27. data/package.json +1 -1
  28. data/test/controllers/insights_cloud/api/machine_telemetries_controller_test.rb +0 -1
  29. data/test/test_plugin_helper.rb +0 -8
  30. data/test/unit/services/foreman_rh_cloud/cloud_request_forwarder_test.rb +0 -1
  31. data/test/unit/services/foreman_rh_cloud/insights_api_forwarder_test.rb +22 -0
  32. data/test/unit/services/foreman_rh_cloud/tags_auth_test.rb +26 -0
  33. data/test/unit/slice_generator_test.rb +29 -0
  34. data/test/unit/tags_generator_test.rb +0 -1
  35. data/webpack/ComplianceHostDetailsTab/ComplianceHostDetailsTab.js +36 -0
  36. data/webpack/ComplianceHostDetailsTab/ComplianceHostDetailsTab.scss +3 -0
  37. data/webpack/ComplianceHostDetailsTab/__tests__/ComplianceHostDetailsTab.test.js +159 -0
  38. data/webpack/ComplianceHostDetailsTab/index.js +3 -0
  39. data/webpack/ForemanInventoryUpload/Components/Dashboard/__tests__/Dashboard.test.js +79 -49
  40. data/webpack/ForemanInventoryUpload/Components/TaskProgress/TaskProgress.js +1 -5
  41. data/webpack/ForemanRhCloudFills.js +11 -0
  42. data/webpack/ForemanRhCloudPages.js +7 -0
  43. data/webpack/InsightsCompliance/ComplianceIframe.js +34 -0
  44. data/webpack/InsightsCompliance/ComplianceIframe.scss +12 -0
  45. data/webpack/InsightsCompliance/InsightsCompliancePage.js +11 -0
  46. data/webpack/InsightsCompliance/InsightsCompliancePage.test.js +35 -0
  47. data/webpack/InsightsCompliance/complianceIframeConstants.js +7 -0
  48. data/webpack/InsightsCompliance/complianceIframeHelpers.js +23 -0
  49. data/webpack/InsightsCompliance/complianceIframeHelpers.test.js +50 -0
  50. data/webpack/common/Hooks/PermissionsHooks.js +10 -0
  51. data/webpack/common/IopIframe/IopAppIframe.js +74 -0
  52. data/webpack/common/IopIframe/IopHostDetailsIframe.js +66 -0
  53. data/webpack/common/IopIframe/IopHostDetailsIframe.scss +6 -0
  54. data/webpack/common/IopIframe/IopHostDetailsIframeTab.js +81 -0
  55. data/webpack/common/IopIframe/__tests__/IopAppIframe.test.js +92 -0
  56. data/webpack/common/IopIframe/__tests__/IopHostDetailsIframeTab.test.js +93 -0
  57. data/webpack/common/IopIframe/__tests__/iopIframeHelpers.test.js +63 -0
  58. data/webpack/common/IopIframe/__tests__/postIopChromeToIframe.test.js +60 -0
  59. data/webpack/common/IopIframe/__tests__/useIopIframeHeight.test.js +37 -0
  60. data/webpack/common/IopIframe/iopIframeConstants.js +5 -0
  61. data/webpack/common/IopIframe/iopIframeHelpers.js +49 -0
  62. data/webpack/common/IopIframe/postIopChromeToIframe.js +35 -0
  63. data/webpack/common/IopIframe/useIopIframeChrome.js +54 -0
  64. data/webpack/common/IopIframe/useIopIframeHeight.js +35 -0
  65. data/webpack/common/IopIframe/useIopIframeNavigate.js +40 -0
  66. metadata +28 -2
@@ -0,0 +1,159 @@
1
+ import React from 'react';
2
+ import { fireEvent, render } from '@testing-library/react';
3
+ import { MemoryRouter } from 'react-router-dom';
4
+ import ComplianceHostDetailsTabWrapper from '../ComplianceHostDetailsTab';
5
+ import { OVERVIEW_TAB_PATH } from '../../ForemanRhCloudHelpers';
6
+
7
+ const mockHistoryReplace = jest.fn();
8
+
9
+ jest.mock('react-router-dom', () => ({
10
+ ...jest.requireActual('react-router-dom'),
11
+ useHistory: () => ({
12
+ replace: mockHistoryReplace,
13
+ }),
14
+ }));
15
+
16
+ jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
17
+ useForemanContext: () => ({
18
+ metadata: {
19
+ permissions: new Set(['view_hosts']),
20
+ },
21
+ }),
22
+ useForemanPermissions: () => new Set(['view_hosts']),
23
+ }));
24
+
25
+ jest.mock('foremanReact/common/I18n', () => ({
26
+ translate: str => str,
27
+ sprintf: str => str,
28
+ }));
29
+
30
+ jest.mock('../../InsightsCompliance/complianceIframeHelpers', () => ({
31
+ getComplianceIframeSrc: () =>
32
+ 'https://example.test/assets/apps/compliance/index.html',
33
+ }));
34
+
35
+ const mockPostChrome = jest.fn();
36
+ jest.mock('../../common/IopIframe/postIopChromeToIframe', () => ({
37
+ postIopChromeToIframe: (...args) => mockPostChrome(...args),
38
+ }));
39
+
40
+ const defaultResponse = {
41
+ id: 1,
42
+ operatingsystem_name: 'Red Hat Enterprise Linux 8',
43
+ insights_attributes: { uuid: 'insights-1' },
44
+ subscription_facet_attributes: { uuid: '1-2-3' },
45
+ };
46
+
47
+ describe('ComplianceHostDetailsTabWrapper', () => {
48
+ beforeEach(() => {
49
+ jest.clearAllMocks();
50
+ });
51
+
52
+ it('renders compliance iframe for a valid host without redirecting', () => {
53
+ const { getByTestId, container } = render(
54
+ <MemoryRouter>
55
+ <ComplianceHostDetailsTabWrapper response={defaultResponse} />
56
+ </MemoryRouter>
57
+ );
58
+
59
+ expect(
60
+ container.querySelector(
61
+ '.rh-cloud-insights-compliance-host-details-component'
62
+ )
63
+ ).toBeTruthy();
64
+
65
+ const iframe = getByTestId('compliance-host-details-iframe');
66
+ expect(iframe.getAttribute('src')).toBe(
67
+ 'https://example.test/assets/apps/compliance/index.html'
68
+ );
69
+ expect(mockHistoryReplace).not.toHaveBeenCalled();
70
+ });
71
+
72
+ it('posts systems/<uuid> appRoute for the host-tab iframe', () => {
73
+ const { getByTestId } = render(
74
+ <MemoryRouter>
75
+ <ComplianceHostDetailsTabWrapper response={defaultResponse} />
76
+ </MemoryRouter>
77
+ );
78
+
79
+ fireEvent.load(getByTestId('compliance-host-details-iframe'));
80
+
81
+ expect(mockPostChrome).toHaveBeenCalledWith(
82
+ expect.anything(),
83
+ expect.objectContaining({
84
+ appRoute: 'systems/1-2-3',
85
+ embedded: 'host-tab',
86
+ })
87
+ );
88
+ });
89
+
90
+ it('posts updated appRoute when inventoryId changes', () => {
91
+ const responseHostA = {
92
+ ...defaultResponse,
93
+ subscription_facet_attributes: { uuid: 'uuid-host-A' },
94
+ };
95
+
96
+ const { rerender, getByTestId } = render(
97
+ <MemoryRouter>
98
+ <ComplianceHostDetailsTabWrapper response={responseHostA} />
99
+ </MemoryRouter>
100
+ );
101
+
102
+ expect(getByTestId('compliance-host-details-iframe')).toBeTruthy();
103
+
104
+ const responseHostB = {
105
+ ...defaultResponse,
106
+ subscription_facet_attributes: { uuid: 'uuid-host-B' },
107
+ };
108
+
109
+ rerender(
110
+ <MemoryRouter>
111
+ <ComplianceHostDetailsTabWrapper response={responseHostB} />
112
+ </MemoryRouter>
113
+ );
114
+
115
+ fireEvent.load(getByTestId('compliance-host-details-iframe'));
116
+
117
+ expect(mockPostChrome).toHaveBeenCalledWith(
118
+ expect.anything(),
119
+ expect.objectContaining({
120
+ appRoute: 'systems/uuid-host-B',
121
+ embedded: 'host-tab',
122
+ })
123
+ );
124
+ });
125
+
126
+ it('redirects to Overview when tab should be hidden', () => {
127
+ const nonRhelResponse = {
128
+ id: 2,
129
+ operatingsystem_name: 'Ubuntu 20.04',
130
+ insights_attributes: { uuid: 'insights-1' },
131
+ subscription_facet_attributes: { uuid: '1-2-3' },
132
+ };
133
+
134
+ const { container } = render(
135
+ <MemoryRouter>
136
+ <ComplianceHostDetailsTabWrapper response={nonRhelResponse} />
137
+ </MemoryRouter>
138
+ );
139
+
140
+ expect(mockHistoryReplace).toHaveBeenCalledWith(OVERVIEW_TAB_PATH);
141
+ expect(
142
+ container.querySelector(
143
+ '.rh-cloud-insights-compliance-host-details-component'
144
+ )
145
+ ).toBeNull();
146
+ });
147
+
148
+ it('does not redirect when host data is not yet loaded', () => {
149
+ const emptyResponse = { subscription_facet_attributes: { uuid: '1-2-3' } };
150
+
151
+ render(
152
+ <MemoryRouter>
153
+ <ComplianceHostDetailsTabWrapper response={emptyResponse} />
154
+ </MemoryRouter>
155
+ );
156
+
157
+ expect(mockHistoryReplace).not.toHaveBeenCalled();
158
+ });
159
+ });
@@ -0,0 +1,3 @@
1
+ import ComplianceHostDetailsTabWrapper from './ComplianceHostDetailsTab';
2
+
3
+ export default ComplianceHostDetailsTabWrapper;
@@ -1,67 +1,97 @@
1
1
  import React from 'react';
2
- import { shallow } from '@theforeman/test';
2
+ import { screen } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import { rtlHelpers } from 'foremanReact/common/rtlTestHelpers';
3
5
  import Dashboard from '../Dashboard';
4
6
 
7
+ const { renderWithStoreAndI18n } = rtlHelpers;
8
+
9
+ jest.mock('../../../../common/Hooks/ConfigHooks', () => ({
10
+ useIopConfig: jest.fn(() => false),
11
+ }));
12
+
13
+ const runningTask = {
14
+ id: 'task-1',
15
+ state: 'running',
16
+ result: null,
17
+ progress: 50,
18
+ };
19
+
20
+ const inventoryStore = {
21
+ API: {
22
+ INVENTORY_SETTINGS: {
23
+ response: { subscriptionConnectionEnabled: true },
24
+ },
25
+ },
26
+ };
27
+
28
+ const renderDashboard = (props = {}) =>
29
+ renderWithStoreAndI18n(<Dashboard {...props} />, inventoryStore);
30
+
5
31
  describe('Dashboard', () => {
6
- it('should render TaskProgress component with correct props', () => {
7
- const account = {
8
- id: 1,
9
- generate_task: {
10
- id: 'task-1',
11
- state: 'running',
12
- result: null,
13
- progress: 50,
14
- },
15
- };
32
+ it('renders report generation progress for a running task', async () => {
16
33
  const onTaskStart = jest.fn();
17
34
 
18
- const wrapper = shallow(
19
- <Dashboard account={account} onTaskStart={onTaskStart} />
20
- );
35
+ renderDashboard({
36
+ account: { id: 1, generate_task: runningTask },
37
+ onTaskStart,
38
+ });
21
39
 
22
- const taskProgress = wrapper.find('TaskProgress');
23
- expect(taskProgress).toHaveLength(1);
24
- expect(taskProgress.prop('task')).toEqual(account.generate_task);
25
- expect(taskProgress.prop('title')).toBe('Report Generation');
26
- expect(taskProgress.prop('organizationId')).toBe(1);
27
- expect(taskProgress.prop('taskType')).toBe('generate');
28
- expect(taskProgress.prop('onTaskStart')).toBe(onTaskStart);
40
+ expect(await screen.findByText('Report Generation')).toBeInTheDocument();
41
+ expect(screen.getByLabelText('task-progress')).toBeInTheDocument();
42
+ expect(screen.getByText('Running')).toBeInTheDocument();
43
+ expect(
44
+ screen.getByRole('link', { name: 'View task details' })
45
+ ).toHaveAttribute('href', '/foreman_tasks/tasks/task-1');
46
+ expect(
47
+ screen.getByRole('button', { name: 'Generate and upload report' })
48
+ ).toBeDisabled();
49
+ expect(
50
+ screen.getByRole('button', { name: 'Generate report' })
51
+ ).toBeDisabled();
29
52
  });
30
53
 
31
- it('should handle missing account gracefully', () => {
32
- const wrapper = shallow(<Dashboard account={null} onTaskStart={null} />);
54
+ it('shows an empty state when account data is missing', async () => {
55
+ renderDashboard({ account: null, onTaskStart: null });
33
56
 
34
- const taskProgress = wrapper.find('TaskProgress');
35
- expect(taskProgress).toHaveLength(1);
36
- expect(taskProgress.prop('task')).toBeNull();
37
- expect(taskProgress.prop('organizationId')).toBeNull();
38
- expect(taskProgress.prop('emptyMessage')).toBe(
39
- 'No account data available.'
40
- );
57
+ expect(await screen.findByText('No recent tasks')).toBeInTheDocument();
58
+ expect(screen.getByText('No account data available.')).toBeInTheDocument();
59
+ expect(
60
+ screen.queryByRole('button', { name: 'Generate report' })
61
+ ).not.toBeInTheDocument();
41
62
  });
42
63
 
43
- it('should handle missing generate_task gracefully', () => {
44
- const account = { id: 1, generate_task: null };
45
- const wrapper = shallow(<Dashboard account={account} onTaskStart={null} />);
64
+ it('shows generate actions when the account has no generate task', async () => {
65
+ renderDashboard({
66
+ account: { id: 1, generate_task: null },
67
+ onTaskStart: null,
68
+ });
46
69
 
47
- const taskProgress = wrapper.find('TaskProgress');
48
- expect(taskProgress).toHaveLength(1);
49
- expect(taskProgress.prop('task')).toBeNull();
50
- expect(taskProgress.prop('organizationId')).toBe(1);
70
+ expect(
71
+ await screen.findByText('No report generation tasks have been run yet.')
72
+ ).toBeInTheDocument();
73
+ expect(
74
+ screen.getByRole('button', { name: 'Generate and upload report' })
75
+ ).toBeInTheDocument();
76
+ expect(
77
+ screen.getByRole('button', { name: 'Generate report' })
78
+ ).toBeInTheDocument();
51
79
  });
52
80
 
53
- it('should handle missing account.id gracefully', () => {
54
- const account = {
55
- generate_task: {
56
- id: 'task-1',
57
- state: 'running',
58
- },
59
- };
60
- const wrapper = shallow(<Dashboard account={account} onTaskStart={null} />);
81
+ it('renders the task without generate actions when account id is missing', async () => {
82
+ renderDashboard({
83
+ account: { generate_task: runningTask },
84
+ onTaskStart: null,
85
+ });
61
86
 
62
- const taskProgress = wrapper.find('TaskProgress');
63
- expect(taskProgress).toHaveLength(1);
64
- expect(taskProgress.prop('organizationId')).toBeNull();
65
- expect(taskProgress.prop('task')).toEqual(account.generate_task);
87
+ expect(
88
+ await screen.findByRole('link', { name: 'View task details' })
89
+ ).toHaveAttribute('href', '/foreman_tasks/tasks/task-1');
90
+ expect(
91
+ screen.queryByRole('button', { name: 'Generate report' })
92
+ ).not.toBeInTheDocument();
93
+ expect(
94
+ screen.queryByRole('button', { name: 'Generate and upload report' })
95
+ ).not.toBeInTheDocument();
66
96
  });
67
97
  });
@@ -255,11 +255,7 @@ const TaskProgress = ({
255
255
  measureLocation="outside"
256
256
  aria-label="task-progress"
257
257
  />
258
- <DescriptionList
259
- isHorizontal
260
- className="task-progress-details"
261
- ouiaId="task-progress-details"
262
- >
258
+ <DescriptionList isHorizontal className="task-progress-details">
263
259
  <DescriptionListGroup>
264
260
  <DescriptionListTerm>{__('Started')}</DescriptionListTerm>
265
261
  <DescriptionListDescription>
@@ -10,6 +10,7 @@ import {
10
10
  hasNoInsightsFacet,
11
11
  } from './ForemanRhCloudHelpers';
12
12
  import CVEsHostDetailsTabWrapper from './CVEsHostDetailsTab/CVEsHostDetailsTab';
13
+ import ComplianceHostDetailsTabWrapper from './ComplianceHostDetailsTab/ComplianceHostDetailsTab';
13
14
  import InsightsVulnerabilityActionsBar from './InsightsVulnerabilityActionsBar';
14
15
 
15
16
  const fills = [
@@ -51,6 +52,16 @@ const fills = [
51
52
  title: __('Vulnerabilities'),
52
53
  },
53
54
  },
55
+ {
56
+ slot: 'host-details-page-tabs',
57
+ name: 'Compliance',
58
+ component: props => <ComplianceHostDetailsTabWrapper {...props} />,
59
+ weight: 350,
60
+ metadata: {
61
+ hideTab: props => isNotRhelHost(props) || hasNoInsightsFacet(props),
62
+ title: __('Compliance'),
63
+ },
64
+ },
54
65
  ];
55
66
 
56
67
  export const registerFills = () => {
@@ -8,6 +8,7 @@ import IopRecommendationDetails from './IopRecommendationDetails/IopRecommendati
8
8
  import IopPathwayDetails from './IopPathwayDetails/IopPathwayDetails';
9
9
  import InsightsHostDetailsTab from './InsightsHostDetailsTab';
10
10
  import CveDetailsPage from './CveDetailsPage';
11
+ import InsightsCompliancePage from './InsightsCompliance/InsightsCompliancePage';
11
12
  import './common/styles.scss';
12
13
 
13
14
  const pages = [
@@ -20,6 +21,7 @@ const pages = [
20
21
  type: InsightsVulnerabilityListPage,
21
22
  },
22
23
  { name: 'CveDetailsPage', type: CveDetailsPage },
24
+ { name: 'InsightsCompliancePage', type: InsightsCompliancePage },
23
25
  ];
24
26
 
25
27
  export const registerPages = () => {
@@ -57,6 +59,11 @@ export const routes = [
57
59
  exact: true,
58
60
  render: props => <CveDetailsPage {...props} />,
59
61
  },
62
+ {
63
+ path: '/foreman_rh_cloud/insights_compliance',
64
+ exact: false,
65
+ render: props => <InsightsCompliancePage {...props} />,
66
+ },
60
67
  ];
61
68
 
62
69
  export const registerRoutes = () => {
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import IopAppIframe from '../common/IopIframe/IopAppIframe';
4
+ import {
5
+ getComplianceAppRoute,
6
+ getComplianceForemanPath,
7
+ getComplianceIframeSrc,
8
+ } from './complianceIframeHelpers';
9
+ import {
10
+ IOP_COMPLIANCE_NAVIGATE,
11
+ IOP_COMPLIANCE_READY,
12
+ } from './complianceIframeConstants';
13
+
14
+ import './ComplianceIframe.scss';
15
+
16
+ const ComplianceIframe = ({ appName }) => (
17
+ <IopAppIframe
18
+ appName={appName}
19
+ iframeSrc={getComplianceIframeSrc()}
20
+ getAppRoute={getComplianceAppRoute}
21
+ getForemanPath={getComplianceForemanPath}
22
+ defaultAppRoute="reports"
23
+ readyMessageType={IOP_COMPLIANCE_READY}
24
+ navigateMessageType={IOP_COMPLIANCE_NAVIGATE}
25
+ className="rh-cloud-insights-compliance-iframe"
26
+ testId="compliance-iframe"
27
+ />
28
+ );
29
+
30
+ ComplianceIframe.propTypes = {
31
+ appName: PropTypes.string.isRequired,
32
+ };
33
+
34
+ export default ComplianceIframe;
@@ -0,0 +1,12 @@
1
+ .rh-cloud-insights-compliance-page {
2
+ display: flex;
3
+ flex-direction: column;
4
+ min-height: calc(100vh - var(--pf-v5-global--spacer--2xl, 3rem));
5
+ }
6
+
7
+ .rh-cloud-insights-compliance-iframe {
8
+ border: 0;
9
+ flex: 1 1 auto;
10
+ min-height: 70vh;
11
+ width: 100%;
12
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+
3
+ import ComplianceIframe from './ComplianceIframe';
4
+
5
+ const InsightsCompliancePage = () => (
6
+ <div className="rh-cloud-insights-compliance-page compliance">
7
+ <ComplianceIframe appName="Compliance" />
8
+ </div>
9
+ );
10
+
11
+ export default InsightsCompliancePage;
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import { MemoryRouter } from 'react-router-dom';
5
+
6
+ import InsightsCompliancePage from './InsightsCompliancePage';
7
+
8
+ jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
9
+ useForemanContext: () => ({
10
+ metadata: {
11
+ permissions: new Set(['view_compliance']),
12
+ },
13
+ }),
14
+ useForemanPermissions: () => new Set(['view_compliance']),
15
+ }));
16
+
17
+ describe('InsightsCompliancePage', () => {
18
+ it('renders the compliance iframe container', () => {
19
+ render(
20
+ <MemoryRouter
21
+ initialEntries={['/foreman_rh_cloud/insights_compliance/reports']}
22
+ >
23
+ <InsightsCompliancePage />
24
+ </MemoryRouter>
25
+ );
26
+
27
+ expect(
28
+ document.querySelector('.rh-cloud-insights-compliance-page')
29
+ ).toBeTruthy();
30
+ expect(screen.getByTestId('compliance-iframe')).toHaveAttribute(
31
+ 'src',
32
+ `${window.location.origin}/assets/apps/compliance/index.html`
33
+ );
34
+ });
35
+ });
@@ -0,0 +1,7 @@
1
+ export const COMPLIANCE_STATIC_APP_PATH = '/assets/apps/compliance/index.html';
2
+
3
+ export const IOP_COMPLIANCE_READY = 'IOP_COMPLIANCE_READY';
4
+
5
+ export const IOP_COMPLIANCE_NAVIGATE = 'IOP_COMPLIANCE_NAVIGATE';
6
+
7
+ export const COMPLIANCE_ROUTE_PREFIX = '/foreman_rh_cloud/insights_compliance';
@@ -0,0 +1,23 @@
1
+ import {
2
+ COMPLIANCE_ROUTE_PREFIX,
3
+ COMPLIANCE_STATIC_APP_PATH,
4
+ } from './complianceIframeConstants';
5
+ import {
6
+ getIopAppRoute,
7
+ getIopForemanPath,
8
+ getIopIframeSrc,
9
+ } from '../common/IopIframe/iopIframeHelpers';
10
+
11
+ const COMPLIANCE_IOP_PATH = {
12
+ routePrefix: COMPLIANCE_ROUTE_PREFIX,
13
+ fallbackRoute: 'reports',
14
+ };
15
+
16
+ export const getComplianceForemanPath = (appRoute = 'reports') =>
17
+ getIopForemanPath(appRoute, COMPLIANCE_IOP_PATH);
18
+
19
+ export const getComplianceAppRoute = (pathname = '') =>
20
+ getIopAppRoute(pathname, COMPLIANCE_IOP_PATH);
21
+
22
+ export const getComplianceIframeSrc = () =>
23
+ getIopIframeSrc(COMPLIANCE_STATIC_APP_PATH);
@@ -0,0 +1,50 @@
1
+ import {
2
+ getComplianceAppRoute,
3
+ getComplianceForemanPath,
4
+ getComplianceIframeSrc,
5
+ } from './complianceIframeHelpers';
6
+
7
+ describe('complianceIframeHelpers', () => {
8
+ describe('getComplianceAppRoute', () => {
9
+ it('maps foreman compliance paths to app routes', () => {
10
+ expect(
11
+ getComplianceAppRoute('/foreman_rh_cloud/insights_compliance/reports')
12
+ ).toBe('reports');
13
+ expect(
14
+ getComplianceAppRoute(
15
+ '/foreman_rh_cloud/insights_compliance/reports/report-1'
16
+ )
17
+ ).toBe('reports/report-1');
18
+ expect(
19
+ getComplianceAppRoute(
20
+ '/foreman_rh_cloud/insights_compliance/scappolicies/new'
21
+ )
22
+ ).toBe('scappolicies/new');
23
+ });
24
+
25
+ it('defaults to reports for unknown paths', () => {
26
+ expect(getComplianceAppRoute('/foreman_rh_cloud/insights_cloud')).toBe(
27
+ 'reports'
28
+ );
29
+ });
30
+ });
31
+
32
+ describe('getComplianceForemanPath', () => {
33
+ it('maps app routes to foreman compliance paths', () => {
34
+ expect(getComplianceForemanPath('reports')).toBe(
35
+ '/foreman_rh_cloud/insights_compliance/reports'
36
+ );
37
+ expect(getComplianceForemanPath('scappolicies/new')).toBe(
38
+ '/foreman_rh_cloud/insights_compliance/scappolicies/new'
39
+ );
40
+ });
41
+ });
42
+
43
+ describe('getComplianceIframeSrc', () => {
44
+ it('returns the static compliance app entry', () => {
45
+ expect(getComplianceIframeSrc()).toBe(
46
+ `${window.location.origin}/assets/apps/compliance/index.html`
47
+ );
48
+ });
49
+ });
50
+ });
@@ -25,6 +25,16 @@ const PERMISSION_MAPPING = {
25
25
  ],
26
26
  view_advisor: ['advisor:recommendation-results:read', 'advisor:exports:read'],
27
27
  edit_advisor: ['advisor:disable-recommendations:write'],
28
+ view_compliance: [
29
+ 'inventory:hosts:read',
30
+ 'compliance:report:read',
31
+ 'compliance:policy:read',
32
+ ],
33
+ edit_compliance: [
34
+ 'compliance:policy:write',
35
+ 'compliance:policy:create',
36
+ 'compliance:policy:delete',
37
+ ],
28
38
  };
29
39
 
30
40
  /**
@@ -0,0 +1,74 @@
1
+ import React, { useRef } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useHistory, useLocation } from 'react-router-dom';
4
+ import { useInsightsPermissions } from '../Hooks/PermissionsHooks';
5
+ import { useIopIframeChrome } from './useIopIframeChrome';
6
+ import { useIopIframeNavigate } from './useIopIframeNavigate';
7
+
8
+ /**
9
+ * Full-page IoP app iframe: chrome sync + Foreman URL updates from NAVIGATE.
10
+ * App shells supply src, route mappers, and message types.
11
+ */
12
+ const IopAppIframe = ({
13
+ appName,
14
+ iframeSrc,
15
+ getAppRoute,
16
+ getForemanPath,
17
+ defaultAppRoute,
18
+ readyMessageType,
19
+ navigateMessageType,
20
+ className = '',
21
+ testId = 'iop-app-iframe',
22
+ }) => {
23
+ const iframeRef = useRef(null);
24
+ const history = useHistory();
25
+ const { pathname } = useLocation();
26
+ const permissions = useInsightsPermissions();
27
+ const appRoute = getAppRoute(pathname);
28
+
29
+ const sendChromeContext = useIopIframeChrome({
30
+ iframeRef,
31
+ permissions,
32
+ appRoute,
33
+ pathname,
34
+ readyMessageType,
35
+ });
36
+
37
+ useIopIframeNavigate({
38
+ history,
39
+ navigateMessageType,
40
+ getForemanPath,
41
+ defaultAppRoute,
42
+ });
43
+
44
+ return (
45
+ // eslint-disable-next-line jsx-a11y/iframe-has-title
46
+ <iframe
47
+ ref={iframeRef}
48
+ className={className}
49
+ data-testid={testId}
50
+ src={iframeSrc}
51
+ aria-label={appName}
52
+ onLoad={sendChromeContext}
53
+ />
54
+ );
55
+ };
56
+
57
+ IopAppIframe.propTypes = {
58
+ appName: PropTypes.string.isRequired,
59
+ iframeSrc: PropTypes.string.isRequired,
60
+ getAppRoute: PropTypes.func.isRequired,
61
+ getForemanPath: PropTypes.func.isRequired,
62
+ defaultAppRoute: PropTypes.string.isRequired,
63
+ readyMessageType: PropTypes.string.isRequired,
64
+ navigateMessageType: PropTypes.string.isRequired,
65
+ className: PropTypes.string,
66
+ testId: PropTypes.string,
67
+ };
68
+
69
+ IopAppIframe.defaultProps = {
70
+ className: '',
71
+ testId: 'iop-app-iframe',
72
+ };
73
+
74
+ export default IopAppIframe;