foreman_rh_cloud 12.2.9 → 12.2.11

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/foreman_rh_cloud/locale/fr/foreman_rh_cloud.js +47 -26
  3. data/app/assets/javascripts/foreman_rh_cloud/locale/ja/foreman_rh_cloud.js +46 -25
  4. data/app/assets/javascripts/foreman_rh_cloud/locale/ka/foreman_rh_cloud.js +32 -11
  5. data/app/assets/javascripts/foreman_rh_cloud/locale/ko/foreman_rh_cloud.js +46 -25
  6. data/app/assets/javascripts/foreman_rh_cloud/locale/zh_CN/foreman_rh_cloud.js +46 -25
  7. data/app/controllers/concerns/insights_cloud/package_profile_upload_extensions.rb +2 -3
  8. data/app/services/foreman_rh_cloud/cert_auth.rb +13 -3
  9. data/app/services/foreman_rh_cloud/insights_api_forwarder.rb +3 -1
  10. data/app/services/foreman_rh_cloud/tags_auth.rb +2 -1
  11. data/lib/foreman_inventory_upload/async/create_missing_insights_facets.rb +29 -0
  12. data/lib/foreman_inventory_upload/async/generate_host_report.rb +20 -0
  13. data/lib/foreman_inventory_upload/async/generate_report_job.rb +1 -1
  14. data/lib/foreman_inventory_upload/async/host_inventory_report_job.rb +39 -0
  15. data/lib/foreman_inventory_upload/async/single_host_report_job.rb +20 -0
  16. data/lib/foreman_inventory_upload/async/upload_report_job.rb +2 -1
  17. data/lib/foreman_inventory_upload/generators/fact_helpers.rb +2 -2
  18. data/lib/foreman_inventory_upload/generators/slice.rb +3 -3
  19. data/lib/foreman_inventory_upload/scripts/uploader.sh.erb +7 -1
  20. data/lib/foreman_rh_cloud/plugin.rb +9 -9
  21. data/lib/foreman_rh_cloud/version.rb +1 -1
  22. data/lib/tasks/rh_cloud_inventory.rake +14 -32
  23. data/locale/foreman_rh_cloud.pot +55 -18
  24. data/locale/fr/foreman_rh_cloud.po +48 -27
  25. data/locale/ja/foreman_rh_cloud.po +47 -26
  26. data/locale/ka/foreman_rh_cloud.po +32 -11
  27. data/locale/ko/foreman_rh_cloud.po +47 -26
  28. data/locale/zh_CN/foreman_rh_cloud.po +47 -26
  29. data/package.json +1 -1
  30. data/test/unit/fact_helpers_test.rb +47 -0
  31. data/test/unit/slice_generator_test.rb +57 -0
  32. data/webpack/InsightsHostDetailsTab/InsightsTotalRiskChart.js +57 -21
  33. data/webpack/InsightsHostDetailsTab/__tests__/InsightsTotalRiskChart.test.js +194 -0
  34. data/webpack/InsightsVulnerabilityHostIndexExtensions/CVECountCell.js +8 -2
  35. data/webpack/InsightsVulnerabilityHostIndexExtensions/__tests__/CVECountCell.test.js +48 -2
  36. metadata +6 -2
  37. data/app/services/foreman_rh_cloud/gateway_request.rb +0 -26
@@ -9,14 +9,14 @@
9
9
  # 0868a4d1af5275b3f70b0a6dac4c99a4, 2023
10
10
  # Amit Upadhye <aupadhye@redhat.com>, 2023
11
11
  # Ewoud Kohl van Wijngaarden <ewoud+transifex@kohlvanwijngaarden.nl>, 2025
12
+ # Ondřej Gajdušek, 2025
12
13
  #
13
14
  msgid ""
14
15
  msgstr ""
15
- "Project-Id-Version: foreman_rh_cloud 11.1.0\n"
16
+ "Project-Id-Version: foreman_rh_cloud 13.0.5\n"
16
17
  "Report-Msgid-Bugs-To: \n"
17
18
  "PO-Revision-Date: 2023-01-20 13:26+0000\n"
18
- "Last-Translator: Ewoud Kohl van Wijngaarden <ewoud+transifex@kohlvanwijngaarde"
19
- "n.nl>, 2025\n"
19
+ "Last-Translator: Ondřej Gajdušek, 2025\n"
20
20
  "Language-Team: Chinese (China) (https://app.transifex.com/foreman/teams/114/zh"
21
21
  "_CN/)\n"
22
22
  "MIME-Version: 1.0\n"
@@ -44,7 +44,7 @@ msgid "All recommendations are now selected."
44
44
  msgstr "现在选择了所有建议。"
45
45
 
46
46
  msgid "Analytics data collection"
47
- msgstr ""
47
+ msgstr "数据收集分析"
48
48
 
49
49
  msgid "Any Organization"
50
50
  msgstr "任意机构"
@@ -127,11 +127,17 @@ msgstr "下载最新的报告"
127
127
  msgid "Enable automatic deletion of mismatched host records from the Red Hat cloud"
128
128
  msgstr "启用自动删除来自红帽云的主机记录"
129
129
 
130
+ msgid "Enable automatic deletion of mismatched host records from the Red Hat cloud. Ignored when using local Insights."
131
+ msgstr ""
132
+
130
133
  msgid "Enable automatic synchronization of Insights recommendations from the Red Hat cloud"
131
134
  msgstr "启用来自红帽云的 Insights 建议自动同步"
132
135
 
133
- msgid "Enable automatic upload of your host inventory to the Red Hat cloud"
134
- msgstr "启用自动将主机清单上传到红帽云"
136
+ msgid "Enable automatic synchronization of Insights recommendations from the Red Hat cloud. Ignored when using local Insights."
137
+ msgstr ""
138
+
139
+ msgid "Enable automatic upload of your host inventory to the Red Hat cloud. Ignored when using local Insights."
140
+ msgstr ""
135
141
 
136
142
  msgid "Enable automatic upload of your hosts inventory to the Red Hat cloud"
137
143
  msgstr "启用自动将主机清单上传到红帽云"
@@ -146,12 +152,12 @@ msgid "Encountered an error while trying to access the server:"
146
152
  msgstr "在尝试访问服务器时遇到错误:"
147
153
 
148
154
  msgid "Exclude installed packages"
149
- msgstr ""
155
+ msgstr "排除安装的软件包"
150
156
 
151
157
  msgid "Exclude installed packages from being uploaded to the Red Hat cloud"
152
158
  msgstr "将安装的软件包上传到红帽云"
153
159
 
154
- msgid "Exclude installed packages from being uploaded to the Red Hat cloud. (If insights_minimal_data_collection is set to true, this setting is ignored and installed packages are always excluded.)"
160
+ msgid "Exclude installed packages from being uploaded to the Red Hat cloud. (If insights_minimal_data_collection is set to true, this setting is ignored and installed packages are always excluded.) Ignored when using local Insights."
155
161
  msgstr ""
156
162
 
157
163
  msgid "Exit Code: %s"
@@ -188,7 +194,7 @@ msgid "Generate and upload report"
188
194
  msgstr "生成并上传报告"
189
195
 
190
196
  msgid "Generate report"
191
- msgstr ""
197
+ msgstr "生成报告"
192
198
 
193
199
  msgid "Generate the report, but do not upload"
194
200
  msgstr "生成报告,但不上传"
@@ -217,6 +223,9 @@ msgstr "获取 RH Cloud 中缺少的主机"
217
223
  msgid "Host Insights recommendations"
218
224
  msgstr "主机 Insights 建议"
219
225
 
226
+ msgid "Host inventory report job"
227
+ msgstr ""
228
+
220
229
  msgid "Host was not uploaded to your RH cloud inventory"
221
230
  msgstr "主机没有上传到您的 RH 云清单"
222
231
 
@@ -260,7 +269,7 @@ msgid "Knowledgebase article"
260
269
  msgstr "知识库文章"
261
270
 
262
271
  msgid "Learn more about {minimalDataCollectionSetting}."
263
- msgstr ""
272
+ msgstr "了解有关 {minimalDataCollectionSetting} 的更多信息。"
264
273
 
265
274
  msgid "List of host UUIDs"
266
275
  msgstr "主机 UUID 列表"
@@ -278,6 +287,9 @@ msgid "Manual"
278
287
  msgstr "手册"
279
288
 
280
289
  msgid "Minimal data collection"
290
+ msgstr "最小数据收集"
291
+
292
+ msgid "Missing Insights facets created: %s"
281
293
  msgstr ""
282
294
 
283
295
  msgid "Moderate"
@@ -314,7 +326,7 @@ msgid "Obfuscate host ipv4 addresses"
314
326
  msgstr "模糊的主机 ipv4 地址"
315
327
 
316
328
  msgid "Obfuscate host ipv4 addresses."
317
- msgstr ""
329
+ msgstr "对主机 ipv4 地址进行模糊化处理"
318
330
 
319
331
  msgid "Obfuscate host names"
320
332
  msgstr "模糊主机名"
@@ -322,20 +334,20 @@ msgstr "模糊主机名"
322
334
  msgid "Obfuscate host names sent to the Red Hat cloud"
323
335
  msgstr "发送到红帽云的模糊主机名"
324
336
 
325
- msgid "Obfuscate host names sent to the Red Hat cloud. (If insights_minimal_data_collection is set to true, this setting is ignored because host names are not included in the report.)"
337
+ msgid "Obfuscate host names sent to the Red Hat cloud. (If insights_minimal_data_collection is set to true, this setting is ignored because host names are not included in the report.) Ignored when using local Insights."
326
338
  msgstr ""
327
339
 
328
340
  msgid "Obfuscate ipv4 addresses sent to the Red Hat cloud"
329
341
  msgstr "发送到红帽云的模糊 ipv4 地址"
330
342
 
331
- msgid "Obfuscate ipv4 addresses sent to the Red Hat cloud. (If insights_minimal_data_collection is set to true, this setting is ignored because host IPv4 addresses are not included in the report.)"
343
+ msgid "Obfuscate ipv4 addresses sent to the Red Hat cloud. (If insights_minimal_data_collection is set to true, this setting is ignored because host IPv4 addresses are not included in the report.) Ignored when using local Insights."
332
344
  msgstr ""
333
345
 
334
- msgid "Only include the minimum required data in inventory reports for uploading to Red Hat cloud. When this is true, installed packages are excluded from the report regardless of the exclude_installed_packages setting, and host names and IPv4 addresses are excluded from the report regardless of obfuscation settings."
346
+ msgid "Only include the minimum required data in inventory reports for uploading to Red Hat cloud. When this is true, installed packages are excluded from the report regardless of the exclude_installed_packages setting, and host names and IPv4 addresses are excluded from the report regardless of obfuscation settings. Ignored when using local Insights."
335
347
  msgstr ""
336
348
 
337
349
  msgid "Only send the minimum required data to Red Hat cloud, obfuscation settings are disabled"
338
- msgstr ""
350
+ msgstr "仅将最低所需的数据发送到红帽云,禁用模糊设置"
339
351
 
340
352
  msgid "Oops! Couldn't find organization that matches your query"
341
353
  msgstr "未找到与您的查询匹配的机构"
@@ -352,8 +364,8 @@ msgstr "运行 playbook"
352
364
  msgid "RH Cloud"
353
365
  msgstr "RH Cloud"
354
366
 
355
- msgid "RHC daemon id"
356
- msgstr "RHC 守护进程 ID"
367
+ msgid "RHC daemon id. Ignored when using local Insights."
368
+ msgstr ""
357
369
 
358
370
  msgid "Read more about it in RH cloud insights"
359
371
  msgstr "在 RH Cloud insights 中了解更多有关它的信息"
@@ -413,7 +425,7 @@ msgid "Run RH Cloud playbook"
413
425
  msgstr "运行 RH Cloud playbook"
414
426
 
415
427
  msgid "Run playbook generated by Red Hat remediations app"
416
- msgstr ""
428
+ msgstr "运行由红帽补救应用生成的 playbook"
417
429
 
418
430
  msgid "Run remediation playbook generated by Insights"
419
431
  msgstr "运行 Insights 生成的补救 playbook"
@@ -428,7 +440,7 @@ msgid "Select recommendations from all pages"
428
440
  msgstr "在所有页面中选择建议"
429
441
 
430
442
  msgid "Send additional data to enhance Insights services, as per the settings"
431
- msgstr ""
443
+ msgstr "根据设置,发送额外数据以增强 Insights 服务"
432
444
 
433
445
  msgid "Set the current organization context for the request"
434
446
  msgstr "设置请求的当前机构上下文"
@@ -436,8 +448,8 @@ msgstr "设置请求的当前机构上下文"
436
448
  msgid "Settings"
437
449
  msgstr "设置"
438
450
 
439
- msgid "Should import include parameter tags from Foreman?"
440
- msgstr "应从 Foreman 导入包含参数标签?"
451
+ msgid "Should import include parameter tags from Foreman? Ignored when using local Insights."
452
+ msgstr ""
441
453
 
442
454
  msgid "Show Advanced Settings"
443
455
  msgstr "显示高级设置"
@@ -445,6 +457,12 @@ msgstr "显示高级设置"
445
457
  msgid "Show if system is configured to use local iop-advisor-engine."
446
458
  msgstr "显示系统是否被配置为使用本地 iop-advisor-engine。"
447
459
 
460
+ msgid "Single-host report job"
461
+ msgstr ""
462
+
463
+ msgid "Single-host report job for host %s"
464
+ msgstr ""
465
+
448
466
  msgid "Start inventory synchronization"
449
467
  msgstr "启动清单同步"
450
468
 
@@ -476,7 +494,7 @@ msgid "The report file %{filename} doesn't exist"
476
494
  msgstr "报告文件 %{filename} 不存在"
477
495
 
478
496
  msgid "The scheduled process is disabled because this Foreman is configured with a local IoP Smart Proxy."
479
- msgstr ""
497
+ msgstr "调度的进程被禁用,因为此 Foreman 配置了一个本地 IoP Smart Proxy。"
480
498
 
481
499
  msgid "The server returned the following error: %s"
482
500
  msgstr "服务器返回以下错误:%s"
@@ -487,6 +505,9 @@ msgstr "任务失败,错误为:"
487
505
  msgid "There are no recommendations for your hosts"
488
506
  msgstr "没有适用于您的主机的建议"
489
507
 
508
+ msgid "There were no missing Insights facets"
509
+ msgstr ""
510
+
490
511
  msgid "This action will also enable automatic reports upload"
491
512
  msgstr "此操作还会启用自动报告上传"
492
513
 
@@ -497,7 +518,7 @@ msgid "To manually upload the data for a specific organization, select an organi
497
518
  msgstr "要手动上传特定机构的数据,请选择机构并点 {restartButtonName}。"
498
519
 
499
520
  msgid "Total CVEs"
500
- msgstr ""
521
+ msgstr "CVE 总数"
501
522
 
502
523
  msgid "Total risk"
503
524
  msgstr "总风险"
@@ -527,10 +548,10 @@ msgid "View in Red Hat Insights"
527
548
  msgstr "禁用 Red Hat Insights"
528
549
 
529
550
  msgid "Vulnerabilities"
530
- msgstr ""
551
+ msgstr "安全漏洞"
531
552
 
532
553
  msgid "Vulnerability"
533
- msgstr ""
554
+ msgstr "安全漏洞"
534
555
 
535
556
  msgid "Wait and %s"
536
557
  msgstr "等待和 %s"
@@ -599,7 +620,7 @@ msgid "rule title"
599
620
  msgstr "rule 标题"
600
621
 
601
622
  msgid "setting minimal data collection"
602
- msgstr ""
623
+ msgstr "设置最小数据收集"
603
624
 
604
625
  msgid "solution url"
605
626
  msgstr "solution url"
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foreman_rh_cloud",
3
- "version": "12.2.9",
3
+ "version": "12.2.11",
4
4
  "description": "Inventory Upload =============",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -313,4 +313,51 @@ class FactHelpersTest < ActiveSupport::TestCase
313
313
  assert_equal '10.230.230.3', ip3
314
314
  end
315
315
  end
316
+
317
+ describe 'IoP smart proxy checks' do
318
+ test 'obfuscate_hostname? returns false when global setting is enabled but IoP is present' do
319
+ Setting.expects(:[]).with(:obfuscate_inventory_hostnames).returns(true)
320
+ ForemanRhCloud.expects(:with_iop_smart_proxy?).returns(true)
321
+ host = mock('host')
322
+ # When IoP is present, it falls back to checking host-specific facts
323
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_hostname_enabled').returns(nil)
324
+
325
+ result = @instance.obfuscate_hostname?(host)
326
+
327
+ refute result
328
+ end
329
+
330
+ test 'obfuscate_hostname? returns true when global setting is enabled and IoP is not present' do
331
+ Setting.expects(:[]).with(:obfuscate_inventory_hostnames).returns(true)
332
+ ForemanRhCloud.expects(:with_iop_smart_proxy?).returns(false)
333
+ host = mock('host')
334
+
335
+ result = @instance.obfuscate_hostname?(host)
336
+
337
+ assert result
338
+ end
339
+
340
+ test 'obfuscate_ips? returns false when global setting is enabled but IoP is present' do
341
+ Setting.expects(:[]).with(:obfuscate_inventory_ips).returns(true)
342
+ ForemanRhCloud.expects(:with_iop_smart_proxy?).returns(true)
343
+ host = mock('host')
344
+ # When IoP is present, it falls back to checking host-specific facts
345
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv4_enabled').returns(nil)
346
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv6_enabled').returns(nil)
347
+
348
+ result = @instance.obfuscate_ips?(host)
349
+
350
+ refute result
351
+ end
352
+
353
+ test 'obfuscate_ips? returns true when global setting is enabled and IoP is not present' do
354
+ Setting.expects(:[]).with(:obfuscate_inventory_ips).returns(true)
355
+ ForemanRhCloud.expects(:with_iop_smart_proxy?).returns(false)
356
+ host = mock('host')
357
+
358
+ result = @instance.obfuscate_ips?(host)
359
+
360
+ assert result
361
+ end
362
+ end
316
363
  end
@@ -107,6 +107,36 @@ class SliceGeneratorTest < ActiveSupport::TestCase
107
107
  assert_equal 'test_nic1', actual_nic['name']
108
108
  end
109
109
 
110
+ test 'does not generate a report with minimal data collection when iop is present' do
111
+ Setting[:insights_minimal_data_collection] = true
112
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
113
+
114
+ batch = Host.where(id: @host.id).in_batches.first
115
+ generator = create_generator(batch)
116
+
117
+ json_str = generator.render
118
+ actual = JSON.parse(json_str.join("\n"))
119
+
120
+ assert_equal '00000000-0000-0000-0000-000000000000', actual['report_slice_id']
121
+ assert_not_nil(actual_host = actual['hosts'].first)
122
+ assert_nil actual_host['ip_addresses']
123
+ assert_nil actual_host['mac_addresses']
124
+ assert_equal @host.fqdn, actual_host['fqdn']
125
+ assert_equal '1234', actual_host['account']
126
+ assert_equal 1, generator.hosts_count
127
+ assert_not_nil(actual_system_profile = actual_host['system_profile'])
128
+ assert_nil actual_system_profile['number_of_cpus']
129
+ assert_nil actual_system_profile['number_of_sockets']
130
+ assert_nil actual_system_profile['cores_per_socket']
131
+ assert_nil actual_system_profile['system_memory_bytes']
132
+ assert_nil actual_system_profile['os_release']
133
+ assert_not_nil(actual_network_interfaces = actual_system_profile['network_interfaces'])
134
+ assert_not_nil(actual_nic = actual_network_interfaces.first)
135
+ refute actual_nic.key?('mtu')
136
+ refute actual_nic.key?('mac_address')
137
+ assert_equal 'test_nic1', actual_nic['name']
138
+ end
139
+
110
140
  test 'generates a report with minimal data collection' do
111
141
  Setting[:insights_minimal_data_collection] = true
112
142
  create_fact_values(@host,
@@ -930,6 +960,33 @@ class SliceGeneratorTest < ActiveSupport::TestCase
930
960
  assert_equal 'alibaba', actual_profile['cloud_provider']
931
961
  end
932
962
 
963
+ test 'do not exclude packages when iop is present' do
964
+ Setting[:exclude_installed_packages] = true
965
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
966
+ installed_package = ::Katello::InstalledPackage.create(name: 'test-package', nvrea: 'test-package-1.0.x86_64', nvra: 'test-package-1.0.x86_64')
967
+
968
+ another_host = FactoryBot.create(
969
+ :host,
970
+ :with_subscription,
971
+ :with_content,
972
+ content_view: @host.content_views.first,
973
+ lifecycle_environment: @host.lifecycle_environments.first,
974
+ organization: @host.organization,
975
+ installed_packages: [installed_package]
976
+ )
977
+
978
+ batch = Host.where(id: another_host.id).in_batches.first
979
+ generator = create_generator(batch)
980
+
981
+ json_str = generator.render
982
+ actual = JSON.parse(json_str.join("\n"))
983
+
984
+ assert_equal '00000000-0000-0000-0000-000000000000', actual['report_slice_id']
985
+ assert_not_nil(actual_host = actual['hosts'].first)
986
+ assert_not_nil(actual_profile = actual_host['system_profile'])
987
+ assert_not_nil(actual_profile['installed_packages'])
988
+ end
989
+
933
990
  test 'include packages installed in the report' do
934
991
  Setting[:exclude_installed_packages] = false
935
992
  installed_package = ::Katello::InstalledPackage.create(name: 'test-package', nvrea: 'test-package-1.0.x86_64', nvra: 'test-package-1.0.x86_64')
@@ -18,36 +18,67 @@ import SkeletonLoader from 'foremanReact/components/common/SkeletonLoader';
18
18
  import { insightsCloudUrl } from '../InsightsCloudSync/InsightsCloudSyncHelpers';
19
19
  import { getInitialRisks, theme } from './InsightsTabConstants';
20
20
 
21
- const InsightsTotalRiskCard = ({ hostDetails: { id } }) => {
21
+ const InsightsTotalRiskCard = ({ hostDetails }) => {
22
+ const { id, insights_attributes: insightsFacet } = hostDetails;
23
+ const uuid = insightsFacet?.uuid;
24
+ // eslint-disable-next-line camelcase
25
+ const isIop = insightsFacet?.use_iop_mode;
22
26
  const [totalRisks, setTotalRisks] = useState(getInitialRisks());
23
27
  const hashHistory = useHistory();
24
28
  const dispatch = useDispatch();
25
29
  const API_KEY = `HOST_${id}_RECOMMENDATIONS`;
26
30
  const API_OPTIONS = useMemo(() => ({ key: API_KEY }), [API_KEY]);
27
- const url = id && insightsCloudUrl(`hits/${id}`); // This will keep the API call from being triggered if there's no host id.
28
- const {
29
- status = STATUS.PENDING,
30
- response: { hits = [] },
31
- } = useAPI('get', url, API_OPTIONS);
32
31
 
33
- useEffect(() => {
34
- if (status === STATUS.RESOLVED) {
35
- const risks = getInitialRisks();
32
+ // This will keep the API call from being triggered if there's no host id.
33
+ const url = isIop
34
+ ? uuid && insightsCloudUrl(`api/insights/v1/system/${uuid}`)
35
+ : id && insightsCloudUrl(`hits/${id}`);
36
+ const { status = STATUS.PENDING, response } = useAPI('get', url, API_OPTIONS);
37
+
38
+ const checkRisks = useMemo(() => {
39
+ if (!response || status !== STATUS.RESOLVED) {
40
+ return getInitialRisks();
41
+ }
42
+
43
+ const risks = getInitialRisks();
44
+ if (isIop) {
45
+ const {
46
+ low_hits: lowHits = 0,
47
+ moderate_hits: moderateHits = 0,
48
+ important_hits: importantHits = 0,
49
+ critical_hits: criticalHits = 0,
50
+ hits = 0,
51
+ } = response;
52
+
53
+ risks[1].value += lowHits;
54
+ risks[2].value += moderateHits;
55
+ risks[3].value += importantHits;
56
+ risks[4].value += criticalHits;
57
+ risks.total = hits;
58
+ } else {
59
+ const { hits = [] } = response;
36
60
  hits.forEach(({ total_risk: risk }) => {
37
61
  risks[risk].value += 1;
38
62
  });
39
63
  risks.total = hits.length;
40
- setTotalRisks(risks);
41
64
  }
42
- }, [hits, status]);
65
+ return risks;
66
+ }, [response, status, isIop]);
67
+
68
+ useEffect(() => {
69
+ setTotalRisks(checkRisks);
70
+ }, [checkRisks]);
71
+
72
+ if (!insightsFacet) return null;
43
73
 
44
74
  const onChartClick = (evt, { index }) => {
45
75
  hashHistory.push(`/Insights`);
46
- dispatch(
47
- push({
48
- search: `search=total_risk+%3D+${index + 1}`,
49
- })
50
- );
76
+ !isIop &&
77
+ dispatch(
78
+ push({
79
+ search: `search=total_risk+%3D+${index + 1}`,
80
+ })
81
+ );
51
82
  };
52
83
 
53
84
  const onChartHover = (evt, { index }) => [
@@ -61,11 +92,16 @@ const InsightsTotalRiskCard = ({ hostDetails: { id } }) => {
61
92
  const { 1: low, 2: moderate, 3: important, 4: critical, total } = totalRisks;
62
93
 
63
94
  // eslint-disable-next-line react/prop-types
64
- const LegendLabel = ({ index, ...rest }) => (
65
- <a key={index} onClick={() => onChartClick(null, { index })}>
66
- <ChartLabel {...rest} />
67
- </a>
68
- );
95
+ const LegendLabel = ({ index, ...rest }) => {
96
+ if (isIop) {
97
+ return <ChartLabel {...rest} />;
98
+ }
99
+ return (
100
+ <a key={index} onClick={() => onChartClick(null, { index })}>
101
+ <ChartLabel {...rest} />
102
+ </a>
103
+ );
104
+ };
69
105
 
70
106
  const legend = (
71
107
  <ChartLegend
@@ -0,0 +1,194 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import { Provider } from 'react-redux';
5
+ import { ConnectedRouter } from 'connected-react-router';
6
+ import { createMemoryHistory } from 'history';
7
+ import configureMockStore from 'redux-mock-store';
8
+ import { STATUS } from 'foremanReact/constants';
9
+ import * as APIHooks from 'foremanReact/common/hooks/API/APIHooks';
10
+ import InsightsTotalRiskCard from '../InsightsTotalRiskChart';
11
+
12
+ jest.mock('foremanReact/common/hooks/API/APIHooks');
13
+ jest.mock('foremanReact/common/I18n', () => ({
14
+ translate: jest.fn(str => str),
15
+ }));
16
+
17
+ const mockStore = configureMockStore();
18
+ const history = createMemoryHistory();
19
+ const store = mockStore({
20
+ router: {
21
+ location: {
22
+ pathname: '/',
23
+ search: '',
24
+ hash: '',
25
+ state: null,
26
+ },
27
+ action: 'POP',
28
+ },
29
+ });
30
+
31
+ const defaultHostDetails = {
32
+ id: 1,
33
+ insights_attributes: {
34
+ uuid: 'test-uuid',
35
+ use_iop_mode: false,
36
+ },
37
+ };
38
+
39
+ const renderComponent = (props = {}) => {
40
+ const allProps = {
41
+ hostDetails: defaultHostDetails,
42
+ ...props,
43
+ };
44
+
45
+ return render(
46
+ <Provider store={store}>
47
+ <ConnectedRouter history={history}>
48
+ <InsightsTotalRiskCard {...allProps} />
49
+ </ConnectedRouter>
50
+ </Provider>
51
+ );
52
+ };
53
+
54
+ describe('InsightsTotalRiskChart', () => {
55
+ beforeEach(() => {
56
+ store.clearActions();
57
+ jest.clearAllMocks();
58
+ });
59
+
60
+ it('should show loading state initially', () => {
61
+ APIHooks.useAPI.mockReturnValue({
62
+ status: STATUS.PENDING,
63
+ response: null,
64
+ });
65
+
66
+ renderComponent();
67
+ // SkeletonLoader shows loading state when status is PENDING
68
+ expect(screen.queryByText('No results found')).not.toBeInTheDocument();
69
+ expect(
70
+ screen.queryByTestId('rh-cloud-total-risk-card')
71
+ ).not.toBeInTheDocument();
72
+ });
73
+
74
+ it('should display error state when API fails', async () => {
75
+ APIHooks.useAPI.mockReturnValue({
76
+ status: STATUS.ERROR,
77
+ response: null,
78
+ });
79
+
80
+ renderComponent();
81
+ expect(screen.getByText('No results found')).toBeInTheDocument();
82
+ expect(
83
+ screen.queryByTestId('rh-cloud-total-risk-card')
84
+ ).not.toBeInTheDocument();
85
+ });
86
+
87
+ it('should handle non-IoP mode API response correctly', async () => {
88
+ const mockResponse = {
89
+ hits: [
90
+ { total_risk: 1 },
91
+ { total_risk: 2 },
92
+ { total_risk: 2 },
93
+ { total_risk: 3 },
94
+ { total_risk: 4 },
95
+ ],
96
+ };
97
+
98
+ APIHooks.useAPI.mockReturnValue({
99
+ status: STATUS.RESOLVED,
100
+ response: mockResponse,
101
+ });
102
+
103
+ renderComponent();
104
+
105
+ await waitFor(() => {
106
+ // Check if total number of recommendations is displayed
107
+ expect(screen.getByText('5')).toBeInTheDocument();
108
+ // Check if risk levels are displayed correctly
109
+ expect(screen.getByText(/Low: 1/)).toBeInTheDocument();
110
+ expect(screen.getByText(/Moderate: 2/)).toBeInTheDocument();
111
+ expect(screen.getByText(/Important: 1/)).toBeInTheDocument();
112
+ expect(screen.getByText(/Critical: 1/)).toBeInTheDocument();
113
+ });
114
+ });
115
+
116
+ it('should handle IOP mode API response correctly', async () => {
117
+ const mockResponse = {
118
+ low_hits: 2,
119
+ moderate_hits: 3,
120
+ important_hits: 1,
121
+ critical_hits: 2,
122
+ hits: 8,
123
+ };
124
+
125
+ APIHooks.useAPI.mockReturnValue({
126
+ status: STATUS.RESOLVED,
127
+ response: mockResponse,
128
+ });
129
+
130
+ renderComponent({
131
+ hostDetails: {
132
+ ...defaultHostDetails,
133
+ insights_attributes: {
134
+ ...defaultHostDetails.insights_attributes,
135
+ use_iop_mode: true,
136
+ },
137
+ },
138
+ });
139
+
140
+ await waitFor(() => {
141
+ // Check if total number of recommendations is displayed
142
+ expect(screen.getByText('8')).toBeInTheDocument();
143
+ // Check if risk levels are displayed correctly
144
+ expect(screen.getByText(/Low: 2/)).toBeInTheDocument();
145
+ expect(screen.getByText(/Moderate: 3/)).toBeInTheDocument();
146
+ expect(screen.getByText(/Important: 1/)).toBeInTheDocument();
147
+ expect(screen.getByText(/Critical: 2/)).toBeInTheDocument();
148
+ });
149
+ });
150
+
151
+ it('should show empty state when no recommendations exist', async () => {
152
+ APIHooks.useAPI.mockReturnValue({
153
+ status: STATUS.RESOLVED,
154
+ response: { hits: [] },
155
+ });
156
+
157
+ renderComponent();
158
+
159
+ await waitFor(() => {
160
+ expect(screen.getByText(/Low: 0/)).toBeInTheDocument();
161
+ expect(screen.getByText(/Moderate: 0/)).toBeInTheDocument();
162
+ expect(screen.getByText(/Important: 0/)).toBeInTheDocument();
163
+ expect(screen.getByText(/Critical: 0/)).toBeInTheDocument();
164
+ });
165
+ });
166
+
167
+ it('should use correct API endpoint based on IOP mode', () => {
168
+ renderComponent({
169
+ hostDetails: {
170
+ ...defaultHostDetails,
171
+ insights_attributes: {
172
+ ...defaultHostDetails.insights_attributes,
173
+ use_iop_mode: true,
174
+ },
175
+ },
176
+ });
177
+
178
+ expect(APIHooks.useAPI).toHaveBeenCalledWith(
179
+ 'get',
180
+ expect.stringContaining('/api/insights/v1/system/test-uuid'),
181
+ expect.any(Object)
182
+ );
183
+
184
+ jest.clearAllMocks();
185
+
186
+ renderComponent();
187
+
188
+ expect(APIHooks.useAPI).toHaveBeenCalledWith(
189
+ 'get',
190
+ expect.stringContaining('/hits/1'),
191
+ expect.any(Object)
192
+ );
193
+ });
194
+ });