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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby_ci.yml +7 -0
- data/.rubocop_todo.yml +1 -0
- data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
- data/app/controllers/job_invocations_controller.rb +1 -1
- data/app/controllers/ui_job_wizard_controller.rb +21 -2
- data/app/graphql/mutations/job_invocations/create.rb +43 -0
- data/app/graphql/types/job_invocation.rb +16 -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/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +5 -1
- data/app/helpers/remote_execution_helper.rb +9 -3
- data/app/lib/actions/remote_execution/run_host_job.rb +10 -1
- data/app/lib/actions/remote_execution/run_hosts_job.rb +58 -4
- data/app/mailers/rex_job_mailer.rb +15 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +10 -0
- data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +6 -0
- data/app/models/host_proxy_invocation.rb +4 -0
- data/app/models/host_status/execution_status.rb +3 -3
- data/app/models/job_invocation.rb +12 -5
- data/app/models/job_invocation_composer.rb +25 -17
- data/app/models/job_template.rb +1 -1
- data/app/models/remote_execution_feature.rb +5 -1
- data/app/models/remote_execution_provider.rb +18 -2
- data/app/models/rex_mail_notification.rb +13 -0
- data/app/models/targeting.rb +7 -3
- data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
- data/app/views/dashboard/_latest-jobs.html.erb +21 -0
- data/app/views/job_invocations/index.html.erb +1 -1
- data/app/views/job_invocations/refresh.js.erb +1 -0
- data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
- data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
- data/app/views/template_invocations/show.html.erb +2 -1
- data/app/views/templates/ssh/module_action.erb +1 -0
- data/app/views/templates/ssh/power_action.erb +2 -0
- data/app/views/templates/ssh/puppet_run_once.erb +1 -0
- data/config/routes.rb +1 -0
- data/db/migrate/2021051713291621250977_add_host_proxy_invocations.rb +12 -0
- data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
- data/db/seeds.d/50-notification_blueprints.rb +14 -0
- data/db/seeds.d/95-mail_notifications.rb +24 -0
- data/foreman_remote_execution.gemspec +2 -3
- data/lib/foreman_remote_execution/engine.rb +114 -8
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +9 -7
- data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
- data/test/functional/cockpit_controller_test.rb +0 -1
- data/test/graphql/mutations/job_invocations/create.rb +58 -0
- data/test/graphql/queries/job_invocation_query_test.rb +31 -0
- data/test/graphql/queries/job_invocations_query_test.rb +35 -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/actions/run_hosts_job_test.rb +99 -4
- data/test/unit/concerns/host_extensions_test.rb +40 -7
- data/test/unit/input_template_renderer_test.rb +1 -89
- data/test/unit/job_invocation_composer_test.rb +18 -18
- data/test/unit/job_invocation_report_template_test.rb +16 -13
- data/test/unit/job_invocation_test.rb +1 -1
- data/test/unit/job_template_effective_user_test.rb +0 -4
- data/test/unit/remote_execution_provider_test.rb +46 -4
- data/test/unit/targeting_test.rb +68 -1
- data/webpack/JobWizard/JobWizard.js +158 -24
- data/webpack/JobWizard/JobWizard.scss +93 -1
- data/webpack/JobWizard/JobWizardConstants.js +54 -0
- data/webpack/JobWizard/JobWizardSelectors.js +41 -0
- data/webpack/JobWizard/__tests__/fixtures.js +188 -3
- data/webpack/JobWizard/__tests__/integration.test.js +41 -106
- 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 +41 -10
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +90 -0
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +116 -55
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +354 -16
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +79 -246
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +123 -51
- 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 +100 -0
- data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
- 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 +214 -0
- 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 +51 -0
- 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 +125 -0
- data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +28 -0
- data/webpack/JobWizard/steps/Schedule/StartEndDates.js +106 -0
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
- data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +32 -0
- data/webpack/JobWizard/steps/Schedule/index.js +178 -0
- data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
- data/webpack/JobWizard/steps/form/FormHelpers.js +5 -0
- data/webpack/JobWizard/steps/form/Formatter.js +181 -0
- data/webpack/JobWizard/steps/form/NumberInput.js +36 -0
- 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 +28 -5
- data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
- 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/components/SearchBar.js +18 -1
- data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
- data/webpack/helpers.js +1 -0
- data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
- data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
- data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
- data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
- data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
- metadata +71 -16
- data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +0 -70
- data/app/models/setting/remote_execution.rb +0 -88
- data/test/models/orchestration/ssh_test.rb +0 -56
- data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -13
- data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -32
- data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -113
- data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
- data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
- data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -37
- data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -23
- 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
|
+
};
|