foreman_remote_execution 8.0.0 → 8.1.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/job_invocations_controller.rb +1 -2
  3. data/app/controllers/job_templates_controller.rb +1 -1
  4. data/app/controllers/ui_job_wizard_controller.rb +1 -1
  5. data/app/helpers/job_invocations_helper.rb +0 -7
  6. data/app/helpers/remote_execution_helper.rb +1 -1
  7. data/app/lib/actions/remote_execution/proxy_action.rb +46 -0
  8. data/app/lib/actions/remote_execution/run_host_job.rb +38 -11
  9. data/app/lib/actions/remote_execution/run_hosts_job.rb +7 -6
  10. data/app/lib/actions/remote_execution/template_invocation_progress_logging.rb +27 -0
  11. data/app/models/job_invocation.rb +5 -9
  12. data/app/models/job_invocation_composer.rb +4 -0
  13. data/app/models/remote_execution_provider.rb +10 -2
  14. data/app/models/ssh_execution_provider.rb +1 -0
  15. data/app/models/template_invocation.rb +1 -0
  16. data/app/models/template_invocation_event.rb +11 -0
  17. data/app/views/job_invocations/_form.html.erb +4 -0
  18. data/app/views/job_invocations/new.html.erb +5 -0
  19. data/app/views/templates/script/package_action.erb +1 -1
  20. data/config/routes.rb +5 -5
  21. data/db/migrate/20220713095705_create_template_invocation_events.rb +17 -0
  22. data/db/migrate/20220822155946_add_time_to_pickup_to_job_invocation.rb +5 -0
  23. data/extra/cockpit/foreman-cockpit-session +303 -230
  24. data/extra/cockpit/foreman-cockpit.service +1 -0
  25. data/foreman_remote_execution.gemspec +1 -1
  26. data/lib/foreman_remote_execution/engine.rb +12 -7
  27. data/lib/foreman_remote_execution/tasks/explain_proxy_selection.rake +131 -0
  28. data/lib/foreman_remote_execution/version.rb +1 -1
  29. data/test/unit/remote_execution_provider_test.rb +22 -0
  30. data/webpack/JobWizard/JobWizard.js +53 -18
  31. data/webpack/JobWizard/JobWizard.scss +3 -0
  32. data/webpack/JobWizard/JobWizardConstants.js +1 -1
  33. data/webpack/JobWizard/JobWizardHelpers.js +15 -0
  34. data/webpack/JobWizard/JobWizardPageRerun.js +29 -5
  35. data/webpack/JobWizard/JobWizardSelectors.js +8 -2
  36. data/webpack/JobWizard/__tests__/JobWizardPageRerun.test.js +5 -0
  37. data/webpack/JobWizard/__tests__/fixtures.js +26 -2
  38. data/webpack/JobWizard/autofill.js +32 -10
  39. data/webpack/JobWizard/index.js +25 -6
  40. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +25 -0
  41. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +12 -1
  42. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +41 -6
  43. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +1 -1
  44. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +1 -1
  45. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +4 -2
  46. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +6 -2
  47. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +28 -20
  48. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +32 -0
  49. data/webpack/JobWizard/steps/HostsAndInputs/index.js +2 -2
  50. data/webpack/JobWizard/steps/ReviewDetails/index.js +1 -0
  51. data/webpack/JobWizard/steps/form/FormHelpers.js +21 -1
  52. data/webpack/JobWizard/steps/form/Formatter.js +22 -6
  53. data/webpack/JobWizard/steps/form/ResourceSelect.js +97 -10
  54. data/webpack/JobWizard/steps/form/SearchSelect.js +2 -2
  55. data/webpack/JobWizard/steps/form/SelectField.js +4 -0
  56. data/webpack/JobWizard/submit.js +3 -1
  57. data/webpack/JobWizard/validation.js +1 -0
  58. data/webpack/Routes/routes.js +3 -3
  59. data/webpack/react_app/components/FeaturesDropdown/actions.js +23 -2
  60. data/webpack/react_app/components/FeaturesDropdown/index.js +2 -0
  61. data/webpack/react_app/components/HostKebab/KebabItems.js +1 -0
  62. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +5 -0
  63. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +51 -59
  64. data/webpack/react_app/extend/Fills.js +3 -3
  65. metadata +12 -5
@@ -0,0 +1,131 @@
1
+ namespace :foreman_remote_execution do
2
+ desc <<~DESC
3
+ Explains which proxies can be used for remote execution against HOST using a specified PROVIDER.
4
+
5
+ * HOST : A scoped search query to find hosts by
6
+ * PROVIDER : The PROVIDER to scope by
7
+ * FORMAT : Output format, one of verbose (or unset), json, pretty-json, csv
8
+ * FOREMAN_USER : Run as if FOREMAN_USER triggered a job (runs as anonymous_admin if unset)
9
+ * ORGANIZATION : Run in the context of ORGANIZATION (runs in any context if unset)
10
+ * LOCATION : Run in the context of LOCATION (runs in any context if unset)
11
+ DESC
12
+ task :explain_proxy_selection => ['environment'] do
13
+ options = {}
14
+ options[:host] = ENV['HOST']
15
+ options[:provider] = ENV['PROVIDER']
16
+
17
+ raise 'Environment variable HOST has to be set' unless options[:host]
18
+ raise 'Environment variable PROVIDER has to be set' unless options[:provider]
19
+
20
+ if ENV['FOREMAN_USER']
21
+ User.current = User.friendly.find(ENV['FOREMAN_USER'])
22
+ else
23
+ User.current = User.anonymous_admin
24
+ end
25
+ Location.current = Location.friendly.find(ENV['LOCATION']) if ENV['LOCATION']
26
+ Organization.current = Organization.friendly.find(ENV['ORGANIZATION']) if ENV['ORGANIZATION']
27
+
28
+ selector = ::RemoteExecutionProxySelector.new
29
+
30
+ results = Host.search_for(options[:host]).map do |host|
31
+ host_base = { :host => host }
32
+ proxies = selector.available_proxies(host, options[:provider])
33
+ determined_proxy = selector.determine_proxy(host, options[:provider])
34
+ counts = selector.instance_variable_get('@tasks')
35
+ counts.default = 0
36
+
37
+ strategies = selector.strategies.map do |strategy|
38
+ base = { :name => strategy, :enabled => !proxies[strategy].nil? }
39
+ next base if proxies[strategy].nil?
40
+
41
+ base.merge(:proxies => proxies[strategy].sort_by { |proxy| counts[proxy] }.map do |proxy|
42
+ {:proxy => proxy, :count => counts[proxy]}
43
+ end)
44
+ end
45
+
46
+ case determined_proxy
47
+ when :not_defined
48
+ settings = {
49
+ global_proxy: 'remote_execution_global_proxy',
50
+ fallback_proxy: 'remote_execution_fallback_proxy',
51
+ provider: options[:provider],
52
+ }
53
+
54
+ host_base[:detail] = _('Could not use any proxy for the %{provider} job. Consider configuring %{global_proxy}, ' +
55
+ '%{fallback_proxy} in settings') % settings
56
+ when :not_available
57
+ offline_proxies = selector.offline
58
+ settings = { :count => offline_proxies.count, :proxy_names => offline_proxies.map(&:name).join(', ') }
59
+ host_base[:detail] = n_('The only applicable proxy %{proxy_names} is down',
60
+ 'All %{count} applicable proxies are down. Tried %{proxy_names}',
61
+ offline_proxies.count) % settings
62
+ else
63
+ winning_strategy = selector.strategies.find { |strategy| !proxies[strategy].empty? && proxies[strategy].include?(determined_proxy) }
64
+ end
65
+
66
+ {
67
+ :host => host,
68
+ :strategies => strategies,
69
+ :selected_proxy => determined_proxy,
70
+ :winning_strategy => winning_strategy,
71
+ }
72
+ end
73
+
74
+ case ENV['FORMAT']
75
+ when nil, 'verbose'
76
+ output_verbose(results, options[:provider])
77
+ when 'csv'
78
+ require 'csv'
79
+ output_csv(results)
80
+ when 'json'
81
+ require 'json'
82
+ puts JSON.generate(results)
83
+ when 'pretty-json'
84
+ require 'json'
85
+ puts JSON.pretty_generate(results)
86
+ end
87
+ end
88
+
89
+ def output_verbose(results, provider)
90
+ errors = [:not_defined, :not_available]
91
+
92
+ results.each do |host|
93
+ puts "=> Host #{host[:host]}"
94
+ host[:strategies].each do |strategy|
95
+ puts "==> Strategy #{strategy[:name]}"
96
+ unless strategy[:enabled]
97
+ puts " strategy is disabled"
98
+ puts
99
+ next
100
+ end
101
+ if strategy[:proxies].empty?
102
+ puts " no proxies available using this strategy"
103
+ else
104
+ strategy[:proxies].each_with_index do |proxy_record, i|
105
+ puts " #{i + 1}) #{proxy_record[:proxy]} - #{proxy_record[:count]} tasks"
106
+ end
107
+ end
108
+ puts
109
+ end
110
+ if errors.include? host[:selected_proxy]
111
+ puts host[:detail]
112
+ else
113
+ puts "As of now, #{provider} job would use proxy #{host[:selected_proxy]}, determined by strategy #{host[:winning_strategy]}."
114
+ end
115
+ puts
116
+ end
117
+ end
118
+
119
+ def output_csv(results)
120
+ writer = CSV.new($stdout)
121
+ writer << %w(host strategy proxy)
122
+ results.each do |host|
123
+ writer << [
124
+ host[:host].name,
125
+ host[:winning_strategy],
126
+ host[:selected_proxy],
127
+ ]
128
+ end
129
+ writer.close
130
+ end
131
+ end
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '8.0.0'.freeze
2
+ VERSION = '8.1.0'.freeze
3
3
  end
@@ -56,6 +56,28 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
56
56
  end
57
57
  end
58
58
 
59
+ describe '.proxy_feature' do
60
+ # rubocop:disable Naming/ConstantName
61
+ it 'handles provider subclasses properly' do
62
+ old = ::RemoteExecutionProvider
63
+
64
+ class P2 < old
65
+ end
66
+ ::RemoteExecutionProvider = P2
67
+
68
+ class CustomProvider < ::RemoteExecutionProvider
69
+ end
70
+
71
+ ::RemoteExecutionProvider.register('custom', CustomProvider)
72
+
73
+ feature = CustomProvider.proxy_feature
74
+ _(feature).must_equal 'custom'
75
+ ensure
76
+ ::RemoteExecutionProvider = old
77
+ end
78
+ # rubocop:enable Naming/ConstantName
79
+ end
80
+
59
81
  describe '.provider_proxy_features' do
60
82
  it 'returns correct values' do
61
83
  RemoteExecutionProvider.stubs(:providers).returns(
@@ -25,6 +25,7 @@ import {
25
25
  selectIsSubmitting,
26
26
  selectRouterSearch,
27
27
  selectIsLoading,
28
+ selectJobCategoriesResponse,
28
29
  } from './JobWizardSelectors';
29
30
  import { ScheduleType } from './steps/Schedule/ScheduleType';
30
31
  import { ScheduleFuture } from './steps/Schedule/ScheduleFuture';
@@ -34,13 +35,18 @@ import ReviewDetails from './steps/ReviewDetails/';
34
35
  import { useValidation } from './validation';
35
36
  import { useAutoFill } from './autofill';
36
37
  import { submit } from './submit';
38
+ import { generateDefaultDescription } from './JobWizardHelpers';
37
39
  import './JobWizard.scss';
38
40
 
39
41
  export const JobWizard = ({ rerunData }) => {
42
+ const jobCategoriesResponse = useSelector(selectJobCategoriesResponse);
40
43
  const [jobTemplateID, setJobTemplateID] = useState(
41
- rerunData?.template_invocations?.[0]?.template_id
44
+ rerunData?.template_invocations?.[0]?.template_id ||
45
+ jobCategoriesResponse?.default_template
46
+ );
47
+ const [category, setCategory] = useState(
48
+ rerunData?.job_category || jobCategoriesResponse?.default_category || ''
42
49
  );
43
- const [category, setCategory] = useState(rerunData?.job_category || '');
44
50
  const [advancedValues, setAdvancedValues] = useState({ templateValues: {} });
45
51
  const [templateValues, setTemplateValues] = useState({}); // TODO use templateValues in advanced fields - description https://github.com/theforeman/foreman_remote_execution/pull/605
46
52
  const [scheduleValue, setScheduleValue] = useState(initialScheduleState);
@@ -56,6 +62,7 @@ export const JobWizard = ({ rerunData }) => {
56
62
  ? {
57
63
  search: rerunData?.targeting?.search_query,
58
64
  ...rerunData.inputs,
65
+ ...routerSearch,
59
66
  }
60
67
  : routerSearch
61
68
  );
@@ -70,6 +77,7 @@ export const JobWizard = ({ rerunData }) => {
70
77
  job_template: {
71
78
  name,
72
79
  execution_timeout_interval,
80
+ time_to_pickup,
73
81
  description_format,
74
82
  job_category,
75
83
  },
@@ -78,8 +86,8 @@ export const JobWizard = ({ rerunData }) => {
78
86
  concurrency_control = {},
79
87
  },
80
88
  }) => {
81
- if (!category.length) {
82
- setCategory(current => (current.length ? current : job_category));
89
+ if (category !== job_category) {
90
+ setCategory(job_category);
83
91
  }
84
92
  const advancedTemplateValues = {};
85
93
  const defaultTemplateValues = {};
@@ -101,21 +109,19 @@ export const JobWizard = ({ rerunData }) => {
101
109
  currentAdvancedValues[input.name] || input?.default || '';
102
110
  });
103
111
  }
104
- const generateDefaultDescription = () => {
105
- if (description_format) return description_format;
106
- const allInputs = [...advancedInputs, ...inputs];
107
- if (!allInputs.length) return name;
108
- const inputsString = allInputs
109
- .map(({ name: inputname }) => `${inputname}="%{${inputname}}"`)
110
- .join(' ');
111
- return `${name} with inputs ${inputsString}`;
112
- };
113
112
  return {
114
113
  ...currentAdvancedValues,
115
114
  effectiveUserValue: effective_user?.value || '',
116
115
  timeoutToKill: execution_timeout_interval || '',
116
+ timeToPickup: time_to_pickup || '',
117
117
  templateValues: advancedTemplateValues,
118
- description: generateDefaultDescription() || '',
118
+ description:
119
+ generateDefaultDescription({
120
+ description_format,
121
+ advancedInputs,
122
+ inputs,
123
+ name,
124
+ }) || '',
119
125
  isRandomizedOrdering: randomized_ordering,
120
126
  sshUser: ssh_user || '',
121
127
  timeSpan: concurrency_control.time_span || '',
@@ -123,6 +129,7 @@ export const JobWizard = ({ rerunData }) => {
123
129
  };
124
130
  });
125
131
  },
132
+ // eslint-disable-next-line react-hooks/exhaustive-deps
126
133
  [category.length]
127
134
  );
128
135
  useEffect(() => {
@@ -134,6 +141,9 @@ export const JobWizard = ({ rerunData }) => {
134
141
  },
135
142
  job_template: {
136
143
  execution_timeout_interval: rerunData.execution_timeout_interval,
144
+ description_format: rerunData.description_format,
145
+ job_category: rerunData.job_category,
146
+ time_to_pickup: rerunData.time_to_pickup,
137
147
  },
138
148
  randomized_ordering: rerunData.targeting.randomized_ordering,
139
149
  ssh_user: rerunData.ssh_user,
@@ -141,18 +151,39 @@ export const JobWizard = ({ rerunData }) => {
141
151
  },
142
152
  });
143
153
  }
144
- }, [rerunData, setDefaults]);
154
+ // eslint-disable-next-line react-hooks/exhaustive-deps
155
+ }, [rerunData]);
145
156
  useEffect(() => {
146
157
  if (jobTemplateID) {
147
158
  dispatch(
148
159
  get({
149
160
  key: JOB_TEMPLATE,
150
161
  url: `/ui_job_wizard/template/${jobTemplateID}`,
151
- handleSuccess: rerunData ? () => {} : setDefaults,
162
+ handleSuccess: rerunData
163
+ ? ({
164
+ data: {
165
+ template_inputs = [],
166
+ advanced_template_inputs = [],
167
+ job_template: { name, description_format },
168
+ },
169
+ }) => {
170
+ setAdvancedValues(currentAdvancedValues => ({
171
+ ...currentAdvancedValues,
172
+ description:
173
+ generateDefaultDescription({
174
+ description_format,
175
+ advancedInputs: advanced_template_inputs,
176
+ inputs: template_inputs,
177
+ name,
178
+ }) || '',
179
+ }));
180
+ }
181
+ : setDefaults,
152
182
  })
153
183
  );
154
184
  }
155
- }, [rerunData, jobTemplateID, setDefaults, dispatch]);
185
+ // eslint-disable-next-line react-hooks/exhaustive-deps
186
+ }, [rerunData, jobTemplateID, dispatch]);
156
187
 
157
188
  const [valid, setValid] = useValidation({
158
189
  advancedValues,
@@ -187,7 +218,9 @@ export const JobWizard = ({ rerunData }) => {
187
218
  setJobTemplate={setJobTemplateID}
188
219
  category={category}
189
220
  setCategory={setCategory}
190
- isCategoryPreselected={!!rerunData || !!fills.feature}
221
+ isCategoryPreselected={
222
+ !!rerunData || !!fills.feature || !!fills.template_id
223
+ }
191
224
  />
192
225
  ),
193
226
  enableNext: isTemplate,
@@ -370,6 +403,7 @@ JobWizard.propTypes = {
370
403
  time_span: PropTypes.number,
371
404
  }),
372
405
  execution_timeout_interval: PropTypes.number,
406
+ time_to_pickup: PropTypes.number,
373
407
  remote_execution_feature_id: PropTypes.string,
374
408
  template_invocations: PropTypes.arrayOf(
375
409
  PropTypes.shape({
@@ -379,6 +413,7 @@ JobWizard.propTypes = {
379
413
  })
380
414
  ),
381
415
  inputs: PropTypes.object,
416
+ description_format: PropTypes.string,
382
417
  }),
383
418
  };
384
419
  JobWizard.defaultProps = {
@@ -145,6 +145,9 @@
145
145
  .pf-c-radio__body {
146
146
  font-size: var(--pf-c-radio__label--FontSize);
147
147
  }
148
+ .reset-default{
149
+ padding-bottom: 0;
150
+ }
148
151
  }
149
152
 
150
153
  .job-wizard-alert.pf-c-alert.pf-m-warning {
@@ -64,7 +64,7 @@ export const dataName = {
64
64
  };
65
65
  export const HOSTS_TO_PREVIEW_AMOUNT = 20;
66
66
 
67
- export const DEBOUNCE_HOST_COUNT = 700;
67
+ export const DEBOUNCE_API = 1500;
68
68
  export const HOST_IDS = 'HOST_IDS';
69
69
  export const REX_FEATURE = 'REX_FEATURE';
70
70
 
@@ -0,0 +1,15 @@
1
+ /* eslint-disable camelcase */
2
+ export const generateDefaultDescription = ({
3
+ description_format,
4
+ advancedInputs,
5
+ inputs,
6
+ name,
7
+ }) => {
8
+ if (description_format) return description_format;
9
+ const allInputs = [...advancedInputs, ...inputs];
10
+ if (!allInputs.length) return name;
11
+ const inputsString = allInputs
12
+ .map(({ name: inputname }) => `${inputname}="%{${inputname}}"`)
13
+ .join(' ');
14
+ return `${name} with inputs ${inputsString}`;
15
+ };
@@ -1,7 +1,15 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import URI from 'urijs';
4
- import { Alert, Title, Divider, Skeleton } from '@patternfly/react-core';
4
+ import {
5
+ Alert,
6
+ Title,
7
+ Divider,
8
+ Skeleton,
9
+ Flex,
10
+ FlexItem,
11
+ Button,
12
+ } from '@patternfly/react-core';
5
13
  import { sprintf, translate as __ } from 'foremanReact/common/I18n';
6
14
  import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
7
15
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
@@ -31,7 +39,7 @@ const JobWizardPageRerun = ({
31
39
  const title = __('Run job');
32
40
  const breadcrumbOptions = {
33
41
  breadcrumbItems: [
34
- { caption: __('Jobs'), url: `/jobs` },
42
+ { caption: __('Jobs'), url: `/job_invocations` },
35
43
  { caption: title },
36
44
  ],
37
45
  };
@@ -48,9 +56,25 @@ const JobWizardPageRerun = ({
48
56
  searchable={false}
49
57
  >
50
58
  <React.Fragment>
51
- <Title headingLevel="h2" size="2xl">
52
- {title}
53
- </Title>
59
+ <React.Fragment>
60
+ <Flex>
61
+ <FlexItem>
62
+ <Title headingLevel="h2" size="2xl">
63
+ {title}
64
+ </Title>
65
+ </FlexItem>
66
+ <FlexItem align={{ default: 'alignRight' }}>
67
+ <Button
68
+ variant="link"
69
+ component="a"
70
+ href={`/old/job_invocations/${id}/rerun${search}`}
71
+ >
72
+ {__('Use old form')}
73
+ </Button>
74
+ </FlexItem>
75
+ </Flex>
76
+ <Divider component="div" />
77
+ </React.Fragment>
54
78
  {!status || status === STATUS.PENDING ? (
55
79
  <div style={{ height: '400px' }}>
56
80
  <Skeleton
@@ -24,11 +24,17 @@ export const filterJobTemplates = templates =>
24
24
  export const selectJobTemplates = state =>
25
25
  filterJobTemplates(selectAPIResponse(state, JOB_TEMPLATES)?.results);
26
26
 
27
+ export const selectJobTemplatesSearch = state =>
28
+ selectAPIResponse(state, JOB_TEMPLATES)?.search;
29
+
30
+ export const selectJobCategoriesResponse = state =>
31
+ selectAPIResponse(state, JOB_CATEGORIES) || {};
32
+
27
33
  export const selectJobCategories = state =>
28
- selectAPIResponse(state, JOB_CATEGORIES).job_categories || [];
34
+ selectJobCategoriesResponse(state).job_categories || [];
29
35
 
30
36
  export const selectWithKatello = state =>
31
- selectAPIResponse(state, JOB_CATEGORIES).with_katello || false;
37
+ selectJobCategoriesResponse(state).with_katello || false;
32
38
 
33
39
  export const selectJobCategoriesStatus = state =>
34
40
  selectAPIStatus(state, JOB_CATEGORIES);
@@ -64,6 +64,11 @@ describe('Job wizard fill', () => {
64
64
  selector: 'input',
65
65
  }).value
66
66
  ).toBe('1');
67
+ expect(
68
+ screen.getByLabelText('time to pickup', {
69
+ selector: 'input',
70
+ }).value
71
+ ).toBe('25');
67
72
 
68
73
  expect(
69
74
  screen.getByLabelText('Concurrency level', {
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  import configureMockStore from 'redux-mock-store';
2
3
  import hostsQuery from '../steps/HostsAndInputs/hosts.gql';
3
4
  import hostgroupsQuery from '../steps/HostsAndInputs/hostgroups.gql';
@@ -14,6 +15,18 @@ export const jobTemplate = {
14
15
  execution_timeout_interval: 2,
15
16
  description: null,
16
17
  };
18
+ export const pupptetJobTemplate = {
19
+ id: 163,
20
+ name: 'Puppet Agent Disable - Script Default',
21
+ template:
22
+ '<% if @host.operatingsystem.family == \'Debian\' -%>\nexport PATH=/opt/puppetlabs/bin:$PATH\n<% end -%>\npuppet agent --disable "<%= input("comment").present? ? input("comment") : "Disabled using Foreman Remote Execution" %> - <%= current_user %> - $(date "+%d/%m/%Y %H:%M")"',
23
+ snippet: false,
24
+ default: true,
25
+ job_category: 'Puppet',
26
+ provider_type: 'script',
27
+ execution_timeout_interval: 2,
28
+ description: null,
29
+ };
17
30
 
18
31
  export const jobTemplates = [jobTemplate];
19
32
 
@@ -120,6 +133,7 @@ export const testSetup = (selectors, api) => {
120
133
  selectors.selectJobCategories.mockImplementation(() => jobCategories);
121
134
  selectors.selectJobTemplates.mockImplementation(() => [
122
135
  jobTemplate,
136
+ pupptetJobTemplate,
123
137
  { ...jobTemplate, id: 2, name: 'template2' },
124
138
  ]);
125
139
  selectors.selectJobTemplate.mockImplementation(() => jobTemplateResponse);
@@ -164,12 +178,21 @@ export const mockApi = api => {
164
178
  } else if (action.key === 'JOB_TEMPLATE') {
165
179
  handleSuccess &&
166
180
  handleSuccess({
167
- data: jobTemplateResponse,
181
+ data:
182
+ action.url === '/ui_job_wizard/template/163'
183
+ ? { ...jobTemplateResponse, job_template: pupptetJobTemplate }
184
+ : jobTemplateResponse,
168
185
  });
169
186
  } else if (action.key === 'JOB_TEMPLATES') {
170
187
  handleSuccess &&
171
188
  handleSuccess({
172
- data: { results: [jobTemplate] },
189
+ data: {
190
+ results:
191
+ action.url.search() ===
192
+ '?search=job_category%3D%22Puppet%22&per_page=all'
193
+ ? [pupptetJobTemplate]
194
+ : [jobTemplate],
195
+ },
173
196
  });
174
197
  } else if (action.key === 'HOST_IDS') {
175
198
  handleSuccess &&
@@ -252,6 +275,7 @@ export const jobInvocation = {
252
275
  time_span: 4,
253
276
  },
254
277
  execution_timeout_interval: 1,
278
+ time_to_pickup: 25,
255
279
  remote_execution_feature_id: null,
256
280
  template_invocations: [
257
281
  {
@@ -17,19 +17,30 @@ export const useAutoFill = ({
17
17
 
18
18
  useEffect(() => {
19
19
  if (Object.keys(fills).length) {
20
- const { 'host_ids[]': hostIds, search, feature, ...rest } = { ...fills };
20
+ const {
21
+ 'host_ids[]': hostIds,
22
+ search,
23
+ feature,
24
+ template_id: templateID,
25
+ ...rest
26
+ } = { ...fills };
21
27
  setFills({});
22
28
  if (hostIds) {
29
+ const hostSearch = Array.isArray(hostIds)
30
+ ? `id = ${hostIds.join(' or id = ')}`
31
+ : `id = ${hostIds}`;
23
32
  dispatch(
24
33
  get({
25
34
  key: HOST_IDS,
26
35
  url: '/api/hosts',
27
- params: { search: `id = ${hostIds.join(' or id = ')}` },
36
+ params: {
37
+ search: hostSearch,
38
+ },
28
39
  handleSuccess: ({ data }) => {
29
40
  setSelectedTargets(currentTargets => ({
30
41
  ...currentTargets,
31
- hosts: (data.results || []).map(({ name }) => ({
32
- id: name,
42
+ hosts: (data.results || []).map(({ id, name }) => ({
43
+ id,
33
44
  name,
34
45
  })),
35
46
  }));
@@ -37,9 +48,12 @@ export const useAutoFill = ({
37
48
  })
38
49
  );
39
50
  }
40
- if (search) {
51
+ if (search && !hostIds?.length) {
41
52
  setHostsSearchQuery(search);
42
53
  }
54
+ if (templateID) {
55
+ setJobTemplateID(+templateID);
56
+ }
43
57
  if (feature) {
44
58
  dispatch(
45
59
  get({
@@ -56,11 +70,19 @@ export const useAutoFill = ({
56
70
  const re = /inputs\[(?<input>.*)\]/g;
57
71
  const input = re.exec(key)?.groups?.input;
58
72
  if (input) {
59
- setTemplateValues(prev => ({ ...prev, [input]: rest[key] }));
60
- setAdvancedValues(prev => ({
61
- ...prev,
62
- templateValues: { ...prev.templateValues, [input]: rest[key] },
63
- }));
73
+ if (typeof rest[key] === 'string') {
74
+ setTemplateValues(prev => ({ ...prev, [input]: rest[key] }));
75
+ } else {
76
+ const { value, advanced } = rest[key];
77
+ if (advanced) {
78
+ setAdvancedValues(prev => ({
79
+ ...prev,
80
+ templateValues: { ...prev.templateValues, [input]: value },
81
+ }));
82
+ } else {
83
+ setTemplateValues(prev => ({ ...prev, [input]: value }));
84
+ }
85
+ }
64
86
  }
65
87
  });
66
88
  }
@@ -1,14 +1,15 @@
1
1
  import React from 'react';
2
- import { Title, Divider } from '@patternfly/react-core';
2
+ import PropTypes from 'prop-types';
3
+ import { Title, Divider, Flex, FlexItem, Button } from '@patternfly/react-core';
3
4
  import { translate as __ } from 'foremanReact/common/I18n';
4
5
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
5
6
  import { JobWizard } from './JobWizard';
6
7
 
7
- const JobWizardPage = () => {
8
+ const JobWizardPage = ({ location: { search } }) => {
8
9
  const title = __('Run job');
9
10
  const breadcrumbOptions = {
10
11
  breadcrumbItems: [
11
- { caption: __('Jobs'), url: `/jobs` },
12
+ { caption: __('Jobs'), url: `/job_invocations` },
12
13
  { caption: title },
13
14
  ],
14
15
  };
@@ -19,9 +20,22 @@ const JobWizardPage = () => {
19
20
  searchable={false}
20
21
  >
21
22
  <React.Fragment>
22
- <Title headingLevel="h2" size="2xl">
23
- {title}
24
- </Title>
23
+ <Flex>
24
+ <FlexItem>
25
+ <Title headingLevel="h2" size="2xl">
26
+ {title}
27
+ </Title>
28
+ </FlexItem>
29
+ <FlexItem align={{ default: 'alignRight' }}>
30
+ <Button
31
+ variant="link"
32
+ component="a"
33
+ href={`/old/job_invocations/new${search}`}
34
+ >
35
+ {__('Use legacy form')}
36
+ </Button>
37
+ </FlexItem>
38
+ </Flex>
25
39
  <Divider component="div" />
26
40
  <JobWizard />
27
41
  </React.Fragment>
@@ -29,4 +43,9 @@ const JobWizardPage = () => {
29
43
  );
30
44
  };
31
45
 
46
+ JobWizardPage.propTypes = {
47
+ location: PropTypes.shape({
48
+ search: PropTypes.string,
49
+ }).isRequired,
50
+ };
32
51
  export default JobWizardPage;