foreman_remote_execution 4.7.0 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +1 -0
  3. data/Gemfile +1 -1
  4. data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
  5. data/app/controllers/ui_job_wizard_controller.rb +16 -4
  6. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  7. data/app/graphql/types/job_invocation_input.rb +13 -0
  8. data/app/graphql/types/recurrence_input.rb +8 -0
  9. data/app/graphql/types/scheduling_input.rb +6 -0
  10. data/app/graphql/types/targeting_enum.rb +7 -0
  11. data/app/lib/actions/remote_execution/run_host_job.rb +8 -1
  12. data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
  13. data/app/lib/foreman_remote_execution/renderer/scope/input.rb +1 -1
  14. data/app/mailers/rex_job_mailer.rb +15 -0
  15. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
  16. data/app/models/job_invocation.rb +6 -0
  17. data/app/models/job_invocation_composer.rb +21 -13
  18. data/app/models/job_template.rb +3 -1
  19. data/app/models/remote_execution_provider.rb +18 -2
  20. data/app/models/rex_mail_notification.rb +13 -0
  21. data/app/models/targeting.rb +2 -2
  22. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  23. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  24. data/app/views/job_invocations/refresh.js.erb +1 -0
  25. data/app/views/job_templates/_custom_tabs.html.erb +4 -9
  26. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  27. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  28. data/app/views/template_invocations/show.html.erb +9 -2
  29. data/config/routes.rb +1 -0
  30. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  31. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  32. data/db/seeds.d/95-mail_notifications.rb +24 -0
  33. data/foreman_remote_execution.gemspec +1 -1
  34. data/lib/foreman_remote_execution/engine.rb +111 -6
  35. data/lib/foreman_remote_execution/version.rb +1 -1
  36. data/package.json +9 -7
  37. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  38. data/test/functional/cockpit_controller_test.rb +0 -1
  39. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  40. data/test/helpers/remote_execution_helper_test.rb +0 -1
  41. data/test/unit/actions/run_host_job_test.rb +21 -0
  42. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  43. data/test/unit/concerns/host_extensions_test.rb +36 -3
  44. data/test/unit/job_invocation_composer_test.rb +3 -5
  45. data/test/unit/job_invocation_report_template_test.rb +17 -14
  46. data/test/unit/job_template_effective_user_test.rb +0 -4
  47. data/test/unit/remote_execution_provider_test.rb +46 -4
  48. data/test/unit/targeting_test.rb +69 -2
  49. data/webpack/JobWizard/JobWizard.js +142 -28
  50. data/webpack/JobWizard/JobWizard.scss +86 -33
  51. data/webpack/JobWizard/JobWizardConstants.js +44 -0
  52. data/webpack/JobWizard/JobWizardSelectors.js +32 -0
  53. data/webpack/JobWizard/__tests__/fixtures.js +89 -6
  54. data/webpack/JobWizard/__tests__/integration.test.js +29 -22
  55. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  56. data/webpack/JobWizard/autofill.js +38 -0
  57. data/webpack/JobWizard/index.js +7 -0
  58. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +23 -9
  59. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
  60. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
  61. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +242 -23
  62. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
  63. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
  64. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
  65. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  66. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  67. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  68. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  69. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  70. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +100 -0
  71. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  72. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  73. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
  74. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  75. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  76. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  77. data/webpack/JobWizard/steps/HostsAndInputs/index.js +214 -0
  78. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  79. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  80. data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
  81. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  82. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  83. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  84. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  85. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
  86. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  87. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
  88. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +78 -23
  89. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  90. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +20 -10
  91. data/webpack/JobWizard/steps/Schedule/index.js +166 -29
  92. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  93. data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
  94. data/webpack/JobWizard/steps/form/Formatter.js +49 -17
  95. data/webpack/JobWizard/steps/form/NumberInput.js +5 -2
  96. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  97. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  98. data/webpack/JobWizard/steps/form/SelectField.js +14 -3
  99. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  100. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  101. data/webpack/JobWizard/submit.js +120 -0
  102. data/webpack/JobWizard/validation.js +53 -0
  103. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  104. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  105. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  106. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  107. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  108. data/webpack/helpers.js +1 -0
  109. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +2 -1
  110. metadata +53 -7
  111. data/app/models/setting/remote_execution.rb +0 -88
  112. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
  113. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
@@ -0,0 +1,402 @@
1
+ /* eslint-disable max-lines */
2
+ import React from 'react';
3
+ import { Provider } from 'react-redux';
4
+ import configureMockStore from 'redux-mock-store';
5
+ import { fireEvent, screen, render, act } from '@testing-library/react';
6
+ import * as api from 'foremanReact/redux/API';
7
+ import { JobWizard } from '../../../JobWizard';
8
+ import * as selectors from '../../../JobWizardSelectors';
9
+ import { jobTemplate, jobTemplateResponse } from '../../../__tests__/fixtures';
10
+
11
+ const lodash = require('lodash');
12
+
13
+ lodash.debounce = fn => fn;
14
+ jest.spyOn(api, 'get');
15
+ jest.spyOn(selectors, 'selectJobTemplate');
16
+ jest.spyOn(selectors, 'selectJobTemplates');
17
+ jest.spyOn(selectors, 'selectJobCategories');
18
+
19
+ const jobCategories = ['Ansible Commands', 'Puppet', 'Services'];
20
+
21
+ selectors.selectJobCategories.mockImplementation(() => jobCategories);
22
+
23
+ selectors.selectJobTemplates.mockImplementation(() => [
24
+ jobTemplate,
25
+ { ...jobTemplate, id: 2, name: 'template2' },
26
+ ]);
27
+ selectors.selectJobTemplate.mockImplementation(() => jobTemplateResponse);
28
+ api.get.mockImplementation(({ handleSuccess, ...action }) => {
29
+ if (action.key === 'JOB_CATEGORIES') {
30
+ handleSuccess && handleSuccess({ data: { job_categories: jobCategories } });
31
+ } else if (action.key === 'JOB_TEMPLATE') {
32
+ handleSuccess &&
33
+ handleSuccess({
34
+ data: jobTemplateResponse,
35
+ });
36
+ } else if (action.key === 'JOB_TEMPLATES') {
37
+ handleSuccess &&
38
+ handleSuccess({
39
+ data: { results: [jobTemplateResponse.job_template] },
40
+ });
41
+ }
42
+ return { type: 'get', ...action };
43
+ });
44
+
45
+ const mockStore = configureMockStore([]);
46
+ const store = mockStore({});
47
+ jest.useFakeTimers();
48
+
49
+ describe('Schedule', () => {
50
+ it('should save date time between steps ', async () => {
51
+ render(
52
+ <Provider store={store}>
53
+ <JobWizard />
54
+ </Provider>
55
+ );
56
+ await act(async () => {
57
+ fireEvent.click(screen.getByText('Schedule'));
58
+ });
59
+ const newStartDate = '2020/03/12';
60
+ const newStartTime = '12:03';
61
+ const newEndsDate = '2030/03/12';
62
+ const newEndsTime = '17:34';
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();
74
+ await act(async () => {
75
+ await fireEvent.change(startsDateField, {
76
+ target: { value: newStartDate },
77
+ });
78
+ await fireEvent.change(startsTimeField, {
79
+ target: { value: newStartTime },
80
+ });
81
+ await fireEvent.change(purpose, {
82
+ target: { value: newPurposeLabel },
83
+ });
84
+ await fireEvent.change(endsDateField, { target: { value: newEndsDate } });
85
+ await fireEvent.change(endsTimeField, { target: { value: newEndsTime } });
86
+
87
+ await fireEvent.click(dynamicQuery);
88
+ jest.runAllTimers(); // to handle pf4 date picker popover useTimer
89
+ });
90
+ await act(async () => {
91
+ fireEvent.click(screen.getByText('Category and Template'));
92
+ });
93
+ expect(screen.getAllByText('Category and Template')).toHaveLength(3);
94
+
95
+ await act(async () => {
96
+ fireEvent.click(screen.getByText('Schedule'));
97
+ jest.runAllTimers();
98
+ });
99
+ expect(startsDateField.value).toBe(newStartDate);
100
+ expect(startsTimeField.value).toBe(newStartTime);
101
+ expect(endsDateField.value).toBe(newEndsDate);
102
+ expect(endsTimeField.value).toBe(newEndsTime);
103
+ expect(dynamicQuery.checked).toBeTruthy();
104
+ expect(purpose.value).toBe(newPurposeLabel);
105
+ });
106
+ it('should remove start date time on execute now', async () => {
107
+ render(
108
+ <Provider store={store}>
109
+ <JobWizard />
110
+ </Provider>
111
+ );
112
+ await act(async () => {
113
+ fireEvent.click(screen.getByText('Schedule'));
114
+ });
115
+ const executeNow = screen.getByLabelText('Execute now');
116
+ const executeFuture = screen.getByLabelText(
117
+ 'Schedule for future execution'
118
+ );
119
+ expect(executeNow.checked).toBeTruthy();
120
+ const newStartDate = '2020/03/12';
121
+ const newStartTime = '12:03';
122
+ const startsDateField = screen.getByLabelText('starts at datepicker');
123
+ const startsTimeField = screen.getByLabelText('starts at timepicker');
124
+ await act(async () => {
125
+ await fireEvent.change(startsDateField, {
126
+ target: { value: newStartDate },
127
+ });
128
+ await fireEvent.change(startsTimeField, {
129
+ target: { value: newStartTime },
130
+ });
131
+ await jest.runOnlyPendingTimers();
132
+ });
133
+ expect(startsDateField.value).toBe(newStartDate);
134
+ expect(startsTimeField.value).toBe(newStartTime);
135
+ expect(executeFuture.checked).toBeTruthy();
136
+ await act(async () => {
137
+ await fireEvent.click(executeNow);
138
+ jest.runAllTimers();
139
+ });
140
+ expect(executeNow.checked).toBeTruthy();
141
+ expect(startsDateField.value).toBe('');
142
+ expect(startsTimeField.value).toBe('');
143
+ });
144
+
145
+ it('should disable end date on never ends', async () => {
146
+ render(
147
+ <Provider store={store}>
148
+ <JobWizard />
149
+ </Provider>
150
+ );
151
+ await act(async () => {
152
+ await fireEvent.click(screen.getByText('Schedule'));
153
+ jest.runAllTimers();
154
+ });
155
+ const neverEnds = screen.getByLabelText('Never ends');
156
+ expect(neverEnds.checked).toBeFalsy();
157
+
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
+ });
166
+ expect(endsDateField.disabled).toBeFalsy();
167
+ expect(endsTimeField.disabled).toBeFalsy();
168
+ await act(async () => {
169
+ fireEvent.click(neverEnds);
170
+ });
171
+ expect(neverEnds.checked).toBeTruthy();
172
+ expect(endsDateField.disabled).toBeTruthy();
173
+ expect(endsTimeField.disabled).toBeTruthy();
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
+ });
402
+ });
@@ -1,22 +1,32 @@
1
1
  import React from 'react';
2
- import { render, fireEvent, screen } from '@testing-library/react';
2
+ import { render, fireEvent, screen, act } from '@testing-library/react';
3
3
  import { StartEndDates } from '../StartEndDates';
4
4
 
5
5
  const setEnds = jest.fn();
6
+ const setIsNeverEnds = jest.fn();
7
+ const setValid = jest.fn();
6
8
  const props = {
7
- starts: '',
8
- setStarts: jest.fn(),
9
+ startsAt: '',
10
+ startsBefore: '',
11
+ setStartsAt: jest.fn(),
12
+ setStartsBefore: jest.fn(),
9
13
  ends: 'some-end-date',
10
14
  setEnds,
15
+ setIsNeverEnds,
16
+ isNeverEnds: false,
17
+ validEnd: true,
18
+ setValidEnd: setValid,
19
+ isFuture: false,
20
+ isStartBeforeDisabled: false,
21
+ isEndDisabled: false,
11
22
  };
12
23
 
13
24
  describe('StartEndDates', () => {
14
- it('never ends', () => {
15
- render(<StartEndDates {...props} />);
16
- const neverEnds = screen.getByLabelText('Never ends', {
17
- selector: 'input',
18
- });
19
- fireEvent.click(neverEnds);
20
- expect(setEnds).toBeCalledWith('');
25
+ it('never ends', async () => {
26
+ await act(async () => render(<StartEndDates {...props} />));
27
+ const neverEnds = screen.getByRole('checkbox', { name: 'Never ends' });
28
+ await act(async () => fireEvent.click(neverEnds));
29
+ expect(setIsNeverEnds).toBeCalledWith(true);
30
+ expect(setValid).toBeCalledWith(true);
21
31
  });
22
32
  });
@@ -1,41 +1,178 @@
1
- import React, { useState } from 'react';
2
- import { Title, Button, Form } from '@patternfly/react-core';
3
- import { translate as __ } from 'foremanReact/common/I18n';
1
+ import React, { useEffect, useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Form } from '@patternfly/react-core';
4
4
  import { ScheduleType } from './ScheduleType';
5
5
  import { RepeatOn } from './RepeatOn';
6
6
  import { QueryType } from './QueryType';
7
7
  import { StartEndDates } from './StartEndDates';
8
- import { repeatTypes } from '../../JobWizardConstants';
8
+ import { WIZARD_TITLES, repeatTypes } from '../../JobWizardConstants';
9
+ import { PurposeField } from './PurposeField';
10
+ import { WizardTitle } from '../form/WizardTitle';
9
11
 
10
- const Schedule = () => {
11
- const [repeatType, setRepeatType] = useState(repeatTypes.noRepeat);
12
- const [repeatAmount, setRepeatAmount] = useState('');
13
- const [starts, setStarts] = useState('');
14
- const [ends, setEnds] = useState('');
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);
15
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]);
16
40
  return (
17
- <Form className="schedule-tab">
18
- <Title headingLevel="h2">{__('Schedule')}</Title>
19
- <ScheduleType />
41
+ <>
42
+ <WizardTitle title={WIZARD_TITLES.schedule} />
43
+ <Form className="schedule-tab">
44
+ <ScheduleType
45
+ isFuture={isFuture}
46
+ setIsFuture={newValue => {
47
+ if (!newValue) {
48
+ // if schedule type is execute now
49
+ setScheduleValue(current => ({
50
+ ...current,
51
+ startsAt: '',
52
+ startsBefore: '',
53
+ isFuture: newValue,
54
+ }));
55
+ } else {
56
+ setScheduleValue(current => ({
57
+ ...current,
58
+ startsAt: new Date().toISOString(),
59
+ isFuture: newValue,
60
+ }));
61
+ }
62
+ }}
63
+ />
20
64
 
21
- <RepeatOn
22
- repeatType={repeatType}
23
- setRepeatType={setRepeatType}
24
- repeatAmount={repeatAmount}
25
- setRepeatAmount={setRepeatAmount}
26
- />
27
- <StartEndDates
28
- starts={starts}
29
- setStarts={setStarts}
30
- ends={ends}
31
- setEnds={setEnds}
32
- />
33
- <Button variant="link" className="advanced-scheduling-button" isInline>
34
- {__('Advanced scheduling')}
35
- </Button>
36
- <QueryType />
37
- </Form>
65
+ <RepeatOn
66
+ repeatType={repeatType}
67
+ repeatData={repeatData}
68
+ setRepeatType={newValue => {
69
+ setScheduleValue(current => ({
70
+ ...current,
71
+ repeatType: newValue,
72
+ startsBefore: '',
73
+ }));
74
+ }}
75
+ setRepeatData={newValue => {
76
+ setScheduleValue(current => ({
77
+ ...current,
78
+ repeatData: newValue,
79
+ }));
80
+ }}
81
+ repeatAmount={repeatAmount}
82
+ setRepeatAmount={newValue => {
83
+ setScheduleValue(current => ({
84
+ ...current,
85
+ repeatAmount: newValue,
86
+ }));
87
+ }}
88
+ setValid={setRepeatValid}
89
+ />
90
+ <StartEndDates
91
+ startsAt={startsAt}
92
+ setStartsAt={newValue => {
93
+ if (!isFuture) {
94
+ setScheduleValue(current => ({
95
+ ...current,
96
+ isFuture: true,
97
+ }));
98
+ }
99
+ setScheduleValue(current => ({
100
+ ...current,
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,
115
+ }));
116
+ }}
117
+ ends={ends}
118
+ setEnds={newValue => {
119
+ setScheduleValue(current => ({
120
+ ...current,
121
+ ends: newValue,
122
+ }));
123
+ }}
124
+ isNeverEnds={isNeverEnds}
125
+ setIsNeverEnds={newValue => {
126
+ setScheduleValue(current => ({
127
+ ...current,
128
+ isNeverEnds: newValue,
129
+ }));
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
+ }}
155
+ />
156
+ </Form>
157
+ </>
38
158
  );
39
159
  };
40
160
 
161
+ Schedule.propTypes = {
162
+ scheduleValue: PropTypes.shape({
163
+ repeatType: PropTypes.string.isRequired,
164
+ repeatAmount: PropTypes.string,
165
+ repeatData: PropTypes.object,
166
+ startsAt: PropTypes.string,
167
+ startsBefore: PropTypes.string,
168
+ ends: PropTypes.string,
169
+ isFuture: PropTypes.bool,
170
+ isNeverEnds: PropTypes.bool,
171
+ isTypeStatic: PropTypes.bool,
172
+ purpose: PropTypes.string,
173
+ }).isRequired,
174
+ setScheduleValue: PropTypes.func.isRequired,
175
+ setValid: PropTypes.func.isRequired,
176
+ };
177
+
41
178
  export default Schedule;