foreman_remote_execution 4.7.0 → 5.0.2

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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +1 -0
  3. data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
  4. data/app/controllers/ui_job_wizard_controller.rb +16 -4
  5. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  6. data/app/graphql/types/job_invocation_input.rb +13 -0
  7. data/app/graphql/types/recurrence_input.rb +8 -0
  8. data/app/graphql/types/scheduling_input.rb +6 -0
  9. data/app/graphql/types/targeting_enum.rb +7 -0
  10. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +20 -9
  11. data/app/helpers/remote_execution_helper.rb +1 -1
  12. data/app/lib/actions/remote_execution/run_host_job.rb +6 -1
  13. data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
  14. data/app/mailers/rex_job_mailer.rb +15 -0
  15. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +12 -0
  16. data/app/models/job_invocation.rb +4 -0
  17. data/app/models/job_invocation_composer.rb +21 -13
  18. data/app/models/remote_execution_provider.rb +18 -2
  19. data/app/models/rex_mail_notification.rb +13 -0
  20. data/app/models/targeting.rb +3 -3
  21. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  22. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  23. data/app/views/job_invocations/_preview_hosts_list.html.erb +1 -1
  24. data/app/views/job_invocations/refresh.js.erb +1 -0
  25. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  26. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  27. data/app/views/template_invocations/show.html.erb +3 -2
  28. data/config/routes.rb +1 -0
  29. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  30. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  31. data/db/seeds.d/95-mail_notifications.rb +24 -0
  32. data/foreman_remote_execution.gemspec +1 -1
  33. data/lib/foreman_remote_execution/engine.rb +116 -7
  34. data/lib/foreman_remote_execution/version.rb +1 -1
  35. data/package.json +9 -7
  36. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  37. data/test/functional/cockpit_controller_test.rb +0 -1
  38. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  39. data/test/helpers/remote_execution_helper_test.rb +0 -1
  40. data/test/unit/actions/run_host_job_test.rb +21 -0
  41. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  42. data/test/unit/concerns/host_extensions_test.rb +36 -3
  43. data/test/unit/job_invocation_composer_test.rb +3 -5
  44. data/test/unit/job_invocation_report_template_test.rb +16 -13
  45. data/test/unit/job_template_effective_user_test.rb +0 -4
  46. data/test/unit/remote_execution_provider_test.rb +46 -4
  47. data/test/unit/targeting_test.rb +68 -1
  48. data/webpack/JobWizard/JobWizard.js +142 -28
  49. data/webpack/JobWizard/JobWizard.scss +86 -33
  50. data/webpack/JobWizard/JobWizardConstants.js +44 -0
  51. data/webpack/JobWizard/JobWizardSelectors.js +32 -0
  52. data/webpack/JobWizard/__tests__/fixtures.js +89 -6
  53. data/webpack/JobWizard/__tests__/integration.test.js +29 -22
  54. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  55. data/webpack/JobWizard/autofill.js +38 -0
  56. data/webpack/JobWizard/index.js +7 -0
  57. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +23 -9
  58. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
  59. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
  60. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +242 -23
  61. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
  62. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
  63. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
  64. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  65. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  66. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  67. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  68. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  69. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +100 -0
  70. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  71. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  72. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
  73. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  74. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  75. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  76. data/webpack/JobWizard/steps/HostsAndInputs/index.js +214 -0
  77. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  78. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  79. data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
  80. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  81. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  82. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  83. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  84. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
  85. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  86. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
  87. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +78 -23
  88. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  89. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +20 -10
  90. data/webpack/JobWizard/steps/Schedule/index.js +166 -29
  91. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  92. data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
  93. data/webpack/JobWizard/steps/form/Formatter.js +49 -17
  94. data/webpack/JobWizard/steps/form/NumberInput.js +5 -2
  95. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  96. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  97. data/webpack/JobWizard/steps/form/SelectField.js +14 -3
  98. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  99. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  100. data/webpack/JobWizard/submit.js +120 -0
  101. data/webpack/JobWizard/validation.js +53 -0
  102. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  103. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  104. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  105. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  106. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  107. data/webpack/helpers.js +1 -0
  108. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +2 -1
  109. metadata +53 -7
  110. data/app/models/setting/remote_execution.rb +0 -88
  111. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
  112. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
@@ -0,0 +1,214 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import {
3
+ Button,
4
+ Form,
5
+ FormGroup,
6
+ InputGroup,
7
+ Text,
8
+ Spinner,
9
+ } from '@patternfly/react-core';
10
+ import PropTypes from 'prop-types';
11
+ import { useSelector, useDispatch } from 'react-redux';
12
+ import { FilterIcon } from '@patternfly/react-icons';
13
+ import { debounce } from 'lodash';
14
+ import { get } from 'foremanReact/redux/API';
15
+ import { translate as __ } from 'foremanReact/common/I18n';
16
+ import { resetData } from 'foremanReact/components/AutoComplete/AutoCompleteActions';
17
+ import {
18
+ selectTemplateInputs,
19
+ selectWithKatello,
20
+ selectHostCount,
21
+ selectIsLoadingHosts,
22
+ } from '../../JobWizardSelectors';
23
+ import { SelectField } from '../form/SelectField';
24
+ import { SelectedChips } from './SelectedChips';
25
+ import { TemplateInputs } from './TemplateInputs';
26
+ import { HostSearch } from './HostSearch';
27
+ import { HostPreviewModal } from './HostPreviewModal';
28
+ import {
29
+ WIZARD_TITLES,
30
+ HOSTS,
31
+ HOST_COLLECTIONS,
32
+ HOST_GROUPS,
33
+ hostMethods,
34
+ hostsController,
35
+ hostQuerySearchID,
36
+ HOSTS_API,
37
+ HOSTS_TO_PREVIEW_AMOUNT,
38
+ DEBOUNCE_HOST_COUNT,
39
+ } from '../../JobWizardConstants';
40
+ import { WizardTitle } from '../form/WizardTitle';
41
+ import { SelectAPI } from './SelectAPI';
42
+ import { SelectGQL } from './SelectGQL';
43
+ import { buildHostQuery } from './buildHostQuery';
44
+
45
+ const HostsAndInputs = ({
46
+ templateValues,
47
+ setTemplateValues,
48
+ selected,
49
+ setSelected,
50
+ hostsSearchQuery,
51
+ setHostsSearchQuery,
52
+ }) => {
53
+ const [hostMethod, setHostMethod] = useState(hostMethods.hosts);
54
+ const isLoading = useSelector(selectIsLoadingHosts);
55
+ const templateInputs = useSelector(selectTemplateInputs);
56
+ const [hostPreviewOpen, setHostPreviewOpen] = useState(false);
57
+ useEffect(() => {
58
+ debounce(() => {
59
+ dispatch(
60
+ get({
61
+ key: HOSTS_API,
62
+ url: '/api/hosts',
63
+ params: {
64
+ search: buildHostQuery(selected, hostsSearchQuery),
65
+ per_page: HOSTS_TO_PREVIEW_AMOUNT,
66
+ },
67
+ })
68
+ );
69
+ }, DEBOUNCE_HOST_COUNT)();
70
+ }, [
71
+ dispatch,
72
+ selected,
73
+ selected.hosts,
74
+ selected.hostCollections,
75
+ selected.hostCollections,
76
+ hostsSearchQuery,
77
+ ]);
78
+ const withKatello = useSelector(selectWithKatello);
79
+ const hostCount = useSelector(selectHostCount);
80
+ const dispatch = useDispatch();
81
+
82
+ const selectedHosts = selected.hosts;
83
+ const setSelectedHosts = newSelected =>
84
+ setSelected(prevSelected => ({
85
+ ...prevSelected,
86
+ hosts: newSelected(prevSelected.hosts),
87
+ }));
88
+ const selectedHostCollections = selected.hostCollections;
89
+ const setSelectedHostCollections = newSelected =>
90
+ setSelected(prevSelected => ({
91
+ ...prevSelected,
92
+ hostCollections: newSelected(prevSelected.hostCollections),
93
+ }));
94
+ const selectedHostGroups = selected.hostGroups;
95
+ const setSelectedHostGroups = newSelected => {
96
+ setSelected(prevSelected => ({
97
+ ...prevSelected,
98
+ hostGroups: newSelected(prevSelected.hostGroups),
99
+ }));
100
+ };
101
+
102
+ const clearSearch = () => {
103
+ dispatch(resetData(hostsController, hostQuerySearchID));
104
+ setHostsSearchQuery('');
105
+ };
106
+ return (
107
+ <div className="target-hosts-and-inputs">
108
+ <WizardTitle title={WIZARD_TITLES.hostsAndInputs} />
109
+ {hostPreviewOpen && (
110
+ <HostPreviewModal
111
+ isOpen={hostPreviewOpen}
112
+ setIsOpen={setHostPreviewOpen}
113
+ searchQuery={buildHostQuery(selected, hostsSearchQuery)}
114
+ />
115
+ )}
116
+ <Form>
117
+ <FormGroup fieldId="host_selection" id="host-selection">
118
+ <InputGroup>
119
+ <SelectField
120
+ isRequired
121
+ className="target-method-select"
122
+ toggleIcon={<FilterIcon />}
123
+ fieldId="host_methods"
124
+ options={Object.values(hostMethods).filter(method => {
125
+ if (method === hostMethods.hostCollections && !withKatello) {
126
+ return false;
127
+ }
128
+ return true;
129
+ })}
130
+ setValue={setHostMethod}
131
+ value={hostMethod}
132
+ />
133
+ {hostMethod === hostMethods.searchQuery && (
134
+ <HostSearch
135
+ setValue={setHostsSearchQuery}
136
+ value={hostsSearchQuery}
137
+ />
138
+ )}
139
+ {hostMethod === hostMethods.hosts && (
140
+ <SelectGQL
141
+ selected={selectedHosts}
142
+ setSelected={setSelectedHosts}
143
+ apiKey={HOSTS}
144
+ name="hosts"
145
+ placeholderText={__('Filter by hosts')}
146
+ />
147
+ )}
148
+ {hostMethod === hostMethods.hostCollections && (
149
+ <SelectAPI
150
+ selected={selectedHostCollections}
151
+ setSelected={setSelectedHostCollections}
152
+ apiKey={HOST_COLLECTIONS}
153
+ name="host collections"
154
+ url="/katello/api/host_collections?per_page=100"
155
+ placeholderText={__('Filter by host collections')}
156
+ />
157
+ )}
158
+ {hostMethod === hostMethods.hostGroups && (
159
+ <SelectGQL
160
+ selected={selectedHostGroups}
161
+ setSelected={setSelectedHostGroups}
162
+ apiKey={HOST_GROUPS}
163
+ name="host groups"
164
+ placeholderText={__('Filter by host groups')}
165
+ />
166
+ )}
167
+ </InputGroup>
168
+ </FormGroup>
169
+ <SelectedChips
170
+ selectedHosts={selectedHosts}
171
+ setSelectedHosts={setSelectedHosts}
172
+ selectedHostCollections={selectedHostCollections}
173
+ setSelectedHostCollections={setSelectedHostCollections}
174
+ selectedHostGroups={selectedHostGroups}
175
+ setSelectedHostGroups={setSelectedHostGroups}
176
+ hostsSearchQuery={hostsSearchQuery}
177
+ clearSearch={clearSearch}
178
+ />
179
+ <Text>
180
+ {__('Apply to')}{' '}
181
+ <Button
182
+ variant="link"
183
+ isInline
184
+ onClick={() => setHostPreviewOpen(true)}
185
+ isDisabled={isLoading}
186
+ >
187
+ {hostCount} {__('hosts')}
188
+ </Button>{' '}
189
+ {isLoading && <Spinner size="sm" />}
190
+ </Text>
191
+ <TemplateInputs
192
+ inputs={templateInputs}
193
+ value={templateValues}
194
+ setValue={setTemplateValues}
195
+ />
196
+ </Form>
197
+ </div>
198
+ );
199
+ };
200
+
201
+ HostsAndInputs.propTypes = {
202
+ templateValues: PropTypes.object.isRequired,
203
+ setTemplateValues: PropTypes.func.isRequired,
204
+ selected: PropTypes.shape({
205
+ hosts: PropTypes.array.isRequired,
206
+ hostCollections: PropTypes.array.isRequired,
207
+ hostGroups: PropTypes.array.isRequired,
208
+ }).isRequired,
209
+ setSelected: PropTypes.func.isRequired,
210
+ hostsSearchQuery: PropTypes.string.isRequired,
211
+ setHostsSearchQuery: PropTypes.func.isRequired,
212
+ };
213
+
214
+ export default HostsAndInputs;
@@ -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
+ };
@@ -1,48 +1,51 @@
1
- import React, { useState } from 'react';
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
2
3
  import { FormGroup, Radio } from '@patternfly/react-core';
3
4
  import { translate as __ } from 'foremanReact/common/I18n';
4
5
  import { helpLabel } from '../form/FormHelpers';
5
6
 
6
- export const QueryType = () => {
7
- const [isTypeStatic, setIsTypeStatic] = useState(true);
8
- return (
9
- <FormGroup
10
- label={__('Query type')}
11
- fieldId="query-type"
12
- labelIcon={helpLabel(
13
- <p>
14
- {__('Type has impact on when is the query evaluated to hosts.')}
15
- <br />
16
- <ul>
17
- <li>
18
- <b>{__('Static')}</b> -{' '}
19
- {__('evaluates just after you submit this form')}
20
- </li>
21
- <li>
22
- <b>{__('Dynamic')}</b> -{' '}
23
- {__(
24
- "evaluates just before the execution is started, so if it's planed in future, targeted hosts set may change before it"
25
- )}
26
- </li>
27
- </ul>
28
- </p>,
29
- 'query-type'
30
- )}
31
- >
32
- <Radio
33
- isChecked={isTypeStatic}
34
- name="query-type"
35
- onChange={() => setIsTypeStatic(true)}
36
- id="query-type-static"
37
- label={__('Static query')}
38
- />
39
- <Radio
40
- isChecked={!isTypeStatic}
41
- name="query-type"
42
- onChange={() => setIsTypeStatic(false)}
43
- id="query-type-dynamic"
44
- label={__('Dynamic query')}
45
- />
46
- </FormGroup>
47
- );
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,
48
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
+ };