foreman_remote_execution 4.5.5 → 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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_ci.yml +7 -0
  3. data/.rubocop_todo.yml +1 -0
  4. data/app/controllers/api/v2/job_invocations_controller.rb +7 -1
  5. data/app/graphql/types/job_invocation.rb +16 -0
  6. data/app/lib/actions/remote_execution/run_host_job.rb +2 -1
  7. data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
  8. data/app/mailers/rex_job_mailer.rb +15 -0
  9. data/app/models/job_invocation.rb +4 -0
  10. data/app/models/job_invocation_composer.rb +21 -13
  11. data/app/models/job_template.rb +1 -1
  12. data/app/models/remote_execution_provider.rb +17 -2
  13. data/app/models/rex_mail_notification.rb +13 -0
  14. data/app/models/setting/remote_execution.rb +7 -1
  15. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  16. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  17. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  18. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  19. data/app/views/template_invocations/show.html.erb +2 -1
  20. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  21. data/db/seeds.d/95-mail_notifications.rb +24 -0
  22. data/foreman_remote_execution.gemspec +2 -4
  23. data/lib/foreman_remote_execution/engine.rb +4 -0
  24. data/lib/foreman_remote_execution/version.rb +1 -1
  25. data/package.json +6 -6
  26. data/test/functional/api/v2/job_invocations_controller_test.rb +10 -0
  27. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  28. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  29. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  30. data/test/unit/concerns/host_extensions_test.rb +4 -4
  31. data/test/unit/input_template_renderer_test.rb +1 -89
  32. data/test/unit/job_invocation_composer_test.rb +1 -12
  33. data/test/unit/job_invocation_report_template_test.rb +15 -12
  34. data/test/unit/remote_execution_provider_test.rb +34 -0
  35. data/webpack/JobWizard/JobWizard.js +53 -20
  36. data/webpack/JobWizard/JobWizard.scss +33 -4
  37. data/webpack/JobWizard/JobWizardConstants.js +17 -0
  38. data/webpack/JobWizard/__tests__/fixtures.js +8 -0
  39. data/webpack/JobWizard/__tests__/integration.test.js +3 -7
  40. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +16 -5
  41. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
  42. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +29 -14
  43. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +4 -2
  44. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
  45. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +25 -0
  46. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  47. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +37 -0
  48. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +50 -0
  49. data/webpack/JobWizard/steps/HostsAndInputs/index.js +66 -0
  50. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
  51. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +36 -21
  52. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +155 -0
  53. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +9 -8
  54. data/webpack/JobWizard/steps/Schedule/index.js +89 -28
  55. data/webpack/JobWizard/steps/form/DateTimePicker.js +93 -0
  56. data/webpack/JobWizard/steps/form/Formatter.js +10 -9
  57. data/webpack/JobWizard/steps/form/NumberInput.js +2 -0
  58. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  59. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  60. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
  61. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  62. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  63. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  64. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  65. metadata +26 -19
  66. data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
@@ -350,12 +350,6 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
350
350
  end
351
351
 
352
352
  describe '#available_bookmarks' do
353
- it 'obeys authorization' do
354
- composer
355
- Bookmark.expects(:authorized).with(:view_bookmarks).returns(Bookmark.where({}))
356
- composer.available_bookmarks
357
- end
358
-
359
353
  context 'there are hostgroups and hosts bookmark' do
360
354
  let(:hostgroups) { Bookmark.create(:name => 'hostgroups', :query => 'name = x', :controller => 'hostgroups') }
361
355
  let(:hosts) { Bookmark.create(:name => 'hosts', :query => 'name = x', :controller => 'hosts') }
@@ -931,12 +925,7 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
931
925
 
932
926
  context 'with template in setting present' do
933
927
  before do
934
- FactoryBot.create(
935
- :setting,
936
- :name => 'remote_execution_form_job_template',
937
- :category => 'Setting::RemoteExecution',
938
- :value => setting_template.name
939
- )
928
+ Setting[:remote_execution_form_job_template] = setting_template.name
940
929
  end
941
930
 
942
931
  it 'should resolve category to the setting value' do
@@ -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
@@ -212,6 +212,40 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
212
212
  host.interfaces.each(&:save)
213
213
  host.reload
214
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
215
249
  end
216
250
  end
217
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,
@@ -1,5 +1,10 @@
1
1
  .job-wizard {
2
+ .wizard-title {
3
+ margin-bottom: 25px;
4
+ }
5
+
2
6
  .pf-c-wizard__main {
7
+ overflow: visible;
3
8
  z-index: calc(
4
9
  var(--pf-c-wizard__footer--ZIndex) + 1
5
10
  ); // So the select box can be shown above the wizard footer
@@ -12,6 +17,9 @@
12
17
  }
13
18
  #advanced-fields-job-template {
14
19
  .foreman-search-field {
20
+ .rbt-input-hint input{
21
+ display: none;
22
+ }
15
23
  // Giving pf3 search bar a pf4 look
16
24
  .search-bar {
17
25
  display: block;
@@ -41,13 +49,34 @@
41
49
  }
42
50
  }
43
51
 
52
+ .hosts-chip-group {
53
+ margin-top: 8px;
54
+ }
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
  }
52
63
  }
64
+
65
+ .pf-c-date-picker {
66
+ vertical-align: top;
67
+ }
68
+
69
+ .time-picker {
70
+ width: 150px;
71
+ }
72
+
73
+ input[type='radio'],
74
+ input[type='checkbox'] {
75
+ // overwriting bootstrap/_forms.scss margin: 4px 0 0;
76
+ margin: 0;
77
+ }
78
+ textarea {
79
+ min-height: 40px;
80
+ min-width: 100px;
81
+ }
53
82
  }
@@ -14,3 +14,20 @@ export const repeatTypes = {
14
14
  daily: __('Daily'),
15
15
  hourly: __('Hourly'),
16
16
  };
17
+
18
+ export const WIZARD_TITLES = {
19
+ categoryAndTemplate: __('Category and Template'),
20
+ hostsAndInputs: __('Target hosts and inputs'),
21
+ advanced: __('Advanced Fields'),
22
+ schedule: __('Schedule'),
23
+ review: __('Review Details'),
24
+ };
25
+
26
+ export const initialScheduleState = {
27
+ repeatType: repeatTypes.noRepeat,
28
+ repeatAmount: '',
29
+ starts: '',
30
+ ends: '',
31
+ isFuture: false,
32
+ isNeverEnds: false,
33
+ };
@@ -93,6 +93,14 @@ export const testSetup = (selectors, api) => {
93
93
  jest.spyOn(selectors, 'selectJobCategories');
94
94
  jest.spyOn(selectors, 'selectJobCategoriesStatus');
95
95
 
96
+ jest.spyOn(selectors, 'selectTemplateInputs');
97
+ jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
98
+ selectors.selectTemplateInputs.mockImplementation(
99
+ () => jobTemplateResponse.template_inputs
100
+ );
101
+ selectors.selectAdvancedTemplateInputs.mockImplementation(
102
+ () => jobTemplateResponse.advanced_template_inputs
103
+ );
96
104
  selectors.selectJobCategories.mockImplementation(() => jobCategories);
97
105
  selectors.selectJobTemplates.mockImplementation(() => [
98
106
  jobTemplate,
@@ -5,6 +5,7 @@ import { render, fireEvent, screen, act } from '@testing-library/react';
5
5
  import * as api from 'foremanReact/redux/API';
6
6
  import { JobWizard } from '../JobWizard';
7
7
  import * as selectors from '../JobWizardSelectors';
8
+ import { WIZARD_TITLES } from '../JobWizardConstants';
8
9
  import {
9
10
  testSetup,
10
11
  mockApi,
@@ -62,13 +63,8 @@ describe('Job wizard fill', () => {
62
63
  <JobWizard />
63
64
  </Provider>
64
65
  );
65
- const steps = [
66
- 'Target Hosts',
67
- 'Advanced Fields',
68
- 'Schedule',
69
- 'Review Details',
70
- 'Category and Template',
71
- ];
66
+ const titles = Object.values(WIZARD_TITLES);
67
+ const steps = [titles[1], titles[0], ...titles.slice(2)]; // the first title is selected at the beggining
72
68
  // eslint-disable-next-line no-unused-vars
73
69
  for await (const step of steps) {
74
70
  const stepSelector = screen.getByText(step);
@@ -1,8 +1,7 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { useSelector } from 'react-redux';
4
- import { Title, Form } from '@patternfly/react-core';
5
- import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { Form } from '@patternfly/react-core';
6
5
  import {
7
6
  selectEffectiveUser,
8
7
  selectAdvancedTemplateInputs,
@@ -17,8 +16,11 @@ import {
17
16
  ConcurrencyLevelField,
18
17
  TimeSpanLevelField,
19
18
  TemplateInputsFields,
19
+ ExecutionOrderingField,
20
20
  } from './Fields';
21
21
  import { DescriptionField } from './DescriptionField';
22
+ import { WIZARD_TITLES } from '../../JobWizardConstants';
23
+ import { WizardTitle } from '../form/WizardTitle';
22
24
 
23
25
  export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
24
26
  const effectiveUser = useSelector(selectEffectiveUser);
@@ -26,9 +28,10 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
26
28
  const templateInputs = useSelector(selectTemplateInputs);
27
29
  return (
28
30
  <>
29
- <Title headingLevel="h2" className="advanced-fields-title">
30
- {__('Advanced Fields')}
31
- </Title>
31
+ <WizardTitle
32
+ title={WIZARD_TITLES.advanced}
33
+ className="advanced-fields-title"
34
+ />
32
35
  <Form id="advanced-fields-job-template" autoComplete="off">
33
36
  <TemplateInputsFields
34
37
  inputs={advancedTemplateInputs}
@@ -98,6 +101,14 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
98
101
  })
99
102
  }
100
103
  />
104
+ <ExecutionOrderingField
105
+ isRandomizedOrdering={advancedValues.isRandomizedOrdering}
106
+ setValue={newValue =>
107
+ setAdvancedValues({
108
+ isRandomizedOrdering: newValue,
109
+ })
110
+ }
111
+ />
101
112
  </Form>
102
113
  </>
103
114
  );
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { FormGroup, TextInput } from '@patternfly/react-core';
3
+ import { FormGroup, TextInput, Radio } from '@patternfly/react-core';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
5
  import { helpLabel } from '../form/FormHelpers';
6
6
  import { formatter } from '../form/Formatter';
@@ -18,6 +18,7 @@ export const EffectiveUserField = ({ value, setValue }) => (
18
18
  fieldId="effective-user"
19
19
  >
20
20
  <TextInput
21
+ aria-label="effective user"
21
22
  autoComplete="effective-user"
22
23
  id="effective-user"
23
24
  type="text"
@@ -61,6 +62,7 @@ export const PasswordField = ({ value, setValue }) => (
61
62
  fieldId="job-password"
62
63
  >
63
64
  <TextInput
65
+ aria-label="job password"
64
66
  autoComplete="new-password" // to prevent firefox from autofilling the user password
65
67
  id="job-password"
66
68
  type="password"
@@ -83,6 +85,7 @@ export const KeyPassphraseField = ({ value, setValue }) => (
83
85
  fieldId="key-passphrase"
84
86
  >
85
87
  <TextInput
88
+ aria-label="key passphrase"
86
89
  autoComplete="key-passphrase"
87
90
  id="key-passphrase"
88
91
  type="password"
@@ -105,6 +108,7 @@ export const EffectiveUserPasswordField = ({ value, setValue }) => (
105
108
  fieldId="effective-user-password"
106
109
  >
107
110
  <TextInput
111
+ aria-label="effective userpassword"
108
112
  autoComplete="effective-user-password"
109
113
  id="effective-user-password"
110
114
  type="password"
@@ -161,6 +165,41 @@ export const TimeSpanLevelField = ({ value, setValue }) => (
161
165
  />
162
166
  );
163
167
 
168
+ export const ExecutionOrderingField = ({ isRandomizedOrdering, setValue }) => (
169
+ <FormGroup
170
+ label={__('Execution ordering')}
171
+ fieldId="schedule-type"
172
+ labelIcon={helpLabel(
173
+ <div
174
+ dangerouslySetInnerHTML={{
175
+ __html: __(
176
+ 'Execution ordering determines whether the jobs should be executed on hosts in alphabetical order or in randomized order.<br><ul><li><b>Ordered</b> - executes the jobs on hosts in alphabetical order</li><li><b>Randomized</b> - randomizes the order in which jobs are executed on hosts</li></ul>'
177
+ ),
178
+ }}
179
+ />,
180
+ 'effective-user-password'
181
+ )}
182
+ isInline
183
+ >
184
+ <Radio
185
+ aria-label="execution order alphabetical"
186
+ isChecked={!isRandomizedOrdering}
187
+ name="execution-order"
188
+ onChange={() => setValue(false)}
189
+ id="execution-order-alphabetical"
190
+ label={__('Alphabetical')}
191
+ />
192
+ <Radio
193
+ aria-label="execution order randomized"
194
+ isChecked={isRandomizedOrdering}
195
+ name="execution-order"
196
+ onChange={() => setValue(true)}
197
+ id="execution-order-randomized"
198
+ label={__('Randomized')}
199
+ />
200
+ </FormGroup>
201
+ );
202
+
164
203
  export const TemplateInputsFields = ({ inputs, value, setValue }) => (
165
204
  <>{inputs?.map(input => formatter(input, value, setValue))}</>
166
205
  );
@@ -184,6 +223,14 @@ ConcurrencyLevelField.propTypes = EffectiveUserField.propTypes;
184
223
  ConcurrencyLevelField.defaultProps = EffectiveUserField.defaultProps;
185
224
  TimeSpanLevelField.propTypes = EffectiveUserField.propTypes;
186
225
  TimeSpanLevelField.defaultProps = EffectiveUserField.defaultProps;
226
+ ExecutionOrderingField.propTypes = {
227
+ isRandomizedOrdering: PropTypes.bool,
228
+ setValue: PropTypes.func.isRequired,
229
+ };
230
+ ExecutionOrderingField.defaultProps = {
231
+ isRandomizedOrdering: false,
232
+ };
233
+
187
234
  TemplateInputsFields.propTypes = {
188
235
  inputs: PropTypes.array.isRequired,
189
236
  value: PropTypes.object,