foreman_remote_execution 8.2.0 → 8.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +1 -0
  3. data/app/controllers/job_invocations_controller.rb +20 -1
  4. data/app/controllers/ui_job_wizard_controller.rb +3 -1
  5. data/app/helpers/remote_execution_helper.rb +1 -1
  6. data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
  7. data/app/views/api/v2/job_invocations/base.json.rabl +1 -1
  8. data/app/views/job_invocations/_form.html.erb +1 -1
  9. data/app/views/job_invocations/show.html.erb +1 -1
  10. data/app/views/job_invocations/welcome.html.erb +1 -1
  11. data/config/routes.rb +1 -0
  12. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +1 -1
  13. data/lib/foreman_remote_execution/engine.rb +1 -1
  14. data/lib/foreman_remote_execution/version.rb +1 -1
  15. data/locale/action_names.rb +2 -2
  16. data/locale/de/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  17. data/locale/de/foreman_remote_execution.po +266 -154
  18. data/locale/en/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  19. data/locale/en/foreman_remote_execution.po +132 -24
  20. data/locale/en_GB/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  21. data/locale/en_GB/foreman_remote_execution.po +149 -41
  22. data/locale/es/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  23. data/locale/es/foreman_remote_execution.po +320 -210
  24. data/locale/foreman_remote_execution.pot +394 -211
  25. data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  26. data/locale/fr/foreman_remote_execution.po +353 -241
  27. data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  28. data/locale/ja/foreman_remote_execution.po +368 -261
  29. data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  30. data/locale/ko/foreman_remote_execution.po +161 -53
  31. data/locale/pt_BR/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  32. data/locale/pt_BR/foreman_remote_execution.po +335 -225
  33. data/locale/ru/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  34. data/locale/ru/foreman_remote_execution.po +161 -53
  35. data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  36. data/locale/zh_CN/foreman_remote_execution.po +465 -359
  37. data/locale/zh_TW/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  38. data/locale/zh_TW/foreman_remote_execution.po +162 -54
  39. data/webpack/JobWizard/JobWizard.js +52 -10
  40. data/webpack/JobWizard/JobWizardConstants.js +1 -1
  41. data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +8 -0
  42. data/webpack/JobWizard/__tests__/fixtures.js +5 -0
  43. data/webpack/JobWizard/__tests__/integration.test.js +15 -0
  44. data/webpack/JobWizard/__tests__/validation.test.js +27 -0
  45. data/webpack/JobWizard/autofill.js +1 -0
  46. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +19 -0
  47. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +8 -0
  48. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +3 -0
  49. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +32 -2
  50. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +16 -10
  51. data/webpack/JobWizard/steps/HostsAndInputs/index.js +51 -3
  52. data/webpack/JobWizard/steps/Schedule/QueryType.js +1 -1
  53. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +0 -1
  54. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +25 -5
  55. data/webpack/JobWizard/steps/form/DateTimePicker.js +0 -1
  56. data/webpack/JobWizard/steps/form/GroupedSelectField.js +0 -1
  57. data/webpack/JobWizard/steps/form/SelectField.js +0 -1
  58. data/webpack/JobWizard/submit.js +13 -2
  59. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +4 -4
  60. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +2 -2
  61. data/webpack/react_app/components/RecentJobsCard/constants.js +2 -2
  62. metadata +6 -6
@@ -209,6 +209,11 @@ export const JobWizard = ({ rerunData }) => {
209
209
  !templateError &&
210
210
  !!jobTemplateID &&
211
211
  templateResponse.job_template;
212
+ const areHostsSelected =
213
+ selectedTargets.hosts.length > 0 ||
214
+ selectedTargets.hostCollections.length > 0 ||
215
+ selectedTargets.hostGroups.length > 0 ||
216
+ hostsSearchQuery.length > 0;
212
217
  const steps = [
213
218
  {
214
219
  name: WIZARD_TITLES.categoryAndTemplate,
@@ -238,7 +243,7 @@ export const JobWizard = ({ rerunData }) => {
238
243
  />
239
244
  ),
240
245
  canJumpTo: isTemplate,
241
- enableNext: isTemplate && valid.hostsAndInputs,
246
+ enableNext: isTemplate && valid.hostsAndInputs && areHostsSelected,
242
247
  },
243
248
  {
244
249
  name: WIZARD_TITLES.advanced,
@@ -254,14 +259,26 @@ export const JobWizard = ({ rerunData }) => {
254
259
  templateValues={templateValues}
255
260
  />
256
261
  ),
257
- canJumpTo: isTemplate && valid.hostsAndInputs,
258
- enableNext: isTemplate && valid.hostsAndInputs && valid.advanced,
262
+ canJumpTo: isTemplate && valid.hostsAndInputs && areHostsSelected,
263
+ enableNext:
264
+ isTemplate &&
265
+ valid.hostsAndInputs &&
266
+ areHostsSelected &&
267
+ valid.advanced,
259
268
  },
260
269
  {
261
270
  name: WIZARD_TITLES.schedule,
262
- canJumpTo: isTemplate && valid.hostsAndInputs && valid.advanced,
271
+ canJumpTo:
272
+ isTemplate &&
273
+ valid.hostsAndInputs &&
274
+ areHostsSelected &&
275
+ valid.advanced,
263
276
  enableNext:
264
- isTemplate && valid.hostsAndInputs && valid.advanced && valid.schedule,
277
+ isTemplate &&
278
+ valid.hostsAndInputs &&
279
+ areHostsSelected &&
280
+ valid.advanced &&
281
+ valid.schedule,
265
282
  steps: [
266
283
  {
267
284
  name: WIZARD_TITLES.typeOfExecution,
@@ -278,9 +295,17 @@ export const JobWizard = ({ rerunData }) => {
278
295
  }}
279
296
  />
280
297
  ),
281
- canJumpTo: isTemplate && valid.hostsAndInputs && valid.advanced,
298
+ canJumpTo:
299
+ isTemplate &&
300
+ valid.hostsAndInputs &&
301
+ areHostsSelected &&
302
+ valid.advanced,
282
303
 
283
- enableNext: isTemplate && valid.hostsAndInputs && valid.advanced,
304
+ enableNext:
305
+ isTemplate &&
306
+ valid.hostsAndInputs &&
307
+ areHostsSelected &&
308
+ valid.advanced,
284
309
  },
285
310
  ...(scheduleValue.scheduleType === SCHEDULE_TYPES.FUTURE
286
311
  ? [
@@ -298,10 +323,15 @@ export const JobWizard = ({ rerunData }) => {
298
323
  }}
299
324
  />
300
325
  ),
301
- canJumpTo: isTemplate && valid.hostsAndInputs && valid.advanced,
326
+ canJumpTo:
327
+ isTemplate &&
328
+ valid.hostsAndInputs &&
329
+ areHostsSelected &&
330
+ valid.advanced,
302
331
  enableNext:
303
332
  isTemplate &&
304
333
  valid.hostsAndInputs &&
334
+ areHostsSelected &&
305
335
  valid.advanced &&
306
336
  valid.schedule,
307
337
  },
@@ -323,10 +353,15 @@ export const JobWizard = ({ rerunData }) => {
323
353
  }}
324
354
  />
325
355
  ),
326
- canJumpTo: isTemplate && valid.hostsAndInputs && valid.advanced,
356
+ canJumpTo:
357
+ isTemplate &&
358
+ valid.hostsAndInputs &&
359
+ areHostsSelected &&
360
+ valid.advanced,
327
361
  enableNext:
328
362
  isTemplate &&
329
363
  valid.hostsAndInputs &&
364
+ areHostsSelected &&
330
365
  valid.advanced &&
331
366
  valid.schedule,
332
367
  },
@@ -349,10 +384,15 @@ export const JobWizard = ({ rerunData }) => {
349
384
  ),
350
385
  nextButtonText: 'Run',
351
386
  canJumpTo:
352
- isTemplate && valid.hostsAndInputs && valid.advanced && valid.schedule,
387
+ isTemplate &&
388
+ valid.advanced &&
389
+ valid.hostsAndInputs &&
390
+ areHostsSelected &&
391
+ valid.schedule,
353
392
  enableNext:
354
393
  isTemplate &&
355
394
  valid.hostsAndInputs &&
395
+ areHostsSelected &&
356
396
  valid.advanced &&
357
397
  valid.schedule &&
358
398
  !isSubmitting,
@@ -379,6 +419,8 @@ export const JobWizard = ({ rerunData }) => {
379
419
  location,
380
420
  organization,
381
421
  feature: routerSearch?.feature,
422
+ provider: templateResponse.provider_name,
423
+ advancedInputs: templateResponse.advanced_template_inputs,
382
424
  });
383
425
  }}
384
426
  />
@@ -23,7 +23,7 @@ export const SCHEDULE_TYPES = {
23
23
  };
24
24
 
25
25
  export const WIZARD_TITLES = {
26
- categoryAndTemplate: __('Category and Template'),
26
+ categoryAndTemplate: __('Category and template'),
27
27
  hostsAndInputs: __('Target hosts and inputs'),
28
28
  advanced: __('Advanced fields'),
29
29
  schedule: __('Schedule'),
@@ -7,6 +7,14 @@ Array [
7
7
  "type": "get",
8
8
  "url": "/ui_job_wizard/categories",
9
9
  },
10
+ Object {
11
+ "key": "HOST_IDS",
12
+ "params": Object {
13
+ "search": "id = 105 or id = 37",
14
+ },
15
+ "type": "get",
16
+ "url": "/api/hosts",
17
+ },
10
18
  Object {
11
19
  "key": "JOB_TEMPLATES",
12
20
  "type": "get",
@@ -161,6 +161,11 @@ export const testSetup = (selectors, api) => {
161
161
  ],
162
162
  },
163
163
  },
164
+ HOSTS_API: {
165
+ response: {
166
+ subtotal: 3,
167
+ },
168
+ },
164
169
  });
165
170
  return store;
166
171
  };
@@ -18,6 +18,15 @@ import {
18
18
  const store = testSetup(selectors, api);
19
19
 
20
20
  describe('Job wizard fill', () => {
21
+ beforeEach(() => {
22
+ jest.spyOn(selectors, 'selectRouterSearch');
23
+ selectors.selectRouterSearch.mockImplementation(() => ({
24
+ 'host_ids[]': ['105', '37'],
25
+ }));
26
+ });
27
+ afterEach(() => {
28
+ selectors.selectRouterSearch.mockRestore();
29
+ });
21
30
  it('should select template', async () => {
22
31
  api.get.mockImplementation(({ handleSuccess, ...action }) => {
23
32
  if (action.key === 'JOB_CATEGORIES') {
@@ -33,7 +42,13 @@ describe('Job wizard fill', () => {
33
42
  handleSuccess({
34
43
  data: jobTemplate,
35
44
  });
45
+ } else if (action.key === 'HOST_IDS') {
46
+ handleSuccess &&
47
+ handleSuccess({
48
+ data: { results: [{ name: 'host1' }, { name: 'host3' }] },
49
+ });
36
50
  }
51
+
37
52
  return { type: 'get', ...action };
38
53
  });
39
54
  selectors.selectJobTemplate.mockRestore();
@@ -41,11 +41,26 @@ describe('Job wizard validation', () => {
41
41
  expect(screen.getByText(WIZARD_TITLES.review)).toBeDisabled();
42
42
  await act(async () => {
43
43
  fireEvent.click(screen.getByText(WIZARD_TITLES.hostsAndInputs));
44
+ await new Promise(resolve => setTimeout(resolve, 0)); // to resolve gql
44
45
  });
46
+ const select = name =>
47
+ screen.getByRole('button', { name: `${name} toggle` });
48
+ fireEvent.click(select('hosts'));
49
+ await act(async () => {
50
+ fireEvent.click(screen.getByText('host1'));
51
+ });
52
+
53
+ expect(screen.getByText(WIZARD_TITLES.advanced)).toBeDisabled();
54
+ expect(screen.getByText(WIZARD_TITLES.schedule)).toBeDisabled();
55
+ expect(screen.getByText(WIZARD_TITLES.review)).toBeDisabled();
45
56
  const textField = screen.getByLabelText('plain hidden', {
46
57
  selector: 'textarea',
47
58
  });
48
59
  await act(async () => {
60
+ fireEvent.click(
61
+ // Close the select
62
+ select('hosts')
63
+ );
49
64
  await fireEvent.change(textField, {
50
65
  target: { value: 'text' },
51
66
  });
@@ -85,8 +100,20 @@ describe('Job wizard validation', () => {
85
100
  // setup
86
101
  await act(async () => {
87
102
  fireEvent.click(screen.getByText(WIZARD_TITLES.hostsAndInputs));
103
+ await new Promise(resolve => setTimeout(resolve, 0)); // to resolve gql
88
104
  });
105
+
106
+ const select = name =>
107
+ screen.getByRole('button', { name: `${name} toggle` });
108
+ fireEvent.click(select('hosts'));
89
109
  await act(async () => {
110
+ fireEvent.click(screen.getByText('host1'));
111
+ });
112
+ await act(async () => {
113
+ fireEvent.click(
114
+ // Close the host select
115
+ select('hosts')
116
+ );
90
117
  await fireEvent.change(
91
118
  screen.getByLabelText('plain hidden', {
92
119
  selector: 'textarea',
@@ -72,6 +72,7 @@ export const useAutoFill = ({
72
72
  if (input) {
73
73
  if (typeof rest[key] === 'string') {
74
74
  setTemplateValues(prev => ({ ...prev, [input]: rest[key] }));
75
+ setAdvancedValues(prev => ({ ...prev, [input]: rest[key] }));
75
76
  } else {
76
77
  const { value, advanced } = rest[key];
77
78
  if (advanced) {
@@ -26,6 +26,15 @@ mockApi(api);
26
26
  jest.useFakeTimers();
27
27
 
28
28
  describe('AdvancedFields', () => {
29
+ beforeEach(() => {
30
+ jest.spyOn(selectors, 'selectRouterSearch');
31
+ selectors.selectRouterSearch.mockImplementation(() => ({
32
+ 'host_ids[]': ['105', '37'],
33
+ }));
34
+ });
35
+ afterEach(() => {
36
+ selectors.selectRouterSearch.mockRestore();
37
+ });
29
38
  it('should save data between steps for advanced fields', async () => {
30
39
  const wrapper = mount(
31
40
  <MockedProvider mocks={gqlMock} addTypename={false}>
@@ -270,6 +279,11 @@ describe('AdvancedFields', () => {
270
279
  handleSuccess({
271
280
  data: { results: [jobTemplate] },
272
281
  });
282
+ } else if (action.key === 'HOST_IDS') {
283
+ handleSuccess &&
284
+ handleSuccess({
285
+ data: { results: [{ name: 'host1' }, { name: 'host3' }] },
286
+ });
273
287
  }
274
288
  return { type: 'get', ...action };
275
289
  });
@@ -339,6 +353,11 @@ describe('AdvancedFields', () => {
339
353
  handleSuccess({
340
354
  data: { results: [jobTemplate] },
341
355
  });
356
+ } else if (action.key === 'HOST_IDS') {
357
+ handleSuccess &&
358
+ handleSuccess({
359
+ data: { results: [{ name: 'host1' }, { name: 'host3' }] },
360
+ });
342
361
  }
343
362
  return { type: 'get', ...action };
344
363
  });
@@ -7,6 +7,14 @@ Array [
7
7
  "type": "get",
8
8
  "url": "/ui_job_wizard/categories",
9
9
  },
10
+ Object {
11
+ "key": "HOST_IDS",
12
+ "params": Object {
13
+ "search": "id = 105 or id = 37",
14
+ },
15
+ "type": "get",
16
+ "url": "/api/hosts",
17
+ },
10
18
  Object {
11
19
  "key": "JOB_TEMPLATES",
12
20
  "type": "get",
@@ -60,3 +60,6 @@ HostPreviewModal.propTypes = {
60
60
  setIsOpen: PropTypes.func.isRequired,
61
61
  searchQuery: PropTypes.string.isRequired,
62
62
  };
63
+ HostPreviewModal.defaultPropTypes = {
64
+ searchQuery: '',
65
+ };
@@ -30,6 +30,27 @@ describe('Hosts', () => {
30
30
  const select = name =>
31
31
  screen.getByRole('button', { name: `${name} toggle` });
32
32
  fireEvent.click(select('hosts'));
33
+ await act(async () => {
34
+ fireEvent.click(screen.getByText('host1'));
35
+ fireEvent.click(select('hosts'));
36
+ });
37
+ expect(
38
+ screen.queryAllByText('Please select at least one host')
39
+ ).toHaveLength(0);
40
+ await act(async () => {
41
+ fireEvent.click(select('hosts'));
42
+ });
43
+ await act(async () => {
44
+ fireEvent.click(
45
+ screen.getByText('host1', {
46
+ selector: '.pf-c-select__menu-item',
47
+ })
48
+ );
49
+ fireEvent.blur(select('hosts'));
50
+ });
51
+ expect(
52
+ screen.queryAllByText('Please select at least one host')
53
+ ).toHaveLength(1);
33
54
  await act(async () => {
34
55
  fireEvent.click(screen.getByText('host1'));
35
56
  fireEvent.click(screen.getByText('host2'));
@@ -70,7 +91,7 @@ describe('Hosts', () => {
70
91
  expect(screen.queryAllByText('host_collection1')).toHaveLength(1);
71
92
 
72
93
  await act(async () => {
73
- fireEvent.click(screen.getByText('Category and Template'));
94
+ fireEvent.click(screen.getByText('Category and template'));
74
95
  });
75
96
  await act(async () => {
76
97
  fireEvent.click(screen.getByText('Target hosts and inputs'));
@@ -151,8 +172,9 @@ describe('Hosts', () => {
151
172
 
152
173
  it('input fill from url', async () => {
153
174
  const inputText = 'test text';
175
+ const advancedInputText = 'test adv text';
154
176
  routerSelectors.selectRouterLocation.mockImplementation(() => ({
155
- search: `feature=test_feature&inputs[plain hidden]=${inputText}`,
177
+ search: `host_ids%5B%5D=host1&host_ids%5B%5D=host3&feature=test_feature&inputs[plain hidden]=${inputText}&inputs[adv plain hidden]=${advancedInputText}`,
156
178
  }));
157
179
  render(
158
180
  <MockedProvider mocks={gqlMock} addTypename={false}>
@@ -175,5 +197,13 @@ describe('Hosts', () => {
175
197
  selector: 'textarea',
176
198
  });
177
199
  expect(textField.value).toBe(inputText);
200
+
201
+ await act(async () => {
202
+ fireEvent.click(screen.getByText('Advanced fields'));
203
+ });
204
+ const advancedTextField = screen.getByLabelText('adv plain hidden', {
205
+ selector: 'textarea',
206
+ });
207
+ expect(advancedTextField.value).toBe(advancedInputText);
178
208
  });
179
209
  });
@@ -1,18 +1,24 @@
1
1
  export const buildHostQuery = (selected, search) => {
2
2
  const { hosts, hostCollections, hostGroups } = selected;
3
- const hostsSearch = `(id ^ (${hosts.map(({ id }) => id).join(',')}))`;
4
- const hostCollectionsSearch = `(host_collection_id ^ (${hostCollections
3
+ const hostsSearch = `id ^ (${hosts.map(({ id }) => id).join(',')})`;
4
+ const hostCollectionsSearch = `host_collection_id ^ (${hostCollections
5
5
  .map(({ id }) => id)
6
- .join(',')}))`;
7
- const hostGroupsSearch = `(hostgroup_id ^ (${hostGroups
6
+ .join(',')})`;
7
+ const hostGroupsSearch = `hostgroup_id ^ (${hostGroups
8
8
  .map(({ id }) => id)
9
- .join(',')}))`;
10
- return [
9
+ .join(',')})`;
10
+ const queryParts = [
11
11
  hosts.length ? hostsSearch : false,
12
12
  hostCollections.length ? hostCollectionsSearch : false,
13
13
  hostGroups.length ? hostGroupsSearch : false,
14
- search.length ? `(${search})` : false,
15
- ]
16
- .filter(Boolean)
17
- .join(' or ');
14
+ search.length ? search : false,
15
+ ].filter(Boolean);
16
+
17
+ if (queryParts.length === 0) {
18
+ return 'name=a AND name=b';
19
+ }
20
+ if (queryParts.length === 1) {
21
+ return queryParts[0] || 'name=a AND name=b';
22
+ }
23
+ return queryParts.map(p => `(${p})`).join(' or ') || 'name=a AND name=b';
18
24
  };
@@ -54,6 +54,30 @@ const HostsAndInputs = ({
54
54
  const isLoading = useSelector(selectIsLoadingHosts);
55
55
  const templateInputs = useSelector(selectTemplateInputs);
56
56
  const [hostPreviewOpen, setHostPreviewOpen] = useState(false);
57
+ const [wasFocus, setWasFocus] = useState(false);
58
+ const [isError, setIsError] = useState(false);
59
+ useEffect(() => {
60
+ if (wasFocus) {
61
+ if (
62
+ selected.hosts.length === 0 &&
63
+ selected.hostCollections.length === 0 &&
64
+ selected.hostGroups.length === 0 &&
65
+ hostsSearchQuery.length === 0
66
+ ) {
67
+ setIsError(true);
68
+ } else {
69
+ setIsError(false);
70
+ }
71
+ }
72
+ }, [
73
+ hostMethod,
74
+ hostsSearchQuery.length,
75
+ selected,
76
+ selected.hostCollections.length,
77
+ selected.hostGroups.length,
78
+ selected.hosts.length,
79
+ wasFocus,
80
+ ]);
57
81
  useEffect(() => {
58
82
  debounce(() => {
59
83
  dispatch(
@@ -103,6 +127,9 @@ const HostsAndInputs = ({
103
127
  dispatch(resetData(hostsController, hostQuerySearchID));
104
128
  setHostsSearchQuery('');
105
129
  };
130
+ const [errorText, setErrorText] = useState(
131
+ __('Please select at least one host')
132
+ );
106
133
  return (
107
134
  <div className="target-hosts-and-inputs">
108
135
  <WizardTitle title={WIZARD_TITLES.hostsAndInputs} />
@@ -114,8 +141,13 @@ const HostsAndInputs = ({
114
141
  />
115
142
  )}
116
143
  <Form>
117
- <FormGroup fieldId="host_selection" id="host-selection">
118
- <InputGroup>
144
+ <FormGroup
145
+ fieldId="host_selection"
146
+ id="host-selection"
147
+ helperTextInvalid={errorText}
148
+ validated={isError ? 'error' : 'default'}
149
+ >
150
+ <InputGroup onBlur={() => setWasFocus(true)}>
119
151
  <SelectField
120
152
  isRequired
121
153
  className="target-method-select"
@@ -127,7 +159,23 @@ const HostsAndInputs = ({
127
159
  }
128
160
  return true;
129
161
  })}
130
- setValue={setHostMethod}
162
+ setValue={val => {
163
+ setHostMethod(val);
164
+ if (val === hostMethods.searchQuery) {
165
+ setErrorText(__('Please enter a search query'));
166
+ }
167
+ if (val === hostMethods.hosts) {
168
+ setErrorText(__('Please select at least one host'));
169
+ }
170
+ if (val === hostMethods.hostCollections) {
171
+ setErrorText(
172
+ __('Please select at least one host collection')
173
+ );
174
+ }
175
+ if (val === hostMethods.hostGroups) {
176
+ setErrorText(__('Please select at least one host group'));
177
+ }
178
+ }}
131
179
  value={hostMethod}
132
180
  />
133
181
  {hostMethod === hostMethods.searchQuery && (
@@ -31,7 +31,7 @@ export const QueryType = ({ isTypeStatic, setIsTypeStatic }) => (
31
31
  id="query-type-dynamic"
32
32
  label={__('Dynamic query')}
33
33
  body={__(
34
- "evaluates just before the execution is started, so if it's planed in future, targeted hosts set may change before it"
34
+ "evaluates just before the execution is started, so if it's planned in future, targeted hosts set may change before it"
35
35
  )}
36
36
  />
37
37
  </FormGroup>
@@ -47,7 +47,6 @@ export const RepeatHour = ({ repeatData, setRepeatData }) => {
47
47
  }}
48
48
  isOpen={minuteOpen}
49
49
  width={125}
50
- menuAppendTo={() => document.querySelector('.pf-c-form.schedule-tab')}
51
50
  toggleAriaLabel="select minute toggle"
52
51
  validated={
53
52
  isValidMinute(minute)
@@ -39,15 +39,35 @@ api.get.mockImplementation(({ handleSuccess, ...action }) => {
39
39
  handleSuccess({
40
40
  data: { results: [jobTemplateResponse.job_template] },
41
41
  });
42
+ } else if (action.key === 'HOST_IDS') {
43
+ handleSuccess &&
44
+ handleSuccess({
45
+ data: { results: [{ name: 'host1' }, { name: 'host3' }] },
46
+ });
42
47
  }
43
48
  return { type: 'get', ...action };
44
49
  });
45
50
 
46
51
  const mockStore = configureMockStore([]);
47
- const store = mockStore({});
52
+ const store = mockStore({
53
+ HOSTS_API: {
54
+ response: {
55
+ subtotal: 3,
56
+ },
57
+ },
58
+ });
48
59
  jest.useFakeTimers();
49
60
 
50
61
  describe('Schedule', () => {
62
+ beforeEach(() => {
63
+ jest.spyOn(selectors, 'selectRouterSearch');
64
+ selectors.selectRouterSearch.mockImplementation(() => ({
65
+ 'host_ids[]': ['105', '37'],
66
+ }));
67
+ });
68
+ afterEach(() => {
69
+ selectors.selectRouterSearch.mockRestore();
70
+ });
51
71
  it('sub steps appear', () => {
52
72
  render(
53
73
  <Provider store={store}>
@@ -119,7 +139,7 @@ describe('Schedule', () => {
119
139
  });
120
140
 
121
141
  act(() => {
122
- fireEvent.click(screen.getByText('Category and Template'));
142
+ fireEvent.click(screen.getByText('Category and template'));
123
143
  });
124
144
  act(() => {
125
145
  fireEvent.click(screen.getByRole('button', { name: 'Future execution' }));
@@ -226,7 +246,7 @@ describe('Schedule', () => {
226
246
  });
227
247
 
228
248
  act(() => {
229
- fireEvent.click(screen.getByText('Category and Template'));
249
+ fireEvent.click(screen.getByText('Category and template'));
230
250
  });
231
251
  act(() => {
232
252
  fireEvent.click(
@@ -286,9 +306,9 @@ describe('Schedule', () => {
286
306
  expect(screen.getByText('Review details').disabled).toBeFalsy();
287
307
 
288
308
  await act(async () => {
289
- fireEvent.click(screen.getByText('Category and Template'));
309
+ fireEvent.click(screen.getByText('Category and template'));
290
310
  });
291
- expect(screen.getAllByText('Category and Template')).toHaveLength(3);
311
+ expect(screen.getAllByText('Category and template')).toHaveLength(3);
292
312
 
293
313
  await act(async () => {
294
314
  fireEvent.click(
@@ -119,7 +119,6 @@ export const DateTimePicker = ({
119
119
  is24Hour
120
120
  isDisabled={isDisabled || formattedDate.length === 0}
121
121
  invalidFormatErrorMessage={__('Invalid time format')}
122
- menuAppendTo={() => document.body}
123
122
  includeSeconds={includeSeconds}
124
123
  />
125
124
  </>
@@ -68,7 +68,6 @@ export const GroupedSelectField = ({
68
68
  selections={selected}
69
69
  className="without_select2"
70
70
  onClear={onClear}
71
- menuAppendTo={() => document.body}
72
71
  aria-labelledby={fieldId}
73
72
  toggleAriaLabel={`${label} toggle`}
74
73
  {...props}
@@ -43,7 +43,6 @@ export const SelectField = ({
43
43
  isOpen={isOpen}
44
44
  className="without_select2"
45
45
  maxHeight="45vh"
46
- menuAppendTo={() => document.body}
47
46
  placeholderText=" " // To prevent showing first option as selected
48
47
  aria-labelledby={fieldId}
49
48
  toggleAriaLabel={`${label} toggle`}
@@ -12,6 +12,8 @@ export const submit = ({
12
12
  location,
13
13
  organization,
14
14
  feature,
15
+ provider,
16
+ advancedInputs,
15
17
  dispatch,
16
18
  }) => {
17
19
  const {
@@ -37,6 +39,13 @@ export const submit = ({
37
39
  keyPassphrase,
38
40
  timeToPickup,
39
41
  } = advancedValues;
42
+ const providerInputs = advancedInputs.filter(v => v.provider_input);
43
+ const providerValues = {};
44
+ providerInputs.forEach(({ name }) => {
45
+ providerValues[name] = advancedTemplateValues[name];
46
+ delete advancedTemplateValues[name];
47
+ });
48
+
40
49
  const getCronLine = () => {
41
50
  const [hour, minute] = repeatData.at
42
51
  ? repeatData.at.split(':')
@@ -104,14 +113,16 @@ export const submit = ({
104
113
  concurrency_level: concurrencyLevel,
105
114
  },
106
115
  bookmark_id: null,
107
- search_query:
108
- buildHostQuery(selectedTargets, hostsSearchQuery) || 'name ~ *',
116
+ search_query: buildHostQuery(selectedTargets, hostsSearchQuery),
109
117
  description_format: description,
110
118
  execution_timeout_interval: timeoutToKill,
111
119
  feature,
112
120
  time_to_pickup: timeToPickup,
113
121
  },
114
122
  };
123
+ if (Object.keys(providerValues).length) {
124
+ api.job_invocation[provider] = providerValues;
125
+ }
115
126
 
116
127
  dispatch(
117
128
  post({