foreman_remote_execution 3.3.1 → 3.3.2

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/app/controllers/api/v2/job_invocations_controller.rb +22 -1
  4. data/app/controllers/api/v2/template_invocations_controller.rb +4 -1
  5. data/app/helpers/job_invocations_helper.rb +1 -1
  6. data/app/helpers/remote_execution_helper.rb +38 -33
  7. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +1 -1
  8. data/app/views/api/v2/job_invocations/main.json.rabl +8 -2
  9. data/app/views/job_invocations/_tab_hosts.html.erb +3 -23
  10. data/app/views/job_invocations/show.html.erb +0 -6
  11. data/app/views/job_invocations/show.json.erb +4 -0
  12. data/foreman_remote_execution.gemspec +1 -1
  13. data/lib/foreman_remote_execution/version.rb +1 -1
  14. data/test/functional/api/v2/job_invocations_controller_test.rb +42 -14
  15. data/test/unit/concerns/host_extensions_test.rb +7 -0
  16. data/webpack/__mocks__/foremanReact/common/I18n.js +1 -0
  17. data/webpack/__mocks__/foremanReact/components/common/ActionButtons/ActionButtons.js +3 -0
  18. data/webpack/__mocks__/foremanReact/constants.js +3 -0
  19. data/webpack/index.js +8 -5
  20. data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +52 -0
  21. data/webpack/react_app/components/TargetingHosts/TargetingHostsActions.js +8 -0
  22. data/webpack/react_app/components/TargetingHosts/TargetingHostsConsts.js +1 -0
  23. data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +12 -0
  24. data/webpack/react_app/components/TargetingHosts/__tests__/HostItem.test.js +6 -0
  25. data/webpack/react_app/components/TargetingHosts/__tests__/HostStatus.test.js +6 -0
  26. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHosts.test.js +6 -0
  27. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostItem.test.js.snap +31 -0
  28. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostStatus.test.js.snap +12 -0
  29. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap +81 -0
  30. data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +43 -0
  31. data/webpack/react_app/components/TargetingHosts/components/HostItem.js +39 -0
  32. data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +54 -0
  33. data/webpack/react_app/components/TargetingHosts/index.js +37 -0
  34. metadata +24 -10
  35. data/app/views/job_invocations/_host_actions_td.html.erb +0 -3
  36. data/app/views/job_invocations/_host_name_td.html.erb +0 -8
  37. data/app/views/job_invocations/_host_status_td.html.erb +0 -1
  38. data/app/views/job_invocations/show.js.erb +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b85a9cedafe92aa9f2183d1dd5569baa99bd15005b43c26e2ae22ac064b810ee
4
- data.tar.gz: ffa0a77c6d12ff977e897933ce9d74072cf5e57495cfbef55a933d1d5f802fd3
3
+ metadata.gz: d093d837d0009e477549fba4700f3e0090045ba23f8b2c50fb05439fee53e403
4
+ data.tar.gz: 54dffc56caa3fa1bb72bb159b152d267d2e348b64b57322ea74cdf0ad9289cc1
5
5
  SHA512:
6
- metadata.gz: d2b5947c13f9831c4312f9ab0af739fb8357cc6f31dbf621021267d906dabd013087a1880a319ea65b59957b91550324fa350ff05789924bbf9aebf61c982693
7
- data.tar.gz: 28e4246d181f3f772e929c8fbc0626cbe4dcceff5de718d8d8da0e87b9fc8363a0b16841868f5a0f0e3b2cc9b785c8c072bfda90f059edd24a993927f06bf0c8
6
+ metadata.gz: e3524503031c73f86a0f3ed0b6c8dd39499f85df47052eca941f455552665b93ff041e5d6df9870cfb95ac1219017a561a483556306905bdc005caf252a5e088
7
+ data.tar.gz: 537da427406ce7c794d5e589e7e82f8c6fbda33a3a4231129905b69171421f41e98563c0496d9c7cc748f675d200729ec52f4d9067a8f7879972922358a2a868
data/Gemfile CHANGED
@@ -2,6 +2,7 @@ source 'http://rubygems.org'
2
2
 
3
3
  gemspec :name => 'foreman_remote_execution'
4
4
 
5
+ gem 'rubocop', '~> 0.80.0'
5
6
  gem 'rubocop-minitest'
6
7
  gem 'rubocop-performance'
7
8
  gem 'rubocop-rails'
@@ -18,7 +18,17 @@ module Api
18
18
 
19
19
  api :GET, '/job_invocations/:id', N_('Show job invocation')
20
20
  param :id, :identifier, :required => true
21
+ param :host_status, String, required: false, allow_blank: true, desc: N_('Show Job status for the hosts.')
21
22
  def show
23
+ @hosts = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
24
+ @template_invocations = @job_invocation.template_invocations
25
+ .where(host: @hosts)
26
+ .includes(:input_values)
27
+
28
+ if params[:host_status]
29
+ template_invocations = @template_invocations.includes(:run_host_job_task).to_a
30
+ @host_statuses = Hash[template_invocations.map { |ti| [ti.host_id, template_invocation_status(ti)] }]
31
+ end
22
32
  end
23
33
 
24
34
  def_param_group :job_invocation do
@@ -146,7 +156,7 @@ module Api
146
156
  end
147
157
 
148
158
  def find_host
149
- @host = Host.authorized(:view_hosts).find(params['host_id'])
159
+ @host = @nested_obj.targeting.hosts.authorized(:view_hosts, Host).find(params['host_id'])
150
160
  rescue ActiveRecord::RecordNotFound
151
161
  not_found({ :error => { :message => (_("Host with id '%{id}' was not found") % { :id => params['host_id'] }) } })
152
162
  end
@@ -204,6 +214,17 @@ module Api
204
214
  def parent_scope
205
215
  resource_class.where(nil)
206
216
  end
217
+
218
+ def template_invocation_status(template_invocation)
219
+ task = template_invocation.try(:run_host_job_task)
220
+ parent_task = @job_invocation.task
221
+
222
+ return(parent_task.result == 'cancelled' ? 'cancelled' : 'N/A') if task.nil?
223
+ return task.state if task.state == 'running' || task.state == 'planned'
224
+ return 'error' if task.result == 'warning'
225
+
226
+ task.result
227
+ end
207
228
  end
208
229
  end
209
230
  end
@@ -26,7 +26,10 @@ module Api
26
26
  private
27
27
 
28
28
  def resource_scope_for_template_invocations
29
- @job_invocation.template_invocations.search_for(*search_options)
29
+ @job_invocation.template_invocations
30
+ .includes(:host)
31
+ .where(host: Host.authorized(:view_hosts, Host))
32
+ .search_for(*search_options)
30
33
  end
31
34
 
32
35
  def find_job_invocation
@@ -29,7 +29,7 @@ module JobInvocationsHelper
29
29
  end
30
30
 
31
31
  def preview_hosts(template_invocation)
32
- hosts = template_invocation.targeting.hosts.take(20)
32
+ hosts = template_invocation.targeting.hosts.authorized(:view_hosts, Host).take(20)
33
33
  hosts.map do |host|
34
34
  collapsed_preview(host) +
35
35
  render(:partial => 'job_invocations/user_input',
@@ -14,43 +14,35 @@ module RemoteExecutionHelper
14
14
  end
15
15
 
16
16
  def template_invocation_status(task, parent_task)
17
- if task.nil?
18
- if parent_task.result == 'cancelled'
19
- icon_text('warning-triangle-o', _('cancelled'), :kind => 'pficon')
20
- else
21
- icon_text('question', 'N/A', :kind => 'fa')
22
- end
23
- elsif task.state == 'running'
24
- icon_text('running', _('running'), :kind => 'pficon')
25
- elsif task.state == 'planned'
26
- icon_text('build', _('planned'), :kind => 'pficon')
27
- else
28
- case task.result
29
- when 'warning', 'error'
30
- icon_text('error-circle-o', _('failed'), :kind => 'pficon')
31
- when 'cancelled'
32
- icon_text('warning-triangle-o', _('cancelled'), :kind => 'pficon')
33
- when 'success'
34
- icon_text('ok', _('success'), :kind => 'pficon')
35
- else
36
- task.result
37
- end
38
- end
17
+ return(parent_task.result == 'cancelled' ? _('cancelled') : 'N/A') if task.nil?
18
+ return task.state if task.state == 'running' || task.state == 'planned'
19
+ return _('error') if task.result == 'warning'
20
+
21
+ task.result
39
22
  end
40
23
 
41
24
  def template_invocation_actions(task, host, job_invocation, template_invocation)
25
+ links = []
42
26
  host_task = template_invocation.try(:run_host_job_task)
43
- [
44
- display_link_if_authorized(_('Host detail'), hash_for_host_path(host).merge(:auth_object => host, :permission => :view_hosts, :authorizer => job_hosts_authorizer)),
45
- display_link_if_authorized(_('Rerun on %s') % host.name, hash_for_rerun_job_invocation_path(:id => job_invocation, :host_ids => [ host.id ], :authorizer => job_hosts_authorizer)),
46
- if host_task.present?
47
- display_link_if_authorized(
48
- _('Host task'),
49
- hash_for_foreman_tasks_task_path(host_task)
50
- .merge(:auth_object => host_task, :permission => :view_foreman_tasks)
51
- )
52
- end,
53
- ]
27
+
28
+ if authorized_for(hash_for_host_path(host).merge(auth_object: host, permission: :view_hosts, authorizer: job_hosts_authorizer))
29
+ links << { title: _('Host detail'),
30
+ action: { href: host_path(host), 'data-method': 'get', id: "#{host.name}-actions-detail" } }
31
+ end
32
+
33
+ if authorized_for(hash_for_rerun_job_invocation_path(id: job_invocation, host_ids: [ host.id ], authorizer: job_hosts_authorizer))
34
+ links << { title: (_('Rerun on %s') % host.name),
35
+ action: { href: rerun_job_invocation_path(job_invocation, host_ids: [ host.id ]),
36
+ 'data-method': 'get', id: "#{host.name}-actions-rerun" } }
37
+ end
38
+
39
+ if host_task.present? && authorized_for(hash_for_foreman_tasks_task_path(host_task).merge(auth_object: host_task, permission: :view_foreman_tasks))
40
+ links << { title: _('Host task'),
41
+ action: { href: foreman_tasks_task_path(host_task),
42
+ 'data-method': 'get', id: "#{host.name}-actions-task" } }
43
+ end
44
+
45
+ links
54
46
  end
55
47
 
56
48
  def remote_execution_provider_for(template_invocation)
@@ -237,4 +229,17 @@ module RemoteExecutionHelper
237
229
 
238
230
  task.execution_plan.actions[1].try(:input).try(:[], 'script')
239
231
  end
232
+
233
+ def targeting_hosts(job_invocation, hosts)
234
+ hosts.map do |host|
235
+ template_invocation = job_invocation.template_invocations.find { |template_inv| template_inv.host_id == host.id }
236
+ task = template_invocation.try(:run_host_job_task)
237
+ link_authorized = !task.nil? && authorized_for(hash_for_template_invocation_path(:id => template_invocation).merge(:auth_object => host, :permission => :view_hosts, :authorizer => job_hosts_authorizer))
238
+
239
+ { name: host.name,
240
+ link: link_authorized ? template_invocation_path(:id => template_invocation) : '',
241
+ status: template_invocation_status(task, job_invocation.task),
242
+ actions: template_invocation_actions(task, host, job_invocation, template_invocation) }
243
+ end
244
+ end
240
245
  end
@@ -49,7 +49,7 @@ module ForemanRemoteExecution
49
49
  keys = remote_execution_ssh_keys
50
50
  source = 'global'
51
51
  if keys.present?
52
- value, safe_value = params.fetch('remote_execution_ssh_keys', {}).values_at(:value, :safe_value).map { |v| v || [] }
52
+ value, safe_value = params.fetch('remote_execution_ssh_keys', {}).values_at(:value, :safe_value).map { |v| [v].flatten.compact }
53
53
  params['remote_execution_ssh_keys'] = {:value => value + keys, :safe_value => safe_value + keys, :source => source}
54
54
  end
55
55
  [:remote_execution_ssh_user, :remote_execution_effective_user_method,
@@ -19,8 +19,14 @@ child :targeting do
19
19
  attributes :bookmark_id, :search_query, :targeting_type, :user_id, :status, :status_label,
20
20
  :randomized_ordering
21
21
 
22
- child :hosts do
22
+ child @hosts do
23
23
  extends 'api/v2/hosts/base'
24
+
25
+ if params[:host_status]
26
+ node :job_status do |host|
27
+ @host_statuses[host.id]
28
+ end
29
+ end
24
30
  end
25
31
  end
26
32
 
@@ -28,7 +34,7 @@ child :task do
28
34
  attributes :id, :state
29
35
  end
30
36
 
31
- child :template_invocations do
37
+ child @template_invocations do
32
38
  attributes :template_id, :template_name
33
39
  child :input_values do
34
40
  attributes :template_input_name, :template_input_id
@@ -15,29 +15,9 @@
15
15
  <% end %>
16
16
  <br>
17
17
 
18
- <table class="<%= table_css_classes('table-condensed') %>">
19
- <thead>
20
- <tr>
21
- <th><%= sort :name, :as => _('Host') %></th>
22
- <th><%= _('Status') %></th>
23
- <th><%= _('Actions') %></th>
24
- </tr>
25
- </thead>
26
-
27
- <tbody>
28
- <% hosts.each do |host| %>
29
- <% template_invocation = job_invocation.template_invocations.find { |template_invocation| template_invocation.host_id == host.id } %>
30
- <% task = template_invocation.try(:run_host_job_task) %>
31
- <tr>
32
- <% options = { :host => host, :task => task, :job_invocation => job_invocation, :template_invocation => template_invocation } %>
33
- <%= render 'host_name_td', options %>
34
- <%= render 'host_status_td', options %>
35
- <%= render 'host_actions_td', options %>
36
- </tr>
37
- <% end %>
38
- </tbody>
39
- </table>
40
-
18
+ <div id="targeting_hosts">
19
+ <%= mount_react_component('TargetingHosts', '#targeting_hosts') %>
20
+ </div>
41
21
  <%= will_paginate_with_info @hosts, :container => true %>
42
22
  <% else %>
43
23
  <div class="alert alert-warning">
@@ -41,9 +41,3 @@
41
41
  <% end %>
42
42
  <%= render_tab_content_for(:main_tabs, subject: @job_invocation) %>
43
43
  </div>
44
-
45
- <script id="job_invocation_refresh" data-refresh-url="<%= job_invocation_path(@job_invocation) %>">
46
- <% if @auto_refresh %>
47
- delayed_refresh($('script#job_invocation_refresh').data('refresh-url'), job_invocation_refresh_data());
48
- <% end %>
49
- </script>
@@ -0,0 +1,4 @@
1
+ {
2
+ "autoRefresh": "<%= @auto_refresh %>",
3
+ "hosts": <%= targeting_hosts(@job_invocation, @hosts).to_json.html_safe %>
4
+ }
@@ -29,6 +29,6 @@ Gem::Specification.new do |s|
29
29
  s.add_dependency 'foreman-tasks', '>= 0.15.1'
30
30
 
31
31
  s.add_development_dependency 'factory_bot_rails', '~> 4.8.0'
32
- s.add_development_dependency 'rubocop'
32
+ s.add_development_dependency 'rubocop', '~> 0.80.0'
33
33
  s.add_development_dependency 'rdoc'
34
34
  end
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '3.3.1'.freeze
2
+ VERSION = '3.3.2'.freeze
3
3
  end
@@ -4,7 +4,7 @@ module Api
4
4
  module V2
5
5
  class JobInvocationsControllerTest < ActionController::TestCase
6
6
  setup do
7
- @invocation = FactoryBot.create(:job_invocation, :with_template, :with_task)
7
+ @invocation = FactoryBot.create(:job_invocation, :with_template, :with_task, :with_unplanned_host)
8
8
  @template = FactoryBot.create(:job_template, :with_input)
9
9
 
10
10
  # Without this the template in template_invocations and in pattern_template_invocations
@@ -20,18 +20,32 @@ module Api
20
20
  assert_response :success
21
21
  end
22
22
 
23
- test 'should get invocation detail' do
24
- get :show, params: { :id => @invocation.id }
25
- assert_response :success
26
- template = ActiveSupport::JSON.decode(@response.body)
27
- assert_not_empty template
28
- assert_equal template['job_category'], @invocation.job_category
29
- end
23
+ describe 'show' do
24
+ test 'should get invocation detail' do
25
+ get :show, params: { :id => @invocation.id }
26
+ assert_response :success
27
+ template = ActiveSupport::JSON.decode(@response.body)
28
+ assert_not_empty template
29
+ assert_equal template['job_category'], @invocation.job_category
30
+ assert_not_empty template['targeting']['hosts']
31
+ end
30
32
 
31
- test 'should get invocation detail when taxonomies are set' do
32
- taxonomy_params = %w(organization location).reduce({}) { |acc, cur| acc.merge("#{cur}_id" => FactoryBot.create(cur)) }
33
- get :show, params: taxonomy_params.merge(:id => @invocation.id)
34
- assert_response :success
33
+ test 'should get invocation detail when taxonomies are set' do
34
+ taxonomy_params = %w(organization location).reduce({}) { |acc, cur| acc.merge("#{cur}_id" => FactoryBot.create(cur)) }
35
+ get :show, params: taxonomy_params.merge(:id => @invocation.id)
36
+ assert_response :success
37
+ end
38
+
39
+ test 'should see only permitted hosts' do
40
+ @user = FactoryBot.create(:user, admin: false)
41
+ setup_user('view', 'job_invocations', nil, @user)
42
+ setup_user('view', 'hosts', 'name ~ nope.example.com', @user)
43
+
44
+ get :show, params: { :id => @invocation.id }, session: set_session_user(@user)
45
+ assert_response :success
46
+ response = ActiveSupport::JSON.decode(@response.body)
47
+ assert_empty response['targeting']['hosts']
48
+ end
35
49
  end
36
50
 
37
51
  context 'creation' do
@@ -108,7 +122,7 @@ module Api
108
122
  end
109
123
 
110
124
  describe '#output' do
111
- let(:host) { @invocation.template_invocations_hosts.first }
125
+ let(:host) { @invocation.targeting.hosts.first }
112
126
 
113
127
  test 'should provide output for delayed task' do
114
128
  ForemanTasks::Task.any_instance.expects(:scheduled?).returns(true)
@@ -137,6 +151,12 @@ module Api
137
151
  assert_equal result['message'], "Job invocation not found by id '#{invocation_id}'"
138
152
  assert_response :missing
139
153
  end
154
+
155
+ test 'should get output only for host in job invocation' do
156
+ get :output, params: { job_invocation_id: @invocation.id,
157
+ host_id: FactoryBot.create(:host).id }
158
+ assert_response :missing
159
+ end
140
160
  end
141
161
 
142
162
  describe 'raw output' do
@@ -148,7 +168,7 @@ module Api
148
168
  let(:fake_task) do
149
169
  OpenStruct.new :pending? => false, :main_action => OpenStruct.new(:live_output => fake_output)
150
170
  end
151
- let(:host) { @invocation.template_invocations_hosts.first }
171
+ let(:host) { @invocation.targeting.hosts.first }
152
172
 
153
173
  test 'should provide raw output for a host' do
154
174
  JobInvocation.any_instance.expects(:task).returns(OpenStruct.new(:scheduled? => false))
@@ -184,6 +204,12 @@ module Api
184
204
  assert_nil result['output']
185
205
  assert_response :success
186
206
  end
207
+
208
+ test 'should get raw output only for host in job invocation' do
209
+ get :raw_output, params: { job_invocation_id: @invocation.id,
210
+ host_id: FactoryBot.create(:host).id }
211
+ assert_response :missing
212
+ end
187
213
  end
188
214
 
189
215
  test 'should cancel a job' do
@@ -232,11 +258,13 @@ module Api
232
258
  end
233
259
 
234
260
  test 'should not raise an exception when reruning failed has no hosts' do
261
+ @invocation.targeting.hosts.first.destroy
235
262
  JobInvocation.any_instance.expects(:generate_description)
236
263
  JobInvocationComposer.any_instance
237
264
  .expects(:validate_job_category)
238
265
  .with(@invocation.job_category)
239
266
  .returns(@invocation.job_category)
267
+
240
268
  post :rerun, params: { :id => @invocation.id, :failed_only => true }
241
269
  assert_response :success
242
270
  result = ActiveSupport::JSON.decode(@response.body)
@@ -48,6 +48,13 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
48
48
  _(host.host_param('remote_execution_ssh_keys')).must_include sshkey
49
49
  end
50
50
 
51
+ it 'merges ssh key as a string from host parameters and proxies' do
52
+ key = 'ssh-rsa not-even-a-key something@somewhere.com'
53
+ host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_ssh_keys', :value => key)
54
+ _(host.host_param('remote_execution_ssh_keys')).must_include key
55
+ _(host.host_param('remote_execution_ssh_keys')).must_include sshkey
56
+ end
57
+
51
58
  it 'has ssh keys in the parameters even when no user specified' do
52
59
  # this is a case, when using the helper in provisioning templates
53
60
  FactoryBot.create(:smart_proxy, :ssh)
@@ -0,0 +1 @@
1
+ export const translate = s => s;
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+
3
+ export const ActionButtons = () => <div />;
@@ -0,0 +1,3 @@
1
+ export const STATUS = {
2
+ ERROR: 'ERROR',
3
+ };
@@ -1,13 +1,16 @@
1
- // eslint-disable-next-line import/no-extraneous-dependencies
2
1
  import { registerReducer } from 'foremanReact/common/MountingService';
3
- // eslint-disable-next-line import/no-extraneous-dependencies
4
2
  import componentRegistry from 'foremanReact/components/componentRegistry';
5
3
  import JobInvocationContainer from './react_app/components/jobInvocations';
4
+ import TargetingHosts from './react_app/components/TargetingHosts';
6
5
  import rootReducer from './react_app/redux/reducers';
7
6
 
8
- componentRegistry.register({
9
- name: 'JobInvocationContainer',
10
- type: JobInvocationContainer,
7
+ const components = [
8
+ { name: 'JobInvocationContainer', type: JobInvocationContainer },
9
+ { name: 'TargetingHosts', type: TargetingHosts },
10
+ ];
11
+
12
+ components.forEach(component => {
13
+ componentRegistry.register(component);
11
14
  });
12
15
 
13
16
  registerReducer('foremanRemoteExecutionReducers', rootReducer);
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { LoadingState, Alert } from 'patternfly-react';
5
+ import { STATUS } from 'foremanReact/constants';
6
+ import HostItem from './components/HostItem';
7
+
8
+ const TargetingHosts = ({ status, items }) => {
9
+ if (status === STATUS.ERROR) {
10
+ return (
11
+ <Alert type="error">
12
+ {__(
13
+ 'There was an error while updating the status, try refreshing the page.'
14
+ )}
15
+ </Alert>
16
+ );
17
+ }
18
+
19
+ return (
20
+ <LoadingState loading={!items.length}>
21
+ <div>
22
+ <table className="table table-bordered table-striped table-hover">
23
+ <thead>
24
+ <tr>
25
+ <th>{__('Host')}</th>
26
+ <th>{__('Status')}</th>
27
+ <th>{__('Actions')}</th>
28
+ </tr>
29
+ </thead>
30
+ <tbody>
31
+ {items.map(host => (
32
+ <HostItem
33
+ key={host.name}
34
+ name={host.name}
35
+ link={host.link}
36
+ status={host.status}
37
+ actions={host.actions}
38
+ />
39
+ ))}
40
+ </tbody>
41
+ </table>
42
+ </div>
43
+ </LoadingState>
44
+ );
45
+ };
46
+
47
+ TargetingHosts.propTypes = {
48
+ status: PropTypes.string.isRequired,
49
+ items: PropTypes.array.isRequired,
50
+ };
51
+
52
+ export default TargetingHosts;
@@ -0,0 +1,8 @@
1
+ import { getURI } from 'foremanReact/common/urlHelpers';
2
+ import { get } from 'foremanReact/redux/API';
3
+ import { withInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
4
+ import { TARGETING_HOSTS } from './TargetingHostsConsts';
5
+
6
+ const url = getURI().addQuery('format', 'json');
7
+ export const getData = () =>
8
+ withInterval(get({ key: TARGETING_HOSTS, url }), 1000);
@@ -0,0 +1 @@
1
+ export const TARGETING_HOSTS = 'TARGETING_HOSTS';
@@ -0,0 +1,12 @@
1
+ import {
2
+ selectAPIStatus,
3
+ selectAPIResponse,
4
+ } from 'foremanReact/redux/API/APISelectors';
5
+ import { TARGETING_HOSTS } from './TargetingHostsConsts';
6
+
7
+ export const selectItems = state =>
8
+ selectAPIResponse(state, TARGETING_HOSTS).hosts || [];
9
+
10
+ export const selectAutoRefresh = state =>
11
+ selectAPIResponse(state, TARGETING_HOSTS).autoRefresh;
12
+ export const selectStatus = state => selectAPIStatus(state, TARGETING_HOSTS);
@@ -0,0 +1,6 @@
1
+ import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
2
+ import HostItem from '../components/HostItem';
3
+ import { HostItemFixtures } from './fixtures';
4
+
5
+ describe('HostItem', () =>
6
+ testComponentSnapshotsWithFixtures(HostItem, HostItemFixtures));
@@ -0,0 +1,6 @@
1
+ import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
2
+ import HostStatus from '../components/HostStatus';
3
+ import { HostStatusFixtures } from './fixtures';
4
+
5
+ describe('HostStatus', () =>
6
+ testComponentSnapshotsWithFixtures(HostStatus, HostStatusFixtures));
@@ -0,0 +1,6 @@
1
+ import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
2
+ import TargetingHosts from '../TargetingHosts';
3
+ import { TargetingHostsFixtures } from './fixtures';
4
+
5
+ describe('TargetingHosts', () =>
6
+ testComponentSnapshotsWithFixtures(TargetingHosts, TargetingHostsFixtures));
@@ -0,0 +1,31 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`HostItem renders 1`] = `
4
+ <tr
5
+ id="targeting-host-Host1"
6
+ >
7
+ <td
8
+ className="host_name"
9
+ >
10
+ <a
11
+ href="/host1"
12
+ >
13
+ Host1
14
+ </a>
15
+ </td>
16
+ <td
17
+ className="host_status"
18
+ >
19
+ <HostStatus
20
+ status="success"
21
+ />
22
+ </td>
23
+ <td
24
+ className="host_actions"
25
+ >
26
+ <ActionButtons
27
+ buttons={Array []}
28
+ />
29
+ </td>
30
+ </tr>
31
+ `;
@@ -0,0 +1,12 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`HostStatus renders 1`] = `
4
+ <div>
5
+ <Icon
6
+ name="ok"
7
+ type="pf"
8
+ />
9
+
10
+ success
11
+ </div>
12
+ `;
@@ -0,0 +1,81 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`TargetingHosts renders 1`] = `
4
+ <LoadingState
5
+ additionalClasses=""
6
+ loading={false}
7
+ loadingText="Loading"
8
+ size="lg"
9
+ timeout={300}
10
+ >
11
+ <div>
12
+ <table
13
+ className="table table-bordered table-striped table-hover"
14
+ >
15
+ <thead>
16
+ <tr>
17
+ <th>
18
+ Host
19
+ </th>
20
+ <th>
21
+ Status
22
+ </th>
23
+ <th>
24
+ Actions
25
+ </th>
26
+ </tr>
27
+ </thead>
28
+ <tbody>
29
+ <HostItem
30
+ actions={Array []}
31
+ key="host"
32
+ link="/link"
33
+ name="host"
34
+ status="success"
35
+ />
36
+ </tbody>
37
+ </table>
38
+ </div>
39
+ </LoadingState>
40
+ `;
41
+
42
+ exports[`TargetingHosts renders with error 1`] = `
43
+ <Alert
44
+ className=""
45
+ onDismiss={null}
46
+ type="error"
47
+ >
48
+ There was an error while updating the status, try refreshing the page.
49
+ </Alert>
50
+ `;
51
+
52
+ exports[`TargetingHosts renders with loading 1`] = `
53
+ <LoadingState
54
+ additionalClasses=""
55
+ loading={true}
56
+ loadingText="Loading"
57
+ size="lg"
58
+ timeout={300}
59
+ >
60
+ <div>
61
+ <table
62
+ className="table table-bordered table-striped table-hover"
63
+ >
64
+ <thead>
65
+ <tr>
66
+ <th>
67
+ Host
68
+ </th>
69
+ <th>
70
+ Status
71
+ </th>
72
+ <th>
73
+ Actions
74
+ </th>
75
+ </tr>
76
+ </thead>
77
+ <tbody />
78
+ </table>
79
+ </div>
80
+ </LoadingState>
81
+ `;
@@ -0,0 +1,43 @@
1
+ export const HostItemFixtures = {
2
+ renders: {
3
+ name: 'Host1',
4
+ link: '/host1',
5
+ status: 'success',
6
+ actions: [],
7
+ },
8
+ };
9
+
10
+ export const HostStatusFixtures = {
11
+ renders: {
12
+ status: 'success',
13
+ },
14
+ };
15
+
16
+ export const TargetingHostsFixtures = {
17
+ renders: {
18
+ status: '',
19
+ items: [
20
+ {
21
+ name: 'host',
22
+ link: '/link',
23
+ status: 'success',
24
+ actions: [],
25
+ },
26
+ ],
27
+ },
28
+ 'renders with error': {
29
+ status: 'ERROR',
30
+ items: [
31
+ {
32
+ name: 'host',
33
+ link: '/link',
34
+ status: 'success',
35
+ actions: [],
36
+ },
37
+ ],
38
+ },
39
+ 'renders with loading': {
40
+ status: '',
41
+ items: [],
42
+ },
43
+ };
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { ActionButtons } from 'foremanReact/components/common/ActionButtons/ActionButtons';
4
+ import HostStatus from './HostStatus';
5
+
6
+ const HostItem = ({ name, link, status, actions }) => {
7
+ const hostLink = link ? (
8
+ <a href={link}>{name}</a>
9
+ ) : (
10
+ <a href="#" className="disabled">
11
+ {name}
12
+ </a>
13
+ );
14
+
15
+ return (
16
+ <tr id={`targeting-host-${name}`}>
17
+ <td className="host_name">{hostLink}</td>
18
+ <td className="host_status">
19
+ <HostStatus status={status} />
20
+ </td>
21
+ <td className="host_actions">
22
+ <ActionButtons buttons={[...actions]} />
23
+ </td>
24
+ </tr>
25
+ );
26
+ };
27
+
28
+ HostItem.propTypes = {
29
+ name: PropTypes.string.isRequired,
30
+ link: PropTypes.string.isRequired,
31
+ status: PropTypes.string.isRequired,
32
+ actions: PropTypes.array,
33
+ };
34
+
35
+ HostItem.defaultProps = {
36
+ actions: [],
37
+ };
38
+
39
+ export default HostItem;
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import { Icon } from 'patternfly-react';
3
+ import PropTypes from 'prop-types';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+
6
+ const HostStatus = ({ status }) => {
7
+ switch (status) {
8
+ case 'cancelled':
9
+ return (
10
+ <div>
11
+ <Icon type="pf" name="warning-triangle-o" /> {status}
12
+ </div>
13
+ );
14
+ case 'N/A':
15
+ return (
16
+ <div>
17
+ <Icon type="fa" name="question" /> {status}
18
+ </div>
19
+ );
20
+ case 'running':
21
+ return (
22
+ <div>
23
+ <Icon type="pf" name="running" /> {status}
24
+ </div>
25
+ );
26
+ case 'planned':
27
+ return (
28
+ <div>
29
+ <Icon type="pf" name="build" /> {status}
30
+ </div>
31
+ );
32
+ case 'warning':
33
+ case 'error':
34
+ return (
35
+ <div>
36
+ <Icon type="pf" name="error-circle-o" /> {__('failed')}
37
+ </div>
38
+ );
39
+ case 'success':
40
+ return (
41
+ <div>
42
+ <Icon type="pf" name="ok" /> {status}
43
+ </div>
44
+ );
45
+ default:
46
+ return <span>{status}</span>;
47
+ }
48
+ };
49
+
50
+ HostStatus.propTypes = {
51
+ status: PropTypes.string.isRequired,
52
+ };
53
+
54
+ export default HostStatus;
@@ -0,0 +1,37 @@
1
+ import React, { useEffect } from 'react';
2
+ import { useSelector, useDispatch } from 'react-redux';
3
+ import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
4
+ import TargetingHosts from './TargetingHosts';
5
+
6
+ import {
7
+ selectItems,
8
+ selectStatus,
9
+ selectAutoRefresh,
10
+ } from './TargetingHostsSelectors';
11
+ import { getData } from './TargetingHostsActions';
12
+ import { TARGETING_HOSTS } from './TargetingHostsConsts';
13
+
14
+ const WrappedTargetingHosts = () => {
15
+ const dispatch = useDispatch();
16
+ const autoRefresh = useSelector(selectAutoRefresh);
17
+ const items = useSelector(selectItems);
18
+ const status = useSelector(selectStatus);
19
+
20
+ useEffect(() => {
21
+ dispatch(getData());
22
+
23
+ return () => {
24
+ dispatch(stopInterval(TARGETING_HOSTS));
25
+ };
26
+ }, [dispatch]);
27
+
28
+ useEffect(() => {
29
+ if (autoRefresh === 'false') {
30
+ dispatch(stopInterval(TARGETING_HOSTS));
31
+ }
32
+ }, [autoRefresh, dispatch]);
33
+
34
+ return <TargetingHosts status={status} items={items} />;
35
+ };
36
+
37
+ export default WrappedTargetingHosts;
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_remote_execution
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.1
4
+ version: 3.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Remote Execution team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-10 00:00:00.000000000 Z
11
+ date: 2020-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deface
@@ -90,16 +90,16 @@ dependencies:
90
90
  name: rubocop
91
91
  requirement: !ruby/object:Gem::Requirement
92
92
  requirements:
93
- - - ">="
93
+ - - "~>"
94
94
  - !ruby/object:Gem::Version
95
- version: '0'
95
+ version: 0.80.0
96
96
  type: :development
97
97
  prerelease: false
98
98
  version_requirements: !ruby/object:Gem::Requirement
99
99
  requirements:
100
- - - ">="
100
+ - - "~>"
101
101
  - !ruby/object:Gem::Version
102
- version: '0'
102
+ version: 0.80.0
103
103
  - !ruby/object:Gem::Dependency
104
104
  name: rdoc
105
105
  requirement: !ruby/object:Gem::Requirement
@@ -238,9 +238,6 @@ files:
238
238
  - app/views/job_invocations/_card_user_input.html.erb
239
239
  - app/views/job_invocations/_description_fields.html.erb
240
240
  - app/views/job_invocations/_form.html.erb
241
- - app/views/job_invocations/_host_actions_td.html.erb
242
- - app/views/job_invocations/_host_name_td.html.erb
243
- - app/views/job_invocations/_host_status_td.html.erb
244
241
  - app/views/job_invocations/_preview_hosts_list.html.erb
245
242
  - app/views/job_invocations/_preview_hosts_modal.html.erb
246
243
  - app/views/job_invocations/_rerun_taxonomies.html.erb
@@ -252,7 +249,7 @@ files:
252
249
  - app/views/job_invocations/new.html.erb
253
250
  - app/views/job_invocations/refresh.js.erb
254
251
  - app/views/job_invocations/show.html.erb
255
- - app/views/job_invocations/show.js.erb
252
+ - app/views/job_invocations/show.json.erb
256
253
  - app/views/job_invocations/welcome.html.erb
257
254
  - app/views/job_templates/_custom_tab_headers.html.erb
258
255
  - app/views/job_templates/_custom_tabs.html.erb
@@ -396,7 +393,24 @@ files:
396
393
  - test/unit/renderer_scope_input.rb
397
394
  - test/unit/targeting_test.rb
398
395
  - test/unit/template_invocation_input_value_test.rb
396
+ - webpack/__mocks__/foremanReact/common/I18n.js
397
+ - webpack/__mocks__/foremanReact/components/common/ActionButtons/ActionButtons.js
398
+ - webpack/__mocks__/foremanReact/constants.js
399
399
  - webpack/index.js
400
+ - webpack/react_app/components/TargetingHosts/TargetingHosts.js
401
+ - webpack/react_app/components/TargetingHosts/TargetingHostsActions.js
402
+ - webpack/react_app/components/TargetingHosts/TargetingHostsConsts.js
403
+ - webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js
404
+ - webpack/react_app/components/TargetingHosts/__tests__/HostItem.test.js
405
+ - webpack/react_app/components/TargetingHosts/__tests__/HostStatus.test.js
406
+ - webpack/react_app/components/TargetingHosts/__tests__/TargetingHosts.test.js
407
+ - webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostItem.test.js.snap
408
+ - webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostStatus.test.js.snap
409
+ - webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap
410
+ - webpack/react_app/components/TargetingHosts/__tests__/fixtures.js
411
+ - webpack/react_app/components/TargetingHosts/components/HostItem.js
412
+ - webpack/react_app/components/TargetingHosts/components/HostStatus.js
413
+ - webpack/react_app/components/TargetingHosts/index.js
400
414
  - webpack/react_app/components/jobInvocations/AggregateStatus/index.js
401
415
  - webpack/react_app/components/jobInvocations/AggregateStatus/index.test.js
402
416
  - webpack/react_app/components/jobInvocations/index.js
@@ -1,3 +0,0 @@
1
- <td class="host_actions" id="<%= dom_id(host) %>-actions" data-refresh_required="" data-id="<%= host.id %>">
2
- <%= action_buttons(template_invocation_actions(task, host, job_invocation, template_invocation)) %>
3
- </td>
@@ -1,8 +0,0 @@
1
- <td class="host_name" id="<%= dom_id(host) %>-name" data-refresh_required="<%= task.nil? ? 'true' : '' %>" data-id="<%= host.id %>">
2
- <% if task %>
3
- <%= link_to_if_authorized host.name, hash_for_template_invocation_path(:id => template_invocation).merge(:auth_object => host, :permission => :view_hosts, :authorizer => job_hosts_authorizer) %>
4
-
5
- <% else %>
6
- <%= host.name %>
7
- <% end %>
8
- </td>
@@ -1 +0,0 @@
1
- <td class="host_status" id="<%= dom_id(host) %>-status" data-refresh_required="<%= task.nil? || task.pending? ? 'true' : '' %>" data-id="<%= host.id %>"><%= template_invocation_status(task, job_invocation.task) %></td>
@@ -1,23 +0,0 @@
1
- $('div#title_action div.btn-group').html('<%= button_group(job_invocation_task_buttons(@job_invocation.task)).html_safe %>');
2
- <% if params[:hosts_needs_refresh] == 'true' && @job_invocation.resolved? %>
3
- var hosts_table = $('div#hosts');
4
- hosts_table.html('<%=j render('tab_hosts', :job_invocation => @job_invocation, :hosts => @hosts) %>');
5
- hosts_table.data('refresh_required', '');
6
- <% end %>
7
-
8
- <% ['name', 'status', 'actions', 'provider'].each do |attribute| %>
9
- <% if params["host_ids_needing_#{attribute}_update"].present? %>
10
- var td_element;
11
- <% Host.authorized(:view_hosts, Host).where(:id => params["host_ids_needing_#{attribute}_update"]).each do |host| %>
12
- <% template_invocation = @job_invocation.template_invocations.find { |template_invocation| template_invocation.host_id == host.id } %>
13
- <% task = template_invocation.try(:run_host_job_task) %>
14
- <% options = { :host => host, :task => task, :job_invocation => @job_invocation, :template_invocation => template_invocation } %>
15
- td_element= $('#<%= dom_id(host) %>-<%= attribute %>');
16
- td_element.replaceWith('<%=j render("host_#{attribute}_td", options) %>');
17
- <% end %>
18
- <% end %>
19
- <% end %>
20
-
21
- <% if @auto_refresh %>
22
- delayed_refresh($('script#job_invocation_refresh').data('refresh-url'), job_invocation_refresh_data());
23
- <% end %>