foreman_remote_execution 4.7.0 → 5.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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +1 -0
  3. data/Gemfile +1 -1
  4. data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
  5. data/app/controllers/ui_job_wizard_controller.rb +16 -4
  6. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  7. data/app/graphql/types/job_invocation_input.rb +13 -0
  8. data/app/graphql/types/recurrence_input.rb +8 -0
  9. data/app/graphql/types/scheduling_input.rb +6 -0
  10. data/app/graphql/types/targeting_enum.rb +7 -0
  11. data/app/lib/actions/remote_execution/run_host_job.rb +8 -1
  12. data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
  13. data/app/lib/foreman_remote_execution/renderer/scope/input.rb +1 -1
  14. data/app/mailers/rex_job_mailer.rb +15 -0
  15. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
  16. data/app/models/job_invocation.rb +6 -0
  17. data/app/models/job_invocation_composer.rb +21 -13
  18. data/app/models/job_template.rb +3 -1
  19. data/app/models/remote_execution_provider.rb +18 -2
  20. data/app/models/rex_mail_notification.rb +13 -0
  21. data/app/models/targeting.rb +2 -2
  22. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  23. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  24. data/app/views/job_invocations/refresh.js.erb +1 -0
  25. data/app/views/job_templates/_custom_tabs.html.erb +4 -9
  26. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  27. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  28. data/app/views/template_invocations/show.html.erb +9 -2
  29. data/config/routes.rb +1 -0
  30. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  31. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  32. data/db/seeds.d/95-mail_notifications.rb +24 -0
  33. data/foreman_remote_execution.gemspec +1 -1
  34. data/lib/foreman_remote_execution/engine.rb +111 -6
  35. data/lib/foreman_remote_execution/version.rb +1 -1
  36. data/package.json +9 -7
  37. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  38. data/test/functional/cockpit_controller_test.rb +0 -1
  39. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  40. data/test/helpers/remote_execution_helper_test.rb +0 -1
  41. data/test/unit/actions/run_host_job_test.rb +21 -0
  42. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  43. data/test/unit/concerns/host_extensions_test.rb +36 -3
  44. data/test/unit/job_invocation_composer_test.rb +3 -5
  45. data/test/unit/job_invocation_report_template_test.rb +17 -14
  46. data/test/unit/job_template_effective_user_test.rb +0 -4
  47. data/test/unit/remote_execution_provider_test.rb +46 -4
  48. data/test/unit/targeting_test.rb +69 -2
  49. data/webpack/JobWizard/JobWizard.js +142 -28
  50. data/webpack/JobWizard/JobWizard.scss +86 -33
  51. data/webpack/JobWizard/JobWizardConstants.js +44 -0
  52. data/webpack/JobWizard/JobWizardSelectors.js +32 -0
  53. data/webpack/JobWizard/__tests__/fixtures.js +89 -6
  54. data/webpack/JobWizard/__tests__/integration.test.js +29 -22
  55. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  56. data/webpack/JobWizard/autofill.js +38 -0
  57. data/webpack/JobWizard/index.js +7 -0
  58. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +23 -9
  59. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
  60. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
  61. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +242 -23
  62. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
  63. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
  64. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
  65. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  66. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  67. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  68. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  69. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  70. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +100 -0
  71. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  72. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  73. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
  74. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  75. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  76. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  77. data/webpack/JobWizard/steps/HostsAndInputs/index.js +214 -0
  78. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  79. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  80. data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
  81. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  82. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  83. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  84. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  85. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
  86. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  87. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
  88. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +78 -23
  89. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  90. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +20 -10
  91. data/webpack/JobWizard/steps/Schedule/index.js +166 -29
  92. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  93. data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
  94. data/webpack/JobWizard/steps/form/Formatter.js +49 -17
  95. data/webpack/JobWizard/steps/form/NumberInput.js +5 -2
  96. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  97. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  98. data/webpack/JobWizard/steps/form/SelectField.js +14 -3
  99. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  100. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  101. data/webpack/JobWizard/submit.js +120 -0
  102. data/webpack/JobWizard/validation.js +53 -0
  103. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  104. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  105. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  106. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  107. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  108. data/webpack/helpers.js +1 -0
  109. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +2 -1
  110. metadata +53 -7
  111. data/app/models/setting/remote_execution.rb +0 -88
  112. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
  113. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
@@ -270,10 +270,6 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
270
270
  composer.pattern_template_invocations.first
271
271
  end
272
272
 
273
- before do
274
- Setting::RemoteExecution.load_defaults
275
- end
276
-
277
273
  context 'when overridable and provided' do
278
274
  let(:overridable) { true }
279
275
  let(:invocation_effective_user) { 'invocation user' }
@@ -370,8 +366,10 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
370
366
  let(:host) { FactoryBot.create(:host) }
371
367
 
372
368
  it 'obeys authorization' do
369
+ fake_scope = mock
373
370
  composer.stubs(:displayed_search_query => "name = #{host.name}")
374
- Host.expects(:authorized).with(:view_hosts, Host).returns(Host.where({}))
371
+ Host.expects(:execution_scope).returns(fake_scope)
372
+ fake_scope.expects(:authorized).with(:view_hosts, Host).returns(Host.where({}))
375
373
  composer.targeted_hosts_count
376
374
  end
377
375
 
@@ -9,7 +9,7 @@ class JobReportTemplateTest < ActiveSupport::TestCase
9
9
 
10
10
  context 'with valid job invocation report template' do
11
11
  let(:job_invocation_template) do
12
- file_path = File.read(File.expand_path(Rails.root + "app/views/unattended/report_templates/jobs_-_invocation_report_template.erb"))
12
+ file_path = File.read(File.expand_path(Rails.root + "app/views/unattended/report_templates/job_invocation_-_report_template.erb"))
13
13
  template = ReportTemplate.import_without_save("Job Invocation Report Template", file_path)
14
14
  template.save!
15
15
  template
@@ -19,7 +19,7 @@ class JobReportTemplateTest < ActiveSupport::TestCase
19
19
  it 'in settings includes only report templates with job_id input' do
20
20
  FactoryBot.create(:report_template, name: 'Template 1')
21
21
  job_invocation_template
22
- templates = Setting::RemoteExecution.job_invocation_report_templates_select
22
+ templates = ForemanRemoteExecution.job_invocation_report_templates_select
23
23
 
24
24
  assert_include templates, 'Job Invocation Report Template'
25
25
  end
@@ -28,15 +28,16 @@ class JobReportTemplateTest < ActiveSupport::TestCase
28
28
  describe 'task reporting' do
29
29
  let(:fake_outputs) do
30
30
  [
31
- { 'output_type' => 'stderr', 'output' => "error", 'timestamp' => Time.new(2020, 12, 1, 0, 0, 0).utc },
32
- { 'output_type' => 'stdout', 'output' => "output", 'timestamp' => Time.new(2020, 12, 1, 0, 0, 0).utc },
33
- { 'output_type' => 'stdebug', 'output' => "debug", 'timestamp' => Time.new(2020, 12, 1, 0, 0, 0).utc },
31
+ { 'output_type' => 'stderr', 'output' => "error" },
32
+ { 'output_type' => 'stdout', 'output' => "output" },
33
+ { 'output_type' => 'debug', 'output' => "debug" },
34
34
  ]
35
35
  end
36
- let(:fake_task) { FakeTask.new(result: 'success', action_continuous_output: fake_outputs) }
36
+ let(:fake_task) { FakeTask.new(result: 'success', action_continuous_output: fake_outputs, :ended_at => Time.new(2020, 12, 1, 0, 0, 0).utc) }
37
+ let(:job_invocation) { FactoryBot.create(:job_invocation, :with_task) }
38
+ let(:host) { job_invocation.template_invocations.first.host }
37
39
 
38
40
  it 'should render task outputs' do
39
- job_invocation = FactoryBot.create(:job_invocation, :with_task)
40
41
  JobInvocation.any_instance.expects(:sub_task_for_host).returns(fake_task)
41
42
 
42
43
  input = job_invocation_template.template_inputs.first
@@ -44,13 +45,15 @@ class JobReportTemplateTest < ActiveSupport::TestCase
44
45
  result = ReportComposer.new(composer_params).render
45
46
 
46
47
  # parsing the CSV result
47
- CSV.parse(result.strip, headers: true).each_with_index do |row, i|
48
- row_hash = row.to_h
49
- assert_equal 'success', row_hash['result']
50
- assert_equal fake_outputs[i]['output_type'], row_hash['type']
51
- assert_equal fake_outputs[i]['output'], row_hash['message']
52
- assert_kind_of Time, Time.zone.parse(row_hash['time']), 'Parsing of time column failed'
53
- end
48
+ rows = CSV.parse(result.strip, headers: true)
49
+ assert_equal 1, rows.count
50
+ row = rows.first
51
+ assert_equal host.name, row['Host']
52
+ assert_equal 'success', row['Result']
53
+ assert_equal 'error', row['stderr']
54
+ assert_equal 'output', row['stdout']
55
+ assert_equal 'debug', row['debug']
56
+ assert_kind_of Time, Time.zone.parse(row['Finished']), 'Parsing of time column failed'
54
57
  end
55
58
  end
56
59
  end
@@ -4,10 +4,6 @@ class JobTemplateEffectiveUserTest < ActiveSupport::TestCase
4
4
  let(:job_template) { FactoryBot.build(:job_template, :job_category => '') }
5
5
  let(:effective_user) { job_template.effective_user }
6
6
 
7
- before do
8
- Setting::RemoteExecution.load_defaults
9
- end
10
-
11
7
  describe 'by default' do
12
8
  it 'is overridable' do
13
9
  assert effective_user.overridable?
@@ -50,14 +50,22 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
50
50
  end
51
51
  end
52
52
 
53
+ describe '.host_setting' do
54
+ let(:host) { FactoryBot.create(:host) }
55
+
56
+ it 'honors falsey values set as a host parameter' do
57
+ key = 'remote_execution_connect_by_ip'
58
+ Setting[key] = true
59
+ host.parameters << HostParameter.new(name: key, value: false)
60
+
61
+ refute RemoteExecutionProvider.host_setting(host, key)
62
+ end
63
+ end
64
+
53
65
  describe SSHExecutionProvider do
54
66
  before { User.current = FactoryBot.build(:user, :admin) }
55
67
  after { User.current = nil }
56
68
 
57
- before do
58
- Setting::RemoteExecution.load_defaults
59
- end
60
-
61
69
  let(:job_invocation) { FactoryBot.create(:job_invocation, :with_template) }
62
70
  let(:template_invocation) { job_invocation.pattern_template_invocations.first }
63
71
  let(:host) { FactoryBot.create(:host) }
@@ -200,6 +208,40 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
200
208
  host.interfaces.each(&:save)
201
209
  host.reload
202
210
  SSHExecutionProvider.find_ip_or_hostname(host).must_equal execution_interface.ip
211
+
212
+ # there is an execution interface with both IPv6 and IPv4: IPv4 is being preferred over IPv6 by default
213
+ execution_interface = FactoryBot.build(:nic_managed,
214
+ flags.merge(:execution => true, :ip => '10.0.0.4', :ip6 => 'fd00::4'))
215
+ host.interfaces = [execution_interface]
216
+ host.interfaces.each(&:save)
217
+ host.reload
218
+ SSHExecutionProvider.find_ip_or_hostname(host).must_equal execution_interface.ip
219
+ end
220
+
221
+ it 'gets ipv6 from flagged interfaces with IPv6 preference' do
222
+ host.host_params['remote_execution_connect_by_ip_prefer_ipv6'] = true
223
+ host.host_params['remote_execution_connect_by_ip'] = true
224
+
225
+ # there is an execution interface with both IPv6 and IPv4: IPv6 is being preferred over IPv4 by host parameter configuration
226
+ execution_interface = FactoryBot.build(:nic_managed,
227
+ flags.merge(:execution => true, :ip => '10.0.0.4', :ip6 => 'fd00::4'))
228
+ host.interfaces = [execution_interface]
229
+ host.interfaces.each(&:save)
230
+ host.reload
231
+ SSHExecutionProvider.find_ip_or_hostname(host).must_equal execution_interface.ip6
232
+ end
233
+
234
+ it 'gets ipv6 from flagged interfaces with IPv4 preference but without IPv4 address' do
235
+ host.host_params['remote_execution_connect_by_ip_prefer_ipv6'] = false
236
+ host.host_params['remote_execution_connect_by_ip'] = true
237
+
238
+ # there is an execution interface with both IPv6 and IPv4: IPv6 is being preferred over IPv4 by host parameter configuration
239
+ execution_interface = FactoryBot.build(:nic_managed,
240
+ flags.merge(:execution => true, :ip => nil, :ip6 => 'fd00::4'))
241
+ host.interfaces = [execution_interface]
242
+ host.interfaces.each(&:save)
243
+ host.reload
244
+ SSHExecutionProvider.find_ip_or_hostname(host).must_equal execution_interface.ip6
203
245
  end
204
246
  end
205
247
  end
@@ -61,7 +61,7 @@ class TargetingTest < ActiveSupport::TestCase
61
61
  users(:one).destroy
62
62
  end
63
63
 
64
- it { assert targeting.reload.user.nil? }
64
+ it { assert_nil targeting.reload.user }
65
65
  it do
66
66
  -> { targeting.resolve_hosts! }.must_raise(Foreman::Exception)
67
67
  end
@@ -77,24 +77,73 @@ class TargetingTest < ActiveSupport::TestCase
77
77
  it { _(targeting.reload.hosts).must_be_empty }
78
78
  end
79
79
 
80
+ describe '#resolve_hosts!' do
81
+ let(:second_host) { FactoryBot.create(:host) }
82
+ let(:infra_host) { FactoryBot.create(:host, :with_infrastructure_facet) }
83
+ let(:targeting) { FactoryBot.build(:targeting) }
84
+
85
+ before do
86
+ host
87
+ second_host
88
+ infra_host
89
+ end
90
+
91
+ context 'with infrastructure host permission' do
92
+ before do
93
+ setup_user('view', 'hosts')
94
+ setup_user('execute_jobs_on', 'infrastructure_hosts')
95
+ end
96
+
97
+ it 'resolves all hosts' do
98
+ hosts = [host, second_host, infra_host]
99
+ targeting.search_query = "name ^ (#{hosts.map(&:name).join(',')})"
100
+ targeting.user = User.current
101
+ targeting.resolve_hosts!
102
+
103
+ targeting.hosts.must_include host
104
+ targeting.hosts.must_include second_host
105
+ targeting.hosts.must_include infra_host
106
+ end
107
+ end
108
+
109
+ context 'without infrastructure host permission' do
110
+ before { setup_user('view', 'hosts') }
111
+
112
+ it 'ignores infrastructure hosts' do
113
+ hosts = [host, second_host, infra_host]
114
+ targeting.search_query = "name ^ (#{hosts.map(&:name).join(',')})"
115
+ targeting.user = User.current
116
+ targeting.resolve_hosts!
117
+
118
+ targeting.hosts.must_include host
119
+ targeting.hosts.must_include second_host
120
+ targeting.hosts.wont_include infra_host
121
+ end
122
+ end
123
+ end
124
+
80
125
  describe '#build_query_from_hosts(ids)' do
81
126
  let(:second_host) { FactoryBot.create(:host) }
127
+ let(:infra_host) { FactoryBot.create(:host, :with_infrastructure_facet) }
82
128
 
83
129
  before do
84
130
  host
85
131
  second_host
132
+ infra_host
86
133
  end
87
134
 
88
135
  context 'for two hosts' do
89
- let(:query) { Targeting.build_query_from_hosts([ host.id, second_host.id ]) }
136
+ let(:query) { Targeting.build_query_from_hosts([ host.id, second_host.id, infra_host.id ]) }
90
137
 
91
138
  it 'builds query using host names joining inside ^' do
92
139
  _(query).must_include host.name
93
140
  _(query).must_include second_host.name
141
+ _(query).must_include infra_host.name
94
142
  _(query).must_include 'name ^'
95
143
 
96
144
  Host.search_for(query).must_include host
97
145
  Host.search_for(query).must_include second_host
146
+ Host.search_for(query).must_include infra_host
98
147
  end
99
148
  end
100
149
 
@@ -105,6 +154,7 @@ class TargetingTest < ActiveSupport::TestCase
105
154
  _(query).must_equal "name ^ (#{host.name})"
106
155
  Host.search_for(query).must_include host
107
156
  Host.search_for(query).wont_include second_host
157
+ Host.search_for(query).wont_include infra_host
108
158
  end
109
159
  end
110
160
 
@@ -114,6 +164,23 @@ class TargetingTest < ActiveSupport::TestCase
114
164
  it 'builds query to find all hosts' do
115
165
  Host.search_for(query).must_include host
116
166
  Host.search_for(query).must_include second_host
167
+ Host.search_for(query).must_include infra_host
168
+ end
169
+ end
170
+
171
+ context 'without infrastructure host permission' do
172
+ before { User.current = nil }
173
+
174
+ it 'ignores the infrastructure host' do
175
+ query = Targeting.build_query_from_hosts([host.id, second_host.id, infra_host.id])
176
+ _(query).must_include host.name
177
+ _(query).must_include second_host.name
178
+ _(query).wont_include infra_host.name
179
+ _(query).must_include 'name ^'
180
+
181
+ Host.search_for(query).must_include host
182
+ Host.search_for(query).must_include second_host
183
+ Host.search_for(query).wont_include infra_host
117
184
  end
118
185
  end
119
186
  end
@@ -3,43 +3,91 @@ import React, { useState, useEffect, useCallback } from 'react';
3
3
  import { useDispatch, useSelector } from 'react-redux';
4
4
  import { Wizard } from '@patternfly/react-core';
5
5
  import { get } from 'foremanReact/redux/API';
6
- import { translate as __ } from 'foremanReact/common/I18n';
7
6
  import history from 'foremanReact/history';
7
+
8
+ import {
9
+ useForemanOrganization,
10
+ useForemanLocation,
11
+ } from 'foremanReact/Root/Context/ForemanContext';
8
12
  import CategoryAndTemplate from './steps/CategoryAndTemplate/';
9
13
  import { AdvancedFields } from './steps/AdvancedFields/AdvancedFields';
10
- import { JOB_TEMPLATE } from './JobWizardConstants';
11
- import { selectTemplateError } from './JobWizardSelectors';
14
+ import {
15
+ JOB_TEMPLATE,
16
+ WIZARD_TITLES,
17
+ initialScheduleState,
18
+ } from './JobWizardConstants';
19
+ import {
20
+ selectTemplateError,
21
+ selectJobTemplate,
22
+ selectIsSubmitting,
23
+ } from './JobWizardSelectors';
12
24
  import Schedule from './steps/Schedule/';
25
+ import HostsAndInputs from './steps/HostsAndInputs/';
26
+ import ReviewDetails from './steps/ReviewDetails/';
27
+ import { useValidation } from './validation';
28
+ import { useAutoFill } from './autofill';
29
+ import { submit } from './submit';
13
30
  import './JobWizard.scss';
14
31
 
15
32
  export const JobWizard = () => {
16
33
  const [jobTemplateID, setJobTemplateID] = useState(null);
17
34
  const [category, setCategory] = useState('');
18
- const [advancedValues, setAdvancedValues] = useState({});
35
+ const [advancedValues, setAdvancedValues] = useState({ templateValues: {} });
36
+ const [templateValues, setTemplateValues] = useState({}); // TODO use templateValues in advanced fields - description https://github.com/theforeman/foreman_remote_execution/pull/605
37
+ const [scheduleValue, setScheduleValue] = useState(initialScheduleState);
38
+ const [selectedTargets, setSelectedTargets] = useState({
39
+ hosts: [],
40
+ hostCollections: [],
41
+ hostGroups: [],
42
+ });
43
+ const [hostsSearchQuery, setHostsSearchQuery] = useState('');
19
44
  const dispatch = useDispatch();
20
45
 
21
46
  const setDefaults = useCallback(
22
47
  ({
23
48
  data: {
49
+ template_inputs,
24
50
  advanced_template_inputs,
25
51
  effective_user,
26
- job_template: { executionTimeoutInterval, description_format },
52
+ job_template: { name, execution_timeout_interval, description_format },
27
53
  },
28
54
  }) => {
29
55
  const advancedTemplateValues = {};
56
+ const defaultTemplateValues = {};
57
+ const inputs = template_inputs;
30
58
  const advancedInputs = advanced_template_inputs;
31
- if (advancedInputs) {
32
- advancedInputs.forEach(input => {
33
- advancedTemplateValues[input.name] = input?.default || '';
59
+ if (inputs) {
60
+ setTemplateValues(() => {
61
+ inputs.forEach(input => {
62
+ defaultTemplateValues[input.name] = input?.default || '';
63
+ });
64
+ return defaultTemplateValues;
34
65
  });
35
66
  }
36
- setAdvancedValues(currentAdvancedValues => ({
37
- ...currentAdvancedValues,
38
- effectiveUserValue: effective_user?.value || '',
39
- timeoutToKill: executionTimeoutInterval || '',
40
- templateValues: advancedTemplateValues,
41
- description: description_format || '',
42
- }));
67
+ setAdvancedValues(currentAdvancedValues => {
68
+ if (advancedInputs) {
69
+ advancedInputs.forEach(input => {
70
+ advancedTemplateValues[input.name] = input?.default || '';
71
+ });
72
+ }
73
+ const generateDefaultDescription = () => {
74
+ if (description_format) return description_format;
75
+ const allInputs = [...advancedInputs, ...inputs];
76
+ if (!allInputs.length) return name;
77
+ const inputsString = allInputs
78
+ .map(({ name: inputname }) => `${inputname}="%{${inputname}}"`)
79
+ .join(' ');
80
+ return `${name} with inputs ${inputsString}`;
81
+ };
82
+ return {
83
+ ...currentAdvancedValues,
84
+ effectiveUserValue: effective_user?.value || '',
85
+ timeoutToKill: execution_timeout_interval || '',
86
+ templateValues: advancedTemplateValues,
87
+ description: generateDefaultDescription() || '',
88
+ isRandomizedOrdering: false,
89
+ };
90
+ });
43
91
  },
44
92
  []
45
93
  );
@@ -55,11 +103,23 @@ export const JobWizard = () => {
55
103
  }
56
104
  }, [jobTemplateID, setDefaults, dispatch]);
57
105
 
106
+ const [valid, setValid] = useValidation({
107
+ advancedValues,
108
+ templateValues,
109
+ });
110
+ useAutoFill({
111
+ setSelectedTargets,
112
+ setHostsSearchQuery,
113
+ });
58
114
  const templateError = !!useSelector(selectTemplateError);
59
- const isTemplate = !templateError && !!jobTemplateID;
115
+ const templateResponse = useSelector(selectJobTemplate);
116
+ const isSubmitting = useSelector(selectIsSubmitting);
117
+ const isTemplate =
118
+ !templateError && !!jobTemplateID && templateResponse.job_template;
119
+
60
120
  const steps = [
61
121
  {
62
- name: __('Category and Template'),
122
+ name: WIZARD_TITLES.categoryAndTemplate,
63
123
  component: (
64
124
  <CategoryAndTemplate
65
125
  jobTemplate={jobTemplateID}
@@ -68,14 +128,25 @@ export const JobWizard = () => {
68
128
  setCategory={setCategory}
69
129
  />
70
130
  ),
131
+ enableNext: isTemplate,
71
132
  },
72
133
  {
73
- name: __('Target Hosts'),
74
- component: <p>Target Hosts</p>,
134
+ name: WIZARD_TITLES.hostsAndInputs,
135
+ component: (
136
+ <HostsAndInputs
137
+ templateValues={templateValues}
138
+ setTemplateValues={setTemplateValues}
139
+ selected={selectedTargets}
140
+ setSelected={setSelectedTargets}
141
+ hostsSearchQuery={hostsSearchQuery}
142
+ setHostsSearchQuery={setHostsSearchQuery}
143
+ />
144
+ ),
75
145
  canJumpTo: isTemplate,
146
+ enableNext: isTemplate && valid.hostsAndInputs,
76
147
  },
77
148
  {
78
- name: __('Advanced Fields'),
149
+ name: WIZARD_TITLES.advanced,
79
150
  component: (
80
151
  <AdvancedFields
81
152
  advancedValues={advancedValues}
@@ -85,23 +156,53 @@ export const JobWizard = () => {
85
156
  ...newValues,
86
157
  }));
87
158
  }}
88
- jobTemplateID={jobTemplateID}
159
+ templateValues={templateValues}
89
160
  />
90
161
  ),
91
- canJumpTo: isTemplate,
162
+ canJumpTo: isTemplate && valid.hostsAndInputs,
163
+ enableNext: isTemplate && valid.hostsAndInputs && valid.advanced,
92
164
  },
93
165
  {
94
- name: __('Schedule'),
95
- component: <Schedule />,
96
- canJumpTo: isTemplate,
166
+ name: WIZARD_TITLES.schedule,
167
+ component: (
168
+ <Schedule
169
+ scheduleValue={scheduleValue}
170
+ setScheduleValue={setScheduleValue}
171
+ setValid={newValue => {
172
+ setValid(currentValid => ({ ...currentValid, schedule: newValue }));
173
+ }}
174
+ />
175
+ ),
176
+ canJumpTo: isTemplate && valid.hostsAndInputs && valid.advanced,
177
+ enableNext:
178
+ isTemplate && valid.hostsAndInputs && valid.advanced && valid.schedule,
97
179
  },
98
180
  {
99
- name: __('Review Details'),
100
- component: <p>Review Details</p>,
181
+ name: WIZARD_TITLES.review,
182
+ component: (
183
+ <ReviewDetails
184
+ jobCategory={category}
185
+ jobTemplateID={jobTemplateID}
186
+ advancedValues={advancedValues}
187
+ scheduleValue={scheduleValue}
188
+ templateValues={templateValues}
189
+ selectedTargets={selectedTargets}
190
+ hostsSearchQuery={hostsSearchQuery}
191
+ />
192
+ ),
101
193
  nextButtonText: 'Run',
102
- canJumpTo: isTemplate,
194
+ canJumpTo:
195
+ isTemplate && valid.hostsAndInputs && valid.advanced && valid.schedule,
196
+ enableNext:
197
+ isTemplate &&
198
+ valid.hostsAndInputs &&
199
+ valid.advanced &&
200
+ valid.schedule &&
201
+ !isSubmitting,
103
202
  },
104
203
  ];
204
+ const location = useForemanLocation();
205
+ const organization = useForemanOrganization();
105
206
  return (
106
207
  <Wizard
107
208
  onClose={() => history.goBack()}
@@ -109,6 +210,19 @@ export const JobWizard = () => {
109
210
  steps={steps}
110
211
  height="100%"
111
212
  className="job-wizard"
213
+ onSave={() => {
214
+ submit({
215
+ jobTemplateID,
216
+ templateValues,
217
+ advancedValues,
218
+ scheduleValue,
219
+ dispatch,
220
+ selectedTargets,
221
+ hostsSearchQuery,
222
+ location,
223
+ organization,
224
+ });
225
+ }}
112
226
  />
113
227
  );
114
228
  };
@@ -1,53 +1,106 @@
1
1
  .job-wizard {
2
+ .wizard-title {
3
+ margin-bottom: 25px;
4
+ }
5
+
6
+ .pf-c-wizard__nav.pf-m-expanded {
7
+ z-index: calc(
8
+ var(--pf-c-wizard__footer--ZIndex) + 2
9
+ ); // So the small screen navigation can be shown above the select box
10
+ }
11
+
2
12
  .pf-c-wizard__main {
13
+ overflow: visible;
3
14
  z-index: calc(
4
15
  var(--pf-c-wizard__footer--ZIndex) + 1
5
16
  ); // So the select box can be shown above the wizard footer
6
17
  }
7
-
18
+ .pf-c-wizard__nav {
19
+ z-index: calc(
20
+ var(--pf-c-wizard__footer--ZIndex) + 2
21
+ ); // So the navigation box can be shown above the wizard body
22
+ }
8
23
  .pf-c-wizard__main-body {
9
24
  max-width: 500px;
10
25
  .advanced-fields-title {
11
26
  margin-bottom: 10px;
12
27
  }
13
- #advanced-fields-job-template {
14
- .foreman-search-field {
15
- // Giving pf3 search bar a pf4 look
16
- .search-bar {
17
- display: block;
18
- }
19
- .input-group-btn {
20
- display: none;
21
- }
22
- li {
23
- font-size: 16px;
24
- }
25
- input {
26
- font-size: 16px;
27
- height: 36px;
28
- }
29
- .foreman-autocomplete .autocomplete-focus-shortcut {
30
- top: 8px;
31
- font-size: 16px;
32
- }
33
- .foreman-autocomplete .autocomplete-aux {
34
- top: 8px;
35
- font-size: 16px;
36
- .autocomplete-clear-button {
37
- font-size: 16px;
38
- }
39
- }
28
+
29
+ }
30
+
31
+ .gray-text {
32
+ color: var(--pf-global--Color--dark-200);
33
+ }
34
+
35
+ .target-hosts-and-inputs {
36
+ .hosts-chip-group {
37
+ margin-top: 8px;
38
+ float: left;
39
+ clear: left;
40
+ display: block;
41
+ }
42
+ .clear-chips {
43
+ margin-top: 8px;
44
+ }
45
+ .pf-c-select__toggle-typeahead {
46
+ border: 0px;
47
+ }
48
+
49
+ .target-method-select {
50
+ .pf-c-select__toggle-wrapper {
51
+ flex-wrap: nowrap;
40
52
  }
41
53
  }
42
54
  }
43
-
55
+ input[type='radio'],
56
+ input[type='checkbox'] {
57
+ margin: 0;
58
+ }
44
59
  .schedule-tab {
45
- input[type='radio'],
46
- input[type='checkbox'] {
47
- margin: 0;
48
- }
49
60
  .advanced-scheduling-button {
50
61
  text-align: start;
51
62
  }
63
+ #repeat-on-weekly {
64
+ display: grid;
65
+ grid-template-columns: repeat(7, 1fr);
66
+ }
67
+ .pf-l-grid {
68
+ gap: var(--pf-c-form--GridGap);
69
+ }
70
+ #repeat-on-hourly {
71
+ max-height: 300px;
72
+ overflow: scroll;
73
+ }
74
+ }
75
+
76
+ .pf-c-date-picker {
77
+ vertical-align: top;
78
+ }
79
+
80
+ .time-picker {
81
+ width: 150px;
82
+ }
83
+
84
+ input[type='radio'],
85
+ input[type='checkbox'] {
86
+ // overwriting bootstrap/_forms.scss margin: 4px 0 0;
87
+ margin: 0;
88
+ }
89
+ textarea {
90
+ min-height: 40px;
91
+ min-width: 100px;
92
+ }
93
+ #host-selection {
94
+ width: 500px;
95
+ }
96
+
97
+ .pf-c-modal-box {
98
+ width: auto;
99
+ }
100
+
101
+ .review-details {
102
+ .advanced-fields {
103
+ margin-left: 10px;
104
+ }
52
105
  }
53
106
  }