foreman_remote_execution 4.8.0 → 5.0.0

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