foreman_remote_execution 8.2.0 → 8.3.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 (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({