foreman_remote_execution 3.3.1 → 3.3.2

Sign up to get free protection for your applications and to get access to all the features.
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 %>