foreman_remote_execution 4.7.0 → 4.8.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +1 -0
  3. data/app/controllers/api/v2/job_invocations_controller.rb +7 -1
  4. data/app/lib/actions/remote_execution/run_host_job.rb +2 -1
  5. data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
  6. data/app/mailers/rex_job_mailer.rb +15 -0
  7. data/app/models/job_invocation.rb +4 -0
  8. data/app/models/job_invocation_composer.rb +20 -12
  9. data/app/models/remote_execution_provider.rb +18 -2
  10. data/app/models/rex_mail_notification.rb +13 -0
  11. data/app/models/setting/remote_execution.rb +7 -1
  12. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  13. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  14. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  15. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  16. data/app/views/template_invocations/show.html.erb +2 -1
  17. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  18. data/db/seeds.d/95-mail_notifications.rb +24 -0
  19. data/foreman_remote_execution.gemspec +1 -1
  20. data/lib/foreman_remote_execution/engine.rb +1 -0
  21. data/lib/foreman_remote_execution/version.rb +1 -1
  22. data/package.json +6 -6
  23. data/test/functional/api/v2/job_invocations_controller_test.rb +10 -0
  24. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  25. data/test/unit/job_invocation_report_template_test.rb +15 -12
  26. data/test/unit/remote_execution_provider_test.rb +46 -0
  27. data/webpack/JobWizard/JobWizard.js +53 -20
  28. data/webpack/JobWizard/JobWizard.scss +33 -4
  29. data/webpack/JobWizard/JobWizardConstants.js +17 -0
  30. data/webpack/JobWizard/__tests__/fixtures.js +8 -0
  31. data/webpack/JobWizard/__tests__/integration.test.js +3 -7
  32. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +16 -5
  33. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
  34. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +29 -14
  35. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +4 -2
  36. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
  37. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +25 -0
  38. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  39. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +37 -0
  40. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +50 -0
  41. data/webpack/JobWizard/steps/HostsAndInputs/index.js +66 -0
  42. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
  43. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +36 -21
  44. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +155 -0
  45. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +9 -8
  46. data/webpack/JobWizard/steps/Schedule/index.js +89 -28
  47. data/webpack/JobWizard/steps/form/DateTimePicker.js +93 -0
  48. data/webpack/JobWizard/steps/form/Formatter.js +10 -9
  49. data/webpack/JobWizard/steps/form/NumberInput.js +2 -0
  50. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  51. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +2 -1
  52. metadata +18 -4
@@ -0,0 +1,24 @@
1
+ N_('Remote execution job')
2
+
3
+ notifications = [
4
+ {
5
+ :name => 'remote_execution_job',
6
+ :description => N_('A notification when a job finishes'),
7
+ :mailer => 'RexJobMailer',
8
+ :method => 'job_finished',
9
+ :subscription_type => 'alert',
10
+ },
11
+ ]
12
+
13
+ notifications.each do |notification|
14
+ if (mail = RexMailNotification.find_by(name: notification[:name]))
15
+ mail.attributes = notification
16
+ mail.save! if mail.changed?
17
+ else
18
+ created_notification = RexMailNotification.create(notification)
19
+ if created_notification.nil? || created_notification.errors.any?
20
+ raise ::Foreman::Exception.new(N_("Unable to create mail notification: %s"),
21
+ format_errors(created_notification))
22
+ end
23
+ end
24
+ end
@@ -24,7 +24,7 @@ Gem::Specification.new do |s|
24
24
 
25
25
  s.add_dependency 'deface'
26
26
  s.add_dependency 'dynflow', '>= 1.0.2', '< 2.0.0'
27
- s.add_dependency 'foreman-tasks', '>= 5.0.0'
27
+ s.add_dependency 'foreman-tasks', '>= 5.1.0'
28
28
 
29
29
  s.add_development_dependency 'factory_bot_rails', '~> 4.8.0'
30
30
  s.add_development_dependency 'rdoc'
@@ -146,6 +146,7 @@ module ForemanRemoteExecution
146
146
  register_custom_status HostStatus::ExecutionStatus
147
147
  # add dashboard widget
148
148
  # widget 'foreman_remote_execution_widget', name: N_('Foreman plugin template widget'), sizex: 4, sizey: 1
149
+ widget 'dashboard/latest-jobs', :name => N_('Latest Jobs'), :sizex => 6, :sizey => 1
149
150
 
150
151
  parameter_filter Subnet, :remote_execution_proxies, :remote_execution_proxy_ids => []
151
152
  parameter_filter Nic::Interface do |ctx|
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '4.7.0'.freeze
2
+ VERSION = '4.8.0'.freeze
3
3
  end
data/package.json CHANGED
@@ -21,11 +21,11 @@
21
21
  },
22
22
  "devDependencies": {
23
23
  "@babel/core": "^7.7.0",
24
- "@theforeman/builder": "^4.14.0",
25
- "@theforeman/eslint-plugin-foreman": "^4.14.0",
26
- "@theforeman/stories": "^4.14.0",
27
- "@theforeman/test": "^4.14.0",
28
- "@theforeman/vendor-dev": "^4.14.0",
24
+ "@theforeman/builder": "^8.10.0",
25
+ "@theforeman/eslint-plugin-foreman": "^8.10.0",
26
+ "@theforeman/stories": "^8.10.0",
27
+ "@theforeman/test": "^8.10.0",
28
+ "@theforeman/vendor-dev": "^8.10.0",
29
29
  "babel-eslint": "^10.0.0",
30
30
  "eslint": "^6.8.0",
31
31
  "prettier": "^1.19.1",
@@ -33,6 +33,6 @@
33
33
  "redux-mock-store": "^1.2.2"
34
34
  },
35
35
  "peerDependencies": {
36
- "@theforeman/vendor": "^8.3.0"
36
+ "@theforeman/vendor": "^8.10.0"
37
37
  }
38
38
  }
@@ -90,6 +90,16 @@ module Api
90
90
  assert_response :success
91
91
  end
92
92
 
93
+ test 'should create with a scheduled recurrence' do
94
+ @attrs[:scheduling] = { start_at: (Time.now + 1.hour) }
95
+ @attrs[:recurrence] = { cron_line: '5 * * * *' }
96
+ post :create, params: { job_invocation: @attrs }
97
+ invocation = ActiveSupport::JSON.decode(@response.body)
98
+ assert_equal 'recurring', invocation['mode']
99
+ assert invocation['start_at']
100
+ assert_response :success
101
+ end
102
+
93
103
  context 'with_feature' do
94
104
  setup do
95
105
  @feature = FactoryBot.create(:remote_execution_feature,
@@ -5,6 +5,16 @@ module ForemanRemoteExecution
5
5
  class RunHostsJobTest < ActiveSupport::TestCase
6
6
  include Dynflow::Testing
7
7
 
8
+ # Adding run_step_id wich is needed in RunHostsJob as a quick fix
9
+ # it will be added to dynflow in the future see https://github.com/Dynflow/dynflow/pull/391
10
+ # rubocop:disable Style/ClassAndModuleChildren
11
+ class Dynflow::Testing::DummyPlannedAction
12
+ def run_step_id
13
+ Dynflow::Testing.get_id
14
+ end
15
+ end
16
+ # rubocop:enable Style/ClassAndModuleChildren
17
+
8
18
  let(:host) { FactoryBot.create(:host, :with_execution) }
9
19
  let(:proxy) { host.remote_execution_proxies('SSH')[:subnet].first }
10
20
  let(:targeting) { FactoryBot.create(:targeting, :search_query => "name = #{host.name}", :user => User.current) }
@@ -94,6 +104,22 @@ module ForemanRemoteExecution
94
104
  planned # To make the expectations happy
95
105
  end
96
106
 
107
+ describe '#proxy_batch_size' do
108
+ it 'defaults to Setting[foreman_tasks_proxy_batch_size]' do
109
+ Setting.expects(:[]).with('foreman_tasks_proxy_batch_size').returns(14)
110
+ planned
111
+ _(planned.proxy_batch_size).must_equal 14
112
+ end
113
+
114
+ it 'gets the provider value' do
115
+ provider = mock('provider')
116
+ provider.expects(:proxy_batch_size).returns(15)
117
+ JobTemplate.any_instance.expects(:provider).returns(provider)
118
+
119
+ _(planned.proxy_batch_size).must_equal 15
120
+ end
121
+ end
122
+
97
123
  describe 'concurrency control' do
98
124
  let(:level) { 5 }
99
125
  let(:span) { 60 }
@@ -127,10 +153,79 @@ module ForemanRemoteExecution
127
153
  end
128
154
 
129
155
  describe 'notifications' do
130
- it 'creates notification on sucess run' do
131
- FactoryBot.create(:notification_blueprint, :name => 'rex_job_succeeded')
132
- assert_difference 'NotificationRecipient.where(:user_id => targeting.user.id).count' do
133
- finalize_action planned
156
+ it 'creates drawer notification on succeess' do
157
+ blueprint = planned.job_invocation.build_notification
158
+ blueprint.expects(:deliver!)
159
+ planned.job_invocation.expects(:build_notification).returns(blueprint)
160
+ planned.notify_on_success(nil)
161
+ end
162
+
163
+ it 'creates drawer notification on failure' do
164
+ blueprint = planned.job_invocation.build_notification
165
+ blueprint.expects(:deliver!)
166
+ planned.job_invocation.expects(:build_notification).returns(blueprint)
167
+ planned.notify_on_failure(nil)
168
+ end
169
+
170
+ describe 'ignoring drawer notification' do
171
+ before do
172
+ blueprint = planned.job_invocation.build_notification
173
+ blueprint.expects(:deliver!)
174
+ planned.job_invocation.expects(:build_notification).returns(blueprint)
175
+ end
176
+
177
+ let(:mail) do
178
+ object = mock
179
+ object.stubs(:deliver_now)
180
+ object
181
+ end
182
+
183
+ describe 'for user subscribed to all' do
184
+ before do
185
+ planned.expects(:mail_notification_preference).returns(UserMailNotification.new(:interval => RexMailNotification::ALL_JOBS))
186
+ end
187
+
188
+ it 'sends the mail notification on success' do
189
+ RexJobMailer.expects(:job_finished).returns(mail)
190
+ planned.notify_on_success(nil)
191
+ end
192
+
193
+ it 'sends the mail notification on failure' do
194
+ RexJobMailer.expects(:job_finished).returns(mail)
195
+ planned.notify_on_failure(nil)
196
+ end
197
+ end
198
+
199
+ describe 'for user subscribed to failures' do
200
+ before do
201
+ planned.expects(:mail_notification_preference).returns(UserMailNotification.new(:interval => RexMailNotification::FAILED_JOBS))
202
+ end
203
+
204
+ it 'it does not send the mail notification on success' do
205
+ RexJobMailer.expects(:job_finished).never
206
+ planned.notify_on_success(nil)
207
+ end
208
+
209
+ it 'sends the mail notification on failure' do
210
+ RexJobMailer.expects(:job_finished).returns(mail)
211
+ planned.notify_on_failure(nil)
212
+ end
213
+ end
214
+
215
+ describe 'for user subscribed to successful jobs' do
216
+ before do
217
+ planned.expects(:mail_notification_preference).returns(UserMailNotification.new(:interval => RexMailNotification::SUCCEEDED_JOBS))
218
+ end
219
+
220
+ it 'sends the mail notification on success' do
221
+ RexJobMailer.expects(:job_finished).returns(mail)
222
+ planned.notify_on_success(nil)
223
+ end
224
+
225
+ it 'does not send the mail notification on failure' do
226
+ RexJobMailer.expects(:job_finished).never
227
+ planned.notify_on_failure(nil)
228
+ end
134
229
  end
135
230
  end
136
231
  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
@@ -50,6 +50,18 @@ 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 }
@@ -200,6 +212,40 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
200
212
  host.interfaces.each(&:save)
201
213
  host.reload
202
214
  SSHExecutionProvider.find_ip_or_hostname(host).must_equal execution_interface.ip
215
+
216
+ # there is an execution interface with both IPv6 and IPv4: IPv4 is being preferred over IPv6 by default
217
+ execution_interface = FactoryBot.build(:nic_managed,
218
+ flags.merge(:execution => true, :ip => '10.0.0.4', :ip6 => 'fd00::4'))
219
+ host.interfaces = [execution_interface]
220
+ host.interfaces.each(&:save)
221
+ host.reload
222
+ SSHExecutionProvider.find_ip_or_hostname(host).must_equal execution_interface.ip
223
+ end
224
+
225
+ it 'gets ipv6 from flagged interfaces with IPv6 preference' do
226
+ host.host_params['remote_execution_connect_by_ip_prefer_ipv6'] = true
227
+ host.host_params['remote_execution_connect_by_ip'] = true
228
+
229
+ # there is an execution interface with both IPv6 and IPv4: IPv6 is being preferred over IPv4 by host parameter configuration
230
+ execution_interface = FactoryBot.build(:nic_managed,
231
+ flags.merge(:execution => true, :ip => '10.0.0.4', :ip6 => 'fd00::4'))
232
+ host.interfaces = [execution_interface]
233
+ host.interfaces.each(&:save)
234
+ host.reload
235
+ SSHExecutionProvider.find_ip_or_hostname(host).must_equal execution_interface.ip6
236
+ end
237
+
238
+ it 'gets ipv6 from flagged interfaces with IPv4 preference but without IPv4 address' do
239
+ host.host_params['remote_execution_connect_by_ip_prefer_ipv6'] = false
240
+ host.host_params['remote_execution_connect_by_ip'] = true
241
+
242
+ # there is an execution interface with both IPv6 and IPv4: IPv6 is being preferred over IPv4 by host parameter configuration
243
+ execution_interface = FactoryBot.build(:nic_managed,
244
+ flags.merge(:execution => true, :ip => nil, :ip6 => 'fd00::4'))
245
+ host.interfaces = [execution_interface]
246
+ host.interfaces.each(&:save)
247
+ host.reload
248
+ SSHExecutionProvider.find_ip_or_hostname(host).must_equal execution_interface.ip6
203
249
  end
204
250
  end
205
251
  end
@@ -3,43 +3,64 @@ 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';
8
7
  import CategoryAndTemplate from './steps/CategoryAndTemplate/';
9
8
  import { AdvancedFields } from './steps/AdvancedFields/AdvancedFields';
10
- import { JOB_TEMPLATE } from './JobWizardConstants';
9
+ import {
10
+ JOB_TEMPLATE,
11
+ WIZARD_TITLES,
12
+ initialScheduleState,
13
+ } from './JobWizardConstants';
11
14
  import { selectTemplateError } from './JobWizardSelectors';
12
15
  import Schedule from './steps/Schedule/';
16
+ import HostsAndInputs from './steps/HostsAndInputs/';
13
17
  import './JobWizard.scss';
14
18
 
15
19
  export const JobWizard = () => {
16
20
  const [jobTemplateID, setJobTemplateID] = useState(null);
17
21
  const [category, setCategory] = useState('');
18
22
  const [advancedValues, setAdvancedValues] = useState({});
23
+ const [templateValues, setTemplateValues] = useState({}); // TODO use templateValues in advanced fields - description https://github.com/theforeman/foreman_remote_execution/pull/605
24
+ const [selectedHosts, setSelectedHosts] = useState(['host1', 'host2']);
25
+ const [scheduleValue, setScheduleValue] = useState(initialScheduleState);
19
26
  const dispatch = useDispatch();
20
27
 
21
28
  const setDefaults = useCallback(
22
29
  ({
23
30
  data: {
31
+ template_inputs,
24
32
  advanced_template_inputs,
25
33
  effective_user,
26
- job_template: { executionTimeoutInterval, description_format },
34
+ job_template: { execution_timeout_interval, description_format },
27
35
  },
28
36
  }) => {
29
37
  const advancedTemplateValues = {};
38
+ const defaultTemplateValues = {};
39
+ const inputs = template_inputs;
30
40
  const advancedInputs = advanced_template_inputs;
31
- if (advancedInputs) {
32
- advancedInputs.forEach(input => {
33
- advancedTemplateValues[input.name] = input?.default || '';
41
+ if (inputs) {
42
+ setTemplateValues(() => {
43
+ inputs.forEach(input => {
44
+ defaultTemplateValues[input.name] = input?.default || '';
45
+ });
46
+ return defaultTemplateValues;
34
47
  });
35
48
  }
36
- setAdvancedValues(currentAdvancedValues => ({
37
- ...currentAdvancedValues,
38
- effectiveUserValue: effective_user?.value || '',
39
- timeoutToKill: executionTimeoutInterval || '',
40
- templateValues: advancedTemplateValues,
41
- description: description_format || '',
42
- }));
49
+ setAdvancedValues(currentAdvancedValues => {
50
+ if (advancedInputs) {
51
+ advancedInputs.forEach(input => {
52
+ advancedTemplateValues[input.name] = input?.default || '';
53
+ });
54
+ }
55
+ return {
56
+ ...currentAdvancedValues,
57
+ effectiveUserValue: effective_user?.value || '',
58
+ timeoutToKill: execution_timeout_interval || '',
59
+ templateValues: advancedTemplateValues,
60
+ description: description_format || '',
61
+ isRandomizedOrdering: false,
62
+ };
63
+ });
43
64
  },
44
65
  []
45
66
  );
@@ -59,7 +80,7 @@ export const JobWizard = () => {
59
80
  const isTemplate = !templateError && !!jobTemplateID;
60
81
  const steps = [
61
82
  {
62
- name: __('Category and Template'),
83
+ name: WIZARD_TITLES.categoryAndTemplate,
63
84
  component: (
64
85
  <CategoryAndTemplate
65
86
  jobTemplate={jobTemplateID}
@@ -70,12 +91,19 @@ export const JobWizard = () => {
70
91
  ),
71
92
  },
72
93
  {
73
- name: __('Target Hosts'),
74
- component: <p>Target Hosts</p>,
94
+ name: WIZARD_TITLES.hostsAndInputs,
95
+ component: (
96
+ <HostsAndInputs
97
+ templateValues={templateValues}
98
+ setTemplateValues={setTemplateValues}
99
+ selectedHosts={selectedHosts}
100
+ setSelectedHosts={setSelectedHosts}
101
+ />
102
+ ),
75
103
  canJumpTo: isTemplate,
76
104
  },
77
105
  {
78
- name: __('Advanced Fields'),
106
+ name: WIZARD_TITLES.advanced,
79
107
  component: (
80
108
  <AdvancedFields
81
109
  advancedValues={advancedValues}
@@ -91,12 +119,17 @@ export const JobWizard = () => {
91
119
  canJumpTo: isTemplate,
92
120
  },
93
121
  {
94
- name: __('Schedule'),
95
- component: <Schedule />,
122
+ name: WIZARD_TITLES.schedule,
123
+ component: (
124
+ <Schedule
125
+ scheduleValue={scheduleValue}
126
+ setScheduleValue={setScheduleValue}
127
+ />
128
+ ),
96
129
  canJumpTo: isTemplate,
97
130
  },
98
131
  {
99
- name: __('Review Details'),
132
+ name: WIZARD_TITLES.review,
100
133
  component: <p>Review Details</p>,
101
134
  nextButtonText: 'Run',
102
135
  canJumpTo: isTemplate,