foreman_remote_execution 8.1.2 → 8.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +1 -0
  3. data/app/controllers/ui_job_wizard_controller.rb +6 -1
  4. data/app/models/job_invocation.rb +3 -2
  5. data/app/models/template_invocation.rb +1 -0
  6. data/app/views/api/v2/job_invocations/base.json.rabl +1 -1
  7. data/app/views/job_invocations/show.html.erb +1 -1
  8. data/app/views/job_invocations/welcome.html.erb +1 -1
  9. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +1 -1
  10. data/lib/foreman_remote_execution/version.rb +1 -1
  11. data/locale/action_names.rb +2 -2
  12. data/locale/de/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  13. data/locale/de/foreman_remote_execution.po +266 -154
  14. data/locale/en/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  15. data/locale/en/foreman_remote_execution.po +132 -24
  16. data/locale/en_GB/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  17. data/locale/en_GB/foreman_remote_execution.po +149 -41
  18. data/locale/es/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  19. data/locale/es/foreman_remote_execution.po +320 -210
  20. data/locale/foreman_remote_execution.pot +394 -211
  21. data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  22. data/locale/fr/foreman_remote_execution.po +353 -241
  23. data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  24. data/locale/ja/foreman_remote_execution.po +368 -261
  25. data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  26. data/locale/ko/foreman_remote_execution.po +161 -53
  27. data/locale/pt_BR/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  28. data/locale/pt_BR/foreman_remote_execution.po +335 -225
  29. data/locale/ru/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  30. data/locale/ru/foreman_remote_execution.po +161 -53
  31. data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  32. data/locale/zh_CN/foreman_remote_execution.po +465 -359
  33. data/locale/zh_TW/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  34. data/locale/zh_TW/foreman_remote_execution.po +162 -54
  35. data/test/unit/job_invocation_test.rb +1 -0
  36. data/webpack/JobWizard/JobWizard.js +52 -10
  37. data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +8 -0
  38. data/webpack/JobWizard/__tests__/fixtures.js +5 -0
  39. data/webpack/JobWizard/__tests__/integration.test.js +15 -0
  40. data/webpack/JobWizard/__tests__/validation.test.js +27 -0
  41. data/webpack/JobWizard/autofill.js +1 -0
  42. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +19 -0
  43. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +8 -0
  44. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +3 -0
  45. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +31 -1
  46. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +16 -10
  47. data/webpack/JobWizard/steps/HostsAndInputs/index.js +51 -3
  48. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +21 -1
  49. data/webpack/JobWizard/submit.js +13 -2
  50. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.js +45 -0
  51. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +26 -0
  52. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +13 -1
  53. data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +5 -0
  54. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +3 -0
  55. data/webpack/react_app/components/TargetingHosts/index.js +23 -3
  56. data/webpack/react_app/components/jobInvocations/AggregateStatus/index.js +25 -9
  57. data/webpack/react_app/components/jobInvocations/AggregateStatus/index.test.js +6 -2
  58. data/webpack/react_app/components/jobInvocations/index.js +19 -2
  59. data/webpack/react_app/redux/actions/jobInvocations/index.js +8 -0
  60. data/webpack/react_app/redux/reducers/jobInvocations/index.js +2 -0
  61. metadata +4 -2
@@ -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
  />
@@ -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'));
@@ -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 && (
@@ -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}>
@@ -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({
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Row, Label } from 'patternfly-react';
4
+ import { noop } from 'foremanReact/common/helpers';
5
+ import { translate as __ } from 'foremanReact/common/I18n';
6
+
7
+ import './TargetingHostsLabelsRow.scss';
8
+
9
+ const TargetingHostsLabelsRow = ({ query, updateQuery }) => {
10
+ const onDeleteClick = keyToDelete => {
11
+ const { [keyToDelete]: deleted, ...queryWithoutDeleted } = query;
12
+ updateQuery(queryWithoutDeleted);
13
+ };
14
+
15
+ const queryEntries = Object.entries(query);
16
+
17
+ return (
18
+ queryEntries.length > 0 && (
19
+ <Row className="tasks-labels-row">
20
+ <span className="title">{__('Active Filters:')}</span>
21
+ {queryEntries.map(([key, value]) => (
22
+ <Label
23
+ bsStyle="info"
24
+ key={key}
25
+ onRemoveClick={() => onDeleteClick(key)}
26
+ >
27
+ {`${key} = ${value}`}
28
+ </Label>
29
+ ))}
30
+ </Row>
31
+ )
32
+ );
33
+ };
34
+
35
+ TargetingHostsLabelsRow.propTypes = {
36
+ query: PropTypes.object,
37
+ updateQuery: PropTypes.func,
38
+ };
39
+
40
+ TargetingHostsLabelsRow.defaultProps = {
41
+ query: {},
42
+ updateQuery: noop,
43
+ };
44
+
45
+ export default TargetingHostsLabelsRow;
@@ -0,0 +1,26 @@
1
+ @import '~@theforeman/vendor/scss/variables';
2
+
3
+ .tasks-labels-row {
4
+ margin: 0;
5
+ padding: 10px;
6
+ .title {
7
+ font-weight: 600;
8
+ font-size: 13px;
9
+ }
10
+ .label {
11
+ font-size: 100%;
12
+
13
+ margin-left: 5px;
14
+ margin-right: 5px;
15
+ a {
16
+ padding-left: 10px;
17
+ }
18
+ }
19
+ .pficon-close {
20
+ color: $color-pf-white;
21
+ }
22
+ .compound-label-pf {
23
+ margin-left: 0;
24
+ margin: 10px;
25
+ }
26
+ }