foreman_remote_execution 9.0.1 → 9.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 (57) 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 +10 -0
  4. data/app/controllers/ui_job_wizard_controller.rb +6 -1
  5. data/app/helpers/remote_execution_helper.rb +1 -1
  6. data/app/lib/actions/remote_execution/run_hosts_job.rb +28 -2
  7. data/app/models/remote_execution_feature.rb +11 -8
  8. data/app/views/api/v2/job_invocations/base.json.rabl +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/db/migrate/20220426145007_add_unique_feature_label_index.rb +14 -0
  14. data/lib/foreman_remote_execution/engine.rb +1 -1
  15. data/lib/foreman_remote_execution/version.rb +1 -1
  16. data/locale/action_names.rb +1 -1
  17. data/locale/de/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  18. data/locale/de/foreman_remote_execution.po +15 -12
  19. data/locale/en/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  20. data/locale/en/foreman_remote_execution.po +4 -1
  21. data/locale/en_GB/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  22. data/locale/en_GB/foreman_remote_execution.po +6 -3
  23. data/locale/es/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  24. data/locale/es/foreman_remote_execution.po +16 -13
  25. data/locale/foreman_remote_execution.pot +25 -17
  26. data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  27. data/locale/fr/foreman_remote_execution.po +51 -48
  28. data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  29. data/locale/ja/foreman_remote_execution.po +17 -14
  30. data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  31. data/locale/ko/foreman_remote_execution.po +12 -9
  32. data/locale/pt_BR/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  33. data/locale/pt_BR/foreman_remote_execution.po +13 -10
  34. data/locale/ru/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  35. data/locale/ru/foreman_remote_execution.po +12 -9
  36. data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  37. data/locale/zh_CN/foreman_remote_execution.po +16 -13
  38. data/locale/zh_TW/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  39. data/locale/zh_TW/foreman_remote_execution.po +12 -9
  40. data/package.json +6 -6
  41. data/webpack/JobWizard/JobWizard.js +97 -32
  42. data/webpack/JobWizard/StartsBeforeErrorAlert.js +17 -0
  43. data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +8 -0
  44. data/webpack/JobWizard/__tests__/fixtures.js +5 -0
  45. data/webpack/JobWizard/__tests__/integration.test.js +15 -0
  46. data/webpack/JobWizard/__tests__/validation.test.js +27 -0
  47. data/webpack/JobWizard/autofill.js +1 -0
  48. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +29 -10
  49. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +8 -0
  50. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +3 -0
  51. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +38 -1
  52. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +16 -10
  53. data/webpack/JobWizard/steps/HostsAndInputs/index.js +51 -3
  54. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +33 -13
  55. data/webpack/JobWizard/steps/form/DateTimePicker.js +1 -1
  56. data/webpack/JobWizard/submit.js +14 -3
  57. metadata +4 -2
@@ -36,6 +36,7 @@ import { useValidation } from './validation';
36
36
  import { useAutoFill } from './autofill';
37
37
  import { submit } from './submit';
38
38
  import { generateDefaultDescription } from './JobWizardHelpers';
39
+ import { StartsBeforeErrorAlert } from './StartsBeforeErrorAlert';
39
40
  import './JobWizard.scss';
40
41
 
41
42
  export const JobWizard = ({ rerunData }) => {
@@ -185,6 +186,24 @@ export const JobWizard = ({ rerunData }) => {
185
186
  // eslint-disable-next-line react-hooks/exhaustive-deps
186
187
  }, [rerunData, jobTemplateID, dispatch]);
187
188
 
189
+ const [isStartsBeforeError, setIsStartsBeforeError] = useState(false);
190
+ useEffect(() => {
191
+ const updateStartsBeforeError = () => {
192
+ setIsStartsBeforeError(
193
+ scheduleValue.scheduleType === SCHEDULE_TYPES.FUTURE &&
194
+ new Date().getTime() >= new Date(scheduleValue.startsBefore).getTime()
195
+ );
196
+ };
197
+ let interval;
198
+ if (scheduleValue.scheduleType === SCHEDULE_TYPES.FUTURE) {
199
+ updateStartsBeforeError();
200
+ interval = setInterval(updateStartsBeforeError, 5000);
201
+ }
202
+ return () => {
203
+ interval && clearInterval(interval);
204
+ };
205
+ }, [scheduleValue.scheduleType, scheduleValue.startsBefore]);
206
+
188
207
  const [valid, setValid] = useValidation({
189
208
  advancedValues,
190
209
  templateValues,
@@ -209,6 +228,11 @@ export const JobWizard = ({ rerunData }) => {
209
228
  !templateError &&
210
229
  !!jobTemplateID &&
211
230
  templateResponse.job_template;
231
+ const areHostsSelected =
232
+ selectedTargets.hosts.length > 0 ||
233
+ selectedTargets.hostCollections.length > 0 ||
234
+ selectedTargets.hostGroups.length > 0 ||
235
+ hostsSearchQuery.length > 0;
212
236
  const steps = [
213
237
  {
214
238
  name: WIZARD_TITLES.categoryAndTemplate,
@@ -238,7 +262,7 @@ export const JobWizard = ({ rerunData }) => {
238
262
  />
239
263
  ),
240
264
  canJumpTo: isTemplate,
241
- enableNext: isTemplate && valid.hostsAndInputs,
265
+ enableNext: isTemplate && valid.hostsAndInputs && areHostsSelected,
242
266
  },
243
267
  {
244
268
  name: WIZARD_TITLES.advanced,
@@ -254,14 +278,26 @@ export const JobWizard = ({ rerunData }) => {
254
278
  templateValues={templateValues}
255
279
  />
256
280
  ),
257
- canJumpTo: isTemplate && valid.hostsAndInputs,
258
- enableNext: isTemplate && valid.hostsAndInputs && valid.advanced,
281
+ canJumpTo: isTemplate && valid.hostsAndInputs && areHostsSelected,
282
+ enableNext:
283
+ isTemplate &&
284
+ valid.hostsAndInputs &&
285
+ areHostsSelected &&
286
+ valid.advanced,
259
287
  },
260
288
  {
261
289
  name: WIZARD_TITLES.schedule,
262
- canJumpTo: isTemplate && valid.hostsAndInputs && valid.advanced,
290
+ canJumpTo:
291
+ isTemplate &&
292
+ valid.hostsAndInputs &&
293
+ areHostsSelected &&
294
+ valid.advanced,
263
295
  enableNext:
264
- isTemplate && valid.hostsAndInputs && valid.advanced && valid.schedule,
296
+ isTemplate &&
297
+ valid.hostsAndInputs &&
298
+ areHostsSelected &&
299
+ valid.advanced &&
300
+ valid.schedule,
265
301
  steps: [
266
302
  {
267
303
  name: WIZARD_TITLES.typeOfExecution,
@@ -278,9 +314,17 @@ export const JobWizard = ({ rerunData }) => {
278
314
  }}
279
315
  />
280
316
  ),
281
- canJumpTo: isTemplate && valid.hostsAndInputs && valid.advanced,
317
+ canJumpTo:
318
+ isTemplate &&
319
+ valid.hostsAndInputs &&
320
+ areHostsSelected &&
321
+ valid.advanced,
282
322
 
283
- enableNext: isTemplate && valid.hostsAndInputs && valid.advanced,
323
+ enableNext:
324
+ isTemplate &&
325
+ valid.hostsAndInputs &&
326
+ areHostsSelected &&
327
+ valid.advanced,
284
328
  },
285
329
  ...(scheduleValue.scheduleType === SCHEDULE_TYPES.FUTURE
286
330
  ? [
@@ -298,10 +342,15 @@ export const JobWizard = ({ rerunData }) => {
298
342
  }}
299
343
  />
300
344
  ),
301
- canJumpTo: isTemplate && valid.hostsAndInputs && valid.advanced,
345
+ canJumpTo:
346
+ isTemplate &&
347
+ valid.hostsAndInputs &&
348
+ areHostsSelected &&
349
+ valid.advanced,
302
350
  enableNext:
303
351
  isTemplate &&
304
352
  valid.hostsAndInputs &&
353
+ areHostsSelected &&
305
354
  valid.advanced &&
306
355
  valid.schedule,
307
356
  },
@@ -323,10 +372,15 @@ export const JobWizard = ({ rerunData }) => {
323
372
  }}
324
373
  />
325
374
  ),
326
- canJumpTo: isTemplate && valid.hostsAndInputs && valid.advanced,
375
+ canJumpTo:
376
+ isTemplate &&
377
+ valid.hostsAndInputs &&
378
+ areHostsSelected &&
379
+ valid.advanced,
327
380
  enableNext:
328
381
  isTemplate &&
329
382
  valid.hostsAndInputs &&
383
+ areHostsSelected &&
330
384
  valid.advanced &&
331
385
  valid.schedule,
332
386
  },
@@ -349,39 +403,50 @@ export const JobWizard = ({ rerunData }) => {
349
403
  ),
350
404
  nextButtonText: 'Run',
351
405
  canJumpTo:
352
- isTemplate && valid.hostsAndInputs && valid.advanced && valid.schedule,
406
+ isTemplate &&
407
+ valid.advanced &&
408
+ valid.hostsAndInputs &&
409
+ areHostsSelected &&
410
+ valid.schedule,
353
411
  enableNext:
354
412
  isTemplate &&
355
413
  valid.hostsAndInputs &&
414
+ areHostsSelected &&
356
415
  valid.advanced &&
357
416
  valid.schedule &&
358
- !isSubmitting,
417
+ !isSubmitting &&
418
+ !isStartsBeforeError,
359
419
  },
360
420
  ];
361
421
  const location = useForemanLocation();
362
422
  const organization = useForemanOrganization();
363
423
  return (
364
- <Wizard
365
- onClose={() => history.goBack()}
366
- navAriaLabel="Run Job steps"
367
- steps={steps}
368
- height="100%"
369
- className="job-wizard"
370
- onSave={() => {
371
- submit({
372
- jobTemplateID,
373
- templateValues,
374
- advancedValues,
375
- scheduleValue,
376
- dispatch,
377
- selectedTargets,
378
- hostsSearchQuery,
379
- location,
380
- organization,
381
- feature: routerSearch?.feature,
382
- });
383
- }}
384
- />
424
+ <>
425
+ {isStartsBeforeError && <StartsBeforeErrorAlert />}
426
+ <Wizard
427
+ onClose={() => history.goBack()}
428
+ navAriaLabel="Run Job steps"
429
+ steps={steps}
430
+ height="100%"
431
+ className="job-wizard"
432
+ onSave={() => {
433
+ submit({
434
+ jobTemplateID,
435
+ templateValues,
436
+ advancedValues,
437
+ scheduleValue,
438
+ dispatch,
439
+ selectedTargets,
440
+ hostsSearchQuery,
441
+ location,
442
+ organization,
443
+ feature: routerSearch?.feature,
444
+ provider: templateResponse.provider_name,
445
+ advancedInputs: templateResponse.advanced_template_inputs,
446
+ });
447
+ }}
448
+ />
449
+ </>
385
450
  );
386
451
  };
387
452
 
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import { Divider, Alert } from '@patternfly/react-core';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+
5
+ export const StartsBeforeErrorAlert = () => (
6
+ <>
7
+ <Alert
8
+ variant="danger"
9
+ title={__("'Starts before' date must in the future")}
10
+ >
11
+ {__(
12
+ 'Please go back to "Schedule" - "Future execution" step to fix the error'
13
+ )}
14
+ </Alert>
15
+ <Divider component="div" />
16
+ </>
17
+ );
@@ -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}>
@@ -51,7 +60,7 @@ describe('AdvancedFields', () => {
51
60
  .simulate('click'); // Advanced step
52
61
 
53
62
  await act(async () => {
54
- jest.runAllTimers(); // to handle pf4 date picker popover
63
+ jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
55
64
  });
56
65
  const effectiveUserInput = () => wrapper.find('input#effective-user');
57
66
  const advancedTemplateInput = () =>
@@ -83,7 +92,7 @@ describe('AdvancedFields', () => {
83
92
  .simulate('click'); // Advanced step
84
93
 
85
94
  await act(async () => {
86
- jest.runOnlyPendingTimers(); // to handle pf4 date picker popover
95
+ jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
87
96
  });
88
97
  expect(effectiveUserInput().prop('value')).toEqual(effectiveUesrValue);
89
98
  expect(advancedTemplateInput().prop('value')).toEqual(
@@ -100,7 +109,7 @@ describe('AdvancedFields', () => {
100
109
  );
101
110
  await act(async () => {
102
111
  fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
103
- jest.runOnlyPendingTimers(); // to handle pf4 date picker popover
112
+ jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
104
113
  });
105
114
 
106
115
  const searchValue = 'search test';
@@ -137,7 +146,7 @@ describe('AdvancedFields', () => {
137
146
  fireEvent.change(timeField, {
138
147
  target: { value: timeValue },
139
148
  });
140
- jest.runAllTimers(); // to handle pf4 date picker popover
149
+ jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
141
150
  });
142
151
  expect(
143
152
  screen.getByLabelText('adv plain hidden', {
@@ -156,7 +165,7 @@ describe('AdvancedFields', () => {
156
165
 
157
166
  await act(async () => {
158
167
  await fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
159
- jest.runOnlyPendingTimers();
168
+ jest.advanceTimersByTime(1000);
160
169
  });
161
170
  expect(textField.value).toBe(textValue);
162
171
  expect(searchField.value).toBe(searchValue);
@@ -177,7 +186,7 @@ describe('AdvancedFields', () => {
177
186
  );
178
187
  await act(async () => {
179
188
  fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
180
- jest.runAllTimers(); // to handle pf4 date picker popover
189
+ jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
181
190
  });
182
191
 
183
192
  expect(
@@ -212,7 +221,7 @@ describe('AdvancedFields', () => {
212
221
  );
213
222
  await act(async () => {
214
223
  fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
215
- jest.runAllTimers(); // to handle pf4 date picker popover
224
+ jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
216
225
  });
217
226
 
218
227
  const textField = screen.getByLabelText('adv plain hidden', {
@@ -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
  });
@@ -280,7 +294,7 @@ describe('AdvancedFields', () => {
280
294
  );
281
295
  await act(async () => {
282
296
  fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
283
- jest.runAllTimers(); // to handle pf4 date picker popover
297
+ jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
284
298
  });
285
299
  expect(
286
300
  screen.getByLabelText('description preview', {
@@ -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
  });
@@ -349,7 +368,7 @@ describe('AdvancedFields', () => {
349
368
  );
350
369
  await act(async () => {
351
370
  fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
352
- jest.runAllTimers(); // to handle pf4 date picker popover
371
+ jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
353
372
  });
354
373
  expect(
355
374
  screen.getByLabelText('description preview', {
@@ -369,7 +388,7 @@ describe('AdvancedFields', () => {
369
388
  );
370
389
  await act(async () => {
371
390
  fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
372
- jest.runAllTimers(); // to handle pf4 date picker popover
391
+ jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
373
392
  });
374
393
  const resourceSelectField = screen.getByLabelText(
375
394
  'adv resource select typeahead input'
@@ -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'));
@@ -107,6 +128,13 @@ describe('Hosts', () => {
107
128
  expect(screen.queryAllByText('Host groups')).toHaveLength(1);
108
129
  expect(screen.queryAllByText('Search query')).toHaveLength(1);
109
130
  expect(screen.queryAllByText('Host collections')).toHaveLength(0);
131
+
132
+ await act(async () => {
133
+ fireEvent.click(
134
+ // Close the select
135
+ screen.getByText('Hosts', { selector: '.pf-c-select__toggle-text' })
136
+ );
137
+ });
110
138
  });
111
139
  it('Host fill list from url', async () => {
112
140
  routerSelectors.selectRouterLocation.mockImplementation(() => ({
@@ -151,8 +179,9 @@ describe('Hosts', () => {
151
179
 
152
180
  it('input fill from url', async () => {
153
181
  const inputText = 'test text';
182
+ const advancedInputText = 'test adv text';
154
183
  routerSelectors.selectRouterLocation.mockImplementation(() => ({
155
- search: `feature=test_feature&inputs[plain hidden]=${inputText}`,
184
+ search: `host_ids%5B%5D=host1&host_ids%5B%5D=host3&feature=test_feature&inputs[plain hidden]=${inputText}&inputs[adv plain hidden]=${advancedInputText}`,
156
185
  }));
157
186
  render(
158
187
  <MockedProvider mocks={gqlMock} addTypename={false}>
@@ -175,5 +204,13 @@ describe('Hosts', () => {
175
204
  selector: 'textarea',
176
205
  });
177
206
  expect(textField.value).toBe(inputText);
207
+
208
+ await act(async () => {
209
+ fireEvent.click(screen.getByText('Advanced fields'));
210
+ });
211
+ const advancedTextField = screen.getByLabelText('adv plain hidden', {
212
+ selector: 'textarea',
213
+ });
214
+ expect(advancedTextField.value).toBe(advancedInputText);
178
215
  });
179
216
  });
@@ -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
  };
@@ -51,6 +51,30 @@ const HostsAndInputs = ({
51
51
  const isLoading = useSelector(selectIsLoadingHosts);
52
52
  const templateInputs = useSelector(selectTemplateInputs);
53
53
  const [hostPreviewOpen, setHostPreviewOpen] = useState(false);
54
+ const [wasFocus, setWasFocus] = useState(false);
55
+ const [isError, setIsError] = useState(false);
56
+ useEffect(() => {
57
+ if (wasFocus) {
58
+ if (
59
+ selected.hosts.length === 0 &&
60
+ selected.hostCollections.length === 0 &&
61
+ selected.hostGroups.length === 0 &&
62
+ hostsSearchQuery.length === 0
63
+ ) {
64
+ setIsError(true);
65
+ } else {
66
+ setIsError(false);
67
+ }
68
+ }
69
+ }, [
70
+ hostMethod,
71
+ hostsSearchQuery.length,
72
+ selected,
73
+ selected.hostCollections.length,
74
+ selected.hostGroups.length,
75
+ selected.hosts.length,
76
+ wasFocus,
77
+ ]);
54
78
  useEffect(() => {
55
79
  debounce(() => {
56
80
  dispatch(
@@ -99,6 +123,9 @@ const HostsAndInputs = ({
99
123
  const clearSearch = () => {
100
124
  setHostsSearchQuery('');
101
125
  };
126
+ const [errorText, setErrorText] = useState(
127
+ __('Please select at least one host')
128
+ );
102
129
  return (
103
130
  <div className="target-hosts-and-inputs">
104
131
  <WizardTitle title={WIZARD_TITLES.hostsAndInputs} />
@@ -110,8 +137,13 @@ const HostsAndInputs = ({
110
137
  />
111
138
  )}
112
139
  <Form>
113
- <FormGroup fieldId="host_selection" id="host-selection">
114
- <InputGroup>
140
+ <FormGroup
141
+ fieldId="host_selection"
142
+ id="host-selection"
143
+ helperTextInvalid={errorText}
144
+ validated={isError ? 'error' : 'default'}
145
+ >
146
+ <InputGroup onBlur={() => setWasFocus(true)}>
115
147
  <SelectField
116
148
  isRequired
117
149
  className="target-method-select"
@@ -123,7 +155,23 @@ const HostsAndInputs = ({
123
155
  }
124
156
  return true;
125
157
  })}
126
- setValue={setHostMethod}
158
+ setValue={val => {
159
+ setHostMethod(val);
160
+ if (val === hostMethods.searchQuery) {
161
+ setErrorText(__('Please enter a search query'));
162
+ }
163
+ if (val === hostMethods.hosts) {
164
+ setErrorText(__('Please select at least one host'));
165
+ }
166
+ if (val === hostMethods.hostCollections) {
167
+ setErrorText(
168
+ __('Please select at least one host collection')
169
+ );
170
+ }
171
+ if (val === hostMethods.hostGroups) {
172
+ setErrorText(__('Please select at least one host group'));
173
+ }
174
+ }}
127
175
  value={hostMethod}
128
176
  />
129
177
  {hostMethod === hostMethods.searchQuery && (