foreman_remote_execution 4.7.0 → 5.1.0

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