foreman_remote_execution 3.3.0 → 3.3.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +101 -0
  3. data/.rubocop_todo.yml +3 -0
  4. data/Gemfile +1 -0
  5. data/app/assets/stylesheets/foreman_remote_execution/job_invocations.scss +6 -5
  6. data/app/controllers/api/v2/job_invocations_controller.rb +23 -1
  7. data/app/controllers/api/v2/template_invocations_controller.rb +4 -1
  8. data/app/helpers/job_invocations_helper.rb +1 -1
  9. data/app/helpers/remote_execution_helper.rb +38 -33
  10. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +1 -1
  11. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +18 -9
  12. data/app/services/default_proxy_proxy_selector.rb +3 -1
  13. data/app/views/api/v2/job_invocations/main.json.rabl +8 -2
  14. data/app/views/job_invocations/_card_target_hosts.html.erb +1 -1
  15. data/app/views/job_invocations/_tab_hosts.html.erb +3 -23
  16. data/app/views/job_invocations/index.html.erb +2 -1
  17. data/app/views/job_invocations/show.html.erb +3 -3
  18. data/app/views/job_invocations/show.js.erb +0 -18
  19. data/app/views/job_invocations/show.json.erb +4 -0
  20. data/app/views/templates/ssh/package_action.erb +1 -0
  21. data/app/views/templates/ssh/puppet_agent_disable.erb +3 -0
  22. data/app/views/templates/ssh/puppet_agent_enable.erb +3 -0
  23. data/app/views/templates/ssh/puppet_install_modules_from_forge.erb +3 -0
  24. data/app/views/templates/ssh/puppet_run_once.erb +3 -0
  25. data/foreman_remote_execution.gemspec +4 -5
  26. data/lib/foreman_remote_execution/version.rb +1 -1
  27. data/locale/action_names.rb +0 -1
  28. data/test/functional/api/v2/job_invocations_controller_test.rb +42 -14
  29. data/test/models/orchestration/ssh_test.rb +32 -0
  30. data/test/unit/concerns/host_extensions_test.rb +7 -0
  31. data/webpack/__mocks__/foremanReact/common/I18n.js +1 -0
  32. data/webpack/__mocks__/foremanReact/components/common/ActionButtons/ActionButtons.js +3 -0
  33. data/webpack/__mocks__/foremanReact/constants.js +3 -0
  34. data/webpack/index.js +8 -5
  35. data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +52 -0
  36. data/webpack/react_app/components/TargetingHosts/TargetingHostsActions.js +8 -0
  37. data/webpack/react_app/components/TargetingHosts/TargetingHostsConsts.js +1 -0
  38. data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +12 -0
  39. data/webpack/react_app/components/TargetingHosts/__tests__/HostItem.test.js +6 -0
  40. data/webpack/react_app/components/TargetingHosts/__tests__/HostStatus.test.js +6 -0
  41. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHosts.test.js +6 -0
  42. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostItem.test.js.snap +31 -0
  43. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostStatus.test.js.snap +12 -0
  44. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap +81 -0
  45. data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +43 -0
  46. data/webpack/react_app/components/TargetingHosts/components/HostItem.js +39 -0
  47. data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +54 -0
  48. data/webpack/react_app/components/TargetingHosts/index.js +37 -0
  49. metadata +26 -12
  50. data/.hound.yml +0 -14
  51. data/.travis.yml +0 -5
  52. data/app/views/job_invocations/_host_actions_td.html.erb +0 -3
  53. data/app/views/job_invocations/_host_name_td.html.erb +0 -8
  54. data/app/views/job_invocations/_host_status_td.html.erb +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d00f4722f74258ad947432c2d8104522d3fb53749f7bac4e7c52914862af3ecc
4
- data.tar.gz: 187645d51578339523b94fb83fe781cba865477a616c2d8c0980255be1fd0377
3
+ metadata.gz: 6add16b63c650250f993954c92620863d9904055d4b175fa76b04b97dd70ee92
4
+ data.tar.gz: 7fca35b0b17e7f2ad0f5769fae4f56b0d5e6f0c85b9ebcc8abab29858bf585a5
5
5
  SHA512:
6
- metadata.gz: 04c6a44e96bb03d75310d7dd48acd249503f04778c62f520d1101bf4719d195c2cd4d11e9a6ae63c97e24d8cbd50884dd090ec56a54aff933ee19f3379bbeec1
7
- data.tar.gz: 165ba49b54b18ed4128c7b72a53ef52fea60f6f9a619ee7bd0a22545910cf68997b763e27f3569892b20dd528e70236c44b241c1d1b6c5fcec15a5a21f76af7e
6
+ metadata.gz: 359e21b88b85d45b1c4faef9a7e841b0c34e008d3e818c111eba4079167683ff6c59e1354f2f2aafc18e7be43c93c00c368eb9d331636d6c3103d6c59f637306
7
+ data.tar.gz: 17aa0693af9ec50fca1a97e4e685c914a665edfe78cf0d9a87b17a9c683490ad7b151748c36adb9b9bcb50389afaca533065d4590d23f2b7550eddad8017538c
@@ -0,0 +1,101 @@
1
+ name: CI
2
+ on: [pull_request]
3
+ env:
4
+ RAILS_ENV: test
5
+ DATABASE_URL: postgresql://postgres:@localhost/test
6
+ DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL: true
7
+ jobs:
8
+ rubocop:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v2
12
+ - name: Setup Ruby
13
+ uses: ruby/setup-ruby@v1
14
+ with:
15
+ ruby-version: 2.6
16
+ - name: Setup
17
+ run: |
18
+ gem install bundler
19
+ bundle install --jobs=3 --retry=3
20
+ - name: Run rubocop
21
+ run: bundle exec rubocop
22
+ test_ruby:
23
+ runs-on: ubuntu-latest
24
+ needs: rubocop
25
+ services:
26
+ postgres:
27
+ image: postgres:12.1
28
+ ports: ['5432:5432']
29
+ options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
30
+ strategy:
31
+ fail-fast: false
32
+ matrix:
33
+ foreman-core-branch: [2.1-stable, develop]
34
+ ruby-version: [2.5, 2.6]
35
+ node-version: [12]
36
+ steps:
37
+ - run: sudo apt-get update
38
+ - run: sudo apt-get install build-essential libcurl4-openssl-dev zlib1g-dev libpq-dev
39
+ - uses: actions/checkout@v2
40
+ with:
41
+ repository: theforeman/foreman
42
+ ref: ${{ matrix.foreman-core-branch }}
43
+ - uses: actions/checkout@v2
44
+ with:
45
+ path: foreman_remote_execution
46
+ - name: Setup Ruby
47
+ uses: ruby/setup-ruby@v1
48
+ with:
49
+ ruby-version: ${{ matrix.ruby-version }}
50
+ - name: Setup Node
51
+ uses: actions/setup-node@v1
52
+ with:
53
+ node-version: ${{ matrix.node-version }}
54
+ - uses: actions/cache@v1
55
+ with:
56
+ path: vendor/bundle
57
+ key: ${{ runner.os }}-fgems-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }}
58
+ restore-keys: |
59
+ ${{ runner.os }}-fgems-${{ matrix.ruby-version }}-
60
+ - name: Setup Bundler
61
+ run: |
62
+ echo "gem 'foreman_remote_execution', path: './foreman_remote_execution'" > bundler.d/foreman_remote_execution.local.rb
63
+ gem install bundler
64
+ bundle config set without journald development console libvirt
65
+ bundle config set path vendor/bundle
66
+ - name: Prepare test env
67
+ run: |
68
+ bundle install --jobs=3 --retry=3
69
+ bundle exec rake db:create
70
+ bundle exec rake db:migrate
71
+ - name: Run plugin tests
72
+ run: |
73
+ bundle exec rake test:foreman_remote_execution
74
+ bundle exec rake test TEST="test/unit/foreman/access_permissions_test.rb"
75
+ test_js:
76
+ runs-on: ubuntu-latest
77
+ needs: rubocop
78
+ strategy:
79
+ fail-fast: false
80
+ matrix:
81
+ ruby-version: [2.6]
82
+ node-version: [10, 12]
83
+ steps:
84
+ - uses: actions/checkout@v2
85
+ - name: Setup Ruby
86
+ uses: ruby/setup-ruby@v1
87
+ with:
88
+ ruby-version: ${{ matrix.ruby-version }}
89
+ - name: Setup Node
90
+ uses: actions/setup-node@v1
91
+ with:
92
+ node-version: ${{ matrix.node-version }}
93
+ - name: Nmp install
94
+ run: |
95
+ npm install
96
+ - name: Run plugin linter
97
+ run: |
98
+ npm run lint
99
+ - name: Run plugin tests
100
+ run: |
101
+ npm run test
@@ -6,6 +6,9 @@
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
+ Minitest/GlobalExpectations:
10
+ Enabled: false
11
+
9
12
  # Offense count: 2
10
13
  # Cop supports --auto-correct.
11
14
  # Configuration parameters: TreatCommentsAsGroupSeparators, Include.
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'
@@ -1,9 +1,6 @@
1
- div.infoblock {
2
- margin-bottom: 20px;
3
- line-height: 2;
4
-
1
+ .target-hosts-card {
5
2
  pre {
6
- margin-top: 5px;
3
+ white-space:pre-line;
7
4
  }
8
5
  }
9
6
 
@@ -29,3 +26,7 @@ div.infoblock {
29
26
  }
30
27
  }
31
28
  }
29
+
30
+ .text_warp{
31
+ word-wrap: break-word;
32
+ }
@@ -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, :bool, required: false, 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] == 'true'
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
@@ -70,6 +80,7 @@ module Api
70
80
  end
71
81
  composer.trigger!
72
82
  @job_invocation = composer.job_invocation
83
+ @hosts = @job_invocation.targeting.hosts
73
84
  process_response @job_invocation
74
85
  end
75
86
 
@@ -146,7 +157,7 @@ module Api
146
157
  end
147
158
 
148
159
  def find_host
149
- @host = Host.authorized(:view_hosts).find(params['host_id'])
160
+ @host = @nested_obj.targeting.hosts.authorized(:view_hosts, Host).find(params['host_id'])
150
161
  rescue ActiveRecord::RecordNotFound
151
162
  not_found({ :error => { :message => (_("Host with id '%{id}' was not found") % { :id => params['host_id'] }) } })
152
163
  end
@@ -204,6 +215,17 @@ module Api
204
215
  def parent_scope
205
216
  resource_class.where(nil)
206
217
  end
218
+
219
+ def template_invocation_status(template_invocation)
220
+ task = template_invocation.try(:run_host_job_task)
221
+ parent_task = @job_invocation.task
222
+
223
+ return(parent_task.result == 'cancelled' ? 'cancelled' : 'N/A') if task.nil?
224
+ return task.state if task.state == 'running' || task.state == 'planned'
225
+ return 'error' if task.result == 'warning'
226
+
227
+ task.result
228
+ end
207
229
  end
208
230
  end
209
231
  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,
@@ -8,14 +8,20 @@ module ForemanRemoteExecution
8
8
  register_rebuild(:queue_ssh_destroy, N_("SSH_#{self.to_s.split('::').first}"))
9
9
  end
10
10
 
11
- def drop_from_known_hosts(args)
12
- proxy_id, target = args
11
+ def drop_from_known_hosts(proxy_id)
12
+ _, _, target = host_kind_target
13
+ return true if target.nil?
14
+
13
15
  proxy = ::SmartProxy.find(proxy_id)
14
16
  begin
15
17
  proxy.drop_host_from_known_hosts(target)
16
- rescue RestClient::ResourceNotFound => e
17
- # ignore 404 when known_hosts entry is missing or the module was not enabled
18
- Foreman::Logging.exception "Proxy failed to delete SSH known_hosts for #{name}, #{ip}", e, :level => :error
18
+ rescue ::ProxyAPI::ProxyException => e
19
+ if e.wrapped_exception.is_a?(RestClient::NotFound)
20
+ # ignore 404 when known_hosts entry is missing or the module was not enabled
21
+ Foreman::Logging.exception "Proxy failed to delete SSH known_hosts for #{name}, #{ip}", e, :level => :error
22
+ else
23
+ raise e
24
+ end
19
25
  rescue => e
20
26
  Rails.logger.warn e.message
21
27
  return false
@@ -26,11 +32,14 @@ module ForemanRemoteExecution
26
32
  def ssh_destroy
27
33
  logger.debug "Scheduling SSH known_hosts cleanup"
28
34
 
29
- host, _kind, target = host_kind_target
30
- proxies = host.remote_execution_proxies('SSH').values
35
+ host, _kind, _target = host_kind_target
36
+ # #remote_execution_proxies may not be defined on the host object in some case
37
+ # for example Host::Discovered does not have it defined, even though these hosts
38
+ # have Nic::Managed interfaces associated with them
39
+ proxies = (host.try(:remote_execution_proxies, 'SSH') || {}).values
31
40
  proxies.flatten.uniq.each do |proxy|
32
41
  queue.create(id: queue_id(proxy.id), name: _("Remove SSH known hosts for %s") % self,
33
- priority: 200, action: [self, :drop_from_known_hosts, [proxy.id, target]])
42
+ priority: 200, action: [self, :drop_from_known_hosts, proxy.id])
34
43
  end
35
44
  end
36
45
 
@@ -40,7 +49,7 @@ module ForemanRemoteExecution
40
49
 
41
50
  def should_drop_from_known_hosts?
42
51
  host, = host_kind_target
43
- host&.build && host&.changes&.key?('build')
52
+ host && !host.new_record? && host.build && host.changes.key?('build')
44
53
  end
45
54
 
46
55
  private
@@ -10,7 +10,9 @@ class DefaultProxyProxySelector < ::RemoteExecutionProxySelector
10
10
  def available_proxies(host, provider)
11
11
  # TODO: Once we have a internal proxy marker/feature on the proxy, we can
12
12
  # swap the implementation
13
- internal_proxy = ::Katello.default_capsule
13
+ raise _('default_capsule method missing from SmartProxy') unless ::SmartProxy.respond_to?(:default_capsule)
14
+
15
+ internal_proxy = ::SmartProxy.default_capsule
14
16
  super.reduce({}) do |acc, (key, proxies)|
15
17
  acc.merge(key => proxies.select { |proxy| proxy == internal_proxy })
16
18
  end
@@ -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] == 'true'
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
@@ -1,5 +1,5 @@
1
1
  <% template_invocations = job_invocation.pattern_template_invocations %>
2
- <div class="card-pf card-pf-accented">
2
+ <div class="card-pf card-pf-accented target-hosts-card">
3
3
  <div class="card-pf-title">
4
4
  <h2 style="height: 18px;" class="card-pf-title">
5
5
  <%= _('Target hosts') %>
@@ -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">