foreman_remote_execution 4.6.0 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_ci.yml +7 -0
  3. data/.rubocop_todo.yml +1 -0
  4. data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
  5. data/app/controllers/job_invocations_controller.rb +1 -1
  6. data/app/controllers/ui_job_wizard_controller.rb +21 -2
  7. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  8. data/app/graphql/types/job_invocation.rb +16 -0
  9. data/app/graphql/types/job_invocation_input.rb +13 -0
  10. data/app/graphql/types/recurrence_input.rb +8 -0
  11. data/app/graphql/types/scheduling_input.rb +6 -0
  12. data/app/graphql/types/targeting_enum.rb +7 -0
  13. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +5 -1
  14. data/app/helpers/remote_execution_helper.rb +9 -3
  15. data/app/lib/actions/remote_execution/run_host_job.rb +10 -1
  16. data/app/lib/actions/remote_execution/run_hosts_job.rb +58 -4
  17. data/app/mailers/rex_job_mailer.rb +15 -0
  18. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +10 -0
  19. data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +6 -0
  20. data/app/models/host_proxy_invocation.rb +4 -0
  21. data/app/models/host_status/execution_status.rb +3 -3
  22. data/app/models/job_invocation.rb +12 -5
  23. data/app/models/job_invocation_composer.rb +25 -17
  24. data/app/models/job_template.rb +1 -1
  25. data/app/models/remote_execution_feature.rb +5 -1
  26. data/app/models/remote_execution_provider.rb +18 -2
  27. data/app/models/rex_mail_notification.rb +13 -0
  28. data/app/models/targeting.rb +7 -3
  29. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  30. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  31. data/app/views/job_invocations/index.html.erb +1 -1
  32. data/app/views/job_invocations/refresh.js.erb +1 -0
  33. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  34. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  35. data/app/views/template_invocations/show.html.erb +2 -1
  36. data/app/views/templates/ssh/module_action.erb +1 -0
  37. data/app/views/templates/ssh/power_action.erb +2 -0
  38. data/app/views/templates/ssh/puppet_run_once.erb +1 -0
  39. data/config/routes.rb +1 -0
  40. data/db/migrate/2021051713291621250977_add_host_proxy_invocations.rb +12 -0
  41. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  42. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  43. data/db/seeds.d/95-mail_notifications.rb +24 -0
  44. data/foreman_remote_execution.gemspec +2 -3
  45. data/lib/foreman_remote_execution/engine.rb +114 -8
  46. data/lib/foreman_remote_execution/version.rb +1 -1
  47. data/package.json +9 -7
  48. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  49. data/test/functional/cockpit_controller_test.rb +0 -1
  50. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  51. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  52. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  53. data/test/helpers/remote_execution_helper_test.rb +0 -1
  54. data/test/unit/actions/run_host_job_test.rb +21 -0
  55. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  56. data/test/unit/concerns/host_extensions_test.rb +40 -7
  57. data/test/unit/input_template_renderer_test.rb +1 -89
  58. data/test/unit/job_invocation_composer_test.rb +18 -18
  59. data/test/unit/job_invocation_report_template_test.rb +16 -13
  60. data/test/unit/job_invocation_test.rb +1 -1
  61. data/test/unit/job_template_effective_user_test.rb +0 -4
  62. data/test/unit/remote_execution_provider_test.rb +46 -4
  63. data/test/unit/targeting_test.rb +68 -1
  64. data/webpack/JobWizard/JobWizard.js +158 -24
  65. data/webpack/JobWizard/JobWizard.scss +93 -1
  66. data/webpack/JobWizard/JobWizardConstants.js +54 -0
  67. data/webpack/JobWizard/JobWizardSelectors.js +41 -0
  68. data/webpack/JobWizard/__tests__/fixtures.js +188 -3
  69. data/webpack/JobWizard/__tests__/integration.test.js +41 -106
  70. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  71. data/webpack/JobWizard/autofill.js +38 -0
  72. data/webpack/JobWizard/index.js +7 -0
  73. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +41 -10
  74. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +90 -0
  75. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +116 -55
  76. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +354 -16
  77. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +79 -246
  78. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
  79. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +123 -51
  80. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  81. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  82. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  83. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  84. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  85. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +100 -0
  86. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  87. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  88. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
  89. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  90. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  91. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  92. data/webpack/JobWizard/steps/HostsAndInputs/index.js +214 -0
  93. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  94. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  95. data/webpack/JobWizard/steps/Schedule/QueryType.js +51 -0
  96. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  97. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  98. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  99. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  100. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +125 -0
  101. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  102. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +28 -0
  103. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +106 -0
  104. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  105. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +32 -0
  106. data/webpack/JobWizard/steps/Schedule/index.js +178 -0
  107. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  108. data/webpack/JobWizard/steps/form/FormHelpers.js +5 -0
  109. data/webpack/JobWizard/steps/form/Formatter.js +181 -0
  110. data/webpack/JobWizard/steps/form/NumberInput.js +36 -0
  111. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  112. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  113. data/webpack/JobWizard/steps/form/SelectField.js +28 -5
  114. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  115. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  116. data/webpack/JobWizard/submit.js +120 -0
  117. data/webpack/JobWizard/validation.js +53 -0
  118. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  119. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  120. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  121. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  122. data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
  123. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  124. data/webpack/helpers.js +1 -0
  125. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  126. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
  127. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  128. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  129. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  130. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
  131. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  132. metadata +71 -16
  133. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +0 -70
  134. data/app/models/setting/remote_execution.rb +0 -88
  135. data/test/models/orchestration/ssh_test.rb +0 -56
  136. data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -13
  137. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -32
  138. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -113
  139. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
  140. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
  141. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -37
  142. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -23
  143. data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
@@ -0,0 +1,193 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import {
3
+ Button,
4
+ DescriptionList,
5
+ DescriptionListTerm,
6
+ DescriptionListGroup,
7
+ DescriptionListDescription,
8
+ } from '@patternfly/react-core';
9
+ import PropTypes from 'prop-types';
10
+ import { useDispatch, useSelector } from 'react-redux';
11
+ import EllipsisWithTooltip from 'react-ellipsis-with-tooltip';
12
+ import { get } from 'foremanReact/redux/API';
13
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
14
+ import {
15
+ selectJobTemplates,
16
+ selectHosts,
17
+ selectHostCount,
18
+ selectTemplateInputs,
19
+ selectAdvancedTemplateInputs,
20
+ } from '../../JobWizardSelectors';
21
+ import {
22
+ HOSTS_API,
23
+ HOSTS_TO_PREVIEW_AMOUNT,
24
+ WIZARD_TITLES,
25
+ } from '../../JobWizardConstants';
26
+ import { buildHostQuery } from '../HostsAndInputs/buildHostQuery';
27
+ import { WizardTitle } from '../form/WizardTitle';
28
+
29
+ const ReviewDetails = ({
30
+ jobCategory,
31
+ jobTemplateID,
32
+ advancedValues,
33
+ scheduleValue,
34
+ templateValues,
35
+ selectedTargets,
36
+ hostsSearchQuery,
37
+ }) => {
38
+ const dispatch = useDispatch();
39
+ useEffect(() => {
40
+ dispatch(
41
+ get({
42
+ key: HOSTS_API,
43
+ url: '/api/hosts',
44
+ params: {
45
+ search: buildHostQuery(selectedTargets, hostsSearchQuery),
46
+ per_page: HOSTS_TO_PREVIEW_AMOUNT,
47
+ },
48
+ })
49
+ );
50
+ }, [dispatch, hostsSearchQuery, selectedTargets]);
51
+ const jobTemplates = useSelector(selectJobTemplates);
52
+ const templateInputs = useSelector(selectTemplateInputs);
53
+ const advancedTemplateInputs = useSelector(selectAdvancedTemplateInputs);
54
+ const jobTemplate = jobTemplates.find(
55
+ template => template.id === jobTemplateID
56
+ )?.name;
57
+
58
+ const hosts = useSelector(selectHosts);
59
+
60
+ const hostsCount = useSelector(selectHostCount);
61
+ const stringHosts = () => {
62
+ if (hosts.length === 0) {
63
+ return __('No Target Hosts');
64
+ }
65
+ if (hosts.length === 1 || hosts.length === 2) {
66
+ return hosts.join(', ');
67
+ }
68
+ return `${hosts[0]}, ${hosts[1]} ${sprintf(
69
+ __(', and %s more'),
70
+ hostsCount - 2
71
+ )}`;
72
+ };
73
+ const [isAdvancedShown, setIsAdvancedShown] = useState(false);
74
+ const detailsFirstHalf = [
75
+ { label: __('Job Category'), value: jobCategory },
76
+ { label: __('Job template'), value: jobTemplate },
77
+ { label: __('Target hosts'), value: stringHosts() },
78
+ ...templateInputs.map(({ name }) => ({
79
+ label: name,
80
+ value: templateValues[name],
81
+ })),
82
+ {
83
+ label: __('Advanced fields'),
84
+ value: isAdvancedShown ? (
85
+ <Button
86
+ variant="link"
87
+ isInline
88
+ onClick={() => {
89
+ setIsAdvancedShown(false);
90
+ }}
91
+ >
92
+ {__('Hide all advanced fields')}
93
+ </Button>
94
+ ) : (
95
+ <Button
96
+ variant="link"
97
+ isInline
98
+ onClick={() => {
99
+ setIsAdvancedShown(true);
100
+ }}
101
+ >
102
+ {__('Show all advanced fields')}
103
+ </Button>
104
+ ),
105
+ },
106
+ ].filter(d => d);
107
+
108
+ const detailsSecondHalf = [
109
+ {
110
+ label: __('Schedule type'),
111
+ value: scheduleValue.isFuture
112
+ ? __('Schedule for future execution')
113
+ : __('Execute now'),
114
+ },
115
+ {
116
+ label: __('Recurrence'),
117
+ value: scheduleValue.repeatType,
118
+ },
119
+ {
120
+ label: __('Type of query'),
121
+ value: scheduleValue.isTypeStatic
122
+ ? __('Static query')
123
+ : __('Dynamic query'),
124
+ },
125
+ ].filter(d => d);
126
+
127
+ const advancedFields = [
128
+ { label: __('Effective user'), value: advancedValues.effectiveUserValue },
129
+ { label: __('Description Template'), value: advancedValues.description },
130
+ { label: __('Timeout to kill'), value: advancedValues.timeoutToKill },
131
+ { label: __('Concurrency level'), value: advancedValues.concurrencyLevel },
132
+ { label: __('Time span'), value: advancedValues.timeSpan },
133
+ {
134
+ label: __('Execution ordering'),
135
+ value: advancedValues.isRandomizedOrdering
136
+ ? __('Randomized')
137
+ : __('Alphabetical'),
138
+ },
139
+ ...advancedTemplateInputs.map(({ name }) => ({
140
+ label: name,
141
+ value: advancedValues.templateValues[name],
142
+ })),
143
+ ];
144
+
145
+ return (
146
+ <>
147
+ <WizardTitle
148
+ title={WIZARD_TITLES.review}
149
+ className="advanced-fields-title"
150
+ />
151
+ <DescriptionList isHorizontal className="review-details">
152
+ {detailsFirstHalf.map(({ label, value }, index) => (
153
+ <DescriptionListGroup key={index}>
154
+ <DescriptionListTerm>{label}</DescriptionListTerm>
155
+ <DescriptionListDescription>
156
+ <EllipsisWithTooltip>{value || ''}</EllipsisWithTooltip>
157
+ </DescriptionListDescription>
158
+ </DescriptionListGroup>
159
+ ))}
160
+ {isAdvancedShown &&
161
+ advancedFields.map(({ label, value }, index) => (
162
+ <DescriptionListGroup key={index} className="advanced-fields">
163
+ <DescriptionListTerm>{label}</DescriptionListTerm>
164
+ <DescriptionListDescription>
165
+ <EllipsisWithTooltip>{value || ''}</EllipsisWithTooltip>
166
+ </DescriptionListDescription>
167
+ </DescriptionListGroup>
168
+ ))}
169
+ {detailsSecondHalf.map(({ label, value }, index) => (
170
+ <DescriptionListGroup key={index}>
171
+ <DescriptionListTerm>{label}</DescriptionListTerm>
172
+ <DescriptionListDescription>
173
+ <EllipsisWithTooltip>{value || ''}</EllipsisWithTooltip>
174
+ </DescriptionListDescription>
175
+ </DescriptionListGroup>
176
+ ))}
177
+ </DescriptionList>
178
+ </>
179
+ );
180
+ };
181
+
182
+ ReviewDetails.propTypes = {
183
+ jobCategory: PropTypes.string.isRequired,
184
+ jobTemplateID: PropTypes.number,
185
+ advancedValues: PropTypes.object.isRequired,
186
+ scheduleValue: PropTypes.object.isRequired,
187
+ templateValues: PropTypes.object.isRequired,
188
+ selectedTargets: PropTypes.object.isRequired,
189
+ hostsSearchQuery: PropTypes.string.isRequired,
190
+ };
191
+
192
+ ReviewDetails.defaultProps = { jobTemplateID: null };
193
+ export default ReviewDetails;
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { TextInput, FormGroup } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { helpLabel } from '../form/FormHelpers';
6
+
7
+ export const PurposeField = ({ isDisabled, purpose, setPurpose }) => (
8
+ <FormGroup
9
+ label={__('Purpose')}
10
+ labelIcon={helpLabel(
11
+ __(
12
+ 'A special label for tracking a recurring job. There can be only one active job with a given purpose at a time.'
13
+ )
14
+ )}
15
+ >
16
+ <TextInput
17
+ isDisabled={isDisabled}
18
+ aria-label="purpose"
19
+ type="text"
20
+ value={purpose}
21
+ onChange={newPurpose => {
22
+ setPurpose(newPurpose);
23
+ }}
24
+ />
25
+ </FormGroup>
26
+ );
27
+ PurposeField.propTypes = {
28
+ isDisabled: PropTypes.bool.isRequired,
29
+ purpose: PropTypes.string.isRequired,
30
+ setPurpose: PropTypes.func.isRequired,
31
+ };
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormGroup, Radio } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { helpLabel } from '../form/FormHelpers';
6
+
7
+ export const QueryType = ({ isTypeStatic, setIsTypeStatic }) => (
8
+ <FormGroup
9
+ label={__('Query type')}
10
+ fieldId="query-type"
11
+ labelIcon={helpLabel(
12
+ <p>
13
+ {__('Type has impact on when is the query evaluated to hosts.')}
14
+ <br />
15
+ <ul>
16
+ <li>
17
+ <b>{__('Static')}</b> -{' '}
18
+ {__('evaluates just after you submit this form')}
19
+ </li>
20
+ <li>
21
+ <b>{__('Dynamic')}</b> -{' '}
22
+ {__(
23
+ "evaluates just before the execution is started, so if it's planed in future, targeted hosts set may change before it"
24
+ )}
25
+ </li>
26
+ </ul>
27
+ </p>,
28
+ 'query-type'
29
+ )}
30
+ >
31
+ <Radio
32
+ isChecked={isTypeStatic}
33
+ name="query-type"
34
+ onChange={() => setIsTypeStatic(true)}
35
+ id="query-type-static"
36
+ label={__('Static query')}
37
+ />
38
+ <Radio
39
+ isChecked={!isTypeStatic}
40
+ name="query-type"
41
+ onChange={() => setIsTypeStatic(false)}
42
+ id="query-type-dynamic"
43
+ label={__('Dynamic query')}
44
+ />
45
+ </FormGroup>
46
+ );
47
+
48
+ QueryType.propTypes = {
49
+ isTypeStatic: PropTypes.bool.isRequired,
50
+ setIsTypeStatic: PropTypes.func.isRequired,
51
+ };
@@ -0,0 +1,53 @@
1
+ import React, { useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { TextInput, FormGroup, ValidatedOptions } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { helpLabel } from '../form/FormHelpers';
6
+
7
+ export const RepeatCron = ({ repeatData, setRepeatData, setValid }) => {
8
+ const { cronline } = repeatData;
9
+ useEffect(() => {
10
+ if (cronline) {
11
+ setValid(true);
12
+ } else {
13
+ setValid(false);
14
+ }
15
+ return () => setValid(true);
16
+ }, [setValid, cronline]);
17
+ return (
18
+ <FormGroup
19
+ label={__('Cron line')}
20
+ labelIcon={helpLabel(
21
+ <div>
22
+ {__("Cron line format 'a b c d e', where:")}
23
+ <br />
24
+ <ol>
25
+ <li>{__('is minute (range: 0-59)')}</li>
26
+ <li>{__('is hour (range: 0-23)')}</li>
27
+ <li>{__('is day of month (range: 1-31)')}</li>
28
+ <li>{__('is month (range: 1-12)')}</li>
29
+ <li>{__('is day of week (range: 0-6)')}</li>
30
+ </ol>
31
+ </div>
32
+ )}
33
+ isRequired
34
+ >
35
+ <TextInput
36
+ isRequired
37
+ validated={cronline ? ValidatedOptions.noval : ValidatedOptions.error}
38
+ aria-label="cronline"
39
+ placeholder="* * * * *"
40
+ type="text"
41
+ value={cronline || ''}
42
+ onChange={newTime => {
43
+ setRepeatData({ cronline: newTime });
44
+ }}
45
+ />
46
+ </FormGroup>
47
+ );
48
+ };
49
+ RepeatCron.propTypes = {
50
+ repeatData: PropTypes.object.isRequired,
51
+ setRepeatData: PropTypes.func.isRequired,
52
+ setValid: PropTypes.func.isRequired,
53
+ };
@@ -0,0 +1,37 @@
1
+ import React, { useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormGroup, TimePicker } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+
6
+ export const RepeatDaily = ({ repeatData, setRepeatData, setValid }) => {
7
+ const { at } = repeatData;
8
+ useEffect(() => {
9
+ if (at) {
10
+ setValid(true);
11
+ } else {
12
+ setValid(false);
13
+ }
14
+ return () => setValid(true);
15
+ }, [setValid, at]);
16
+ return (
17
+ <FormGroup label={__('At')} isRequired>
18
+ <TimePicker
19
+ aria-label="repeat-at"
20
+ className="time-picker"
21
+ time={repeatData.at}
22
+ placeholder="hh:mm"
23
+ onChange={newTime => {
24
+ setRepeatData({ ...repeatData, at: newTime });
25
+ }}
26
+ is24Hour
27
+ invalidFormatErrorMessage={__('Invalid time format')}
28
+ />
29
+ </FormGroup>
30
+ );
31
+ };
32
+
33
+ RepeatDaily.propTypes = {
34
+ repeatData: PropTypes.object.isRequired,
35
+ setRepeatData: PropTypes.func.isRequired,
36
+ setValid: PropTypes.func.isRequired,
37
+ };
@@ -0,0 +1,54 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ FormGroup,
5
+ Select,
6
+ SelectOption,
7
+ SelectVariant,
8
+ } from '@patternfly/react-core';
9
+ import { range } from 'lodash';
10
+ import { translate as __ } from 'foremanReact/common/I18n';
11
+
12
+ export const RepeatHour = ({ repeatData, setRepeatData, setValid }) => {
13
+ const { minute } = repeatData;
14
+ useEffect(() => {
15
+ if (minute) {
16
+ setValid(true);
17
+ } else {
18
+ setValid(false);
19
+ }
20
+ return () => setValid(true);
21
+ }, [setValid, minute]);
22
+ const [minuteOpen, setMinuteOpen] = useState(false);
23
+ return (
24
+ <FormGroup label={__('At minute')} isRequired>
25
+ <Select
26
+ id="repeat-on-hourly"
27
+ variant={SelectVariant.typeahead}
28
+ typeAheadAriaLabel="repeat-at-minute-typeahead"
29
+ onSelect={(event, selection) => {
30
+ setRepeatData({ minute: selection });
31
+ setMinuteOpen(false);
32
+ }}
33
+ selections={minute || ''}
34
+ onToggle={toggle => {
35
+ setMinuteOpen(toggle);
36
+ }}
37
+ isOpen={minuteOpen}
38
+ width={85}
39
+ menuAppendTo={() => document.querySelector('.pf-c-form.schedule-tab')}
40
+ toggleAriaLabel="select minute toggle"
41
+ validated={minute ? 'success' : 'error'}
42
+ >
43
+ {range(60).map(minuteNumber => (
44
+ <SelectOption key={minuteNumber} value={`${minuteNumber}`} />
45
+ ))}
46
+ </Select>
47
+ </FormGroup>
48
+ );
49
+ };
50
+ RepeatHour.propTypes = {
51
+ repeatData: PropTypes.object.isRequired,
52
+ setRepeatData: PropTypes.func.isRequired,
53
+ setValid: PropTypes.func.isRequired,
54
+ };
@@ -0,0 +1,46 @@
1
+ import React, { useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { TextInput, FormGroup, ValidatedOptions } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { RepeatDaily } from './RepeatDaily';
6
+ import { noop } from '../../../helpers';
7
+
8
+ export const RepeatMonth = ({ repeatData, setRepeatData, setValid }) => {
9
+ const { days, at } = repeatData;
10
+ useEffect(() => {
11
+ if (days && at) {
12
+ setValid(true);
13
+ } else {
14
+ setValid(false);
15
+ }
16
+ return () => setValid(true);
17
+ }, [setValid, days, at]);
18
+ return (
19
+ <>
20
+ <FormGroup label={__('Days')} isRequired>
21
+ <TextInput
22
+ isRequired
23
+ validated={days ? ValidatedOptions.noval : ValidatedOptions.error}
24
+ aria-label="days"
25
+ placeholder="1,2..."
26
+ type="text"
27
+ value={repeatData.days || ''}
28
+ onChange={newTime => {
29
+ setRepeatData({ ...repeatData, days: newTime });
30
+ }}
31
+ />
32
+ </FormGroup>
33
+ <RepeatDaily
34
+ repeatData={repeatData}
35
+ setRepeatData={setRepeatData}
36
+ setValid={noop}
37
+ />
38
+ </>
39
+ );
40
+ };
41
+
42
+ RepeatMonth.propTypes = {
43
+ repeatData: PropTypes.object.isRequired,
44
+ setRepeatData: PropTypes.func.isRequired,
45
+ setValid: PropTypes.func.isRequired,
46
+ };
@@ -0,0 +1,125 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { TextInput, Grid, GridItem, FormGroup } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { SelectField } from '../form/SelectField';
6
+ import { repeatTypes } from '../../JobWizardConstants';
7
+ import { RepeatCron } from './RepeatCron';
8
+ import { RepeatHour } from './RepeatHour';
9
+ import { RepeatMonth } from './RepeatMonth';
10
+ import { RepeatDaily } from './RepeatDaily';
11
+ import { RepeatWeek } from './RepeatWeek';
12
+
13
+ export const RepeatOn = ({
14
+ repeatType,
15
+ setRepeatType,
16
+ repeatAmount,
17
+ setRepeatAmount,
18
+ repeatData,
19
+ setRepeatData,
20
+ setValid,
21
+ }) => {
22
+ const [repeatValidated, setRepeatValidated] = useState('default');
23
+ const handleRepeatInputChange = newValue => {
24
+ setRepeatValidated(newValue >= 1 ? 'default' : 'error');
25
+ setRepeatAmount(newValue);
26
+ };
27
+
28
+ const getRepeatComponent = () => {
29
+ switch (repeatType) {
30
+ case repeatTypes.cronline:
31
+ return (
32
+ <RepeatCron
33
+ repeatData={repeatData}
34
+ setRepeatData={setRepeatData}
35
+ setValid={setValid}
36
+ />
37
+ );
38
+ case repeatTypes.monthly:
39
+ return (
40
+ <RepeatMonth
41
+ repeatData={repeatData}
42
+ setRepeatData={setRepeatData}
43
+ setValid={setValid}
44
+ />
45
+ );
46
+ case repeatTypes.weekly:
47
+ return (
48
+ <RepeatWeek
49
+ repeatData={repeatData}
50
+ setRepeatData={setRepeatData}
51
+ setValid={setValid}
52
+ />
53
+ );
54
+ case repeatTypes.daily:
55
+ return (
56
+ <RepeatDaily
57
+ repeatData={repeatData}
58
+ setRepeatData={setRepeatData}
59
+ setValid={setValid}
60
+ />
61
+ );
62
+ case repeatTypes.hourly:
63
+ return (
64
+ <RepeatHour
65
+ repeatData={repeatData}
66
+ setRepeatData={setRepeatData}
67
+ setValid={setValid}
68
+ />
69
+ );
70
+ case repeatTypes.noRepeat:
71
+ default:
72
+ return null;
73
+ }
74
+ };
75
+ 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>
113
+ );
114
+ };
115
+
116
+ RepeatOn.propTypes = {
117
+ repeatType: PropTypes.oneOf(Object.values(repeatTypes)).isRequired,
118
+ setRepeatType: PropTypes.func.isRequired,
119
+ repeatAmount: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
120
+ .isRequired,
121
+ setRepeatAmount: PropTypes.func.isRequired,
122
+ repeatData: PropTypes.object.isRequired,
123
+ setRepeatData: PropTypes.func.isRequired,
124
+ setValid: PropTypes.func.isRequired,
125
+ };
@@ -0,0 +1,70 @@
1
+ import React, { useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormGroup, Checkbox } from '@patternfly/react-core';
4
+ import { translate as __, documentLocale } from 'foremanReact/common/I18n';
5
+ import { RepeatDaily } from './RepeatDaily';
6
+ import { noop } from '../../../helpers';
7
+
8
+ const getWeekDays = () => {
9
+ const locale = documentLocale().replace(/-/g, '_');
10
+ const baseDate = new Date(Date.UTC(2017, 0, 2)); // just a Monday
11
+ const weekDays = [];
12
+ for (let i = 0; i < 7; i++) {
13
+ try {
14
+ weekDays.push(baseDate.toLocaleDateString(locale, { weekday: 'short' }));
15
+ } catch {
16
+ weekDays.push(baseDate.toLocaleDateString('en', { weekday: 'short' }));
17
+ }
18
+ baseDate.setDate(baseDate.getDate() + 1);
19
+ }
20
+ return weekDays;
21
+ };
22
+
23
+ export const RepeatWeek = ({ repeatData, setRepeatData, setValid }) => {
24
+ const { daysOfWeek, at } = repeatData;
25
+ useEffect(() => {
26
+ if (daysOfWeek && Object.values(daysOfWeek).includes(true) && at) {
27
+ setValid(true);
28
+ } else {
29
+ setValid(false);
30
+ }
31
+ return () => setValid(true);
32
+ }, [setValid, daysOfWeek, at]);
33
+ const days = getWeekDays();
34
+ const handleChangeDays = (checked, { target: { name } }) => {
35
+ setRepeatData({
36
+ ...repeatData,
37
+ daysOfWeek: { ...repeatData.daysOfWeek, [name]: checked },
38
+ });
39
+ };
40
+ return (
41
+ <>
42
+ <FormGroup label={__('Days of week')} isRequired>
43
+ <div id="repeat-on-weekly">
44
+ {days.map((day, index) => (
45
+ <Checkbox
46
+ aria-label={`${day} checkbox`}
47
+ key={index}
48
+ isChecked={daysOfWeek?.[index]}
49
+ name={index}
50
+ id={`repeat-on-day-${index}`}
51
+ onChange={handleChangeDays}
52
+ label={day}
53
+ />
54
+ ))}
55
+ </div>
56
+ </FormGroup>
57
+
58
+ <RepeatDaily
59
+ repeatData={repeatData}
60
+ setRepeatData={setRepeatData}
61
+ setValid={noop}
62
+ />
63
+ </>
64
+ );
65
+ };
66
+ RepeatWeek.propTypes = {
67
+ repeatData: PropTypes.object.isRequired,
68
+ setRepeatData: PropTypes.func.isRequired,
69
+ setValid: PropTypes.func.isRequired,
70
+ };
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormGroup, Radio } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+
6
+ export const ScheduleType = ({ isFuture, setIsFuture }) => (
7
+ <FormGroup label={__('Schedule type')} fieldId="schedule-type">
8
+ <Radio
9
+ isChecked={!isFuture}
10
+ name="schedule-type"
11
+ onChange={() => setIsFuture(false)}
12
+ id="schedule-type-now"
13
+ label={__('Execute now')}
14
+ />
15
+ <Radio
16
+ isChecked={isFuture}
17
+ name="schedule-type"
18
+ onChange={() => setIsFuture(true)}
19
+ id="schedule-type-future"
20
+ label={__('Schedule for future execution')}
21
+ />
22
+ </FormGroup>
23
+ );
24
+
25
+ ScheduleType.propTypes = {
26
+ isFuture: PropTypes.bool.isRequired,
27
+ setIsFuture: PropTypes.func.isRequired,
28
+ };