foreman_rh_cloud 12.2.16 → 12.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/models/concerns/rh_cloud_host.rb +31 -1
- data/lib/foreman_inventory_upload/async/create_missing_insights_facets.rb +8 -2
- data/lib/foreman_inventory_upload/async/upload_report_direct_job.rb +3 -2
- data/lib/foreman_inventory_upload.rb +16 -1
- data/lib/foreman_rh_cloud/version.rb +1 -1
- data/lib/tasks/rh_cloud_inventory.rake +4 -0
- data/package.json +1 -1
- data/test/controllers/accounts_controller_test.rb +43 -0
- data/test/test_plugin_helper.rb +1 -0
- data/test/unit/rh_cloud_host_test.rb +154 -0
- data/webpack/InsightsHostDetailsTab/NewHostDetailsTab.js +18 -3
- data/webpack/InsightsHostDetailsTab/__tests__/NewHostDetailsTab.test.js +154 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79c93d459f33a153d6d5ef0477cddc3b0f4b992716bb1a9b9352d0d8274b9185
|
|
4
|
+
data.tar.gz: 0f7d20917c23789936882787bcb492353a56ac4061b27fcabb138a93dc9c9909
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 61917464374fddd39409ba0e4dd5c8f2b40a8eb18091a0e4af381b78930a460186c334f5e7b93a5b052b6dff98101551da59390f82ee94880601abe08bf645dc
|
|
7
|
+
data.tar.gz: 49af34b52d989a0558c538f5da8e027d26953261d69cbea8ed5535c11ae407ab50b0b7c2dcf78302b7281cfff2a6f1c74dc93cf0f8cb942a15d141263000a93d
|
|
@@ -21,7 +21,8 @@ module RhCloudHost
|
|
|
21
21
|
scoped_search :relation => :inventory_sync_status_object, :on => :status, :rename => :insights_inventory_sync_status,
|
|
22
22
|
:complete_value => { :disconnect => ::InventorySync::InventoryStatus::DISCONNECT,
|
|
23
23
|
:sync => ::InventorySync::InventoryStatus::SYNC }
|
|
24
|
-
scoped_search :
|
|
24
|
+
scoped_search :on => :id, :rename => :insights_uuid, :only_explicit => true,
|
|
25
|
+
:ext_method => :search_by_insights_uuid, :complete_value => false
|
|
25
26
|
|
|
26
27
|
def insights_facet
|
|
27
28
|
insights
|
|
@@ -41,4 +42,33 @@ module RhCloudHost
|
|
|
41
42
|
insights_facet.update!(uuid: subscription_facet.uuid)
|
|
42
43
|
end
|
|
43
44
|
end
|
|
45
|
+
|
|
46
|
+
module ClassMethods
|
|
47
|
+
def search_by_insights_uuid(_key, operator, value)
|
|
48
|
+
# Determine which facet table to search based on IoP mode
|
|
49
|
+
facet_table = ForemanRhCloud.with_iop_smart_proxy? ? Katello::Host::SubscriptionFacet.table_name : InsightsFacet.table_name
|
|
50
|
+
|
|
51
|
+
# Build SQL condition
|
|
52
|
+
if ['IN', 'NOT IN'].include?(operator)
|
|
53
|
+
# For IN/NOT IN, value may be an array or comma-separated string
|
|
54
|
+
# Convert to array and build placeholders for each value
|
|
55
|
+
values = value.is_a?(Array) ? value : value.to_s.split(',').map(&:strip)
|
|
56
|
+
placeholders = (['?'] * values.size).join(',')
|
|
57
|
+
condition = sanitize_sql_for_conditions(
|
|
58
|
+
["#{facet_table}.uuid #{operator} (#{placeholders})", *values]
|
|
59
|
+
)
|
|
60
|
+
else
|
|
61
|
+
# For other operators (=, !=, LIKE, etc.), use value_to_sql for proper SQL formatting
|
|
62
|
+
condition = sanitize_sql_for_conditions(
|
|
63
|
+
["#{facet_table}.uuid #{operator} ?", value_to_sql(operator, value)]
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Return search parameters with LEFT JOIN to include hosts without facets
|
|
68
|
+
{
|
|
69
|
+
joins: "LEFT JOIN #{facet_table} ON #{facet_table}.host_id = #{Host::Managed.table_name}.id",
|
|
70
|
+
conditions: condition,
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
44
74
|
end
|
|
@@ -7,9 +7,15 @@ module ForemanInventoryUpload
|
|
|
7
7
|
|
|
8
8
|
def run
|
|
9
9
|
organization = ::Organization.find(input[:organization_id])
|
|
10
|
-
|
|
10
|
+
# Find hosts with subscription facets but without insights facets
|
|
11
|
+
# Note: We can't use scoped_search 'null? insights_uuid' because the null? operator
|
|
12
|
+
# doesn't work with ext_methods - it would check hosts.id IS NULL instead of the facet
|
|
13
|
+
hosts_without_facets = ::ForemanInventoryUpload::Generators::Queries.for_org(organization, use_batches: false)
|
|
14
|
+
.left_outer_joins(:insights)
|
|
15
|
+
.where(insights_facets: { id: nil })
|
|
16
|
+
|
|
11
17
|
facet_count = 0
|
|
12
|
-
hosts_without_facets.
|
|
18
|
+
hosts_without_facets.in_batches(of: ForemanInventoryUpload.slice_size) do |batch|
|
|
13
19
|
facets = batch.pluck(:id, 'katello_subscription_facets.uuid').map do |host_id, uuid|
|
|
14
20
|
{
|
|
15
21
|
host_id: host_id,
|
|
@@ -95,8 +95,8 @@ module ForemanInventoryUpload
|
|
|
95
95
|
begin
|
|
96
96
|
upload_file(cer_path)
|
|
97
97
|
progress_output.write_line("Upload completed successfully")
|
|
98
|
-
move_to_done_folder
|
|
99
|
-
progress_output.write_line("Uploaded
|
|
98
|
+
done_path = move_to_done_folder
|
|
99
|
+
progress_output.write_line("Uploaded report moved to #{done_path}")
|
|
100
100
|
progress_output.status = "pid #{Process.pid} exit 0"
|
|
101
101
|
rescue StandardError => e
|
|
102
102
|
progress_output.write_line("Upload failed: #{e.message}")
|
|
@@ -139,6 +139,7 @@ module ForemanInventoryUpload
|
|
|
139
139
|
done_file = ForemanInventoryUpload.done_file_path(File.basename(filename))
|
|
140
140
|
FileUtils.mv(filename, done_file)
|
|
141
141
|
logger.debug("Moved #{filename} to #{done_file}")
|
|
142
|
+
done_file
|
|
142
143
|
end
|
|
143
144
|
|
|
144
145
|
def certificate
|
|
@@ -32,7 +32,18 @@ module ForemanInventoryUpload
|
|
|
32
32
|
|
|
33
33
|
def self.report_file_paths(organization_id)
|
|
34
34
|
filename = facts_archive_name(organization_id)
|
|
35
|
-
|
|
35
|
+
# Report files start in generated
|
|
36
|
+
# They are then MOVED (not copied) to uploads, then done.
|
|
37
|
+
# When they are moved to the new folder, they overwrite any file with the same name.
|
|
38
|
+
# If it's a generate-only, it will be in generated
|
|
39
|
+
# Failed or incomplete uploads will be in uploads
|
|
40
|
+
# Completed uploads will be in done
|
|
41
|
+
# The ordering here ensures we get the correct file path every time.
|
|
42
|
+
Dir[
|
|
43
|
+
ForemanInventoryUpload.generated_reports_file_path(filename),
|
|
44
|
+
ForemanInventoryUpload.uploads_file_path(filename),
|
|
45
|
+
ForemanInventoryUpload.done_file_path(filename)
|
|
46
|
+
]
|
|
36
47
|
end
|
|
37
48
|
|
|
38
49
|
def self.generated_reports_folder
|
|
@@ -44,6 +55,10 @@ module ForemanInventoryUpload
|
|
|
44
55
|
)
|
|
45
56
|
end
|
|
46
57
|
|
|
58
|
+
def self.generated_reports_file_path(filename)
|
|
59
|
+
File.join(ForemanInventoryUpload.generated_reports_folder, filename)
|
|
60
|
+
end
|
|
61
|
+
|
|
47
62
|
def self.outputs_folder
|
|
48
63
|
@outputs_folder ||= ensure_folder(File.join(ForemanInventoryUpload.base_folder, 'outputs/'))
|
|
49
64
|
end
|
|
@@ -46,6 +46,10 @@ namespace :rh_cloud_inventory do
|
|
|
46
46
|
filter,
|
|
47
47
|
false # don't upload; the user ran report:generate and not report:generate_upload
|
|
48
48
|
)
|
|
49
|
+
unless Setting[:subscription_connection_enabled]
|
|
50
|
+
report_paths = ForemanInventoryUpload.report_file_paths(organization_id)
|
|
51
|
+
puts "Report saved to #{report_paths.first}" if report_paths.any?
|
|
52
|
+
end
|
|
49
53
|
end
|
|
50
54
|
puts "Check the Uploading tab for report uploading status." if Setting[:subscription_connection_enabled]
|
|
51
55
|
end
|
data/package.json
CHANGED
|
@@ -5,6 +5,49 @@ class AccountsControllerTest < ActionController::TestCase
|
|
|
5
5
|
|
|
6
6
|
include FolderIsolation
|
|
7
7
|
|
|
8
|
+
test 'report_file_paths finds report in generated_reports folder' do
|
|
9
|
+
test_org = FactoryBot.create(:organization)
|
|
10
|
+
filename = ForemanInventoryUpload.facts_archive_name(test_org.id)
|
|
11
|
+
|
|
12
|
+
generated_path = ForemanInventoryUpload.generated_reports_file_path(filename)
|
|
13
|
+
FileUtils.mkdir_p(File.dirname(generated_path))
|
|
14
|
+
FileUtils.touch(generated_path)
|
|
15
|
+
|
|
16
|
+
paths = ForemanInventoryUpload.report_file_paths(test_org.id)
|
|
17
|
+
assert_includes paths, generated_path
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
test 'report_file_paths finds report in uploads folder' do
|
|
21
|
+
test_org = FactoryBot.create(:organization)
|
|
22
|
+
filename = ForemanInventoryUpload.facts_archive_name(test_org.id)
|
|
23
|
+
|
|
24
|
+
uploads_path = ForemanInventoryUpload.uploads_file_path(filename)
|
|
25
|
+
FileUtils.mkdir_p(File.dirname(uploads_path))
|
|
26
|
+
FileUtils.touch(uploads_path)
|
|
27
|
+
|
|
28
|
+
paths = ForemanInventoryUpload.report_file_paths(test_org.id)
|
|
29
|
+
assert_includes paths, uploads_path
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
test 'report_file_paths finds report in done folder' do
|
|
33
|
+
test_org = FactoryBot.create(:organization)
|
|
34
|
+
filename = ForemanInventoryUpload.facts_archive_name(test_org.id)
|
|
35
|
+
|
|
36
|
+
done_path = ForemanInventoryUpload.done_file_path(filename)
|
|
37
|
+
FileUtils.mkdir_p(File.dirname(done_path))
|
|
38
|
+
FileUtils.touch(done_path)
|
|
39
|
+
|
|
40
|
+
paths = ForemanInventoryUpload.report_file_paths(test_org.id)
|
|
41
|
+
assert_includes paths, done_path
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
test 'report_file_paths returns empty when no report exists' do
|
|
45
|
+
test_org = FactoryBot.create(:organization)
|
|
46
|
+
|
|
47
|
+
paths = ForemanInventoryUpload.report_file_paths(test_org.id)
|
|
48
|
+
assert_empty paths
|
|
49
|
+
end
|
|
50
|
+
|
|
8
51
|
test 'Returns statuses for each process type' do
|
|
9
52
|
test_org = FactoryBot.create(:organization)
|
|
10
53
|
|
data/test/test_plugin_helper.rb
CHANGED
|
@@ -31,6 +31,7 @@ module FolderIsolation
|
|
|
31
31
|
ForemanInventoryUpload.stubs(:base_folder).returns(@tmpdir)
|
|
32
32
|
ForemanInventoryUpload.instance_variable_set(:@outputs_folder, nil)
|
|
33
33
|
ForemanInventoryUpload.instance_variable_set(:@uploads_folders, nil)
|
|
34
|
+
ForemanInventoryUpload.instance_variable_set(:@generated_reports_folder, nil)
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
teardown do
|
|
@@ -188,4 +188,158 @@ 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
|
|
191
345
|
end
|
|
@@ -32,13 +32,28 @@ const NewHostDetailsTab = ({ hostName, router }) => {
|
|
|
32
32
|
const hits = useSelector(selectHits);
|
|
33
33
|
const isIop = useIopConfig();
|
|
34
34
|
|
|
35
|
-
useEffect(
|
|
35
|
+
useEffect(
|
|
36
|
+
() => () => {
|
|
37
|
+
// Preserve hash when clearing search params to prevent tab navigation bugs
|
|
38
|
+
if (router && typeof router.replace === 'function') {
|
|
39
|
+
const replaceOptions = { search: null };
|
|
40
|
+
if (router.location && router.location.hash) {
|
|
41
|
+
replaceOptions.hash = router.location.hash;
|
|
42
|
+
}
|
|
43
|
+
router.replace(replaceOptions);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
[router]
|
|
47
|
+
);
|
|
36
48
|
|
|
37
49
|
const onSearch = q => dispatch(fetchInsights({ query: q, page: 1 }));
|
|
38
50
|
|
|
39
51
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
40
|
-
const onSatInsightsClick = () =>
|
|
41
|
-
router.push
|
|
52
|
+
const onSatInsightsClick = () => {
|
|
53
|
+
if (router && typeof router.push === 'function') {
|
|
54
|
+
router.push({ pathname: '/foreman_rh_cloud/insights_cloud' });
|
|
55
|
+
}
|
|
56
|
+
};
|
|
42
57
|
|
|
43
58
|
const dropdownItems = [
|
|
44
59
|
<DropdownItem key="insights-link" ouiaId="insights-link">
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import { Provider } from 'react-redux';
|
|
5
|
+
import configureMockStore from 'redux-mock-store';
|
|
6
|
+
import thunk from 'redux-thunk';
|
|
7
|
+
import NewHostDetailsTab from '../NewHostDetailsTab';
|
|
8
|
+
|
|
9
|
+
jest.mock('../../common/Hooks/ConfigHooks', () => ({
|
|
10
|
+
useIopConfig: jest.fn(() => false),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
jest.mock('foremanReact/common/I18n', () => ({
|
|
14
|
+
translate: jest.fn(str => str),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const mockStore = configureMockStore([thunk]);
|
|
18
|
+
|
|
19
|
+
describe('NewHostDetailsTab', () => {
|
|
20
|
+
let store;
|
|
21
|
+
let mockRouter;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
mockRouter = {
|
|
25
|
+
push: jest.fn(),
|
|
26
|
+
replace: jest.fn(),
|
|
27
|
+
location: {
|
|
28
|
+
pathname: '/new/hosts/test-host.example.com',
|
|
29
|
+
search: '?page=1&per_page=20',
|
|
30
|
+
hash: '#/Insights',
|
|
31
|
+
query: { page: '1', per_page: '20' },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
store = mockStore({
|
|
36
|
+
API: {},
|
|
37
|
+
ForemanRhCloud: {
|
|
38
|
+
InsightsCloudSync: {
|
|
39
|
+
table: {
|
|
40
|
+
selectedIds: {},
|
|
41
|
+
isAllSelected: false,
|
|
42
|
+
showSelectAllAlert: false,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
insightsHostDetailsTab: {
|
|
47
|
+
query: '',
|
|
48
|
+
hits: [],
|
|
49
|
+
selectedIds: {},
|
|
50
|
+
error: null,
|
|
51
|
+
},
|
|
52
|
+
router: {
|
|
53
|
+
location: {
|
|
54
|
+
pathname: '/new/hosts/test-host.example.com',
|
|
55
|
+
search: '?page=1&per_page=20',
|
|
56
|
+
hash: '#/Insights',
|
|
57
|
+
query: { page: '1', per_page: '20' },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
jest.clearAllMocks();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('cleanup effect', () => {
|
|
68
|
+
it('should preserve hash when clearing search params on unmount', () => {
|
|
69
|
+
const { unmount } = render(
|
|
70
|
+
<Provider store={store}>
|
|
71
|
+
<NewHostDetailsTab
|
|
72
|
+
hostName="test-host.example.com"
|
|
73
|
+
router={mockRouter}
|
|
74
|
+
/>
|
|
75
|
+
</Provider>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Unmount the component to trigger cleanup
|
|
79
|
+
unmount();
|
|
80
|
+
|
|
81
|
+
// Verify router.replace was called with both search: null AND the existing hash
|
|
82
|
+
expect(mockRouter.replace).toHaveBeenCalledWith({
|
|
83
|
+
search: null,
|
|
84
|
+
hash: '#/Insights',
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should only clear search params when no hash exists', () => {
|
|
89
|
+
mockRouter.location.hash = '';
|
|
90
|
+
|
|
91
|
+
const { unmount } = render(
|
|
92
|
+
<Provider store={store}>
|
|
93
|
+
<NewHostDetailsTab
|
|
94
|
+
hostName="test-host.example.com"
|
|
95
|
+
router={mockRouter}
|
|
96
|
+
/>
|
|
97
|
+
</Provider>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
unmount();
|
|
101
|
+
|
|
102
|
+
// When there's no hash, should only pass search: null
|
|
103
|
+
expect(mockRouter.replace).toHaveBeenCalledWith({
|
|
104
|
+
search: null,
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should handle router.location being undefined gracefully', () => {
|
|
109
|
+
const routerWithoutLocation = {
|
|
110
|
+
push: jest.fn(),
|
|
111
|
+
replace: jest.fn(),
|
|
112
|
+
location: undefined,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const { unmount } = render(
|
|
116
|
+
<Provider store={store}>
|
|
117
|
+
<NewHostDetailsTab
|
|
118
|
+
hostName="test-host.example.com"
|
|
119
|
+
router={routerWithoutLocation}
|
|
120
|
+
/>
|
|
121
|
+
</Provider>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
unmount();
|
|
125
|
+
|
|
126
|
+
// Should still call replace with search: null even if location is undefined
|
|
127
|
+
expect(routerWithoutLocation.replace).toHaveBeenCalledWith({
|
|
128
|
+
search: null,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should use the latest hash value at unmount time, not a stale captured value', () => {
|
|
133
|
+
const { unmount } = render(
|
|
134
|
+
<Provider store={store}>
|
|
135
|
+
<NewHostDetailsTab
|
|
136
|
+
hostName="test-host.example.com"
|
|
137
|
+
router={mockRouter}
|
|
138
|
+
/>
|
|
139
|
+
</Provider>
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Change the hash after mount, before unmount
|
|
143
|
+
mockRouter.location.hash = '#/Overview';
|
|
144
|
+
|
|
145
|
+
unmount();
|
|
146
|
+
|
|
147
|
+
// Verify router.replace was called with the UPDATED hash, not the initial '#/Insights'
|
|
148
|
+
expect(mockRouter.replace).toHaveBeenCalledWith({
|
|
149
|
+
search: null,
|
|
150
|
+
hash: '#/Overview',
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
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: 12.2.
|
|
4
|
+
version: 12.2.18
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foreman Red Hat Cloud team
|
|
@@ -629,6 +629,7 @@ files:
|
|
|
629
629
|
- webpack/InsightsHostDetailsTab/__tests__/InsightsTabReducer.test.js
|
|
630
630
|
- webpack/InsightsHostDetailsTab/__tests__/InsightsTabSelectors.test.js
|
|
631
631
|
- webpack/InsightsHostDetailsTab/__tests__/InsightsTotalRiskChart.test.js
|
|
632
|
+
- webpack/InsightsHostDetailsTab/__tests__/NewHostDetailsTab.test.js
|
|
632
633
|
- webpack/InsightsHostDetailsTab/__tests__/__snapshots__/InsightsTab.test.js.snap
|
|
633
634
|
- webpack/InsightsHostDetailsTab/__tests__/__snapshots__/InsightsTabActions.test.js.snap
|
|
634
635
|
- webpack/InsightsHostDetailsTab/__tests__/__snapshots__/InsightsTabReducer.test.js.snap
|