foreman_remote_execution 3.3.4 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/app/controllers/api/v2/job_invocations_controller.rb +1 -0
  4. data/app/controllers/foreman_remote_execution/concerns/api/v2/subnets_controller_extensions.rb +21 -0
  5. data/app/controllers/job_invocations_controller.rb +22 -8
  6. data/app/helpers/job_invocations_helper.rb +3 -2
  7. data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
  8. data/app/lib/actions/remote_execution/run_hosts_job.rb +4 -3
  9. data/app/lib/foreman_remote_execution/renderer/scope/input.rb +35 -0
  10. data/app/models/concerns/api/v2/interfaces_controller_extensions.rb +13 -0
  11. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +11 -4
  12. data/app/models/job_invocation.rb +11 -4
  13. data/app/models/job_invocation_composer.rb +2 -2
  14. data/app/models/remote_execution_provider.rb +2 -2
  15. data/app/models/setting/remote_execution.rb +2 -2
  16. data/app/models/ssh_execution_provider.rb +1 -1
  17. data/app/views/api/v2/interfaces/execution_flag.json.rabl +1 -0
  18. data/app/views/api/v2/job_invocations/base.json.rabl +1 -0
  19. data/app/views/api/v2/job_invocations/main.json.rabl +1 -1
  20. data/app/views/api/v2/subnets/remote_execution_proxies.json.rabl +3 -0
  21. data/app/views/job_invocations/_form.html.erb +1 -1
  22. data/app/views/job_invocations/_tab_hosts.html.erb +1 -20
  23. data/app/views/job_invocations/_tab_overview.html.erb +13 -1
  24. data/app/views/job_invocations/show.html.erb +9 -0
  25. data/app/views/job_invocations/show.js.erb +5 -0
  26. data/app/views/job_invocations/show.json.erb +2 -1
  27. data/db/migrate/20200623073022_rename_sudo_password_to_effective_user_password.rb +34 -0
  28. data/db/seeds.d/20-permissions.rb +9 -0
  29. data/lib/foreman_remote_execution/engine.rb +19 -1
  30. data/lib/foreman_remote_execution/version.rb +1 -1
  31. data/test/functional/api/v2/job_invocations_controller_test.rb +65 -2
  32. data/test/functional/job_invocations_controller_test.rb +71 -0
  33. data/test/models/orchestration/ssh_test.rb +1 -1
  34. data/test/support/remote_execution_helper.rb +5 -0
  35. data/test/unit/actions/run_host_job_test.rb +3 -3
  36. data/test/unit/actions/run_hosts_job_test.rb +2 -2
  37. data/test/unit/job_invocation_composer_test.rb +5 -5
  38. data/test/unit/remote_execution_provider_test.rb +6 -6
  39. data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +2 -0
  40. data/webpack/__mocks__/foremanReact/components/SearchBar.js +2 -0
  41. data/webpack/__mocks__/foremanReact/constants.js +21 -0
  42. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +2 -0
  43. data/webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware/IntervalSelectors.js +1 -0
  44. data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +21 -15
  45. data/webpack/react_app/components/TargetingHosts/TargetingHostsHelpers.js +10 -0
  46. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +62 -0
  47. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +6 -0
  48. data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +10 -2
  49. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsPage.test.js +9 -0
  50. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +26 -0
  51. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap +16 -1
  52. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +68 -0
  53. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +11 -0
  54. data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +35 -19
  55. data/webpack/react_app/components/TargetingHosts/index.js +73 -13
  56. metadata +22 -3
  57. data/webpack/react_app/components/TargetingHosts/TargetingHostsActions.js +0 -8
@@ -2,6 +2,9 @@
2
2
  <% stylesheet 'foreman_remote_execution/foreman_remote_execution' %>
3
3
  <% javascript 'charts', 'foreman_remote_execution/template_invocation' %>
4
4
  <% javascript *webpack_asset_paths('foreman_remote_execution', :extension => 'js') %>
5
+ <% content_for(:stylesheets) do %>
6
+ <%= webpacked_plugins_css_for :foreman_remote_execution %>
7
+ <% end %>
5
8
 
6
9
  <%= breadcrumbs name_field: 'description' %>
7
10
 
@@ -41,3 +44,9 @@
41
44
  <% end %>
42
45
  <%= render_tab_content_for(:main_tabs, subject: @job_invocation) %>
43
46
  </div>
47
+
48
+ <script id="job_invocation_refresh" data-refresh-url="<%= job_invocation_path(@job_invocation) %>">
49
+ <% if @auto_refresh %>
50
+ delayed_refresh($('script#job_invocation_refresh').data('refresh-url'), {});
51
+ <% end %>
52
+ </script>
@@ -0,0 +1,5 @@
1
+ $('div#title_action div.btn-group').html('<%= button_group(job_invocation_task_buttons(@job_invocation.task)).html_safe %>');
2
+
3
+ <% if @auto_refresh %>
4
+ delayed_refresh($('script#job_invocation_refresh').data('refresh-url'), job_invocation_refresh_data());
5
+ <% end %>
@@ -1,4 +1,5 @@
1
1
  {
2
2
  "autoRefresh": "<%= @auto_refresh %>",
3
- "hosts": <%= targeting_hosts(@job_invocation, @hosts).to_json.html_safe %>
3
+ "hosts": <%= targeting_hosts(@job_invocation, @hosts).to_json.html_safe %>,
4
+ "total_hosts": <%= @total_hosts %>
4
5
  }
@@ -0,0 +1,34 @@
1
+ class RenameSudoPasswordToEffectiveUserPassword < ActiveRecord::Migration[6.0]
2
+ def up
3
+ rename_column :job_invocations, :sudo_password, :effective_user_password
4
+
5
+ Parameter.where(name: 'remote_execution_sudo_password').each do |parameter|
6
+ record = Parameter.find_by(type: parameter.type, reference_id: parameter.reference_id, name: "remote_execution_effective_user_password")
7
+ if record.nil?
8
+ parameter.update(name: "remote_execution_effective_user_password")
9
+ end
10
+ end
11
+
12
+ return unless (password = Setting.find_by(:name => 'remote_execution_sudo_password').try(:value))
13
+
14
+ Setting.find_by(:name => 'remote_execution_effective_user_password').update(value: password)
15
+
16
+ Setting.find_by(:name => 'remote_execution_sudo_password').delete
17
+ end
18
+
19
+ def down
20
+ rename_column :job_invocations, :effective_user_password, :sudo_password
21
+
22
+ Parameter.where(name: 'remote_execution_effective_user_password').each do |parameter|
23
+ record = Parameter.find_by(type: parameter.type, reference_id: parameter.reference_id, name: "remote_execution_sudo_password")
24
+ if record.nil?
25
+ parameter.update(name: "remote_execution_sudo_password")
26
+ end
27
+ end
28
+
29
+ return unless (password = Setting.find_by(:name => 'remote_execution_effective_user_password').try(:value))
30
+
31
+ Setting.create!(name: 'remote_execution_sudo_password', value: password, description: 'Sudo password', category: 'Setting::RemoteExecution', settings_type: 'string', full_name: 'Sudo password',encrypted: true, default: nil)
32
+ Setting.find_by(:name => 'remote_execution_effective_user_password').delete
33
+ end
34
+ end
@@ -0,0 +1,9 @@
1
+ view_permission = Permission.find_by(name: "view_job_invocations", resource_type: 'JobInvocation')
2
+ default_role = Role.default
3
+
4
+ # the view_permissions can be nil in tests: skipping in that case
5
+ if view_permission && !default_role.permissions.include?(view_permission)
6
+ default_role.filters.create(:search => 'user = current_user') do |filter|
7
+ filter.filterings.build { |f| f.permission = view_permission }
8
+ end
9
+ end
@@ -32,6 +32,15 @@ module ForemanRemoteExecution
32
32
  end
33
33
  end
34
34
 
35
+ # A workaround for https://projects.theforeman.org/issues/30685
36
+ initializer 'foreman_remote_execution.rails_loading_workaround' do
37
+ # Without this, in production environment the module gets prepended too
38
+ # late and the extensions do not get applied
39
+ # TODO: Remove this and from config.to_prepare once there is an extension
40
+ # point in Foreman
41
+ ProvisioningTemplatesHelper.prepend ForemanRemoteExecution::JobTemplatesExtensions
42
+ end
43
+
35
44
  initializer 'foreman_remote_execution.apipie' do
36
45
  Apipie.configuration.checksum_path += ['/api/']
37
46
  end
@@ -44,9 +53,12 @@ module ForemanRemoteExecution
44
53
 
45
54
  initializer 'foreman_remote_execution.register_plugin', before: :finisher_hook do |_app|
46
55
  Foreman::Plugin.register :foreman_remote_execution do
47
- requires_foreman '>= 1.25'
56
+ requires_foreman '>= 2.2'
48
57
 
49
58
  apipie_documented_controllers ["#{ForemanRemoteExecution::Engine.root}/app/controllers/api/v2/*.rb"]
59
+ ApipieDSL.configuration.dsl_classes_matchers += [
60
+ "#{ForemanRemoteExecution::Engine.root}/app/lib/foreman_remote_execution/renderer/**/*.rb",
61
+ ]
50
62
 
51
63
  automatic_assets(false)
52
64
  precompile_assets(*assets_to_precompile)
@@ -135,6 +147,9 @@ module ForemanRemoteExecution
135
147
  end
136
148
 
137
149
  extend_rabl_template 'api/v2/smart_proxies/main', 'api/v2/smart_proxies/pubkey'
150
+ extend_rabl_template 'api/v2/interfaces/main', 'api/v2/interfaces/execution_flag'
151
+ extend_rabl_template 'api/v2/subnets/show', 'api/v2/subnets/remote_execution_proxies'
152
+ parameter_filter ::Subnet, :remote_execution_proxy_ids
138
153
  describe_host { overview_buttons_provider :host_overview_buttons }
139
154
  end
140
155
  end
@@ -181,6 +196,7 @@ module ForemanRemoteExecution
181
196
  SmartProxy.prepend ForemanRemoteExecution::SmartProxyExtensions
182
197
  Subnet.include ForemanRemoteExecution::SubnetExtensions
183
198
 
199
+ ::Api::V2::InterfacesController.include Api::V2::InterfacesControllerExtensions
184
200
  # We need to explicitly force to load the Task model due to Rails loader
185
201
  # having issues with resolving it to Rake::Task otherwise
186
202
  require_dependency 'foreman_tasks/task'
@@ -189,6 +205,8 @@ module ForemanRemoteExecution
189
205
  RemoteExecutionProvider.register(:SSH, SSHExecutionProvider)
190
206
 
191
207
  ForemanRemoteExecution.register_rex_feature
208
+
209
+ ::Api::V2::SubnetsController.include ::ForemanRemoteExecution::Concerns::Api::V2::SubnetsControllerExtensions
192
210
  end
193
211
 
194
212
  initializer 'foreman_remote_execution.register_gettext', after: :load_config_initializers do |_app|
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '3.3.4'.freeze
2
+ VERSION = '4.1.0'.freeze
3
3
  end
@@ -38,13 +38,14 @@ module Api
38
38
 
39
39
  test 'should see only permitted hosts' do
40
40
  @user = FactoryBot.create(:user, admin: false)
41
+ @invocation.task.update(user: @user)
41
42
  setup_user('view', 'job_invocations', nil, @user)
42
43
  setup_user('view', 'hosts', 'name ~ nope.example.com', @user)
43
44
 
44
- get :show, params: { :id => @invocation.id }, session: set_session_user(@user)
45
+ get :show, params: { :id => @invocation.id }, session: prepare_user(@user)
45
46
  assert_response :success
46
47
  response = ActiveSupport::JSON.decode(@response.body)
47
- assert_empty response['targeting']['hosts']
48
+ assert_equal response['targeting']['hosts'], []
48
49
  end
49
50
  end
50
51
 
@@ -298,6 +299,68 @@ module Api
298
299
  post :rerun, params: { :id => @invocation.id }
299
300
  assert_response 404
300
301
  end
302
+
303
+ describe 'restricted access' do
304
+ setup do
305
+ @admin = FactoryBot.create(:user, mail: 'admin@test.foreman.com', admin: true)
306
+ @user = FactoryBot.create(:user, mail: 'user@test.foreman.com', admin: false)
307
+ @invocation = FactoryBot.create(:job_invocation, :with_template, :with_task, :with_unplanned_host)
308
+ @invocation2 = FactoryBot.create(:job_invocation, :with_template, :with_task, :with_unplanned_host)
309
+
310
+ @invocation.task.update(user: @admin)
311
+ @invocation2.task.update(user: @user)
312
+
313
+ setup_user 'view', 'hosts', nil, @user
314
+ setup_user 'view', 'job_invocations', 'user = current_user', @user
315
+ setup_user 'create', 'job_invocations', 'user = current_user', @user
316
+ setup_user 'cancel', 'job_invocations', 'user = current_user', @user
317
+ end
318
+
319
+ let(:host) { @invocation.targeting.hosts.first }
320
+ let(:host2) { @invocation2.targeting.hosts.first }
321
+
322
+ context 'without user filter' do
323
+ test '#index' do
324
+ get :index, session: prepare_user(@admin)
325
+ assert_response :success
326
+ assert JSON.parse(@response.body)['results'].size >= 2
327
+ end
328
+
329
+ test '#show' do
330
+ get :show, params: { id: @invocation2.id }, session: prepare_user(@admin)
331
+ assert_response :success
332
+ end
333
+
334
+ test '#output' do
335
+ get :output, params: { job_invocation_id: @invocation2.id, host_id: host2.id }, session: prepare_user(@admin)
336
+ assert_response :success
337
+ end
338
+ end
339
+
340
+ context 'with user filter' do
341
+ test '#index' do
342
+ get :index, session: prepare_user(@user)
343
+ assert_response :success
344
+ assert_equal 1, JSON.parse(@response.body)['results'].size
345
+ end
346
+
347
+ test '#show' do
348
+ get :show, params: { id: @invocation.id }, session: prepare_user(@user)
349
+ assert_response :not_found
350
+ end
351
+
352
+ test '#output' do
353
+ get :output, params: { job_invocation_id: @invocation.id, host_id: host.id }, session: prepare_user(@user)
354
+ assert_response :not_found
355
+ assert_includes @response.body, 'Job invocation not found'
356
+ end
357
+ end
358
+ end
359
+
360
+ def prepare_user(user)
361
+ User.current = user
362
+ set_session_user(user)
363
+ end
301
364
  end
302
365
  end
303
366
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'test_plugin_helper'
4
+ require_relative '../support/remote_execution_helper'
4
5
 
5
6
  class JobInvocationsControllerTest < ActionController::TestCase
6
7
  test 'should parse inputs coming from the URL params' do
@@ -58,4 +59,74 @@ class JobInvocationsControllerTest < ActionController::TestCase
58
59
  post :new, params: params, session: set_session_user
59
60
  assert_response :success
60
61
  end
62
+
63
+ context 'restricted access' do
64
+ setup do
65
+ @admin = users(:admin)
66
+ @user = FactoryBot.create(:user, mail: 'test23@test.foreman.com', admin: false)
67
+ @invocation = FactoryBot.create(:job_invocation, :with_template, :with_task)
68
+ @invocation2 = FactoryBot.create(:job_invocation, :with_template, :with_task)
69
+
70
+ @invocation.task.update(user: @admin)
71
+ @invocation2.task.update(user: @user)
72
+
73
+ setup_user 'view', 'hosts', nil, @user
74
+ setup_user 'view', 'job_invocations', 'user = current_user', @user
75
+ setup_user 'create', 'job_invocations', 'user = current_user', @user
76
+ setup_user 'cancel', 'job_invocations', 'user = current_user', @user
77
+ end
78
+
79
+ context 'without user filter' do
80
+ test '#index' do
81
+ get :index, session: prepare_user(@admin)
82
+ assert_response :success
83
+ assert 2, assigns(:job_invocations).size
84
+ end
85
+
86
+ test '#show' do
87
+ get :show, params: { id: @invocation2.id }, session: prepare_user(@admin)
88
+ assert_response :success
89
+ end
90
+
91
+ test '#rerun' do
92
+ get :rerun, params: { id: @invocation2.id }, session: prepare_user(@admin)
93
+ assert_response :success
94
+ end
95
+
96
+ test '#cancel' do
97
+ ForemanTasks::Task.any_instance.expects(:cancel).returns(true)
98
+ post :cancel, params: { id: @invocation2.id }, session: prepare_user(@admin)
99
+ assert_response :redirect
100
+ end
101
+ end
102
+
103
+ context 'with user filter' do
104
+ test '#index' do
105
+ get :index, session: prepare_user(@user)
106
+ assert_response :success
107
+ assert_equal 1, assigns(:job_invocations).size
108
+ assert_equal @invocation2, assigns(:job_invocations)[0]
109
+ end
110
+
111
+ test '#show' do
112
+ get :show, params: { id: @invocation.id }, session: prepare_user(@user)
113
+ assert_response :not_found
114
+ end
115
+
116
+ test '#rerun' do
117
+ get :rerun, params: { id: @invocation.id }, session: prepare_user(@user)
118
+ assert_response :not_found
119
+ end
120
+
121
+ test 'cancel' do
122
+ post :cancel, params: { id: @invocation.id }, session: prepare_user(@user)
123
+ assert_response :not_found
124
+ end
125
+ end
126
+ end
127
+
128
+ def prepare_user(user)
129
+ User.current = user
130
+ set_session_user(user)
131
+ end
61
132
  end
@@ -30,7 +30,7 @@ class SSHOrchestrationTest < ActiveSupport::TestCase
30
30
 
31
31
  it 'does not fail on 404 from the smart proxy' do
32
32
  host.stubs(:skip_orchestration?).returns false
33
- SmartProxy.any_instance.expects(:drop_host_from_known_hosts).raises(RestClient::ResourceNotFound).twice
33
+ ::ProxyAPI::RemoteExecutionSSH.any_instance.expects(:delete).raises(RestClient::ResourceNotFound).twice
34
34
  host.build = true
35
35
  host.save!
36
36
  ids = ["ssh_remove_known_hosts_interface_#{interface.ip}_#{proxy.id}",
@@ -0,0 +1,5 @@
1
+ module RemoteExecutionHelper
2
+ def job_invocation_task_buttons(task)
3
+ return []
4
+ end
5
+ end
@@ -12,7 +12,7 @@ module ForemanRemoteExecution
12
12
  let(:provider) do
13
13
  provider = ::SSHExecutionProvider
14
14
  provider.expects(:ssh_password).with(host).returns('sshpass')
15
- provider.expects(:sudo_password).with(host).returns('sudopass')
15
+ provider.expects(:effective_user_password).with(host).returns('sudopass')
16
16
  provider.expects(:ssh_key_passphrase).with(host).returns('keypass')
17
17
  provider
18
18
  end
@@ -21,7 +21,7 @@ module ForemanRemoteExecution
21
21
  secrets = subject.secrets(host, job_invocation, provider)
22
22
 
23
23
  assert_equal 'sshpass', secrets[:ssh_password]
24
- assert_equal 'sudopass', secrets[:sudo_password]
24
+ assert_equal 'sudopass', secrets[:effective_user_password]
25
25
  assert_equal 'keypass', secrets[:key_passphrase]
26
26
  end
27
27
 
@@ -31,7 +31,7 @@ module ForemanRemoteExecution
31
31
  secrets = subject.secrets(host, job_invocation, provider)
32
32
 
33
33
  assert_equal 'jobsshpass', secrets[:ssh_password]
34
- assert_equal 'sudopass', secrets[:sudo_password]
34
+ assert_equal 'sudopass', secrets[:effective_user_password]
35
35
  assert_equal 'jobkeypass', secrets[:key_passphrase]
36
36
  end
37
37
  end
@@ -14,7 +14,7 @@ module ForemanRemoteExecution
14
14
  invocation.description = 'Some short description'
15
15
  invocation.password = 'changeme'
16
16
  invocation.key_passphrase = 'changemetoo'
17
- invocation.sudo_password = 'sudopassword'
17
+ invocation.effective_user_password = 'sudopassword'
18
18
  invocation.save
19
19
  end
20
20
  end
@@ -73,7 +73,7 @@ module ForemanRemoteExecution
73
73
  end
74
74
 
75
75
  it 'triggers the RunHostJob actions on the resolved hosts in run phase' do
76
- planned.expects(:output).returns(:planned_count => 0)
76
+ planned.expects(:output).at_most(5).returns(:planned_count => 0)
77
77
  planned.expects(:trigger).with { |*args| args[0] == Actions::RemoteExecution::RunHostJob }
78
78
  planned.create_sub_plans
79
79
  end
@@ -523,15 +523,15 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
523
523
  end
524
524
  end
525
525
 
526
- describe '#sudo_password' do
527
- let(:sudo_password) { 'password' }
526
+ describe '#effective_user_password' do
527
+ let(:effective_user_password) { 'password' }
528
528
  let(:params) do
529
- { :job_invocation => { :sudo_password => sudo_password }}
529
+ { :job_invocation => { :effective_user_password => effective_user_password }}
530
530
  end
531
531
 
532
- it 'sets the sudo password properly' do
532
+ it 'sets the effective_user_password password properly' do
533
533
  composer
534
- _(composer.job_invocation.sudo_password).must_equal sudo_password
534
+ _(composer.job_invocation.effective_user_password).must_equal effective_user_password
535
535
  end
536
536
  end
537
537
 
@@ -78,12 +78,12 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
78
78
  end
79
79
  end
80
80
 
81
- describe 'sudo password' do
82
- it 'uses the remote_execution_sudo_password on the host param' do
83
- host.params['remote_execution_sudo_password'] = 'mypassword'
84
- host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_sudo_password', :value => 'mypassword')
85
- assert_not proxy_options.key?(:sudo_password)
86
- _(secrets[:sudo_password]).must_equal 'mypassword'
81
+ describe 'effective user password' do
82
+ it 'uses the remote_execution_effective_user_password on the host param' do
83
+ host.params['remote_execution_effective_user_password'] = 'mypassword'
84
+ host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_effective_user_password', :value => 'mypassword')
85
+ assert_not proxy_options.key?(:effective_user_password)
86
+ _(secrets[:effective_user_password]).must_equal 'mypassword'
87
87
  end
88
88
  end
89
89
 
@@ -0,0 +1,2 @@
1
+ const PaginationWrapper = () => jest.fn();
2
+ export default PaginationWrapper;
@@ -0,0 +1,2 @@
1
+ const SearchBar = () => jest.fn();
2
+ export default SearchBar;
@@ -1,3 +1,24 @@
1
1
  export const STATUS = {
2
+ PENDING: 'PENDING',
3
+ RESOLVED: 'RESOLVED',
2
4
  ERROR: 'ERROR',
3
5
  };
6
+
7
+ export const getControllerSearchProps = (
8
+ controller,
9
+ id = 'searchBar',
10
+ canCreate = true
11
+ ) => ({
12
+ controller,
13
+ autocomplete: {
14
+ id,
15
+ searchQuery: '',
16
+ url: `${controller}/auto_complete_search`,
17
+ useKeyShortcuts: true,
18
+ },
19
+ bookmarks: {
20
+ url: '/api/bookmarks',
21
+ canCreate,
22
+ documentationUrl: `4.1.5Searching`,
23
+ },
24
+ });