foreman_remote_execution 4.7.0 → 4.8.0

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