foreman_rh_cloud 14.0.3 → 14.1.1

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/insights_cloud/candlepin_proxies_extensions.rb +23 -0
  3. data/app/controllers/concerns/insights_cloud/package_profile_upload_extensions.rb +9 -0
  4. data/app/controllers/foreman_inventory_upload/accounts_controller.rb +1 -1
  5. data/app/controllers/foreman_inventory_upload/api/tasks_controller.rb +2 -2
  6. data/app/controllers/insights_cloud/api/machine_telemetries_controller.rb +9 -2
  7. data/app/models/concerns/rh_cloud_host.rb +35 -3
  8. data/app/models/insights_client_report_status.rb +9 -1
  9. data/app/models/inventory_sync/inventory_status.rb +16 -4
  10. data/lib/foreman_inventory_upload/async/create_missing_insights_facets.rb +8 -2
  11. data/lib/foreman_rh_cloud/engine.rb +1 -0
  12. data/lib/foreman_rh_cloud/version.rb +1 -1
  13. data/lib/insights_cloud/async/vmaas_reposcan_sync.rb +23 -8
  14. data/lib/inventory_sync/async/inventory_full_sync.rb +39 -3
  15. data/package.json +1 -1
  16. data/test/controllers/insights_cloud/api/machine_telemetries_controller_test.rb +40 -0
  17. data/test/controllers/insights_cloud/candlepin_proxies_extensions_test.rb +70 -0
  18. data/test/jobs/insights_client_status_aging_test.rb +40 -0
  19. data/test/jobs/inventory_full_sync_test.rb +212 -0
  20. data/test/models/insights_client_report_status_test.rb +109 -0
  21. data/test/models/inventory_sync/inventory_status_test.rb +85 -0
  22. data/test/unit/lib/insights_cloud/async/vmaas_reposcan_sync_test.rb +80 -25
  23. data/test/unit/rh_cloud_host_test.rb +214 -0
  24. data/webpack/CVEsHostDetailsTab/CVEsHostDetailsTab.js +30 -3
  25. data/webpack/CVEsHostDetailsTab/__tests__/CVEsHostDetailsTab.test.js +112 -10
  26. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/SyncButtonActions.js +8 -2
  27. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/__snapshots__/integrations.test.js.snap +1 -0
  28. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/integrations.test.js +1 -0
  29. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/components/Toast.js +43 -17
  30. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/components/__tests__/Toast.test.js +82 -0
  31. data/webpack/ForemanRhCloudHelpers.js +22 -0
  32. data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsTable.js +7 -4
  33. data/webpack/InsightsCloudSync/Components/InsightsTable/table.scss +9 -0
  34. data/webpack/InsightsHostDetailsTab/NewHostDetailsTab.js +53 -12
  35. data/webpack/InsightsHostDetailsTab/__tests__/NewHostDetailsTab.test.js +134 -22
  36. metadata +7 -1
@@ -188,4 +188,218 @@ class RhCloudHostTest < ActiveSupport::TestCase
188
188
  assert_equal local_uuid, @host.insights_uuid
189
189
  end
190
190
  end
191
+
192
+ context 'scoped search on insights_uuid' do
193
+ setup do
194
+ @org = FactoryBot.create(:organization)
195
+ end
196
+
197
+ teardown do
198
+ ForemanRhCloud.unstub(:with_iop_smart_proxy?)
199
+ end
200
+
201
+ test 'searches insights_facet.uuid in non-IoP mode with = operator' do
202
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
203
+ host1 = FactoryBot.create(:host, :managed, organization: @org)
204
+ host1.insights = FactoryBot.create(:insights_facet, host_id: host1.id, uuid: 'insights-uuid-123')
205
+ host2 = FactoryBot.create(:host, :managed, organization: @org)
206
+ host2.insights = FactoryBot.create(:insights_facet, host_id: host2.id, uuid: 'insights-uuid-456')
207
+
208
+ results = Host::Managed.search_for('insights_uuid = insights-uuid-123')
209
+
210
+ assert_includes results, host1
211
+ assert_not_includes results, host2
212
+ end
213
+
214
+ test 'searches subscription_facet.uuid in IoP mode with = operator' do
215
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
216
+ host1 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
217
+ host2 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
218
+
219
+ # Even if insights_facet has different UUID, should use subscription_facet UUID
220
+ host1.insights = FactoryBot.create(:insights_facet, host_id: host1.id, uuid: 'stale-123')
221
+
222
+ results = Host::Managed.search_for("insights_uuid = #{host1.subscription_facet.uuid}")
223
+
224
+ assert_includes results, host1
225
+ assert_not_includes results, host2
226
+ end
227
+
228
+ test 'searches with ^ operator (IN) in non-IoP mode' do
229
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
230
+ host1 = FactoryBot.create(:host, :managed, organization: @org)
231
+ host1.insights = FactoryBot.create(:insights_facet, host_id: host1.id, uuid: 'uuid-1')
232
+ host2 = FactoryBot.create(:host, :managed, organization: @org)
233
+ host2.insights = FactoryBot.create(:insights_facet, host_id: host2.id, uuid: 'uuid-2')
234
+ host3 = FactoryBot.create(:host, :managed, organization: @org)
235
+ host3.insights = FactoryBot.create(:insights_facet, host_id: host3.id, uuid: 'uuid-3')
236
+
237
+ results = Host::Managed.search_for('insights_uuid ^ (uuid-1,uuid-2)')
238
+
239
+ assert_includes results, host1
240
+ assert_includes results, host2
241
+ assert_not_includes results, host3
242
+ end
243
+
244
+ test 'searches with ^ operator (IN) in IoP mode - THE BUG FIX' do
245
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
246
+ host1 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
247
+ host2 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
248
+ host3 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
249
+
250
+ # Create insights facets with stale UUIDs to verify we're using subscription_facet
251
+ host1.insights = FactoryBot.create(:insights_facet, host_id: host1.id, uuid: 'stale-1')
252
+ host2.insights = FactoryBot.create(:insights_facet, host_id: host2.id, uuid: 'stale-2')
253
+ host3.insights = FactoryBot.create(:insights_facet, host_id: host3.id, uuid: 'stale-3')
254
+
255
+ uuid1 = host1.subscription_facet.uuid
256
+ uuid2 = host2.subscription_facet.uuid
257
+
258
+ # This is the search query that remediation modal creates
259
+ results = Host::Managed.search_for("insights_uuid ^ (#{uuid1},#{uuid2})")
260
+
261
+ # Should find hosts by subscription_facet UUID, not insights_facet UUID
262
+ assert_includes results, host1
263
+ assert_includes results, host2
264
+ assert_not_includes results, host3
265
+ end
266
+
267
+ test 'searches with !^ operator (NOT IN) in non-IoP mode' do
268
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
269
+ host1 = FactoryBot.create(:host, :managed, organization: @org)
270
+ host1.insights = FactoryBot.create(:insights_facet, host_id: host1.id, uuid: 'uuid-1')
271
+ host2 = FactoryBot.create(:host, :managed, organization: @org)
272
+ host2.insights = FactoryBot.create(:insights_facet, host_id: host2.id, uuid: 'uuid-2')
273
+ host3 = FactoryBot.create(:host, :managed, organization: @org)
274
+ host3.insights = FactoryBot.create(:insights_facet, host_id: host3.id, uuid: 'uuid-3')
275
+
276
+ results = Host::Managed.search_for('insights_uuid !^ (uuid-1,uuid-2)')
277
+
278
+ assert_not_includes results, host1
279
+ assert_not_includes results, host2
280
+ assert_includes results, host3
281
+ end
282
+
283
+ test 'searches with !^ operator (NOT IN) in IoP mode' do
284
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
285
+ host1 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
286
+ host2 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
287
+ host3 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
288
+
289
+ uuid1 = host1.subscription_facet.uuid
290
+ uuid2 = host2.subscription_facet.uuid
291
+
292
+ results = Host::Managed.search_for("insights_uuid !^ (#{uuid1},#{uuid2})")
293
+
294
+ assert_not_includes results, host1
295
+ assert_not_includes results, host2
296
+ assert_includes results, host3
297
+ end
298
+
299
+ test 'handles hosts without facets in non-IoP mode' do
300
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
301
+ host_without_facet = FactoryBot.create(:host, :managed, organization: @org)
302
+ host_with_facet = FactoryBot.create(:host, :managed, organization: @org)
303
+ host_with_facet.insights = FactoryBot.create(:insights_facet, host_id: host_with_facet.id, uuid: 'uuid-1')
304
+
305
+ results = Host::Managed.search_for('insights_uuid = uuid-1')
306
+
307
+ assert_includes results, host_with_facet
308
+ assert_not_includes results, host_without_facet
309
+ end
310
+
311
+ test 'handles hosts without subscription_facet in IoP mode' do
312
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
313
+ host_without_sub = FactoryBot.create(:host, :managed, organization: @org)
314
+ host_with_sub = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
315
+
316
+ uuid = host_with_sub.subscription_facet.uuid
317
+
318
+ results = Host::Managed.search_for("insights_uuid = #{uuid}")
319
+
320
+ assert_includes results, host_with_sub
321
+ assert_not_includes results, host_without_sub
322
+ end
323
+
324
+ test 'mode changes are reflected in searches' do
325
+ host1 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
326
+ host1.insights = FactoryBot.create(:insights_facet, host_id: host1.id, uuid: 'insights-uuid-abc')
327
+ insights_uuid = 'insights-uuid-abc'
328
+ subscription_uuid = host1.subscription_facet.uuid
329
+
330
+ # Non-IoP mode: should find by insights_facet UUID
331
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
332
+ results = Host::Managed.search_for("insights_uuid = #{insights_uuid}")
333
+ assert_includes results, host1
334
+
335
+ # IoP mode: should find by subscription_facet UUID
336
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
337
+ results = Host::Managed.search_for("insights_uuid = #{subscription_uuid}")
338
+ assert_includes results, host1
339
+
340
+ # Should NOT find by old insights_facet UUID in IoP mode
341
+ results = Host::Managed.search_for("insights_uuid = #{insights_uuid}")
342
+ assert_not_includes results, host1
343
+ end
344
+ end
345
+
346
+ test 'scoped search for user_omitted inventory status works' do
347
+ host1 = FactoryBot.create(:host, :managed)
348
+ host2 = FactoryBot.create(:host, :managed)
349
+ host3 = FactoryBot.create(:host, :managed)
350
+
351
+ # Create different inventory statuses
352
+ InventorySync::InventoryStatus.create!(
353
+ host_id: host1.id,
354
+ status: InventorySync::InventoryStatus::SYNC,
355
+ reported_at: Time.zone.now
356
+ )
357
+
358
+ InventorySync::InventoryStatus.create!(
359
+ host_id: host2.id,
360
+ status: InventorySync::InventoryStatus::DISCONNECT,
361
+ reported_at: Time.zone.now
362
+ )
363
+
364
+ InventorySync::InventoryStatus.create!(
365
+ host_id: host3.id,
366
+ status: InventorySync::InventoryStatus::USER_OMITTED,
367
+ reported_at: Time.zone.now
368
+ )
369
+
370
+ # Search for user_omitted status
371
+ results = Host.search_for('insights_inventory_sync_status = user_omitted')
372
+ result_ids = results.pluck(:id)
373
+
374
+ assert_includes result_ids, host3.id, 'Host with USER_OMITTED status should be in search results'
375
+ assert_not_includes result_ids, host1.id, 'Host with SYNC status should not be in search results'
376
+ assert_not_includes result_ids, host2.id, 'Host with DISCONNECT status should not be in search results'
377
+ end
378
+
379
+ test 'scoped search for user_omitted insights client report status works' do
380
+ host1 = FactoryBot.create(:host, :managed)
381
+ host2 = FactoryBot.create(:host, :managed)
382
+ host3 = FactoryBot.create(:host, :managed)
383
+
384
+ # Create different insights client report statuses
385
+ status1 = host1.get_status(InsightsClientReportStatus)
386
+ status1.status = InsightsClientReportStatus::REPORTING
387
+ status1.save!
388
+
389
+ status2 = host2.get_status(InsightsClientReportStatus)
390
+ status2.status = InsightsClientReportStatus::NO_REPORT
391
+ status2.save!
392
+
393
+ status3 = host3.get_status(InsightsClientReportStatus)
394
+ status3.status = InsightsClientReportStatus::USER_OMITTED
395
+ status3.save!
396
+
397
+ # Search for user_omitted status
398
+ results = Host.search_for('insights_client_report_status = user_omitted')
399
+ result_ids = results.pluck(:id)
400
+
401
+ assert_includes result_ids, host3.id, 'Host with USER_OMITTED status should be in search results'
402
+ assert_not_includes result_ids, host1.id, 'Host with REPORTING status should not be in search results'
403
+ assert_not_includes result_ids, host2.id, 'Host with NO_REPORT status should not be in search results'
404
+ end
191
405
  end
@@ -3,6 +3,10 @@ import PropTypes from 'prop-types';
3
3
  import { ScalprumComponent, ScalprumProvider } from '@scalprum/react-core';
4
4
  import { createProviderOptions } from '../common/ScalprumModule/ScalprumContext';
5
5
  import { useInsightsPermissions } from '../common/Hooks/PermissionsHooks';
6
+ import {
7
+ vulnerabilityDisabled,
8
+ useTabRedirect,
9
+ } from '../ForemanRhCloudHelpers';
6
10
  import './CVEsHostDetailsTab.scss';
7
11
 
8
12
  const CVEsHostDetailsTab = ({ systemId }) => {
@@ -10,7 +14,12 @@ const CVEsHostDetailsTab = ({ systemId }) => {
10
14
  const module = './SystemDetailTable';
11
15
  return (
12
16
  <div className="rh-cloud-insights-vulnerability-host-details-component vulnerability">
13
- <ScalprumComponent scope={scope} module={module} systemId={systemId} />
17
+ <ScalprumComponent
18
+ key={systemId}
19
+ scope={scope}
20
+ module={module}
21
+ systemId={systemId}
22
+ />
14
23
  </div>
15
24
  );
16
25
  };
@@ -21,6 +30,15 @@ CVEsHostDetailsTab.propTypes = {
21
30
 
22
31
  const CVEsHostDetailsTabWrapper = ({ response }) => {
23
32
  const permissions = useInsightsPermissions();
33
+ const isHostDataLoaded = Boolean(response?.id);
34
+ const shouldHideTab = useTabRedirect(
35
+ isHostDataLoaded && vulnerabilityDisabled({ hostDetails: response })
36
+ );
37
+
38
+ if (shouldHideTab) {
39
+ return null;
40
+ }
41
+
24
42
  return (
25
43
  <ScalprumProvider {...createProviderOptions(permissions)}>
26
44
  <CVEsHostDetailsTab
@@ -33,10 +51,19 @@ const CVEsHostDetailsTabWrapper = ({ response }) => {
33
51
 
34
52
  CVEsHostDetailsTabWrapper.propTypes = {
35
53
  response: PropTypes.shape({
54
+ id: PropTypes.number,
55
+ operatingsystem_name: PropTypes.string,
56
+ vulnerability: PropTypes.shape({
57
+ enabled: PropTypes.bool,
58
+ }),
36
59
  subscription_facet_attributes: PropTypes.shape({
37
- uuid: PropTypes.string.isRequired,
60
+ uuid: PropTypes.string,
38
61
  }),
39
- }).isRequired,
62
+ }),
63
+ };
64
+
65
+ CVEsHostDetailsTabWrapper.defaultProps = {
66
+ response: {},
40
67
  };
41
68
 
42
69
  export default CVEsHostDetailsTabWrapper;
@@ -1,6 +1,17 @@
1
1
  import React from 'react';
2
2
  import { render } from '@testing-library/react';
3
+ import { MemoryRouter } from 'react-router-dom';
3
4
  import CVEsHostDetailsTabWrapper from '../CVEsHostDetailsTab';
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
+ }));
4
15
 
5
16
  jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
6
17
  useForemanContext: () => ({
@@ -11,24 +22,115 @@ jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
11
22
  useForemanPermissions: () => new Set(['view_vulnerability']),
12
23
  }));
13
24
 
14
- jest.mock('@scalprum/react-core', () => ({
15
- ScalprumComponent: jest.fn(props => (
16
- <div data-testid="mock-scalprum-component">{JSON.stringify(props)}</div>
17
- )),
18
- ScalprumProvider: jest.fn(({ children }) => <div>{children}</div>),
19
- }));
25
+ const mockUnmountTracker = jest.fn();
26
+ jest.mock('@scalprum/react-core', () => {
27
+ const ReactMock = require('react');
28
+ return {
29
+ ScalprumComponent: jest.fn(props => {
30
+ ReactMock.useEffect(() => mockUnmountTracker, []);
31
+ return (
32
+ <div data-testid="mock-scalprum-component">{JSON.stringify(props)}</div>
33
+ );
34
+ }),
35
+ ScalprumProvider: jest.fn(({ children }) => <div>{children}</div>),
36
+ };
37
+ });
38
+
39
+ const defaultResponse = {
40
+ id: 1,
41
+ operatingsystem_name: 'Red Hat Enterprise Linux 8',
42
+ vulnerability: { enabled: true },
43
+ subscription_facet_attributes: { uuid: '1-2-3' },
44
+ };
20
45
 
21
46
  describe('CVEsHostDetailsTabWrapper', () => {
22
- it('renders without crashing', () => {
47
+ beforeEach(() => {
48
+ jest.clearAllMocks();
49
+ });
50
+
51
+ it('renders without crashing and does not redirect for valid host', () => {
23
52
  const { container } = render(
24
- <CVEsHostDetailsTabWrapper
25
- response={{ subscription_facet_attributes: { uuid: '1-2-3' } }}
26
- />
53
+ <MemoryRouter>
54
+ <CVEsHostDetailsTabWrapper response={defaultResponse} />
55
+ </MemoryRouter>
27
56
  );
28
57
  expect(
29
58
  container.querySelector(
30
59
  '.rh-cloud-insights-vulnerability-host-details-component'
31
60
  )
32
61
  ).toBeTruthy();
62
+ expect(mockHistoryReplace).not.toHaveBeenCalled();
63
+ });
64
+
65
+ it('remounts ScalprumComponent when systemId changes', () => {
66
+ const { ScalprumComponent } = require('@scalprum/react-core');
67
+
68
+ const responseHostA = {
69
+ ...defaultResponse,
70
+ subscription_facet_attributes: { uuid: 'uuid-host-A' },
71
+ };
72
+
73
+ const { rerender } = render(
74
+ <MemoryRouter>
75
+ <CVEsHostDetailsTabWrapper response={responseHostA} />
76
+ </MemoryRouter>
77
+ );
78
+
79
+ expect(mockUnmountTracker).not.toHaveBeenCalled();
80
+ expect(ScalprumComponent).toHaveBeenLastCalledWith(
81
+ expect.objectContaining({ systemId: 'uuid-host-A' }),
82
+ expect.anything()
83
+ );
84
+
85
+ const responseHostB = {
86
+ ...defaultResponse,
87
+ subscription_facet_attributes: { uuid: 'uuid-host-B' },
88
+ };
89
+
90
+ rerender(
91
+ <MemoryRouter>
92
+ <CVEsHostDetailsTabWrapper response={responseHostB} />
93
+ </MemoryRouter>
94
+ );
95
+
96
+ expect(mockUnmountTracker).toHaveBeenCalledTimes(1);
97
+ expect(ScalprumComponent).toHaveBeenLastCalledWith(
98
+ expect.objectContaining({ systemId: 'uuid-host-B' }),
99
+ expect.anything()
100
+ );
101
+ });
102
+
103
+ it('redirects to Overview when tab should be hidden', () => {
104
+ const nonRhelResponse = {
105
+ id: 2,
106
+ operatingsystem_name: 'Ubuntu 20.04',
107
+ vulnerability: { enabled: false },
108
+ subscription_facet_attributes: { uuid: '1-2-3' },
109
+ };
110
+
111
+ const { container } = render(
112
+ <MemoryRouter>
113
+ <CVEsHostDetailsTabWrapper response={nonRhelResponse} />
114
+ </MemoryRouter>
115
+ );
116
+
117
+ expect(mockHistoryReplace).toHaveBeenCalledWith(OVERVIEW_TAB_PATH);
118
+ expect(
119
+ container.querySelector(
120
+ '.rh-cloud-insights-vulnerability-host-details-component'
121
+ )
122
+ ).toBeNull();
123
+ });
124
+
125
+ it('does not redirect when host data is not yet loaded', () => {
126
+ const emptyResponse = { subscription_facet_attributes: { uuid: '1-2-3' } };
127
+
128
+ render(
129
+ <MemoryRouter>
130
+ <CVEsHostDetailsTabWrapper response={emptyResponse} />
131
+ </MemoryRouter>
132
+ );
133
+
134
+ expect(mockHistoryReplace).not.toHaveBeenCalled();
33
135
  });
34
136
  });
@@ -39,14 +39,20 @@ export const setupInventorySyncTaskPolling = (id, dispatch) =>
39
39
  key: INVENTORY_SYNC_TASK_UPDATE,
40
40
  onTaskSuccess: ({
41
41
  output: {
42
- host_statuses: { sync, disconnect },
42
+ host_statuses: { sync, disconnect, user_omitted: userOmitted },
43
43
  },
44
44
  }) =>
45
45
  dispatch(
46
46
  addToast({
47
47
  sticky: true,
48
48
  type: 'success',
49
- message: <Toast syncHosts={sync} disconnectHosts={disconnect} />,
49
+ message: (
50
+ <Toast
51
+ syncHosts={sync}
52
+ disconnectHosts={disconnect}
53
+ userOmittedHosts={userOmitted}
54
+ />
55
+ ),
50
56
  })
51
57
  ),
52
58
  dispatch,
@@ -9,6 +9,7 @@ Array [
9
9
  "message": <Toast
10
10
  disconnectHosts={2}
11
11
  syncHosts={0}
12
+ userOmittedHosts={1}
12
13
  />,
13
14
  "sticky": true,
14
15
  "type": "success",
@@ -30,6 +30,7 @@ describe('SyncButton integration test', () => {
30
30
  host_statuses: {
31
31
  sync: 0,
32
32
  disconnect: 2,
33
+ user_omitted: 1,
33
34
  },
34
35
  },
35
36
  result: 'success',
@@ -1,33 +1,55 @@
1
1
  import React from 'react';
2
+ import { Link } from 'react-router-dom';
2
3
  import PropTypes from 'prop-types';
3
4
  import { translate as __ } from 'foremanReact/common/I18n';
4
- import { foremanUrl } from '../../../../../../ForemanRhCloudHelpers';
5
5
 
6
- const Toast = ({ syncHosts, disconnectHosts }) => {
7
- const totalHosts = syncHosts + disconnectHosts;
6
+ const statusSearchParams = statusName =>
7
+ `/new/hosts?search=insights_inventory_sync_status+%3D+${statusName}&page=1`;
8
+ const DISCONNECT = 'disconnect';
9
+ const SYNC = 'sync';
10
+ const USER_OMITTED = 'user_omitted';
11
+ const HostsWithStatusLink = ({ statusName, children }) => (
12
+ <Link to={statusSearchParams(statusName)}>{children}</Link>
13
+ );
14
+ HostsWithStatusLink.propTypes = {
15
+ statusName: PropTypes.string.isRequired,
16
+ children: PropTypes.node.isRequired,
17
+ };
18
+
19
+ const Toast = ({ syncHosts, disconnectHosts, userOmittedHosts }) => {
20
+ const totalHosts = syncHosts + disconnectHosts + userOmittedHosts;
8
21
  return (
9
22
  <span>
10
23
  <p>
11
- {__('Hosts with subscription in organization: ')}
12
- <strong>{totalHosts}</strong>
24
+ {__('Registered hosts in organization: ')}
25
+ <Link to="/new/hosts?search=set%3F+subscription_uuid&page=1">
26
+ {totalHosts}
27
+ </Link>
13
28
  </p>
14
29
  <p>
15
- {__('Successfully synced hosts: ')}
16
- <strong>{syncHosts}</strong>
30
+ {__('Uploaded and present on console.redhat.com Inventory service: ')}
31
+ <HostsWithStatusLink statusName={SYNC}>{syncHosts}</HostsWithStatusLink>
17
32
  </p>
18
33
  <p>
19
- {__('Disconnected hosts: ')}
20
- <strong>{disconnectHosts}</strong>
34
+ {__('Not present on console.redhat.com Inventory service: ')}
35
+ <HostsWithStatusLink statusName={DISCONNECT}>
36
+ {disconnectHosts}
37
+ </HostsWithStatusLink>
21
38
  </p>
39
+ {!!userOmittedHosts && (
40
+ <p>
41
+ {__(
42
+ 'Excluded from upload to console.redhat.com Inventory service because host_registration_insights_inventory parameter value is false: '
43
+ )}
44
+ <HostsWithStatusLink statusName={USER_OMITTED}>
45
+ {userOmittedHosts}
46
+ </HostsWithStatusLink>
47
+ </p>
48
+ )}
22
49
  <p>
23
- {__('For more info, please visit the')}{' '}
24
- <a
25
- href={foremanUrl('/hosts')}
26
- target="_blank"
27
- rel="noopener noreferrer"
28
- >
29
- {__('hosts page')}
30
- </a>
50
+ {__(
51
+ 'You can review this information later by looking at the Inventory status of each host.'
52
+ )}
31
53
  </p>
32
54
  </span>
33
55
  );
@@ -36,6 +58,10 @@ const Toast = ({ syncHosts, disconnectHosts }) => {
36
58
  Toast.propTypes = {
37
59
  syncHosts: PropTypes.number.isRequired,
38
60
  disconnectHosts: PropTypes.number.isRequired,
61
+ userOmittedHosts: PropTypes.number,
62
+ };
63
+ Toast.defaultProps = {
64
+ userOmittedHosts: 0,
39
65
  };
40
66
 
41
67
  export default Toast;
@@ -0,0 +1,82 @@
1
+ import React from 'react';
2
+ import { shallow } from '@theforeman/test';
3
+ import Toast from '../Toast';
4
+
5
+ describe('Toast', () => {
6
+ it('renders with all three status counts including user_omitted', () => {
7
+ const wrapper = shallow(
8
+ <Toast syncHosts={5} disconnectHosts={3} userOmittedHosts={2} />
9
+ );
10
+
11
+ const links = wrapper.find('HostsWithStatusLink');
12
+ expect(links).toHaveLength(3);
13
+
14
+ // Check the children (numbers) of each link
15
+ expect(
16
+ links
17
+ .at(0)
18
+ .children()
19
+ .text()
20
+ ).toBe('5');
21
+ expect(
22
+ links
23
+ .at(1)
24
+ .children()
25
+ .text()
26
+ ).toBe('3');
27
+ expect(
28
+ links
29
+ .at(2)
30
+ .children()
31
+ .text()
32
+ ).toBe('2');
33
+ });
34
+
35
+ it('does not render user_omitted section when count is 0', () => {
36
+ const wrapper = shallow(
37
+ <Toast syncHosts={5} disconnectHosts={3} userOmittedHosts={0} />
38
+ );
39
+
40
+ // Should have only 2 HostsWithStatusLink components (sync and disconnect)
41
+ const links = wrapper.find('HostsWithStatusLink');
42
+ expect(links).toHaveLength(2);
43
+
44
+ // Should not contain the user_omitted explanation text
45
+ expect(wrapper.text()).not.toContain(
46
+ 'host_registration_insights_inventory parameter value is false'
47
+ );
48
+ });
49
+
50
+ it('renders without crashing when userOmittedHosts is not provided (default)', () => {
51
+ const wrapper = shallow(<Toast syncHosts={5} disconnectHosts={3} />);
52
+
53
+ // Should use default value of 0, so only 2 links
54
+ const links = wrapper.find('HostsWithStatusLink');
55
+ expect(links).toHaveLength(2);
56
+
57
+ // Verify the count values
58
+ expect(
59
+ links
60
+ .at(0)
61
+ .children()
62
+ .text()
63
+ ).toBe('5');
64
+ expect(
65
+ links
66
+ .at(1)
67
+ .children()
68
+ .text()
69
+ ).toBe('3');
70
+ });
71
+
72
+ it('renders correct status links for each category', () => {
73
+ const wrapper = shallow(
74
+ <Toast syncHosts={5} disconnectHosts={3} userOmittedHosts={2} />
75
+ );
76
+
77
+ const links = wrapper.find('HostsWithStatusLink');
78
+ expect(links.at(0).prop('statusName')).toBe('sync');
79
+ expect(links.at(1).prop('statusName')).toBe('disconnect');
80
+ expect(links.at(2).prop('statusName')).toBe('user_omitted');
81
+ });
82
+ });
@@ -1,3 +1,6 @@
1
+ import { useEffect } from 'react';
2
+ import { useHistory } from 'react-router-dom';
3
+
1
4
  /**
2
5
  * copied from core, since it's not in the ReactApp folder,
3
6
  * it's complicated to import it and mock it in tests.
@@ -5,6 +8,25 @@
5
8
  */
6
9
  export const foremanUrl = path => `${window.URL_PREFIX}${path}`;
7
10
 
11
+ export const OVERVIEW_TAB_PATH = '/Overview';
12
+
13
+ /**
14
+ * Redirects to Overview tab when the current tab should be hidden
15
+ * @param {boolean} shouldRedirect - Whether to redirect (e.g., host loaded AND tab should hide)
16
+ * @returns {boolean} - Returns shouldRedirect for convenience
17
+ */
18
+ export const useTabRedirect = shouldRedirect => {
19
+ const history = useHistory();
20
+
21
+ useEffect(() => {
22
+ if (shouldRedirect && history) {
23
+ history.replace(OVERVIEW_TAB_PATH);
24
+ }
25
+ }, [shouldRedirect, history]);
26
+
27
+ return shouldRedirect;
28
+ };
29
+
8
30
  export const isNotRhelHost = ({ hostDetails }) =>
9
31
  // This regex tries matches sane variations of "RedHat", "RHEL" and "RHCOS"
10
32
  !new RegExp('red[\\s\\-]?hat|rh[\\s\\-]?el|rhc[\\s\\-]?os', 'i').test(