foreman_remote_execution 16.6.4 → 16.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9b021c23fae39af103f3ac3bdb0fd3e208b74fb8ad234f66f7770a5e4dfc78f
4
- data.tar.gz: 4fd6498051640caae5cf6e4580d01106883a0f33d486b672a0d77ed49205e52c
3
+ metadata.gz: 87f61f660cc9bb15e87453747aca6cef5d600d4fd3994d090c5af8a4e48f1b8a
4
+ data.tar.gz: 16e86afd877b2c2dec48daba1ff05c98822208a9c777d5351a9bf04edbd21322
5
5
  SHA512:
6
- metadata.gz: bf4e127076278dca8d3ec7d4b86a625a89bb95c4a4c5cd728e9207e8ff3151f983965061498bbfd813c6d9d08f49ec59f9ba2bc06be113f8e721e8f04dfc0285
7
- data.tar.gz: 0124e5ae5e0f484269adf05e9e9e3aa10e1262c4c6f635707dee035f217a5206565bc231fa65dee356609ce6e9db332d7c1faf96b8d1b7b3760483171997e5fd
6
+ metadata.gz: 16a08a5afdbab46351b5fe9eb4fcf55c82ad0a56160585dbedbec48a32e2d47c9f595a8562505d5c97f69141d5e6174c0fcbb6e3fd0df481dd692b6c2b811b91
7
+ data.tar.gz: f1e886b6f85fc08820986b812d2d959b65c42e49ab411a42df47da1ec5ad077842f472f58e78f635fd71712fdf15a7bf2541d6921fc1d3788ebec9be1bcad995
@@ -19,13 +19,16 @@ module Api
19
19
 
20
20
  api :GET, '/job_invocations/:id', N_('Show job invocation')
21
21
  param :id, :identifier, :required => true
22
- param :host_status, :bool, required: false, desc: N_('Show Job status for the hosts')
22
+ param :include_hosts, :bool, :required => false, :default_value => true, :desc => N_('Include hosts and template invocations in the response. Defaults to true for backwards compatibility. Pass false to skip serializing all hosts.')
23
+ param :host_status, :bool, :required => false, :default_value => false, :desc => N_('Show job status for each host, only applicable when include_hosts is true')
23
24
  def show
24
- set_hosts_and_template_invocations
25
+ @pattern_template_invocations = @job_invocation.pattern_template_invocations.includes(:input_values)
25
26
  @job_organization = Taxonomy.find_by(id: @job_invocation.task.input[:current_organization_id])
26
27
  @job_location = Taxonomy.find_by(id: @job_invocation.task.input[:current_location_id])
27
- if params[:host_status] == 'true'
28
- set_statuses_and_smart_proxies
28
+
29
+ if include_hosts?
30
+ set_hosts_and_template_invocations
31
+ set_statuses_and_smart_proxies if Foreman::Cast.to_bool(params[:host_status])
29
32
  end
30
33
  end
31
34
 
@@ -84,13 +87,14 @@ module Api
84
87
 
85
88
  api :POST, '/job_invocations/', N_('Create a job invocation')
86
89
  param_group :job_invocation, :as => :create
90
+ param :include_hosts, :bool, :required => false, :default_value => true, :desc => N_('Include hosts and template invocations in the response. Defaults to true for backwards compatibility. Pass false to skip serializing all hosts.')
87
91
  def create
88
92
  composer = JobInvocationComposer.from_api_params(
89
93
  job_invocation_params
90
94
  )
91
95
  composer.trigger!
92
96
  @job_invocation = composer.job_invocation
93
- @hosts = @job_invocation.targeting.hosts
97
+ set_hosts_and_template_invocations if include_hosts?
94
98
  process_response @job_invocation
95
99
  rescue JobInvocationComposer::JobTemplateNotFound, JobInvocationComposer::FeatureNotFound => e
96
100
  not_found(error: { message: e.message })
@@ -115,10 +119,10 @@ module Api
115
119
  param :id, :identifier, :required => true
116
120
  def hosts
117
121
  set_hosts_and_template_invocations
118
- set_statuses_and_smart_proxies
119
122
  @total = @hosts.size
120
123
  @hosts = @hosts.search_for(params[:search], :order => params[:order]).paginate(:page => params[:page], :per_page => params[:per_page])
121
124
  @subtotal = @hosts.total_entries
125
+ set_statuses_and_smart_proxies
122
126
  if params[:awaiting]
123
127
  @hosts = @hosts.select { |host| @host_statuses[host.id] == 'N/A' }
124
128
  end
@@ -298,7 +302,6 @@ module Api
298
302
  end
299
303
 
300
304
  def set_hosts_and_template_invocations
301
- @pattern_template_invocations = @job_invocation.pattern_template_invocations.includes(:input_values)
302
305
  @hosts = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
303
306
 
304
307
  if params[:search].present?
@@ -310,16 +313,22 @@ module Api
310
313
  .includes(:input_values)
311
314
  end
312
315
 
316
+ def include_hosts?
317
+ params[:include_hosts].nil? || Foreman::Cast.to_bool(params[:include_hosts])
318
+ end
319
+
313
320
  def set_statuses_and_smart_proxies
314
- template_invocations = @template_invocations.includes(:run_host_job_task).to_a
321
+ template_invocations = @template_invocations.where(host_id: @hosts.select(:id))
322
+ .includes(:run_host_job_task).to_a
315
323
  hosts = @hosts.to_a
316
- @host_statuses = Hash[hosts.map do |host|
317
- template_invocation = template_invocations.find { |ti| ti.host_id == host.id }
324
+ template_invocations_by_host_id = template_invocations.index_by(&:host_id)
325
+ @host_statuses = hosts.to_h do |host|
326
+ template_invocation = template_invocations_by_host_id[host.id]
318
327
  task = template_invocation.try(:run_host_job_task)
319
328
  [host.id, template_invocation_status(task, @job_invocation.task)]
320
- end]
321
- @smart_proxy_id = Hash[template_invocations.map { |ti| [ti.host_id, ti.smart_proxy_id] }]
322
- @smart_proxy_name = Hash[template_invocations.map { |ti| [ti.host_id, ti.smart_proxy_name] }]
329
+ end
330
+ @smart_proxy_id = template_invocations.to_h { |ti| [ti.host_id, ti.smart_proxy_id] }
331
+ @smart_proxy_name = template_invocations.to_h { |ti| [ti.host_id, ti.smart_proxy_name] }
323
332
  end
324
333
  end
325
334
  end
@@ -19,12 +19,14 @@ child :targeting do
19
19
  attributes :bookmark_id, :bookmark_name, :search_query, :targeting_type, :user_id, :status, :status_label,
20
20
  :randomized_ordering
21
21
 
22
- child @hosts => :hosts do
23
- extends 'api/v2/hosts/base'
22
+ if @hosts
23
+ child @hosts => :hosts do
24
+ extends 'api/v2/hosts/base'
24
25
 
25
- if params[:host_status] == 'true'
26
- node :job_status do |host|
27
- @host_statuses[host.id]
26
+ if @host_statuses
27
+ node :job_status do |host|
28
+ @host_statuses[host.id]
29
+ end
28
30
  end
29
31
  end
30
32
  end
@@ -34,12 +36,14 @@ child :task do
34
36
  attributes :id, :state, :started_at
35
37
  end
36
38
 
37
- child @template_invocations do
38
- attributes :template_id, :template_name, :host_id
39
- child :input_values do
40
- attributes :template_input_name, :template_input_id
41
- node :value do |iv|
42
- iv.template_input.respond_to?(:hidden_value) && iv.template_input.hidden_value? ? '*' * 5 : iv.value
39
+ if @template_invocations
40
+ child @template_invocations => :template_invocations do
41
+ attributes :template_id, :template_name, :host_id
42
+ child :input_values do
43
+ attributes :template_input_name, :template_input_id
44
+ node :value do |iv|
45
+ iv.template_input.respond_to?(:hidden_value) && iv.template_input.hidden_value? ? '*' * 5 : iv.value
46
+ end
43
47
  end
44
48
  end
45
49
  end
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '16.6.4'.freeze
2
+ VERSION = '16.7.0'.freeze
3
3
  end
data/package.json CHANGED
@@ -5,9 +5,9 @@
5
5
  "scripts": {
6
6
  "lint": "tfm-lint --plugin -d /webpack",
7
7
  "lint:custom": "eslint ./webpack",
8
- "test": "tfm-test --plugin",
9
- "test:watch": "tfm-test --plugin --watchAll",
10
- "test:current": "tfm-test --plugin --watch",
8
+ "test": "tfm-test --plugin --config jest.config.js",
9
+ "test:watch": "tfm-test --plugin --config jest.config.js --watchAll",
10
+ "test:current": "tfm-test --plugin --config jest.config.js --watch",
11
11
  "publish-coverage": "tfm-publish-coverage"
12
12
  },
13
13
  "repository": {
@@ -27,7 +27,10 @@ module Api
27
27
  template = ActiveSupport::JSON.decode(@response.body)
28
28
  assert_not_empty template
29
29
  assert_equal template['job_category'], @invocation.job_category
30
- assert_not_empty template['targeting']['hosts']
30
+ assert_not_nil template['targeting']
31
+ assert_not_empty template['pattern_template_invocations']
32
+ assert_not_nil template['targeting']['hosts']
33
+ assert_not_nil template['template_invocations']
31
34
  end
32
35
 
33
36
  test 'should get invocation detail when taxonomies are set' do
@@ -36,9 +39,17 @@ module Api
36
39
  assert_response :success
37
40
  end
38
41
 
42
+ test 'should exclude hosts when include_hosts=false' do
43
+ get :show, params: { :id => @invocation.id, :include_hosts => false }
44
+ assert_response :success
45
+ result = ActiveSupport::JSON.decode(@response.body)
46
+ assert_nil result['targeting']['hosts']
47
+ assert_nil result['template_invocations']
48
+ end
49
+
39
50
  test 'should see only permitted hosts' do
40
- @user = FactoryBot.create(:user, admin: false)
41
- @invocation.task.update(user: @user)
51
+ @user = FactoryBot.create(:user, :admin => false)
52
+ @invocation.task.update(:user => @user)
42
53
  setup_user('view', 'job_invocations', nil, @user)
43
54
  setup_user('view', 'hosts', 'name ~ nope.example.com', @user)
44
55
 
@@ -47,6 +58,29 @@ module Api
47
58
  response = ActiveSupport::JSON.decode(@response.body)
48
59
  assert_equal response['targeting']['hosts'], []
49
60
  end
61
+
62
+ test 'should ignore host_status when include_hosts=false' do
63
+ get :show, params: { :id => @invocation.id, :include_hosts => false, :host_status => true }
64
+ assert_response :success
65
+ result = ActiveSupport::JSON.decode(@response.body)
66
+ assert_nil result['targeting']['hosts']
67
+ assert_nil result['template_invocations']
68
+ end
69
+
70
+ test 'should include job_status per host when host_status=true' do
71
+ invocation = FactoryBot.create(:job_invocation, :with_template, :with_task)
72
+ invocation.template_invocations << FactoryBot.create(:template_invocation, :with_task, :with_host, :job_invocation => invocation)
73
+ invocation.job_category = invocation.pattern_template_invocations.first.template.job_category
74
+ invocation.targeting.hosts = invocation.template_invocations.map(&:host)
75
+ invocation.save!
76
+
77
+ get :show, params: { :id => invocation.id, :include_hosts => true, :host_status => true }
78
+ assert_response :success
79
+ result = ActiveSupport::JSON.decode(@response.body)
80
+ hosts = result['targeting']['hosts']
81
+ assert_not_empty hosts
82
+ assert hosts.all? { |h| h.key?('job_status') }, 'Expected job_status to be present on each host'
83
+ end
50
84
  end
51
85
 
52
86
  context 'creation' do
@@ -66,6 +100,20 @@ module Api
66
100
  assert_response :success
67
101
  end
68
102
 
103
+ test 'should include hosts in create response by default' do
104
+ post :create, params: { job_invocation: @attrs }
105
+ invocation = ActiveSupport::JSON.decode(@response.body)
106
+ assert_not_nil invocation['targeting']['hosts']
107
+ assert_response :success
108
+ end
109
+
110
+ test 'should exclude hosts from create response when include_hosts=false' do
111
+ post :create, params: { job_invocation: @attrs, include_hosts: false }
112
+ invocation = ActiveSupport::JSON.decode(@response.body)
113
+ assert_nil invocation['targeting']['hosts']
114
+ assert_response :success
115
+ end
116
+
69
117
  test 'should create with description format overridden' do
70
118
  @attrs[:description_format] = 'format'
71
119
  post :create, params: { job_invocation: @attrs }
@@ -235,6 +283,23 @@ module Api
235
283
  end
236
284
  end
237
285
 
286
+ describe '#hosts' do
287
+ test 'should compute job_status for the paginated subset' do
288
+ invocation = FactoryBot.create(:job_invocation, :with_template, :with_task)
289
+ 2.times { invocation.template_invocations << FactoryBot.create(:template_invocation, :with_task, :with_host, :job_invocation => invocation) }
290
+ invocation.job_category = invocation.pattern_template_invocations.first.template.job_category
291
+ invocation.targeting.hosts = invocation.template_invocations.map(&:host)
292
+ invocation.save!
293
+
294
+ get :hosts, params: { :id => invocation.id, :page => 2, :per_page => 1 }
295
+ assert_response :success
296
+ result = ActiveSupport::JSON.decode(@response.body)
297
+ assert_equal 3, result['total']
298
+ assert_equal 1, result['results'].size
299
+ assert_equal(['success'], result['results'].map { |r| r['job_status'] })
300
+ end
301
+ end
302
+
238
303
  describe 'raw output' do
239
304
  let(:fake_output) do
240
305
  (1..5).map do |i|
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path('../test_plugin_helper', __dir__)
4
+ require 'integration_test_helper'
5
+ require 'securerandom'
6
+
7
+ class JobWizardCategoryJsTest < IntegrationTestWithJavascript
8
+ WIZARD_INPUT_NAME_BASE = 'wizard_integration_input'
9
+ WIZARD_FEATURE_INPUT_NAME_BASE = 'wizard_integration_feature_input'
10
+
11
+ setup do
12
+ unique_suffix = SecureRandom.hex(6)
13
+ @wizard_input_name = "#{WIZARD_INPUT_NAME_BASE}_#{unique_suffix}"
14
+ @wizard_feature_input_name = "#{WIZARD_FEATURE_INPUT_NAME_BASE}_#{unique_suffix}"
15
+ default_template_name = "Integration Default REX Form Template #{unique_suffix}"
16
+ feature_template_name = "Integration Feature REX Template #{unique_suffix}"
17
+
18
+ # Prevent real Dynflow proxy calls in CI while planning jobs.
19
+ ProxyAPI::ForemanDynflow::DynflowProxy.any_instance.stubs(:tasks_count).returns(0)
20
+
21
+ @original_rex_form_template = Setting[:remote_execution_form_job_template]
22
+ as_admin do
23
+ @default_job_template = FactoryBot.create(
24
+ :job_template,
25
+ :name => default_template_name,
26
+ :job_category => 'Default Form Category'
27
+ )
28
+ @feature_job_template = FactoryBot.create(
29
+ :job_template,
30
+ :name => feature_template_name,
31
+ :job_category => 'Feature Linked Category'
32
+ )
33
+ add_plain_template_input!(@default_job_template, :name => @wizard_input_name)
34
+ add_plain_template_input!(@feature_job_template, :name => @wizard_feature_input_name)
35
+ Setting[:remote_execution_form_job_template] = @default_job_template.name
36
+ @remote_execution_feature = FactoryBot.create(
37
+ :remote_execution_feature,
38
+ :label => "integration_rex_category_feature_#{unique_suffix}",
39
+ :name => "Integration REX Category Feature #{unique_suffix}",
40
+ :job_template => @feature_job_template
41
+ )
42
+ @rex_host = FactoryBot.create(:host, :with_execution)
43
+ end
44
+ end
45
+
46
+ teardown do
47
+ Setting[:remote_execution_form_job_template] = @original_rex_form_template
48
+ end
49
+
50
+ test 'job wizard shows feature template job category when feature query param is present' do
51
+ visit new_job_invocation_path(:feature => @remote_execution_feature.label)
52
+ assert_text 'Category and template', :wait => 30
53
+ wait_for_ajax
54
+ assert_selector(:ouia_component_id, 'job_category', :text => 'Feature Linked Category', :wait => 30)
55
+ end
56
+
57
+ test 'job wizard shows default form category when no feature query param' do
58
+ visit new_job_invocation_path
59
+ assert_text 'Category and template', :wait => 30
60
+ wait_for_ajax
61
+ assert_selector(:ouia_component_id, 'job_category', :text => 'Default Form Category', :wait => 30)
62
+ end
63
+
64
+ test 'job wizard prefills template input from inputs query param' do
65
+ visit new_job_invocation_path(
66
+ :inputs => { @wizard_input_name => 'from_query_string' },
67
+ :search => ''
68
+ )
69
+ go_to_target_hosts_and_inputs_step!
70
+ assert_selector "textarea##{@wizard_input_name}", :wait => 30
71
+ assert_equal 'from_query_string', find("textarea##{@wizard_input_name}").value
72
+ end
73
+
74
+ test 'job wizard does not prefill template input when inputs query param is omitted' do
75
+ visit new_job_invocation_path(:search => '')
76
+ go_to_target_hosts_and_inputs_step!
77
+ assert_selector "textarea##{@wizard_input_name}", :wait => 30
78
+ assert_equal '', find("textarea##{@wizard_input_name}").value
79
+ end
80
+
81
+ test 'job wizard prefills template input when feature and inputs query params are present' do
82
+ visit new_job_invocation_path(
83
+ :feature => @remote_execution_feature.label,
84
+ :inputs => { @wizard_feature_input_name => 'feature_and_inputs' },
85
+ :search => ''
86
+ )
87
+ go_to_target_hosts_and_inputs_step!
88
+ assert_selector "textarea##{@wizard_feature_input_name}", :wait => 30
89
+ assert_equal 'feature_and_inputs', find("textarea##{@wizard_feature_input_name}").value
90
+ end
91
+
92
+ test 'job wizard run on selected hosts is enabled on review and posts to job invocations API' do
93
+ posted_to_job_invocations_create = false
94
+ subscriber = ActiveSupport::Notifications.subscribe('start_processing.action_controller') do |_n, _s, _f, _i, payload|
95
+ next unless payload[:controller] == 'Api::V2::JobInvocationsController' && payload[:action].to_s == 'create'
96
+ root = payload[:params]
97
+ root = root.to_unsafe_h if root.respond_to?(:to_unsafe_h)
98
+ job_inv = (root[:job_invocation] || root['job_invocation'])
99
+ next unless job_inv
100
+ job_inv = job_inv.to_unsafe_h if job_inv.respond_to?(:to_unsafe_h)
101
+ next unless job_inv['job_template_id'].to_i == @default_job_template.id
102
+ posted_to_job_invocations_create = true
103
+ end
104
+
105
+ begin
106
+ visit new_job_invocation_path(:search => "id = #{@rex_host.id}")
107
+ navigate_job_wizard_to_review_step!
108
+
109
+ assert_selector(:ouia_component_id, 'run-on-selected-hosts-footer', :wait => 30)
110
+ run_on_hosts = find(:ouia_component_id, 'run-on-selected-hosts-footer')
111
+ assert_not_equal 'true', run_on_hosts[:'aria-disabled'], 'expected run-on-selected-hosts button to be enabled'
112
+ assert_text 'Run on selected hosts'
113
+ find(:ouia_component_id, 'run-on-selected-hosts-footer').click
114
+ wait_for_ajax
115
+ assert posted_to_job_invocations_create, 'expected Api::V2::JobInvocationsController#create to run'
116
+ assert_match(%r{\A/job_invocations/\d+\z}, page.current_path)
117
+ ensure
118
+ ActiveSupport::Notifications.unsubscribe(subscriber)
119
+ end
120
+ end
121
+
122
+ test 'job wizard create API request sends job_template_id and inputs for default template' do
123
+ input_value = 'api_payload_default_template'
124
+ job_inv_params = capture_api_job_invocation_params_during(expected_template_id: @default_job_template.id) do
125
+ visit new_job_invocation_path(
126
+ :search => "id = #{@rex_host.id}",
127
+ :inputs => { @wizard_input_name => input_value }
128
+ )
129
+ navigate_job_wizard_to_review_step!
130
+ find(:ouia_component_id, 'run-on-selected-hosts-footer').click
131
+ wait_for_ajax
132
+ end
133
+
134
+ assert job_inv_params, 'expected job_invocation key in POST /api/job_invocations'
135
+ assert job_inv_params[:job_template_id].present?, 'expected job_template_id to be present'
136
+ assert_equal @default_job_template.id, job_inv_params[:job_template_id].to_i
137
+ assert job_inv_params[:feature].blank?
138
+ assert_equal input_value, hash_from_params_object(job_inv_params[:inputs])[@wizard_input_name]
139
+ job_invocation_id = page.current_path.split('/').last.to_i
140
+ assert_equal @default_job_template.job_category, JobInvocation.find(job_invocation_id).job_category, 'expected job category to be the default template category'
141
+ end
142
+
143
+ test 'job wizard create API request sends feature label and inputs without job_template_id' do
144
+ input_value = 'api_payload_feature_template'
145
+ job_inv_params = capture_api_job_invocation_params_during(expected_feature: @remote_execution_feature.label) do
146
+ visit new_job_invocation_path(
147
+ :feature => @remote_execution_feature.label,
148
+ :search => "id = #{@rex_host.id}",
149
+ :inputs => { @wizard_feature_input_name => input_value }
150
+ )
151
+ navigate_job_wizard_to_review_step!
152
+ find(:ouia_component_id, 'run-on-selected-hosts-footer').click
153
+ wait_for_ajax
154
+ end
155
+
156
+ assert job_inv_params, 'expected job_invocation key in POST /api/job_invocations'
157
+ assert_not job_inv_params[:job_template_id].present?
158
+ assert_equal @remote_execution_feature.label, job_inv_params[:feature]
159
+ assert_equal input_value, hash_from_params_object(job_inv_params[:inputs])[@wizard_feature_input_name]
160
+ job_invocation_id = page.current_path.split('/').last.to_i
161
+ assert_equal @feature_job_template.job_category, JobInvocation.find(job_invocation_id).job_category, 'expected job category to be the feature template category'
162
+ end
163
+
164
+ private
165
+
166
+ def capture_api_job_invocation_params_during(expected_template_id: nil, expected_feature: nil)
167
+ captured = nil
168
+ subscriber = ActiveSupport::Notifications.subscribe('start_processing.action_controller') do |_n, _s, _f, _i, payload|
169
+ next unless payload[:controller] == 'Api::V2::JobInvocationsController' && payload[:action].to_s == 'create'
170
+ root = payload[:params]
171
+ root = root.to_unsafe_h if root.respond_to?(:to_unsafe_h)
172
+ job_inv_params = root[:job_invocation] || root['job_invocation']
173
+ next unless job_inv_params
174
+ job_inv_params = job_inv_params.to_unsafe_h if job_inv_params.respond_to?(:to_unsafe_h)
175
+ job_inv_params = job_inv_params.with_indifferent_access
176
+ next if expected_template_id && job_inv_params[:job_template_id].to_i != expected_template_id
177
+ next if expected_feature && job_inv_params[:feature] != expected_feature
178
+ captured = job_inv_params
179
+ end
180
+ begin
181
+ yield
182
+ ensure
183
+ ActiveSupport::Notifications.unsubscribe(subscriber)
184
+ end
185
+ captured
186
+ end
187
+
188
+ def hash_from_params_object(obj)
189
+ return {}.with_indifferent_access if obj.blank?
190
+ h = obj.respond_to?(:to_unsafe_h) ? obj.to_unsafe_h : obj.to_h
191
+ h.with_indifferent_access
192
+ end
193
+
194
+ def add_plain_template_input!(job_template, name:)
195
+ job_template.template_inputs << FactoryBot.build(
196
+ :template_input,
197
+ :name => name,
198
+ :input_type => 'user',
199
+ :value_type => 'plain',
200
+ :required => false
201
+ )
202
+ job_template.save!
203
+ end
204
+
205
+ def job_wizard_click_next!
206
+ wait_for_ajax
207
+ button = find(:ouia_component_id, 'next-footer')
208
+ page.execute_script('arguments[0].scrollIntoView({block: "center"})', button.native)
209
+ button.click
210
+ wait_for_ajax
211
+ end
212
+
213
+ def go_to_target_hosts_and_inputs_step!
214
+ assert_text 'Category and template', :wait => 30
215
+ wait_for_ajax
216
+ job_wizard_click_next!
217
+ assert_text 'Target hosts and inputs', :wait => 30
218
+ wait_for_ajax
219
+ end
220
+
221
+ def navigate_job_wizard_to_review_step!
222
+ assert_text 'Category and template', :wait => 30
223
+ wait_for_ajax
224
+ job_wizard_click_next!
225
+ assert_text 'Target hosts and inputs', :wait => 30
226
+ job_wizard_click_next!
227
+ assert_text 'Advanced fields', :wait => 30
228
+ job_wizard_click_next!
229
+ assert_text 'Immediate execution', :wait => 30
230
+ job_wizard_click_next!
231
+ assert_text 'Review details', :wait => 30
232
+ wait_for_ajax
233
+ end
234
+ end
@@ -14,6 +14,7 @@ import {
14
14
  } from '@patternfly/react-icons';
15
15
  import axios from 'axios';
16
16
  import { foremanUrl } from 'foremanReact/common/helpers';
17
+ import { usePermissions } from 'foremanReact/common/hooks/Permissions/permissionHooks';
17
18
  import { translate as __, sprintf } from 'foremanReact/common/I18n';
18
19
  import { addToast } from 'foremanReact/components/ToastsList';
19
20
  import PropTypes from 'prop-types';
@@ -24,10 +25,7 @@ import {
24
25
  DIRECT_OPEN_HOST_LIMIT,
25
26
  templateInvocationPageUrl,
26
27
  } from './JobInvocationConstants';
27
- import {
28
- selectHasPermission,
29
- selectTaskCancelable,
30
- } from './JobInvocationSelectors';
28
+ import { selectTaskCancelable } from './JobInvocationSelectors';
31
29
  import OpenAllInvocationsModal from './OpenAllInvocationsModal';
32
30
 
33
31
  /* eslint-disable camelcase */
@@ -115,12 +113,8 @@ export const CheckboxesActions = ({
115
113
  const dispatch = useDispatch();
116
114
  const [toBeOpened, setToBeOpened] = useState([]);
117
115
 
118
- const hasCreatePermission = useSelector(
119
- selectHasPermission('create_job_invocations')
120
- );
121
- const hasCancelPermission = useSelector(
122
- selectHasPermission('cancel_job_invocations')
123
- );
116
+ const hasCreatePermission = usePermissions(['create_job_invocations']);
117
+ const hasCancelPermission = usePermissions(['cancel_job_invocations']);
124
118
  const jobSearchQuery = `job_invocation.id = ${jobID}`;
125
119
  const filterQuery =
126
120
  filter && filter !== 'all_statuses'
@@ -19,7 +19,7 @@ export const getJobInvocation = url => dispatch => {
19
19
  const fetchData = withInterval(
20
20
  get({
21
21
  key: JOB_INVOCATION_KEY,
22
- params: { include_permissions: true },
22
+ params: { include_permissions: true, include_hosts: false },
23
23
  url,
24
24
  handleError: () => {
25
25
  dispatch(stopInterval(JOB_INVOCATION_KEY));
@@ -44,6 +44,7 @@ export const updateJob = jobId => dispatch => {
44
44
  APIActions.get({
45
45
  url,
46
46
  key: UPDATE_JOB,
47
+ params: { include_hosts: false },
47
48
  })
48
49
  );
49
50
  };
@@ -6,7 +6,6 @@ import { useForemanHostDetailsPageUrl } from 'foremanReact/Root/Context/ForemanC
6
6
  import JobStatusIcon from '../react_app/components/RecentJobsCard/JobStatusIcon';
7
7
 
8
8
  export const JOB_INVOCATION_KEY = 'JOB_INVOCATION_KEY';
9
- export const CURRENT_PERMISSIONS = 'CURRENT_PERMISSIONS';
10
9
  export const UPDATE_JOB = 'UPDATE_JOB';
11
10
  export const CANCEL_JOB = 'CANCEL_JOB';
12
11
  export const GET_TASK = 'GET_TASK';
@@ -20,9 +19,6 @@ export const GET_TEMPLATE_INVOCATION = 'GET_TEMPLATE_INVOCATION';
20
19
  export const DIRECT_OPEN_HOST_LIMIT = 3;
21
20
  export const ALL_JOB_HOSTS = 'ALL_JOB_HOSTS';
22
21
  export const AWAITING_STATUS_FILTER = '(job_invocation.result = N/A)';
23
- export const currentPermissionsUrl = foremanUrl(
24
- '/api/v2/permissions/current_permissions'
25
- );
26
22
 
27
23
  export const showTemplateInvocationUrl = (hostID, jobID) =>
28
24
  `/show_template_invocation_by_host/${hostID}/job_invocation/${jobID}`;
@@ -32,6 +28,10 @@ export const templateInvocationPageUrl = (hostID, jobID) =>
32
28
  `/job_invocations_detail/${jobID}/host_invocation/${hostID}`;
33
29
 
34
30
  export const jobInvocationDetailsUrl = id => `/job_invocations/${id}`;
31
+ export const jobInvocationsIndexPath = '/job_invocations';
32
+ export const jobInvocationsNewPath = '/job_invocations/new';
33
+ export const jobInvocationsIndexUrl = foremanUrl(jobInvocationsIndexPath);
34
+ export const jobInvocationsNewUrl = foremanUrl(jobInvocationsNewPath);
35
35
 
36
36
  export const STATUS = {
37
37
  PENDING: 'pending',
@@ -61,6 +61,11 @@ section.job-additional-info {
61
61
  }
62
62
 
63
63
  .job-details-table-section {
64
+ &.table-section {
65
+ padding-left: 0;
66
+ padding-right: 0;
67
+ }
68
+
64
69
  section:nth-child(1) {
65
70
  padding: 0;
66
71
  }
@@ -0,0 +1,52 @@
1
+ import PropTypes from 'prop-types';
2
+ import React from 'react';
3
+ import { PageSection, PageSectionVariants } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import ResourceLoadFailedEmptyState from 'foremanReact/components/common/EmptyState/ResourceLoadFailedEmptyState';
6
+ import {
7
+ jobInvocationsIndexUrl,
8
+ jobInvocationsNewUrl,
9
+ } from './JobInvocationConstants';
10
+
11
+ const JobInvocationEmptyState = ({
12
+ jobInvocationId,
13
+ httpStatus,
14
+ errorMessage,
15
+ }) => (
16
+ <PageSection variant={PageSectionVariants.light}>
17
+ <ResourceLoadFailedEmptyState
18
+ resourceLabel={__('job invocation')}
19
+ resourceId={jobInvocationId}
20
+ httpStatus={httpStatus}
21
+ errorMessage={errorMessage}
22
+ viewPermissions={['view_job_invocations']}
23
+ requiredPermissions={['view_job_invocations']}
24
+ primaryAction={{
25
+ label: __('Go to job invocations'),
26
+ url: jobInvocationsIndexUrl,
27
+ ouiaId: 'job-invocation-empty-state-go-to-job-invocations-button',
28
+ }}
29
+ secondaryActions={[
30
+ {
31
+ label: __('Create a new job invocation'),
32
+ url: jobInvocationsNewUrl,
33
+ ouiaId: 'job-invocation-empty-state-create-new-job-invocation-button',
34
+ },
35
+ ]}
36
+ ouiaIdPrefix="job-invocation-empty-state"
37
+ />
38
+ </PageSection>
39
+ );
40
+
41
+ JobInvocationEmptyState.propTypes = {
42
+ jobInvocationId: PropTypes.string.isRequired,
43
+ httpStatus: PropTypes.number,
44
+ errorMessage: PropTypes.string,
45
+ };
46
+
47
+ JobInvocationEmptyState.defaultProps = {
48
+ httpStatus: null,
49
+ errorMessage: null,
50
+ };
51
+
52
+ export default JobInvocationEmptyState;