foreman_remote_execution 4.8.0 → 5.0.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +9 -0
  3. data/app/controllers/ui_job_wizard_controller.rb +16 -4
  4. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  5. data/app/graphql/types/job_invocation_input.rb +13 -0
  6. data/app/graphql/types/recurrence_input.rb +8 -0
  7. data/app/graphql/types/scheduling_input.rb +6 -0
  8. data/app/graphql/types/targeting_enum.rb +7 -0
  9. data/app/lib/actions/remote_execution/run_host_job.rb +4 -0
  10. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
  11. data/app/models/job_invocation_composer.rb +1 -1
  12. data/app/models/targeting.rb +2 -2
  13. data/app/views/job_invocations/refresh.js.erb +1 -0
  14. data/config/routes.rb +1 -0
  15. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  16. data/lib/foreman_remote_execution/engine.rb +110 -6
  17. data/lib/foreman_remote_execution/version.rb +1 -1
  18. data/package.json +6 -6
  19. data/test/functional/api/v2/job_invocations_controller_test.rb +10 -0
  20. data/test/functional/cockpit_controller_test.rb +0 -1
  21. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  22. data/test/helpers/remote_execution_helper_test.rb +0 -1
  23. data/test/unit/actions/run_host_job_test.rb +21 -0
  24. data/test/unit/concerns/host_extensions_test.rb +36 -3
  25. data/test/unit/job_invocation_composer_test.rb +3 -5
  26. data/test/unit/job_invocation_report_template_test.rb +1 -1
  27. data/test/unit/job_template_effective_user_test.rb +0 -4
  28. data/test/unit/remote_execution_provider_test.rb +0 -4
  29. data/test/unit/targeting_test.rb +68 -1
  30. data/webpack/JobWizard/JobWizard.js +94 -13
  31. data/webpack/JobWizard/JobWizard.scss +59 -35
  32. data/webpack/JobWizard/JobWizardConstants.js +28 -1
  33. data/webpack/JobWizard/JobWizardSelectors.js +32 -0
  34. data/webpack/JobWizard/__tests__/fixtures.js +81 -6
  35. data/webpack/JobWizard/__tests__/integration.test.js +26 -15
  36. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  37. data/webpack/JobWizard/autofill.js +38 -0
  38. data/webpack/JobWizard/index.js +7 -0
  39. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +7 -4
  40. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
  41. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +216 -12
  42. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
  43. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +1 -0
  44. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  45. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  46. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  47. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  48. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  49. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +82 -7
  50. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  51. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +7 -4
  52. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  53. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  54. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  55. data/webpack/JobWizard/steps/HostsAndInputs/index.js +182 -34
  56. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  57. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  58. data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
  59. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  60. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  61. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  62. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  63. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
  64. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  65. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +59 -19
  66. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +258 -11
  67. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +11 -2
  68. data/webpack/JobWizard/steps/Schedule/index.js +97 -21
  69. data/webpack/JobWizard/steps/form/DateTimePicker.js +41 -8
  70. data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
  71. data/webpack/JobWizard/steps/form/Formatter.js +39 -8
  72. data/webpack/JobWizard/steps/form/NumberInput.js +3 -2
  73. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  74. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  75. data/webpack/JobWizard/steps/form/SelectField.js +14 -3
  76. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  77. data/webpack/JobWizard/submit.js +120 -0
  78. data/webpack/JobWizard/validation.js +53 -0
  79. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  80. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  81. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  82. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  83. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  84. data/webpack/helpers.js +1 -0
  85. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +1 -1
  86. metadata +38 -6
  87. data/app/models/setting/remote_execution.rb +0 -94
  88. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
  89. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +0 -37
  90. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  import React from 'react';
2
3
  import { Provider } from 'react-redux';
3
4
  import configureMockStore from 'redux-mock-store';
@@ -59,12 +60,17 @@ describe('Schedule', () => {
59
60
  const newStartTime = '12:03';
60
61
  const newEndsDate = '2030/03/12';
61
62
  const newEndsTime = '17:34';
62
- const [startsDateField, endsDateField] = screen.getAllByPlaceholderText(
63
- 'yyyy/mm/dd'
64
- );
65
- const [startsTimeField, endsTimeField] = screen.getAllByPlaceholderText(
66
- 'hh:mm'
67
- );
63
+ const startsDateField = screen.getByLabelText('starts at datepicker');
64
+ const endsDateField = screen.getByLabelText('ends datepicker');
65
+
66
+ const startsTimeField = screen.getByLabelText('starts at timepicker');
67
+ const endsTimeField = screen.getByLabelText('ends timepicker');
68
+
69
+ const staticQuery = screen.getByLabelText('Static query');
70
+ const dynamicQuery = screen.getByLabelText('Dynamic query');
71
+ const purpose = screen.getByLabelText('purpose');
72
+ const newPurposeLabel = 'some fun text';
73
+ expect(staticQuery.checked).toBeTruthy();
68
74
  await act(async () => {
69
75
  await fireEvent.change(startsDateField, {
70
76
  target: { value: newStartDate },
@@ -72,9 +78,14 @@ describe('Schedule', () => {
72
78
  await fireEvent.change(startsTimeField, {
73
79
  target: { value: newStartTime },
74
80
  });
81
+ await fireEvent.change(purpose, {
82
+ target: { value: newPurposeLabel },
83
+ });
75
84
  await fireEvent.change(endsDateField, { target: { value: newEndsDate } });
76
85
  await fireEvent.change(endsTimeField, { target: { value: newEndsTime } });
77
- jest.runAllTimers(); // to handle pf4 date picket popover useTimer
86
+
87
+ await fireEvent.click(dynamicQuery);
88
+ jest.runAllTimers(); // to handle pf4 date picker popover useTimer
78
89
  });
79
90
  await act(async () => {
80
91
  fireEvent.click(screen.getByText('Category and Template'));
@@ -89,6 +100,8 @@ describe('Schedule', () => {
89
100
  expect(startsTimeField.value).toBe(newStartTime);
90
101
  expect(endsDateField.value).toBe(newEndsDate);
91
102
  expect(endsTimeField.value).toBe(newEndsTime);
103
+ expect(dynamicQuery.checked).toBeTruthy();
104
+ expect(purpose.value).toBe(newPurposeLabel);
92
105
  });
93
106
  it('should remove start date time on execute now', async () => {
94
107
  render(
@@ -106,8 +119,8 @@ describe('Schedule', () => {
106
119
  expect(executeNow.checked).toBeTruthy();
107
120
  const newStartDate = '2020/03/12';
108
121
  const newStartTime = '12:03';
109
- const [startsDateField] = screen.getAllByPlaceholderText('yyyy/mm/dd');
110
- const [startsTimeField] = screen.getAllByPlaceholderText('hh:mm');
122
+ const startsDateField = screen.getByLabelText('starts at datepicker');
123
+ const startsTimeField = screen.getByLabelText('starts at timepicker');
111
124
  await act(async () => {
112
125
  await fireEvent.change(startsDateField, {
113
126
  target: { value: newStartDate },
@@ -122,6 +135,7 @@ describe('Schedule', () => {
122
135
  expect(executeFuture.checked).toBeTruthy();
123
136
  await act(async () => {
124
137
  await fireEvent.click(executeNow);
138
+ jest.runAllTimers();
125
139
  });
126
140
  expect(executeNow.checked).toBeTruthy();
127
141
  expect(startsDateField.value).toBe('');
@@ -141,8 +155,14 @@ describe('Schedule', () => {
141
155
  const neverEnds = screen.getByLabelText('Never ends');
142
156
  expect(neverEnds.checked).toBeFalsy();
143
157
 
144
- const [, endsDateField] = screen.getAllByPlaceholderText('yyyy/mm/dd');
145
- const [, endsTimeField] = screen.getAllByPlaceholderText('hh:mm');
158
+ const endsDateField = screen.getByLabelText('ends datepicker');
159
+ const endsTimeField = screen.getByLabelText('ends timepicker');
160
+ fireEvent.click(
161
+ screen.getByLabelText('Does not repeat', { selector: 'button' })
162
+ );
163
+ await act(async () => {
164
+ fireEvent.click(screen.getByText('Cronline'));
165
+ });
146
166
  expect(endsDateField.disabled).toBeFalsy();
147
167
  expect(endsTimeField.disabled).toBeFalsy();
148
168
  await act(async () => {
@@ -152,4 +172,231 @@ describe('Schedule', () => {
152
172
  expect(endsDateField.disabled).toBeTruthy();
153
173
  expect(endsTimeField.disabled).toBeTruthy();
154
174
  });
175
+
176
+ it('should change between repeat on states', async () => {
177
+ render(
178
+ <Provider store={store}>
179
+ <JobWizard />
180
+ </Provider>
181
+ );
182
+ await act(async () => {
183
+ fireEvent.click(screen.getByText('Schedule'));
184
+ jest.runAllTimers(); // to handle pf4 date picker popover useTimer
185
+ });
186
+ expect(
187
+ screen.getByPlaceholderText('Repeat N times').hasAttribute('disabled')
188
+ ).toBeTruthy();
189
+ expect(screen.getByText('Review Details').disabled).toBeFalsy();
190
+ await act(async () => {
191
+ fireEvent.click(
192
+ screen.getByLabelText('Does not repeat', { selector: 'button' })
193
+ );
194
+ });
195
+
196
+ await act(async () => {
197
+ fireEvent.click(screen.getByText('Cronline'));
198
+ });
199
+ expect(screen.getByText('Review Details').disabled).toBeTruthy();
200
+ const newRepeatTimes = '3';
201
+ const repeatNTimes = screen.getByPlaceholderText('Repeat N times');
202
+ expect(repeatNTimes.value).toBe('');
203
+ await act(async () => {
204
+ fireEvent.change(repeatNTimes, {
205
+ target: { value: newRepeatTimes },
206
+ });
207
+ });
208
+ expect(repeatNTimes.value).toBe(newRepeatTimes);
209
+
210
+ const newCronline = '1 2';
211
+ const cronline = screen.getByLabelText('cronline');
212
+ expect(cronline.value).toBe('');
213
+ await act(async () => {
214
+ fireEvent.change(cronline, {
215
+ target: { value: newCronline },
216
+ });
217
+ });
218
+ expect(cronline.value).toBe(newCronline);
219
+ expect(screen.getByText('Review Details').disabled).toBeFalsy();
220
+
221
+ await act(async () => {
222
+ fireEvent.click(screen.getByText('Category and Template'));
223
+ });
224
+ expect(screen.getAllByText('Category and Template')).toHaveLength(3);
225
+
226
+ await act(async () => {
227
+ fireEvent.click(screen.getByText('Schedule'));
228
+ jest.runAllTimers();
229
+ });
230
+ expect(screen.queryAllByText('Schedule')).toHaveLength(3);
231
+ expect(repeatNTimes.value).toBe(newRepeatTimes);
232
+ expect(cronline.value).toBe(newCronline);
233
+
234
+ fireEvent.click(screen.getByText('Cronline'));
235
+ await act(async () => {
236
+ fireEvent.click(screen.getByText('Monthly'));
237
+ });
238
+
239
+ expect(screen.getByText('Review Details').disabled).toBeTruthy();
240
+ const newDays = '1,2,3';
241
+ const days = screen.getByLabelText('days');
242
+ expect(days.value).toBe('');
243
+ await act(async () => {
244
+ fireEvent.change(days, {
245
+ target: { value: newDays },
246
+ });
247
+ fireEvent.click(repeatNTimes);
248
+ });
249
+ expect(days.value).toBe(newDays);
250
+
251
+ expect(screen.getByText('Review Details').disabled).toBeTruthy();
252
+ const newAtMonthly = '13:07';
253
+ const at = () => screen.getByLabelText('repeat-at');
254
+ expect(at().value).toBe('');
255
+ await act(async () => {
256
+ fireEvent.change(at(), {
257
+ target: { value: newAtMonthly },
258
+ });
259
+ });
260
+ expect(at().value).toBe(newAtMonthly);
261
+
262
+ expect(screen.getByText('Review Details').disabled).toBeFalsy();
263
+ fireEvent.click(screen.getByText('Monthly'));
264
+ await act(async () => {
265
+ fireEvent.click(screen.getByText('Weekly'));
266
+ });
267
+
268
+ expect(screen.getByText('Review Details').disabled).toBeTruthy();
269
+ const dayTue = screen.getByLabelText('Tue checkbox');
270
+ const daySat = screen.getByLabelText('Sat checkbox');
271
+ expect(dayTue.checked).toBe(false);
272
+ expect(daySat.checked).toBe(false);
273
+ await act(async () => {
274
+ fireEvent.click(dayTue);
275
+ fireEvent.change(dayTue, {
276
+ target: { checked: true },
277
+ });
278
+ });
279
+ await act(async () => {
280
+ fireEvent.click(daySat);
281
+ fireEvent.change(daySat, {
282
+ target: { checked: true },
283
+ });
284
+ });
285
+ expect(dayTue.checked).toBe(true);
286
+ expect(daySat.checked).toBe(true);
287
+ const newAtWeekly = '17:53';
288
+ expect(at().value).toBe(newAtMonthly);
289
+ await act(async () => {
290
+ fireEvent.change(at(), {
291
+ target: { value: newAtWeekly },
292
+ });
293
+ });
294
+ expect(at().value).toBe(newAtWeekly);
295
+
296
+ expect(screen.getByText('Review Details').disabled).toBeFalsy();
297
+ fireEvent.click(screen.getByText('Weekly'));
298
+ await act(async () => {
299
+ fireEvent.click(screen.getByText('Daily'));
300
+ });
301
+
302
+ expect(screen.getByText('Review Details').disabled).toBeFalsy();
303
+ await act(async () => {
304
+ fireEvent.change(at(), {
305
+ target: { value: '' },
306
+ });
307
+ });
308
+ expect(screen.getByText('Review Details').disabled).toBeTruthy();
309
+ const newAtDaily = '17:07';
310
+ expect(at().value).toBe('');
311
+ await act(async () => {
312
+ fireEvent.change(at(), {
313
+ target: { value: newAtDaily },
314
+ });
315
+ });
316
+ expect(at().value).toBe(newAtDaily);
317
+ expect(screen.getByText('Review Details').disabled).toBeFalsy();
318
+
319
+ fireEvent.click(screen.getByText('Daily'));
320
+ await act(async () => {
321
+ fireEvent.click(screen.getByText('Hourly'));
322
+ });
323
+
324
+ expect(screen.getByText('Review Details').disabled).toBeTruthy();
325
+ const newMinutes = '6';
326
+ const atHourly = screen.getByLabelText('repeat-at-minute-typeahead');
327
+ expect(atHourly.value).toBe('');
328
+ await act(async () => {
329
+ fireEvent.click(screen.getByLabelText('select minute toggle'));
330
+ });
331
+ await act(async () => {
332
+ fireEvent.click(screen.getByText(newMinutes));
333
+ });
334
+ expect(screen.getByText('Review Details').disabled).toBeFalsy();
335
+ expect(atHourly.value).toBe(newMinutes);
336
+ });
337
+ it('should show invalid error on start date after end', async () => {
338
+ render(
339
+ <Provider store={store}>
340
+ <JobWizard />
341
+ </Provider>
342
+ );
343
+ await act(async () => {
344
+ await fireEvent.click(screen.getByText('Schedule'));
345
+ jest.runAllTimers();
346
+ });
347
+ const neverEnds = screen.getByLabelText('Never ends');
348
+ expect(neverEnds.checked).toBeFalsy();
349
+
350
+ const startsDateField = screen.getByLabelText('starts at datepicker');
351
+ const endsDateField = screen.getByLabelText('ends datepicker');
352
+
353
+ expect(
354
+ screen.queryAllByText('End time needs to be after start time')
355
+ ).toHaveLength(0);
356
+ expect(screen.getByText('Review Details').disabled).toBeFalsy();
357
+ await act(async () => {
358
+ await fireEvent.change(startsDateField, {
359
+ target: { value: '2020/10/15' },
360
+ });
361
+ await fireEvent.change(endsDateField, {
362
+ target: { value: '2020/10/14' },
363
+ });
364
+ await jest.runOnlyPendingTimers();
365
+ });
366
+
367
+ expect(
368
+ screen.queryAllByText('End time needs to be after start time')
369
+ ).toHaveLength(1);
370
+
371
+ expect(screen.getByText('Review Details').disabled).toBeTruthy();
372
+ });
373
+ it('purpose and ends should be disabled when no reaccurence ', async () => {
374
+ render(
375
+ <Provider store={store}>
376
+ <JobWizard />
377
+ </Provider>
378
+ );
379
+ await act(async () => {
380
+ await fireEvent.click(screen.getByText('Schedule'));
381
+ jest.runAllTimers();
382
+ });
383
+
384
+ const endsDateField = screen.getByLabelText('ends datepicker');
385
+ const endsTimeField = screen.getByLabelText('ends timepicker');
386
+ const purpose = screen.getByLabelText('purpose');
387
+ expect(endsDateField.disabled).toBeTruthy();
388
+ expect(endsTimeField.disabled).toBeTruthy();
389
+ expect(purpose.disabled).toBeTruthy();
390
+ await act(async () => {
391
+ fireEvent.click(
392
+ screen.getByLabelText('Does not repeat', { selector: 'button' })
393
+ );
394
+ });
395
+ await act(async () => {
396
+ fireEvent.click(screen.getByText('Cronline'));
397
+ });
398
+ expect(endsDateField.disabled).toBeFalsy();
399
+ expect(endsTimeField.disabled).toBeFalsy();
400
+ expect(purpose.disabled).toBeFalsy();
401
+ });
155
402
  });
@@ -4,13 +4,21 @@ import { StartEndDates } from '../StartEndDates';
4
4
 
5
5
  const setEnds = jest.fn();
6
6
  const setIsNeverEnds = jest.fn();
7
+ const setValid = jest.fn();
7
8
  const props = {
8
- starts: '',
9
- setStarts: jest.fn(),
9
+ startsAt: '',
10
+ startsBefore: '',
11
+ setStartsAt: jest.fn(),
12
+ setStartsBefore: jest.fn(),
10
13
  ends: 'some-end-date',
11
14
  setEnds,
12
15
  setIsNeverEnds,
13
16
  isNeverEnds: false,
17
+ validEnd: true,
18
+ setValidEnd: setValid,
19
+ isFuture: false,
20
+ isStartBeforeDisabled: false,
21
+ isEndDisabled: false,
14
22
  };
15
23
 
16
24
  describe('StartEndDates', () => {
@@ -19,5 +27,6 @@ describe('StartEndDates', () => {
19
27
  const neverEnds = screen.getByRole('checkbox', { name: 'Never ends' });
20
28
  await act(async () => fireEvent.click(neverEnds));
21
29
  expect(setIsNeverEnds).toBeCalledWith(true);
30
+ expect(setValid).toBeCalledWith(true);
22
31
  });
23
32
  });
@@ -1,44 +1,81 @@
1
- import React from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { Button, Form } from '@patternfly/react-core';
4
- import { translate as __ } from 'foremanReact/common/I18n';
3
+ import { Form } from '@patternfly/react-core';
5
4
  import { ScheduleType } from './ScheduleType';
6
5
  import { RepeatOn } from './RepeatOn';
7
6
  import { QueryType } from './QueryType';
8
7
  import { StartEndDates } from './StartEndDates';
9
- import { WIZARD_TITLES } from '../../JobWizardConstants';
8
+ import { WIZARD_TITLES, repeatTypes } from '../../JobWizardConstants';
9
+ import { PurposeField } from './PurposeField';
10
10
  import { WizardTitle } from '../form/WizardTitle';
11
11
 
12
- const Schedule = ({ scheduleValue, setScheduleValue }) => {
13
- const { repeatType, repeatAmount, starts, ends, isNeverEnds } = scheduleValue;
12
+ const Schedule = ({ scheduleValue, setScheduleValue, setValid }) => {
13
+ const {
14
+ repeatType,
15
+ repeatAmount,
16
+ repeatData,
17
+ startsAt,
18
+ startsBefore,
19
+ ends,
20
+ isNeverEnds,
21
+ isFuture,
22
+ isTypeStatic,
23
+ purpose,
24
+ } = scheduleValue;
25
+ const [validEnd, setValidEnd] = useState(true);
26
+ const [repeatValid, setRepeatValid] = useState(true);
14
27
 
28
+ useEffect(() => {
29
+ if (!validEnd || !repeatValid) {
30
+ setValid(false);
31
+ } else if (isFuture && (startsAt.length || startsBefore.length)) {
32
+ setValid(true);
33
+ } else if (!isFuture) {
34
+ setValid(true);
35
+ } else {
36
+ setValid(false);
37
+ }
38
+ // eslint-disable-next-line react-hooks/exhaustive-deps
39
+ }, [startsAt, startsBefore, isFuture, validEnd, repeatValid]);
15
40
  return (
16
41
  <>
17
42
  <WizardTitle title={WIZARD_TITLES.schedule} />
18
43
  <Form className="schedule-tab">
19
44
  <ScheduleType
20
- isFuture={scheduleValue.isFuture}
45
+ isFuture={isFuture}
21
46
  setIsFuture={newValue => {
22
47
  if (!newValue) {
23
48
  // if schedule type is execute now
24
49
  setScheduleValue(current => ({
25
50
  ...current,
26
- starts: '',
51
+ startsAt: '',
52
+ startsBefore: '',
53
+ isFuture: newValue,
54
+ }));
55
+ } else {
56
+ setScheduleValue(current => ({
57
+ ...current,
58
+ startsAt: new Date().toISOString(),
59
+ isFuture: newValue,
27
60
  }));
28
61
  }
29
- setScheduleValue(current => ({
30
- ...current,
31
- isFuture: newValue,
32
- }));
33
62
  }}
34
63
  />
35
64
 
36
65
  <RepeatOn
37
66
  repeatType={repeatType}
67
+ repeatData={repeatData}
38
68
  setRepeatType={newValue => {
39
69
  setScheduleValue(current => ({
40
70
  ...current,
41
71
  repeatType: newValue,
72
+ startsBefore: '',
73
+ }));
74
+ }}
75
+ setRepeatData={newValue => {
76
+ setScheduleValue(current => ({
77
+ ...current,
78
+ repeatData: newValue,
42
79
  }));
43
80
  }}
44
81
  repeatAmount={repeatAmount}
@@ -48,11 +85,12 @@ const Schedule = ({ scheduleValue, setScheduleValue }) => {
48
85
  repeatAmount: newValue,
49
86
  }));
50
87
  }}
88
+ setValid={setRepeatValid}
51
89
  />
52
90
  <StartEndDates
53
- starts={starts}
54
- setStarts={newValue => {
55
- if (!scheduleValue.isFuture) {
91
+ startsAt={startsAt}
92
+ setStartsAt={newValue => {
93
+ if (!isFuture) {
56
94
  setScheduleValue(current => ({
57
95
  ...current,
58
96
  isFuture: true,
@@ -60,7 +98,20 @@ const Schedule = ({ scheduleValue, setScheduleValue }) => {
60
98
  }
61
99
  setScheduleValue(current => ({
62
100
  ...current,
63
- starts: newValue,
101
+ startsAt: newValue,
102
+ }));
103
+ }}
104
+ startsBefore={startsBefore}
105
+ setStartsBefore={newValue => {
106
+ if (!isFuture) {
107
+ setScheduleValue(current => ({
108
+ ...current,
109
+ isFuture: true,
110
+ }));
111
+ }
112
+ setScheduleValue(current => ({
113
+ ...current,
114
+ startsBefore: newValue,
64
115
  }));
65
116
  }}
66
117
  ends={ends}
@@ -77,11 +128,31 @@ const Schedule = ({ scheduleValue, setScheduleValue }) => {
77
128
  isNeverEnds: newValue,
78
129
  }));
79
130
  }}
131
+ validEnd={validEnd}
132
+ setValidEnd={setValidEnd}
133
+ isFuture={isFuture}
134
+ isStartBeforeDisabled={repeatType !== repeatTypes.noRepeat}
135
+ isEndDisabled={repeatType === repeatTypes.noRepeat}
136
+ />
137
+ <QueryType
138
+ isTypeStatic={isTypeStatic}
139
+ setIsTypeStatic={newValue => {
140
+ setScheduleValue(current => ({
141
+ ...current,
142
+ isTypeStatic: newValue,
143
+ }));
144
+ }}
145
+ />
146
+ <PurposeField
147
+ isDisabled={repeatType === repeatTypes.noRepeat}
148
+ purpose={purpose}
149
+ setPurpose={newValue => {
150
+ setScheduleValue(current => ({
151
+ ...current,
152
+ purpose: newValue,
153
+ }));
154
+ }}
80
155
  />
81
- <Button variant="link" className="advanced-scheduling-button" isInline>
82
- {__('Advanced scheduling')}
83
- </Button>
84
- <QueryType />
85
156
  </Form>
86
157
  </>
87
158
  );
@@ -91,12 +162,17 @@ Schedule.propTypes = {
91
162
  scheduleValue: PropTypes.shape({
92
163
  repeatType: PropTypes.string.isRequired,
93
164
  repeatAmount: PropTypes.string,
94
- starts: PropTypes.string,
165
+ repeatData: PropTypes.object,
166
+ startsAt: PropTypes.string,
167
+ startsBefore: PropTypes.string,
95
168
  ends: PropTypes.string,
96
169
  isFuture: PropTypes.bool,
97
170
  isNeverEnds: PropTypes.bool,
171
+ isTypeStatic: PropTypes.bool,
172
+ purpose: PropTypes.string,
98
173
  }).isRequired,
99
174
  setScheduleValue: PropTypes.func.isRequired,
175
+ setValid: PropTypes.func.isRequired,
100
176
  };
101
177
 
102
178
  export default Schedule;
@@ -1,10 +1,21 @@
1
- import React from 'react';
1
+ import React, { useState } from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { DatePicker, TimePicker } from '@patternfly/react-core';
3
+ import {
4
+ DatePicker,
5
+ TimePicker,
6
+ ValidatedOptions,
7
+ } from '@patternfly/react-core';
4
8
  import { debounce } from 'lodash';
5
- import { translate as __ } from 'foremanReact/common/I18n';
9
+ import { translate as __, documentLocale } from 'foremanReact/common/I18n';
6
10
 
7
- export const DateTimePicker = ({ dateTime, setDateTime, isDisabled }) => {
11
+ export const DateTimePicker = ({
12
+ dateTime,
13
+ setDateTime,
14
+ isDisabled,
15
+ ariaLabel,
16
+ allowEmpty,
17
+ }) => {
18
+ const [validated, setValidated] = useState();
8
19
  const dateFormat = date =>
9
20
  `${date.getFullYear()}/${(date.getMonth() + 1)
10
21
  .toString()
@@ -36,23 +47,36 @@ export const DateTimePicker = ({ dateTime, setDateTime, isDisabled }) => {
36
47
 
37
48
  const onDateChange = newDate => {
38
49
  const parsedNewDate = new Date(newDate);
39
-
40
- if (isValidDate(parsedNewDate)) {
50
+ if (!newDate.length && allowEmpty) {
51
+ setDateTime('');
52
+ setValidated(ValidatedOptions.noval);
53
+ } else if (isValidDate(parsedNewDate)) {
41
54
  parsedNewDate.setHours(dateObject.getHours());
42
55
  parsedNewDate.setMinutes(dateObject.getMinutes());
43
56
  setDateTime(parsedNewDate.toString());
57
+ setValidated(ValidatedOptions.noval);
58
+ } else {
59
+ setValidated(ValidatedOptions.error);
44
60
  }
45
61
  };
46
62
 
47
63
  const onTimeChange = newTime => {
48
- if (isValidTime(newTime)) {
64
+ if (!newTime.length && allowEmpty) {
65
+ const parsedNewTime = new Date(`${formattedDate} 00:00`);
66
+ setDateTime(parsedNewTime.toString());
67
+ setValidated(ValidatedOptions.noval);
68
+ } else if (isValidTime(newTime)) {
49
69
  const parsedNewTime = new Date(`${formattedDate} ${newTime}`);
50
70
  setDateTime(parsedNewTime.toString());
71
+ setValidated(ValidatedOptions.noval);
72
+ } else {
73
+ setValidated(ValidatedOptions.error);
51
74
  }
52
75
  };
53
76
  return (
54
77
  <>
55
78
  <DatePicker
79
+ aria-label={`${ariaLabel} datepicker`}
56
80
  value={formattedDate}
57
81
  placeholder="yyyy/mm/dd"
58
82
  onChange={debounce(onDateChange, 1000, {
@@ -62,9 +86,14 @@ export const DateTimePicker = ({ dateTime, setDateTime, isDisabled }) => {
62
86
  dateFormat={dateFormat}
63
87
  dateParse={dateParse}
64
88
  isDisabled={isDisabled}
65
- invalidFormatText={__('Invalid date')}
89
+ locale={documentLocale()}
90
+ invalidFormatText={
91
+ validated === ValidatedOptions.error ? __('Invalid date') : ''
92
+ }
93
+ inputProps={{ validated }}
66
94
  />
67
95
  <TimePicker
96
+ aria-label={`${ariaLabel} timepicker`}
68
97
  className="time-picker"
69
98
  time={dateTime ? dateObject.toString() : ''}
70
99
  inputProps={dateTime ? {} : { value: '' }}
@@ -86,8 +115,12 @@ DateTimePicker.propTypes = {
86
115
  dateTime: PropTypes.string,
87
116
  setDateTime: PropTypes.func.isRequired,
88
117
  isDisabled: PropTypes.bool,
118
+ ariaLabel: PropTypes.string,
119
+ allowEmpty: PropTypes.bool,
89
120
  };
90
121
  DateTimePicker.defaultProps = {
91
122
  dateTime: null,
92
123
  isDisabled: false,
124
+ ariaLabel: '',
125
+ allowEmpty: true,
93
126
  };
@@ -18,3 +18,7 @@ export const helpLabel = (text, id) => {
18
18
  </Popover>
19
19
  );
20
20
  };
21
+
22
+ export const isPositiveNumber = text => parseInt(text, 10) > 0;
23
+
24
+ export const isValidDate = d => d instanceof Date && !Number.isNaN(d);