foreman_remote_execution 7.2.2 → 8.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_ci.yml +2 -2
  3. data/app/controllers/ui_job_wizard_controller.rb +15 -0
  4. data/app/helpers/remote_execution_helper.rb +1 -1
  5. data/app/models/job_invocation.rb +2 -4
  6. data/app/models/job_invocation_composer.rb +5 -2
  7. data/app/models/remote_execution_provider.rb +1 -1
  8. data/app/views/templates/script/package_action.erb +8 -3
  9. data/config/routes.rb +3 -1
  10. data/lib/foreman_remote_execution/engine.rb +5 -5
  11. data/lib/foreman_remote_execution/version.rb +1 -1
  12. data/test/functional/api/v2/job_invocations_controller_test.rb +8 -0
  13. data/test/helpers/remote_execution_helper_test.rb +4 -0
  14. data/test/unit/job_invocation_report_template_test.rb +1 -1
  15. data/test/unit/job_invocation_test.rb +1 -2
  16. data/test/unit/remote_execution_provider_test.rb +0 -22
  17. data/webpack/JobWizard/JobWizard.js +154 -20
  18. data/webpack/JobWizard/JobWizard.scss +43 -1
  19. data/webpack/JobWizard/JobWizardConstants.js +11 -1
  20. data/webpack/JobWizard/JobWizardPageRerun.js +112 -0
  21. data/webpack/JobWizard/__tests__/JobWizardPageRerun.test.js +79 -0
  22. data/webpack/JobWizard/__tests__/fixtures.js +73 -0
  23. data/webpack/JobWizard/__tests__/integration.test.js +17 -3
  24. data/webpack/JobWizard/autofill.js +8 -1
  25. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +36 -17
  26. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -3
  27. data/webpack/JobWizard/steps/ReviewDetails/index.js +1 -3
  28. data/webpack/JobWizard/steps/Schedule/PurposeField.js +1 -3
  29. data/webpack/JobWizard/steps/Schedule/QueryType.js +33 -40
  30. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +55 -16
  31. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +19 -56
  32. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +1 -1
  33. data/webpack/JobWizard/steps/Schedule/ScheduleFuture.js +126 -0
  34. data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +287 -0
  35. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +88 -20
  36. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +206 -186
  37. data/webpack/JobWizard/steps/form/DateTimePicker.js +23 -6
  38. data/webpack/JobWizard/steps/form/Formatter.js +7 -8
  39. data/webpack/JobWizard/submit.js +8 -3
  40. data/webpack/Routes/routes.js +8 -2
  41. data/webpack/__mocks__/foremanReact/common/hooks/API/APIHooks.js +1 -0
  42. data/webpack/react_app/components/HostKebab/KebabItems.js +0 -1
  43. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +0 -5
  44. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +59 -51
  45. data/webpack/react_app/extend/Fills.js +4 -4
  46. metadata +8 -6
  47. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +0 -106
  48. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +0 -32
  49. data/webpack/JobWizard/steps/Schedule/index.js +0 -178
@@ -5,50 +5,89 @@ import {
5
5
  Select,
6
6
  SelectOption,
7
7
  SelectVariant,
8
+ Alert,
9
+ AlertActionCloseButton,
10
+ ValidatedOptions,
8
11
  } from '@patternfly/react-core';
9
- import { range } from 'lodash';
10
12
  import { translate as __ } from 'foremanReact/common/I18n';
13
+ import { helpLabel } from '../form/FormHelpers';
14
+
15
+ export const RepeatHour = ({ repeatData, setRepeatData }) => {
16
+ const isValidMinute = newMinute =>
17
+ Number.isInteger(parseInt(newMinute, 10)) &&
18
+ newMinute >= 0 &&
19
+ newMinute < 60;
11
20
 
12
- export const RepeatHour = ({ repeatData, setRepeatData, setValid }) => {
13
21
  const { minute } = repeatData;
14
22
  useEffect(() => {
15
- if (minute) {
16
- setValid(true);
17
- } else {
18
- setValid(false);
23
+ if (!isValidMinute(minute)) {
24
+ setRepeatData({ minute: 0 });
19
25
  }
20
- return () => setValid(true);
21
- }, [setValid, minute]);
26
+ }, [minute, setRepeatData]);
22
27
  const [minuteOpen, setMinuteOpen] = useState(false);
28
+ const [options, setOptions] = useState([0, 15, 30, 45]);
29
+ const [isAlertOpen, setIsAlertOpen] = useState(false);
23
30
  return (
24
- <FormGroup label={__('At minute')} isRequired>
31
+ <FormGroup
32
+ label={__('At minute')}
33
+ labelIcon={helpLabel(<div>{__('range: 0-59')}</div>)}
34
+ isRequired
35
+ >
25
36
  <Select
26
37
  id="repeat-on-hourly"
27
38
  variant={SelectVariant.typeahead}
28
39
  typeAheadAriaLabel="repeat-at-minute-typeahead"
29
40
  onSelect={(event, selection) => {
30
- setRepeatData({ minute: selection });
41
+ setRepeatData({ minute: parseInt(selection, 10) });
31
42
  setMinuteOpen(false);
32
43
  }}
33
- selections={minute || ''}
44
+ selections={`${minute}` || ''}
34
45
  onToggle={toggle => {
35
46
  setMinuteOpen(toggle);
36
47
  }}
37
48
  isOpen={minuteOpen}
38
- width={85}
49
+ width={125}
39
50
  menuAppendTo={() => document.querySelector('.pf-c-form.schedule-tab')}
40
51
  toggleAriaLabel="select minute toggle"
41
- validated={minute ? 'success' : 'error'}
52
+ validated={
53
+ isValidMinute(minute)
54
+ ? ValidatedOptions.noval
55
+ : ValidatedOptions.error
56
+ }
57
+ onCreateOption={newValue => {
58
+ if (isValidMinute(newValue)) {
59
+ setOptions(prev => [...prev, parseInt(newValue, 10)].sort());
60
+ setRepeatData({ minute: parseInt(newValue, 10) });
61
+ setIsAlertOpen(false);
62
+ } else {
63
+ setIsAlertOpen(true);
64
+ }
65
+ }}
66
+ isCreatable
67
+ createText={__('Create')}
42
68
  >
43
- {range(60).map(minuteNumber => (
44
- <SelectOption key={minuteNumber} value={`${minuteNumber}`} />
69
+ {options.map(minuteNumber => (
70
+ <SelectOption
71
+ key={minuteNumber}
72
+ value={`${minuteNumber}`}
73
+ onClick={() => setIsAlertOpen(false)}
74
+ />
45
75
  ))}
46
76
  </Select>
77
+ {isAlertOpen && (
78
+ <Alert
79
+ variant="danger"
80
+ isInline
81
+ title={__('Minute can only be a number between 0-59')}
82
+ actionClose={
83
+ <AlertActionCloseButton onClose={() => setIsAlertOpen(false)} />
84
+ }
85
+ />
86
+ )}
47
87
  </FormGroup>
48
88
  );
49
89
  };
50
90
  RepeatHour.propTypes = {
51
91
  repeatData: PropTypes.object.isRequired,
52
92
  setRepeatData: PropTypes.func.isRequired,
53
- setValid: PropTypes.func.isRequired,
54
93
  };
@@ -1,6 +1,6 @@
1
- import React, { useState } from 'react';
1
+ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { TextInput, Grid, GridItem, FormGroup } from '@patternfly/react-core';
3
+ import { FormGroup } from '@patternfly/react-core';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
5
  import { SelectField } from '../form/SelectField';
6
6
  import { repeatTypes } from '../../JobWizardConstants';
@@ -13,18 +13,10 @@ import { RepeatWeek } from './RepeatWeek';
13
13
  export const RepeatOn = ({
14
14
  repeatType,
15
15
  setRepeatType,
16
- repeatAmount,
17
- setRepeatAmount,
18
16
  repeatData,
19
17
  setRepeatData,
20
18
  setValid,
21
19
  }) => {
22
- const [repeatValidated, setRepeatValidated] = useState('default');
23
- const handleRepeatInputChange = newValue => {
24
- setRepeatValidated(!newValue || newValue >= 1 ? 'default' : 'error');
25
- setRepeatAmount(newValue);
26
- };
27
-
28
20
  const getRepeatComponent = () => {
29
21
  switch (repeatType) {
30
22
  case repeatTypes.cronline:
@@ -61,64 +53,35 @@ export const RepeatOn = ({
61
53
  );
62
54
  case repeatTypes.hourly:
63
55
  return (
64
- <RepeatHour
65
- repeatData={repeatData}
66
- setRepeatData={setRepeatData}
67
- setValid={setValid}
68
- />
56
+ <RepeatHour repeatData={repeatData} setRepeatData={setRepeatData} />
69
57
  );
70
- case repeatTypes.noRepeat:
71
58
  default:
72
59
  return null;
73
60
  }
74
61
  };
75
62
  return (
76
- <FormGroup label={__('Repeat On')}>
77
- <Grid>
78
- <GridItem span={6}>
79
- <SelectField
80
- isRequired
81
- fieldId="repeat-select"
82
- options={Object.values(repeatTypes)}
83
- setValue={newValue => {
84
- setRepeatType(newValue);
85
- if (newValue === repeatTypes.noRepeat) {
86
- setRepeatValidated('default');
87
- }
88
- }}
89
- value={repeatType}
90
- />
91
- </GridItem>
92
- <GridItem span={1} />
93
- <GridItem span={5}>
94
- <FormGroup
95
- helperTextInvalid={__(
96
- 'Repeat amount can only be a positive number'
97
- )}
98
- validated={repeatValidated}
99
- >
100
- <TextInput
101
- isDisabled={repeatType === repeatTypes.noRepeat}
102
- id="repeat-amount"
103
- value={repeatAmount}
104
- type="text"
105
- onChange={newValue => handleRepeatInputChange(newValue)}
106
- placeholder={__('Repeat N times')}
107
- />
108
- </FormGroup>
109
- </GridItem>
110
- {getRepeatComponent()}
111
- </Grid>
112
- </FormGroup>
63
+ <>
64
+ <FormGroup label={__('Repeats')}>
65
+ <SelectField
66
+ isRequired
67
+ fieldId="repeat-select"
68
+ options={Object.values(repeatTypes).filter(
69
+ type => type !== repeatTypes.noRepeat
70
+ )}
71
+ setValue={newValue => {
72
+ setRepeatType(newValue);
73
+ }}
74
+ value={repeatType}
75
+ />
76
+ </FormGroup>
77
+ {getRepeatComponent()}
78
+ </>
113
79
  );
114
80
  };
115
81
 
116
82
  RepeatOn.propTypes = {
117
83
  repeatType: PropTypes.oneOf(Object.values(repeatTypes)).isRequired,
118
84
  setRepeatType: PropTypes.func.isRequired,
119
- repeatAmount: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
120
- .isRequired,
121
- setRepeatAmount: PropTypes.func.isRequired,
122
85
  repeatData: PropTypes.object.isRequired,
123
86
  setRepeatData: PropTypes.func.isRequired,
124
87
  setValid: PropTypes.func.isRequired,
@@ -7,7 +7,7 @@ import { noop } from '../../../helpers';
7
7
 
8
8
  const getWeekDays = () => {
9
9
  const locale = documentLocale().replace(/-/g, '_');
10
- const baseDate = new Date(Date.UTC(2017, 0, 2)); // just a Monday
10
+ const baseDate = new Date(Date.UTC(2017, 0, 1)); // just a Sunday
11
11
  const weekDays = [];
12
12
  for (let i = 0; i < 7; i++) {
13
13
  try {
@@ -0,0 +1,126 @@
1
+ import React, { useEffect, useState, useCallback } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ FormGroup,
5
+ Form,
6
+ Button,
7
+ ValidatedOptions,
8
+ } from '@patternfly/react-core';
9
+ import { translate as __ } from 'foremanReact/common/I18n';
10
+ import { DateTimePicker } from '../form/DateTimePicker';
11
+ import { helpLabel } from '../form/FormHelpers';
12
+ import { SCHEDULE_TYPES } from '../../JobWizardConstants';
13
+ import { WizardTitle } from '../form/WizardTitle';
14
+
15
+ export const ScheduleFuture = ({
16
+ scheduleValue: { startsAt, startsBefore },
17
+ setScheduleValue,
18
+ setValid,
19
+ }) => {
20
+ const [error, setError] = useState(null);
21
+
22
+ const wrappedSetValid = useCallback(setValid, []);
23
+ useEffect(() => {
24
+ if (!startsBefore?.length && !startsAt?.length) {
25
+ wrappedSetValid(false);
26
+ setError(
27
+ __(
28
+ "For Future execution a 'Starts at' date or 'Starts before' date must be selected. Immediate execution can be selected in the previous step."
29
+ )
30
+ );
31
+ } else if (!startsBefore?.length) {
32
+ wrappedSetValid(true);
33
+ setError(null);
34
+ } else if (
35
+ new Date(startsAt).getTime() >= new Date(startsBefore).getTime()
36
+ ) {
37
+ wrappedSetValid(false);
38
+ setError(__("'Starts before' date must be after 'Starts at' date"));
39
+ } else if (new Date().getTime() >= new Date(startsBefore).getTime()) {
40
+ wrappedSetValid(false);
41
+ setError(__("'Starts before' date must in the future"));
42
+ } else {
43
+ wrappedSetValid(true);
44
+ setError(null);
45
+ }
46
+ }, [wrappedSetValid, startsAt, startsBefore]);
47
+
48
+ return (
49
+ <>
50
+ <WizardTitle title={SCHEDULE_TYPES.FUTURE} />
51
+ <Form className="future-schedule-tab">
52
+ <FormGroup label={__('Starts at')} fieldId="start-at-date">
53
+ <DateTimePicker
54
+ ariaLabel="starts at"
55
+ dateTime={startsAt}
56
+ setDateTime={newValue =>
57
+ setScheduleValue(current => ({
58
+ ...current,
59
+ startsAt: newValue,
60
+ }))
61
+ }
62
+ />
63
+ <Button
64
+ variant="link"
65
+ isInline
66
+ className="clear-datetime-button"
67
+ onClick={() =>
68
+ setScheduleValue(current => ({
69
+ ...current,
70
+ startsAt: null,
71
+ }))
72
+ }
73
+ >
74
+ {__('Clear input')}
75
+ </Button>
76
+ </FormGroup>
77
+
78
+ <FormGroup
79
+ label={__('Starts before')}
80
+ fieldId="start-before-date"
81
+ labelIcon={helpLabel(
82
+ __(
83
+ 'Indicates that the action should be cancelled if it cannot be started before this time.'
84
+ ),
85
+ 'start-before-date'
86
+ )}
87
+ validated={error ? ValidatedOptions.error : ValidatedOptions.noval}
88
+ helperTextInvalid={error}
89
+ >
90
+ <DateTimePicker
91
+ ariaLabel="starts before"
92
+ dateTime={startsBefore}
93
+ setDateTime={newValue =>
94
+ setScheduleValue(current => ({
95
+ ...current,
96
+ startsBefore: newValue,
97
+ }))
98
+ }
99
+ />
100
+ <Button
101
+ variant="link"
102
+ isInline
103
+ className="clear-datetime-button"
104
+ onClick={() =>
105
+ setScheduleValue(current => ({
106
+ ...current,
107
+ startsBefore: null,
108
+ }))
109
+ }
110
+ >
111
+ {__('Clear input')}
112
+ </Button>
113
+ </FormGroup>
114
+ </Form>
115
+ </>
116
+ );
117
+ };
118
+
119
+ ScheduleFuture.propTypes = {
120
+ scheduleValue: PropTypes.shape({
121
+ startsAt: PropTypes.string,
122
+ startsBefore: PropTypes.string,
123
+ }).isRequired,
124
+ setScheduleValue: PropTypes.func.isRequired,
125
+ setValid: PropTypes.func.isRequired,
126
+ };
@@ -0,0 +1,287 @@
1
+ import React, { useEffect, useState, useCallback } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ Form,
5
+ FormGroup,
6
+ Radio,
7
+ TextInput,
8
+ ValidatedOptions,
9
+ Divider,
10
+ } from '@patternfly/react-core';
11
+ import { ExclamationCircleIcon } from '@patternfly/react-icons';
12
+ import { translate as __ } from 'foremanReact/common/I18n';
13
+ import { RepeatOn } from './RepeatOn';
14
+ import { SCHEDULE_TYPES } from '../../JobWizardConstants';
15
+ import { PurposeField } from './PurposeField';
16
+ import { DateTimePicker } from '../form/DateTimePicker';
17
+ import { WizardTitle } from '../form/WizardTitle';
18
+
19
+ export const ScheduleRecurring = ({
20
+ scheduleValue,
21
+ setScheduleValue,
22
+ setValid,
23
+ }) => {
24
+ const {
25
+ repeatType,
26
+ repeatAmount,
27
+ repeatData,
28
+ startsAt,
29
+ startsBefore,
30
+ ends,
31
+ isNeverEnds,
32
+ isFuture,
33
+ purpose,
34
+ } = scheduleValue;
35
+ const [validEnd, setValidEnd] = useState(true);
36
+ const [repeatValidated, setRepeatValidated] = useState('default');
37
+ const handleRepeatInputChange = newValue => {
38
+ if (!newValue.length) newValue = 0;
39
+ setRepeatValidated(
40
+ !newValue || parseInt(newValue, 10) >= 1 ? 'default' : 'error'
41
+ );
42
+ setScheduleValue(current => ({
43
+ ...current,
44
+ repeatAmount: newValue,
45
+ }));
46
+ };
47
+ const [repeatValid, setRepeatValid] = useState(true);
48
+
49
+ const wrappedSetValid = useCallback(setValid, []);
50
+ useEffect(() => {
51
+ if (isNeverEnds) setValidEnd(true);
52
+ else if (!ends) setValidEnd(true);
53
+ else if (
54
+ !startsAt.length &&
55
+ new Date().getTime() <= new Date(ends).getTime()
56
+ )
57
+ setValidEnd(true);
58
+ else if (new Date(startsAt).getTime() <= new Date(ends).getTime())
59
+ setValidEnd(true);
60
+ else {
61
+ setValidEnd(false);
62
+ }
63
+
64
+ if (!validEnd || !repeatValid) {
65
+ wrappedSetValid(false);
66
+ } else if (isFuture && startsAt.length) {
67
+ wrappedSetValid(true);
68
+ } else if (!isFuture) {
69
+ wrappedSetValid(true);
70
+ } else {
71
+ wrappedSetValid(false);
72
+ }
73
+ }, [
74
+ wrappedSetValid,
75
+ isNeverEnds,
76
+ startsAt,
77
+ startsBefore,
78
+ isFuture,
79
+ validEnd,
80
+ repeatValid,
81
+ ends,
82
+ ]);
83
+
84
+ return (
85
+ <>
86
+ <WizardTitle title={SCHEDULE_TYPES.RECURRING} />
87
+ <Form className="schedule-tab">
88
+ <FormGroup label={__('Starts')} fieldId="schedule-starts">
89
+ <div className="pf-c-form">
90
+ <FormGroup fieldId="schedule-starts-now">
91
+ <Radio
92
+ isChecked={!isFuture}
93
+ onChange={() =>
94
+ setScheduleValue(current => ({
95
+ ...current,
96
+ startsAt: '',
97
+ startsBefore: '',
98
+ isFuture: false,
99
+ }))
100
+ }
101
+ name="start-now"
102
+ id="start-now"
103
+ label={__('Now')}
104
+ />
105
+ </FormGroup>
106
+ <FormGroup fieldId="start-at-date">
107
+ <Radio
108
+ isChecked={isFuture}
109
+ onChange={() =>
110
+ setScheduleValue(current => ({
111
+ ...current,
112
+ startsAt: new Date().toISOString(),
113
+ isFuture: true,
114
+ }))
115
+ }
116
+ name="start-at"
117
+ id="start-at"
118
+ className="schedule-radio"
119
+ label={
120
+ <div className="scheudle-radio-wrapper">
121
+ <div className="schedule-radio-title">{__('At')}</div>
122
+ <DateTimePicker
123
+ ariaLabel="starts at"
124
+ dateTime={startsAt}
125
+ setDateTime={newValue =>
126
+ setScheduleValue(current => ({
127
+ ...current,
128
+ startsAt: newValue,
129
+ }))
130
+ }
131
+ isDisabled={!isFuture}
132
+ />
133
+ </div>
134
+ }
135
+ />
136
+ </FormGroup>
137
+ </div>
138
+ </FormGroup>
139
+
140
+ <Divider component="div" />
141
+ <RepeatOn
142
+ repeatType={repeatType}
143
+ repeatData={repeatData}
144
+ setRepeatType={newValue => {
145
+ setScheduleValue(current => ({
146
+ ...current,
147
+ repeatType: newValue,
148
+ startsBefore: '',
149
+ }));
150
+ }}
151
+ setRepeatData={newValue => {
152
+ setScheduleValue(current => ({
153
+ ...current,
154
+ repeatData: newValue,
155
+ }));
156
+ }}
157
+ setValid={setRepeatValid}
158
+ />
159
+ <Divider component="div" />
160
+ <FormGroup label={__('Ends')} fieldId="schedule-ends">
161
+ <div className="pf-c-form">
162
+ <FormGroup fieldId="schedule-ends-never">
163
+ <Radio
164
+ isChecked={isNeverEnds}
165
+ onChange={() =>
166
+ setScheduleValue(current => ({
167
+ ...current,
168
+ isNeverEnds: true,
169
+ ends: null,
170
+ repeatAmount: null,
171
+ }))
172
+ }
173
+ name="never-ends"
174
+ id="never-ends"
175
+ label={__('Never')}
176
+ />
177
+ </FormGroup>
178
+ <FormGroup
179
+ fieldId="ends-on-date"
180
+ validated={
181
+ validEnd ? ValidatedOptions.noval : ValidatedOptions.error
182
+ }
183
+ helperTextInvalid={__('End time needs to be after start time')}
184
+ helperTextInvalidIcon={<ExclamationCircleIcon />}
185
+ >
186
+ <Radio
187
+ isChecked={!!ends}
188
+ onChange={() =>
189
+ setScheduleValue(current => ({
190
+ ...current,
191
+ ends: new Date().toISOString(),
192
+ isNeverEnds: false,
193
+ repeatAmount: null,
194
+ }))
195
+ }
196
+ name="ends-on"
197
+ id="ends-on"
198
+ className="schedule-radio"
199
+ label={
200
+ <div className="scheudle-radio-wrapper">
201
+ <div className="schedule-radio-title">{__('On')}</div>
202
+ <DateTimePicker
203
+ ariaLabel="ends on"
204
+ dateTime={ends}
205
+ isDisabled={!ends}
206
+ setDateTime={newValue => {
207
+ setScheduleValue(current => ({
208
+ ...current,
209
+ ends: newValue,
210
+ }));
211
+ }}
212
+ />
213
+ </div>
214
+ }
215
+ />
216
+ </FormGroup>
217
+ <FormGroup fieldId="ends-after">
218
+ <Radio
219
+ isChecked={repeatAmount === 0 || !!repeatAmount}
220
+ onChange={() =>
221
+ setScheduleValue(current => ({
222
+ ...current,
223
+ ends: null,
224
+ isNeverEnds: false,
225
+ repeatAmount: 1,
226
+ }))
227
+ }
228
+ name="ends-after"
229
+ id="ends-after"
230
+ className="schedule-radio"
231
+ label={
232
+ <div className="scheudle-radio-wrapper">
233
+ <div className="schedule-radio-title">{__('After')}</div>
234
+ <FormGroup
235
+ helperTextInvalid={__(
236
+ 'Repeat amount can only be a positive number'
237
+ )}
238
+ validated={repeatValidated}
239
+ className="schedule-radio-repeat-text"
240
+ >
241
+ <TextInput
242
+ id="repeat-amount"
243
+ value={repeatAmount || ''}
244
+ type="number"
245
+ onChange={handleRepeatInputChange}
246
+ isDisabled={!(repeatAmount === 0 || !!repeatAmount)}
247
+ />
248
+ </FormGroup>
249
+ <div className="schedule-radio-occurences">
250
+ {__('occurences')}
251
+ </div>
252
+ </div>
253
+ }
254
+ />
255
+ </FormGroup>
256
+ </div>
257
+ </FormGroup>
258
+ <Divider component="div" />
259
+ <PurposeField
260
+ purpose={purpose}
261
+ setPurpose={newValue => {
262
+ setScheduleValue(current => ({
263
+ ...current,
264
+ purpose: newValue,
265
+ }));
266
+ }}
267
+ />
268
+ </Form>
269
+ </>
270
+ );
271
+ };
272
+
273
+ ScheduleRecurring.propTypes = {
274
+ scheduleValue: PropTypes.shape({
275
+ repeatType: PropTypes.string.isRequired,
276
+ repeatAmount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
277
+ repeatData: PropTypes.object,
278
+ startsAt: PropTypes.string,
279
+ startsBefore: PropTypes.string,
280
+ ends: PropTypes.string,
281
+ isFuture: PropTypes.bool,
282
+ isNeverEnds: PropTypes.bool,
283
+ purpose: PropTypes.string,
284
+ }).isRequired,
285
+ setScheduleValue: PropTypes.func.isRequired,
286
+ setValid: PropTypes.func.isRequired,
287
+ };