foreman_remote_execution 4.6.0 → 5.0.1

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 (143) 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 +16 -1
  5. data/app/controllers/job_invocations_controller.rb +1 -1
  6. data/app/controllers/ui_job_wizard_controller.rb +21 -2
  7. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  8. data/app/graphql/types/job_invocation.rb +16 -0
  9. data/app/graphql/types/job_invocation_input.rb +13 -0
  10. data/app/graphql/types/recurrence_input.rb +8 -0
  11. data/app/graphql/types/scheduling_input.rb +6 -0
  12. data/app/graphql/types/targeting_enum.rb +7 -0
  13. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +5 -1
  14. data/app/helpers/remote_execution_helper.rb +9 -3
  15. data/app/lib/actions/remote_execution/run_host_job.rb +10 -1
  16. data/app/lib/actions/remote_execution/run_hosts_job.rb +58 -4
  17. data/app/mailers/rex_job_mailer.rb +15 -0
  18. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +10 -0
  19. data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +6 -0
  20. data/app/models/host_proxy_invocation.rb +4 -0
  21. data/app/models/host_status/execution_status.rb +3 -3
  22. data/app/models/job_invocation.rb +12 -5
  23. data/app/models/job_invocation_composer.rb +25 -17
  24. data/app/models/job_template.rb +1 -1
  25. data/app/models/remote_execution_feature.rb +5 -1
  26. data/app/models/remote_execution_provider.rb +18 -2
  27. data/app/models/rex_mail_notification.rb +13 -0
  28. data/app/models/targeting.rb +7 -3
  29. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  30. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  31. data/app/views/job_invocations/index.html.erb +1 -1
  32. data/app/views/job_invocations/refresh.js.erb +1 -0
  33. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  34. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  35. data/app/views/template_invocations/show.html.erb +2 -1
  36. data/app/views/templates/ssh/module_action.erb +1 -0
  37. data/app/views/templates/ssh/power_action.erb +2 -0
  38. data/app/views/templates/ssh/puppet_run_once.erb +1 -0
  39. data/config/routes.rb +1 -0
  40. data/db/migrate/2021051713291621250977_add_host_proxy_invocations.rb +12 -0
  41. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  42. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  43. data/db/seeds.d/95-mail_notifications.rb +24 -0
  44. data/foreman_remote_execution.gemspec +2 -3
  45. data/lib/foreman_remote_execution/engine.rb +114 -8
  46. data/lib/foreman_remote_execution/version.rb +1 -1
  47. data/package.json +9 -7
  48. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  49. data/test/functional/cockpit_controller_test.rb +0 -1
  50. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  51. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  52. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  53. data/test/helpers/remote_execution_helper_test.rb +0 -1
  54. data/test/unit/actions/run_host_job_test.rb +21 -0
  55. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  56. data/test/unit/concerns/host_extensions_test.rb +40 -7
  57. data/test/unit/input_template_renderer_test.rb +1 -89
  58. data/test/unit/job_invocation_composer_test.rb +18 -18
  59. data/test/unit/job_invocation_report_template_test.rb +16 -13
  60. data/test/unit/job_invocation_test.rb +1 -1
  61. data/test/unit/job_template_effective_user_test.rb +0 -4
  62. data/test/unit/remote_execution_provider_test.rb +46 -4
  63. data/test/unit/targeting_test.rb +68 -1
  64. data/webpack/JobWizard/JobWizard.js +158 -24
  65. data/webpack/JobWizard/JobWizard.scss +93 -1
  66. data/webpack/JobWizard/JobWizardConstants.js +54 -0
  67. data/webpack/JobWizard/JobWizardSelectors.js +41 -0
  68. data/webpack/JobWizard/__tests__/fixtures.js +188 -3
  69. data/webpack/JobWizard/__tests__/integration.test.js +41 -106
  70. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  71. data/webpack/JobWizard/autofill.js +38 -0
  72. data/webpack/JobWizard/index.js +7 -0
  73. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +41 -10
  74. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +90 -0
  75. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +116 -55
  76. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +354 -16
  77. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +79 -246
  78. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
  79. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +123 -51
  80. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  81. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  82. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  83. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  84. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  85. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +100 -0
  86. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  87. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  88. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
  89. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  90. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  91. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  92. data/webpack/JobWizard/steps/HostsAndInputs/index.js +214 -0
  93. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  94. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  95. data/webpack/JobWizard/steps/Schedule/QueryType.js +51 -0
  96. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  97. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  98. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  99. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  100. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +125 -0
  101. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  102. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +28 -0
  103. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +106 -0
  104. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  105. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +32 -0
  106. data/webpack/JobWizard/steps/Schedule/index.js +178 -0
  107. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  108. data/webpack/JobWizard/steps/form/FormHelpers.js +5 -0
  109. data/webpack/JobWizard/steps/form/Formatter.js +181 -0
  110. data/webpack/JobWizard/steps/form/NumberInput.js +36 -0
  111. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  112. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  113. data/webpack/JobWizard/steps/form/SelectField.js +28 -5
  114. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  115. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  116. data/webpack/JobWizard/submit.js +120 -0
  117. data/webpack/JobWizard/validation.js +53 -0
  118. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  119. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  120. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  121. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  122. data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
  123. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  124. data/webpack/helpers.js +1 -0
  125. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  126. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
  127. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  128. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  129. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  130. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
  131. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  132. metadata +71 -16
  133. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +0 -70
  134. data/app/models/setting/remote_execution.rb +0 -88
  135. data/test/models/orchestration/ssh_test.rb +0 -56
  136. data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -13
  137. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -32
  138. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -113
  139. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
  140. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
  141. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -37
  142. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -23
  143. data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
@@ -10,7 +10,7 @@ class JobInvocationTest < ActiveSupport::TestCase
10
10
  end
11
11
 
12
12
  it 'is able to perform search through job invocations' do
13
- found_jobs = JobInvocation.search_for(%{job_category = "#{job_invocation.job_category}"}).paginate(:page => 1).with_task.order('job_invocations.id DESC')
13
+ found_jobs = JobInvocation.search_for(%{job_category = "#{job_invocation.job_category}"}).paginate(:page => 1).order('job_invocations.id DESC')
14
14
  _(found_jobs).must_equal [job_invocation]
15
15
  end
16
16
 
@@ -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
@@ -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
@@ -1,28 +1,96 @@
1
+ /* eslint-disable camelcase */
1
2
  import React, { useState, useEffect, useCallback } from 'react';
2
3
  import { useDispatch, useSelector } from 'react-redux';
3
4
  import { Wizard } from '@patternfly/react-core';
4
5
  import { get } from 'foremanReact/redux/API';
5
- import { translate as __ } from 'foremanReact/common/I18n';
6
6
  import history from 'foremanReact/history';
7
+
8
+ import {
9
+ useForemanOrganization,
10
+ useForemanLocation,
11
+ } from 'foremanReact/Root/Context/ForemanContext';
7
12
  import CategoryAndTemplate from './steps/CategoryAndTemplate/';
8
13
  import { AdvancedFields } from './steps/AdvancedFields/AdvancedFields';
9
- import { JOB_TEMPLATE } from './JobWizardConstants';
10
- 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';
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';
11
30
  import './JobWizard.scss';
12
31
 
13
32
  export const JobWizard = () => {
14
33
  const [jobTemplateID, setJobTemplateID] = useState(null);
15
34
  const [category, setCategory] = useState('');
16
- 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('');
17
44
  const dispatch = useDispatch();
18
45
 
19
- const setDefaults = useCallback(response => {
20
- const responseJob = response.data;
21
- setAdvancedValues({
22
- effectiveUserValue: responseJob.effective_user?.value || '',
23
- timeoutToKill: responseJob.job_template.execution_timeout_interval || '',
24
- });
25
- }, []);
46
+ const setDefaults = useCallback(
47
+ ({
48
+ data: {
49
+ template_inputs,
50
+ advanced_template_inputs,
51
+ effective_user,
52
+ job_template: { name, execution_timeout_interval, description_format },
53
+ },
54
+ }) => {
55
+ const advancedTemplateValues = {};
56
+ const defaultTemplateValues = {};
57
+ const inputs = template_inputs;
58
+ const advancedInputs = advanced_template_inputs;
59
+ if (inputs) {
60
+ setTemplateValues(() => {
61
+ inputs.forEach(input => {
62
+ defaultTemplateValues[input.name] = input?.default || '';
63
+ });
64
+ return defaultTemplateValues;
65
+ });
66
+ }
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
+ });
91
+ },
92
+ []
93
+ );
26
94
  useEffect(() => {
27
95
  if (jobTemplateID) {
28
96
  dispatch(
@@ -35,11 +103,23 @@ export const JobWizard = () => {
35
103
  }
36
104
  }, [jobTemplateID, setDefaults, dispatch]);
37
105
 
106
+ const [valid, setValid] = useValidation({
107
+ advancedValues,
108
+ templateValues,
109
+ });
110
+ useAutoFill({
111
+ setSelectedTargets,
112
+ setHostsSearchQuery,
113
+ });
38
114
  const templateError = !!useSelector(selectTemplateError);
39
- const isTemplate = !templateError && !!jobTemplateID;
115
+ const templateResponse = useSelector(selectJobTemplate);
116
+ const isSubmitting = useSelector(selectIsSubmitting);
117
+ const isTemplate =
118
+ !templateError && !!jobTemplateID && templateResponse.job_template;
119
+
40
120
  const steps = [
41
121
  {
42
- name: __('Category and Template'),
122
+ name: WIZARD_TITLES.categoryAndTemplate,
43
123
  component: (
44
124
  <CategoryAndTemplate
45
125
  jobTemplate={jobTemplateID}
@@ -48,14 +128,25 @@ export const JobWizard = () => {
48
128
  setCategory={setCategory}
49
129
  />
50
130
  ),
131
+ enableNext: isTemplate,
51
132
  },
52
133
  {
53
- name: __('Target Hosts'),
54
- 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
+ ),
55
145
  canJumpTo: isTemplate,
146
+ enableNext: isTemplate && valid.hostsAndInputs,
56
147
  },
57
148
  {
58
- name: __('Advanced Fields'),
149
+ name: WIZARD_TITLES.advanced,
59
150
  component: (
60
151
  <AdvancedFields
61
152
  advancedValues={advancedValues}
@@ -65,23 +156,53 @@ export const JobWizard = () => {
65
156
  ...newValues,
66
157
  }));
67
158
  }}
68
- jobTemplateID={jobTemplateID}
159
+ templateValues={templateValues}
69
160
  />
70
161
  ),
71
- canJumpTo: isTemplate,
162
+ canJumpTo: isTemplate && valid.hostsAndInputs,
163
+ enableNext: isTemplate && valid.hostsAndInputs && valid.advanced,
72
164
  },
73
165
  {
74
- name: __('Schedule'),
75
- component: <p>Schedule</p>,
76
- 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,
77
179
  },
78
180
  {
79
- name: __('Review Details'),
80
- 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
+ ),
81
193
  nextButtonText: 'Run',
82
- 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,
83
202
  },
84
203
  ];
204
+ const location = useForemanLocation();
205
+ const organization = useForemanOrganization();
85
206
  return (
86
207
  <Wizard
87
208
  onClose={() => history.goBack()}
@@ -89,6 +210,19 @@ export const JobWizard = () => {
89
210
  steps={steps}
90
211
  height="100%"
91
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
+ }}
92
226
  />
93
227
  );
94
228
  };
@@ -1,14 +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
  }
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;
52
+ }
53
+ }
54
+ }
55
+ input[type='radio'],
56
+ input[type='checkbox'] {
57
+ margin: 0;
58
+ }
59
+ .schedule-tab {
60
+ .advanced-scheduling-button {
61
+ text-align: start;
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
+ }
13
105
  }
14
106
  }
@@ -1,6 +1,60 @@
1
+ import { translate as __ } from 'foremanReact/common/I18n';
1
2
  import { foremanUrl } from 'foremanReact/common/helpers';
2
3
 
3
4
  export const JOB_TEMPLATES = 'JOB_TEMPLATES';
4
5
  export const JOB_CATEGORIES = 'JOB_CATEGORIES';
5
6
  export const JOB_TEMPLATE = 'JOB_TEMPLATE';
7
+ export const JOB_INVOCATION = 'JOB_INVOCATION';
6
8
  export const templatesUrl = foremanUrl('/api/v2/job_templates');
9
+
10
+ export const repeatTypes = {
11
+ noRepeat: __('Does not repeat'),
12
+ cronline: __('Cronline'),
13
+ monthly: __('Monthly'),
14
+ weekly: __('Weekly'),
15
+ daily: __('Daily'),
16
+ hourly: __('Hourly'),
17
+ };
18
+
19
+ export const WIZARD_TITLES = {
20
+ categoryAndTemplate: __('Category and Template'),
21
+ hostsAndInputs: __('Target hosts and inputs'),
22
+ advanced: __('Advanced Fields'),
23
+ schedule: __('Schedule'),
24
+ review: __('Review Details'),
25
+ };
26
+
27
+ export const initialScheduleState = {
28
+ repeatType: repeatTypes.noRepeat,
29
+ repeatAmount: '',
30
+ repeatData: {},
31
+ startsAt: '',
32
+ startsBefore: '',
33
+ ends: '',
34
+ isFuture: false,
35
+ isNeverEnds: false,
36
+ isTypeStatic: true,
37
+ purpose: '',
38
+ };
39
+ export const HOSTS_API = 'HOSTS_API';
40
+ export const HOSTS = 'HOSTS';
41
+ export const HOST_COLLECTIONS = 'HOST_COLLECTIONS';
42
+ export const HOST_GROUPS = 'HOST_GROUPS';
43
+ export const hostMethods = {
44
+ hosts: __('Hosts'),
45
+ hostCollections: __('Host collections'),
46
+ hostGroups: __('Host groups'),
47
+ searchQuery: __('Search query'),
48
+ };
49
+
50
+ export const hostQuerySearchID = 'searchBar'; // until https://projects.theforeman.org/issues/33737 is used
51
+ export const hostsController = 'hosts';
52
+
53
+ export const dataName = {
54
+ [HOSTS]: 'hosts',
55
+ [HOST_GROUPS]: 'hostgroups',
56
+ };
57
+ export const HOSTS_TO_PREVIEW_AMOUNT = 20;
58
+
59
+ export const DEBOUNCE_HOST_COUNT = 700;
60
+ export const HOST_IDS = 'HOST_IDS';
@@ -1,13 +1,18 @@
1
+ import URI from 'urijs';
1
2
  import {
2
3
  selectAPIResponse,
3
4
  selectAPIStatus,
4
5
  selectAPIErrorMessage,
5
6
  } from 'foremanReact/redux/API/APISelectors';
7
+ import { STATUS } from 'foremanReact/constants';
8
+ import { selectRouterLocation } from 'foremanReact/routes/RouterSelector';
6
9
 
7
10
  import {
8
11
  JOB_TEMPLATES,
9
12
  JOB_CATEGORIES,
10
13
  JOB_TEMPLATE,
14
+ HOSTS_API,
15
+ JOB_INVOCATION,
11
16
  } from './JobWizardConstants';
12
17
 
13
18
  export const selectJobTemplatesStatus = state =>
@@ -22,6 +27,9 @@ export const selectJobTemplates = state =>
22
27
  export const selectJobCategories = state =>
23
28
  selectAPIResponse(state, JOB_CATEGORIES).job_categories || [];
24
29
 
30
+ export const selectWithKatello = state =>
31
+ selectAPIResponse(state, JOB_CATEGORIES).with_katello || false;
32
+
25
33
  export const selectJobCategoriesStatus = state =>
26
34
  selectAPIStatus(state, JOB_CATEGORIES);
27
35
 
@@ -36,3 +44,36 @@ export const selectTemplateError = state =>
36
44
 
37
45
  export const selectJobTemplate = state =>
38
46
  selectAPIResponse(state, JOB_TEMPLATE);
47
+
48
+ export const selectEffectiveUser = state =>
49
+ selectAPIResponse(state, JOB_TEMPLATE).effective_user;
50
+
51
+ export const selectAdvancedTemplateInputs = state =>
52
+ selectAPIResponse(state, JOB_TEMPLATE).advanced_template_inputs || [];
53
+
54
+ export const selectTemplateInputs = state =>
55
+ selectAPIResponse(state, JOB_TEMPLATE).template_inputs || [];
56
+
57
+ export const selectHostCount = state =>
58
+ selectAPIResponse(state, HOSTS_API).subtotal || 0;
59
+
60
+ export const selectHosts = state =>
61
+ (selectAPIResponse(state, HOSTS_API).results || []).map(host => host.name);
62
+
63
+ export const selectIsLoadingHosts = state =>
64
+ !selectAPIStatus(state, HOSTS_API) ||
65
+ selectAPIStatus(state, HOSTS_API) === STATUS.PENDING;
66
+
67
+ export const selectResponse = selectAPIResponse;
68
+
69
+ export const selectIsLoading = (state, key) =>
70
+ selectAPIStatus(state, key) === STATUS.PENDING;
71
+
72
+ export const selectIsSubmitting = state =>
73
+ selectAPIStatus(state, JOB_INVOCATION) === STATUS.PENDING ||
74
+ selectAPIStatus(state, JOB_INVOCATION) === STATUS.RESOLVED;
75
+
76
+ export const selectRouterSearch = state => {
77
+ const { search } = selectRouterLocation(state);
78
+ return URI.parseQuery(search);
79
+ };