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.
- checksums.yaml +4 -4
- data/app/controllers/api/v2/job_invocations_controller.rb +9 -0
- data/app/controllers/ui_job_wizard_controller.rb +16 -4
- data/app/graphql/mutations/job_invocations/create.rb +43 -0
- data/app/graphql/types/job_invocation_input.rb +13 -0
- data/app/graphql/types/recurrence_input.rb +8 -0
- data/app/graphql/types/scheduling_input.rb +6 -0
- data/app/graphql/types/targeting_enum.rb +7 -0
- data/app/lib/actions/remote_execution/run_host_job.rb +4 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
- data/app/models/job_invocation_composer.rb +1 -1
- data/app/models/targeting.rb +2 -2
- data/app/views/job_invocations/refresh.js.erb +1 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
- data/lib/foreman_remote_execution/engine.rb +110 -6
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +6 -6
- data/test/functional/api/v2/job_invocations_controller_test.rb +10 -0
- data/test/functional/cockpit_controller_test.rb +0 -1
- data/test/graphql/mutations/job_invocations/create.rb +58 -0
- data/test/helpers/remote_execution_helper_test.rb +0 -1
- data/test/unit/actions/run_host_job_test.rb +21 -0
- data/test/unit/concerns/host_extensions_test.rb +36 -3
- data/test/unit/job_invocation_composer_test.rb +3 -5
- data/test/unit/job_invocation_report_template_test.rb +1 -1
- data/test/unit/job_template_effective_user_test.rb +0 -4
- data/test/unit/remote_execution_provider_test.rb +0 -4
- data/test/unit/targeting_test.rb +68 -1
- data/webpack/JobWizard/JobWizard.js +94 -13
- data/webpack/JobWizard/JobWizard.scss +59 -35
- data/webpack/JobWizard/JobWizardConstants.js +28 -1
- data/webpack/JobWizard/JobWizardSelectors.js +32 -0
- data/webpack/JobWizard/__tests__/fixtures.js +81 -6
- data/webpack/JobWizard/__tests__/integration.test.js +26 -15
- data/webpack/JobWizard/__tests__/validation.test.js +141 -0
- data/webpack/JobWizard/autofill.js +38 -0
- data/webpack/JobWizard/index.js +7 -0
- data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +7 -4
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +216 -12
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +1 -0
- data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
- data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
- data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +82 -7
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +7 -4
- data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
- data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
- data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +182 -34
- data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
- data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
- data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
- data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
- data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
- data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
- data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
- data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
- data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
- data/webpack/JobWizard/steps/Schedule/StartEndDates.js +59 -19
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +258 -11
- data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +11 -2
- data/webpack/JobWizard/steps/Schedule/index.js +97 -21
- data/webpack/JobWizard/steps/form/DateTimePicker.js +41 -8
- data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
- data/webpack/JobWizard/steps/form/Formatter.js +39 -8
- data/webpack/JobWizard/steps/form/NumberInput.js +3 -2
- data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
- data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
- data/webpack/JobWizard/steps/form/SelectField.js +14 -3
- data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
- data/webpack/JobWizard/submit.js +120 -0
- data/webpack/JobWizard/validation.js +53 -0
- data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
- data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
- data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
- data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
- data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
- data/webpack/helpers.js +1 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +1 -1
- metadata +38 -6
- data/app/models/setting/remote_execution.rb +0 -94
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +0 -37
- 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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
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
|
-
|
|
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
|
|
110
|
-
const
|
|
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
|
|
145
|
-
const
|
|
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
|
-
|
|
9
|
-
|
|
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 {
|
|
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 {
|
|
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={
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 = ({
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
};
|