foreman_remote_execution 9.0.1 → 9.1.0

Sign up to get free protection for your applications and to get access to all the features.
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 && (