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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +1 -0
- data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
- data/app/controllers/ui_job_wizard_controller.rb +16 -4
- data/app/graphql/mutations/job_invocations/create.rb +43 -0
- data/app/graphql/types/job_invocation_input.rb +13 -0
- data/app/graphql/types/recurrence_input.rb +8 -0
- data/app/graphql/types/scheduling_input.rb +6 -0
- data/app/graphql/types/targeting_enum.rb +7 -0
- data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +20 -9
- data/app/helpers/remote_execution_helper.rb +1 -1
- data/app/lib/actions/remote_execution/run_host_job.rb +6 -1
- data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
- data/app/mailers/rex_job_mailer.rb +15 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +12 -0
- data/app/models/job_invocation.rb +4 -0
- data/app/models/job_invocation_composer.rb +21 -13
- data/app/models/remote_execution_provider.rb +18 -2
- data/app/models/rex_mail_notification.rb +13 -0
- data/app/models/targeting.rb +3 -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/_preview_hosts_list.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 +3 -2
- data/config/routes.rb +1 -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 +1 -1
- data/lib/foreman_remote_execution/engine.rb +116 -7
- 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/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 +36 -3
- data/test/unit/job_invocation_composer_test.rb +3 -5
- data/test/unit/job_invocation_report_template_test.rb +16 -13
- 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 +142 -28
- data/webpack/JobWizard/JobWizard.scss +86 -33
- data/webpack/JobWizard/JobWizardConstants.js +44 -0
- data/webpack/JobWizard/JobWizardSelectors.js +32 -0
- data/webpack/JobWizard/__tests__/fixtures.js +89 -6
- data/webpack/JobWizard/__tests__/integration.test.js +29 -22
- 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 +23 -9
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +242 -23
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
- 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 +46 -43
- data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
- data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
- data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
- data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
- data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
- data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
- data/webpack/JobWizard/steps/Schedule/StartEndDates.js +78 -23
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
- data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +20 -10
- data/webpack/JobWizard/steps/Schedule/index.js +166 -29
- data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
- data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
- data/webpack/JobWizard/steps/form/Formatter.js +49 -17
- data/webpack/JobWizard/steps/form/NumberInput.js +5 -2
- data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
- data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
- data/webpack/JobWizard/steps/form/SelectField.js +14 -3
- data/webpack/JobWizard/steps/form/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/routes/RouterSelector.js +1 -0
- data/webpack/helpers.js +1 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +2 -1
- metadata +53 -7
- data/app/models/setting/remote_execution.rb +0 -88
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
- data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { useSelector } from 'react-redux';
|
|
4
|
-
import {
|
|
5
|
-
import { translate as __ } from 'foremanReact/common/I18n';
|
|
4
|
+
import { Form } from '@patternfly/react-core';
|
|
6
5
|
import {
|
|
7
6
|
selectEffectiveUser,
|
|
8
7
|
selectAdvancedTemplateInputs,
|
|
9
|
-
selectTemplateInputs,
|
|
10
8
|
} from '../../JobWizardSelectors';
|
|
11
9
|
import {
|
|
12
10
|
EffectiveUserField,
|
|
@@ -17,18 +15,25 @@ import {
|
|
|
17
15
|
ConcurrencyLevelField,
|
|
18
16
|
TimeSpanLevelField,
|
|
19
17
|
TemplateInputsFields,
|
|
18
|
+
ExecutionOrderingField,
|
|
20
19
|
} from './Fields';
|
|
21
20
|
import { DescriptionField } from './DescriptionField';
|
|
21
|
+
import { WIZARD_TITLES } from '../../JobWizardConstants';
|
|
22
|
+
import { WizardTitle } from '../form/WizardTitle';
|
|
22
23
|
|
|
23
|
-
export const AdvancedFields = ({
|
|
24
|
+
export const AdvancedFields = ({
|
|
25
|
+
templateValues,
|
|
26
|
+
advancedValues,
|
|
27
|
+
setAdvancedValues,
|
|
28
|
+
}) => {
|
|
24
29
|
const effectiveUser = useSelector(selectEffectiveUser);
|
|
25
30
|
const advancedTemplateInputs = useSelector(selectAdvancedTemplateInputs);
|
|
26
|
-
const templateInputs = useSelector(selectTemplateInputs);
|
|
27
31
|
return (
|
|
28
32
|
<>
|
|
29
|
-
<
|
|
30
|
-
{
|
|
31
|
-
|
|
33
|
+
<WizardTitle
|
|
34
|
+
title={WIZARD_TITLES.advanced}
|
|
35
|
+
className="advanced-fields-title"
|
|
36
|
+
/>
|
|
32
37
|
<Form id="advanced-fields-job-template" autoComplete="off">
|
|
33
38
|
<TemplateInputsFields
|
|
34
39
|
inputs={advancedTemplateInputs}
|
|
@@ -46,7 +51,7 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
|
|
|
46
51
|
/>
|
|
47
52
|
)}
|
|
48
53
|
<DescriptionField
|
|
49
|
-
|
|
54
|
+
inputValues={{ ...templateValues, ...advancedValues.templateValues }}
|
|
50
55
|
value={advancedValues.description}
|
|
51
56
|
setValue={newValue => setAdvancedValues({ description: newValue })}
|
|
52
57
|
/>
|
|
@@ -98,6 +103,14 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
|
|
|
98
103
|
})
|
|
99
104
|
}
|
|
100
105
|
/>
|
|
106
|
+
<ExecutionOrderingField
|
|
107
|
+
isRandomizedOrdering={advancedValues.isRandomizedOrdering}
|
|
108
|
+
setValue={newValue =>
|
|
109
|
+
setAdvancedValues({
|
|
110
|
+
isRandomizedOrdering: newValue,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
/>
|
|
101
114
|
</Form>
|
|
102
115
|
</>
|
|
103
116
|
);
|
|
@@ -106,5 +119,6 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
|
|
|
106
119
|
AdvancedFields.propTypes = {
|
|
107
120
|
advancedValues: PropTypes.object.isRequired,
|
|
108
121
|
setAdvancedValues: PropTypes.func.isRequired,
|
|
122
|
+
templateValues: PropTypes.object.isRequired,
|
|
109
123
|
};
|
|
110
124
|
export default AdvancedFields;
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { useSelector } from 'react-redux';
|
|
2
3
|
import PropTypes from 'prop-types';
|
|
3
|
-
import { FormGroup, TextInput, Button } from '@patternfly/react-core';
|
|
4
|
+
import { FormGroup, TextInput, Tooltip, Button } from '@patternfly/react-core';
|
|
4
5
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
6
|
+
import {
|
|
7
|
+
selectTemplateInputs,
|
|
8
|
+
selectAdvancedTemplateInputs,
|
|
9
|
+
} from '../../JobWizardSelectors';
|
|
5
10
|
|
|
6
|
-
export const DescriptionField = ({
|
|
7
|
-
const
|
|
11
|
+
export const DescriptionField = ({ inputValues, value, setValue }) => {
|
|
12
|
+
const inputs = [
|
|
13
|
+
...useSelector(selectTemplateInputs),
|
|
14
|
+
...useSelector(selectAdvancedTemplateInputs),
|
|
15
|
+
].map(input => input.name);
|
|
16
|
+
const generateDesc = useCallback(() => {
|
|
8
17
|
let newDesc = value;
|
|
9
18
|
if (value) {
|
|
10
19
|
const re = new RegExp('%\\{([^\\}]+)\\}', 'gm');
|
|
@@ -15,16 +24,19 @@ export const DescriptionField = ({ inputs, value, setValue }) => {
|
|
|
15
24
|
results.forEach(result => {
|
|
16
25
|
newDesc = newDesc.replace(
|
|
17
26
|
result.text,
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
inputValues[result.name] ||
|
|
28
|
+
(inputs.includes(result.name) ? '' : result.text)
|
|
20
29
|
);
|
|
21
30
|
});
|
|
22
31
|
}
|
|
23
32
|
return newDesc;
|
|
24
|
-
};
|
|
33
|
+
}, [inputs, value, inputValues]);
|
|
25
34
|
const [generatedDesc, setGeneratedDesc] = useState(generateDesc());
|
|
26
35
|
const [isPreview, setIsPreview] = useState(true);
|
|
27
36
|
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
setGeneratedDesc(generateDesc());
|
|
39
|
+
}, [generateDesc]);
|
|
28
40
|
const togglePreview = () => {
|
|
29
41
|
setGeneratedDesc(generateDesc());
|
|
30
42
|
setIsPreview(v => !v);
|
|
@@ -43,9 +55,20 @@ export const DescriptionField = ({ inputs, value, setValue }) => {
|
|
|
43
55
|
}
|
|
44
56
|
>
|
|
45
57
|
{isPreview ? (
|
|
46
|
-
<
|
|
58
|
+
<Tooltip content={generatedDesc}>
|
|
59
|
+
<div>
|
|
60
|
+
{/* div wrapper so the tooltip will be shown in chrome */}
|
|
61
|
+
<TextInput
|
|
62
|
+
aria-label="description preview"
|
|
63
|
+
id="description-preview"
|
|
64
|
+
value={generatedDesc}
|
|
65
|
+
isDisabled
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
</Tooltip>
|
|
47
69
|
) : (
|
|
48
70
|
<TextInput
|
|
71
|
+
aria-label="description edit"
|
|
49
72
|
type="text"
|
|
50
73
|
autoComplete="description"
|
|
51
74
|
id="description"
|
|
@@ -58,7 +81,7 @@ export const DescriptionField = ({ inputs, value, setValue }) => {
|
|
|
58
81
|
};
|
|
59
82
|
|
|
60
83
|
DescriptionField.propTypes = {
|
|
61
|
-
|
|
84
|
+
inputValues: PropTypes.object.isRequired,
|
|
62
85
|
value: PropTypes.string,
|
|
63
86
|
setValue: PropTypes.func.isRequired,
|
|
64
87
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
import { FormGroup, TextInput } from '@patternfly/react-core';
|
|
3
|
+
import { FormGroup, TextInput, Radio } from '@patternfly/react-core';
|
|
4
4
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
5
|
import { helpLabel } from '../form/FormHelpers';
|
|
6
6
|
import { formatter } from '../form/Formatter';
|
|
@@ -18,6 +18,7 @@ export const EffectiveUserField = ({ value, setValue }) => (
|
|
|
18
18
|
fieldId="effective-user"
|
|
19
19
|
>
|
|
20
20
|
<TextInput
|
|
21
|
+
aria-label="effective user"
|
|
21
22
|
autoComplete="effective-user"
|
|
22
23
|
id="effective-user"
|
|
23
24
|
type="text"
|
|
@@ -61,6 +62,7 @@ export const PasswordField = ({ value, setValue }) => (
|
|
|
61
62
|
fieldId="job-password"
|
|
62
63
|
>
|
|
63
64
|
<TextInput
|
|
65
|
+
aria-label="job password"
|
|
64
66
|
autoComplete="new-password" // to prevent firefox from autofilling the user password
|
|
65
67
|
id="job-password"
|
|
66
68
|
type="password"
|
|
@@ -83,6 +85,7 @@ export const KeyPassphraseField = ({ value, setValue }) => (
|
|
|
83
85
|
fieldId="key-passphrase"
|
|
84
86
|
>
|
|
85
87
|
<TextInput
|
|
88
|
+
aria-label="key passphrase"
|
|
86
89
|
autoComplete="key-passphrase"
|
|
87
90
|
id="key-passphrase"
|
|
88
91
|
type="password"
|
|
@@ -105,6 +108,7 @@ export const EffectiveUserPasswordField = ({ value, setValue }) => (
|
|
|
105
108
|
fieldId="effective-user-password"
|
|
106
109
|
>
|
|
107
110
|
<TextInput
|
|
111
|
+
aria-label="effective userpassword"
|
|
108
112
|
autoComplete="effective-user-password"
|
|
109
113
|
id="effective-user-password"
|
|
110
114
|
type="password"
|
|
@@ -161,6 +165,41 @@ export const TimeSpanLevelField = ({ value, setValue }) => (
|
|
|
161
165
|
/>
|
|
162
166
|
);
|
|
163
167
|
|
|
168
|
+
export const ExecutionOrderingField = ({ isRandomizedOrdering, setValue }) => (
|
|
169
|
+
<FormGroup
|
|
170
|
+
label={__('Execution ordering')}
|
|
171
|
+
fieldId="schedule-type"
|
|
172
|
+
labelIcon={helpLabel(
|
|
173
|
+
<div
|
|
174
|
+
dangerouslySetInnerHTML={{
|
|
175
|
+
__html: __(
|
|
176
|
+
'Execution ordering determines whether the jobs should be executed on hosts in alphabetical order or in randomized order.<br><ul><li><b>Ordered</b> - executes the jobs on hosts in alphabetical order</li><li><b>Randomized</b> - randomizes the order in which jobs are executed on hosts</li></ul>'
|
|
177
|
+
),
|
|
178
|
+
}}
|
|
179
|
+
/>,
|
|
180
|
+
'effective-user-password'
|
|
181
|
+
)}
|
|
182
|
+
isInline
|
|
183
|
+
>
|
|
184
|
+
<Radio
|
|
185
|
+
aria-label="execution order alphabetical"
|
|
186
|
+
isChecked={!isRandomizedOrdering}
|
|
187
|
+
name="execution-order"
|
|
188
|
+
onChange={() => setValue(false)}
|
|
189
|
+
id="execution-order-alphabetical"
|
|
190
|
+
label={__('Alphabetical')}
|
|
191
|
+
/>
|
|
192
|
+
<Radio
|
|
193
|
+
aria-label="execution order randomized"
|
|
194
|
+
isChecked={isRandomizedOrdering}
|
|
195
|
+
name="execution-order"
|
|
196
|
+
onChange={() => setValue(true)}
|
|
197
|
+
id="execution-order-randomized"
|
|
198
|
+
label={__('Randomized')}
|
|
199
|
+
/>
|
|
200
|
+
</FormGroup>
|
|
201
|
+
);
|
|
202
|
+
|
|
164
203
|
export const TemplateInputsFields = ({ inputs, value, setValue }) => (
|
|
165
204
|
<>{inputs?.map(input => formatter(input, value, setValue))}</>
|
|
166
205
|
);
|
|
@@ -184,6 +223,14 @@ ConcurrencyLevelField.propTypes = EffectiveUserField.propTypes;
|
|
|
184
223
|
ConcurrencyLevelField.defaultProps = EffectiveUserField.defaultProps;
|
|
185
224
|
TimeSpanLevelField.propTypes = EffectiveUserField.propTypes;
|
|
186
225
|
TimeSpanLevelField.defaultProps = EffectiveUserField.defaultProps;
|
|
226
|
+
ExecutionOrderingField.propTypes = {
|
|
227
|
+
isRandomizedOrdering: PropTypes.bool,
|
|
228
|
+
setValue: PropTypes.func.isRequired,
|
|
229
|
+
};
|
|
230
|
+
ExecutionOrderingField.defaultProps = {
|
|
231
|
+
isRandomizedOrdering: false,
|
|
232
|
+
};
|
|
233
|
+
|
|
187
234
|
TemplateInputsFields.propTypes = {
|
|
188
235
|
inputs: PropTypes.array.isRequired,
|
|
189
236
|
value: PropTypes.object,
|
|
@@ -1,39 +1,38 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
import React from 'react';
|
|
2
3
|
import { Provider } from 'react-redux';
|
|
3
4
|
import { mount } from '@theforeman/test';
|
|
4
5
|
import { fireEvent, screen, render, act } from '@testing-library/react';
|
|
6
|
+
import { MockedProvider } from '@apollo/client/testing';
|
|
5
7
|
import * as api from 'foremanReact/redux/API';
|
|
6
8
|
import { JobWizard } from '../../../JobWizard';
|
|
7
9
|
import * as selectors from '../../../JobWizardSelectors';
|
|
8
10
|
import {
|
|
9
|
-
jobTemplateResponse
|
|
11
|
+
jobTemplateResponse,
|
|
12
|
+
jobTemplate,
|
|
10
13
|
testSetup,
|
|
11
14
|
mockApi,
|
|
15
|
+
jobCategories,
|
|
16
|
+
gqlMock,
|
|
12
17
|
} from '../../../__tests__/fixtures';
|
|
18
|
+
import { WIZARD_TITLES } from '../../../JobWizardConstants';
|
|
13
19
|
|
|
14
20
|
const store = testSetup(selectors, api);
|
|
15
21
|
mockApi(api);
|
|
16
22
|
|
|
17
23
|
jest.spyOn(selectors, 'selectEffectiveUser');
|
|
18
|
-
jest.spyOn(selectors, 'selectTemplateInputs');
|
|
19
|
-
jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
|
|
20
24
|
|
|
21
25
|
selectors.selectEffectiveUser.mockImplementation(
|
|
22
|
-
() =>
|
|
23
|
-
);
|
|
24
|
-
selectors.selectTemplateInputs.mockImplementation(
|
|
25
|
-
() => jobTemplate.template_inputs
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
selectors.selectAdvancedTemplateInputs.mockImplementation(
|
|
29
|
-
() => jobTemplate.advanced_template_inputs
|
|
26
|
+
() => jobTemplateResponse.effective_user
|
|
30
27
|
);
|
|
31
28
|
describe('AdvancedFields', () => {
|
|
32
29
|
it('should save data between steps for advanced fields', async () => {
|
|
33
30
|
const wrapper = mount(
|
|
34
|
-
<
|
|
35
|
-
<
|
|
36
|
-
|
|
31
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
32
|
+
<Provider store={store}>
|
|
33
|
+
<JobWizard />
|
|
34
|
+
</Provider>
|
|
35
|
+
</MockedProvider>
|
|
37
36
|
);
|
|
38
37
|
// setup
|
|
39
38
|
wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
|
|
@@ -72,7 +71,7 @@ describe('AdvancedFields', () => {
|
|
|
72
71
|
.simulate('click');
|
|
73
72
|
|
|
74
73
|
expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-current').text()).toEqual(
|
|
75
|
-
'Target
|
|
74
|
+
'Target hosts and inputs'
|
|
76
75
|
);
|
|
77
76
|
wrapper
|
|
78
77
|
.find('.pf-c-wizard__nav-link')
|
|
@@ -86,12 +85,14 @@ describe('AdvancedFields', () => {
|
|
|
86
85
|
});
|
|
87
86
|
it('fill template fields', async () => {
|
|
88
87
|
render(
|
|
89
|
-
<
|
|
90
|
-
<
|
|
91
|
-
|
|
88
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
89
|
+
<Provider store={store}>
|
|
90
|
+
<JobWizard />
|
|
91
|
+
</Provider>
|
|
92
|
+
</MockedProvider>
|
|
92
93
|
);
|
|
93
94
|
await act(async () => {
|
|
94
|
-
fireEvent.click(screen.getByText(
|
|
95
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
95
96
|
});
|
|
96
97
|
const searchValue = 'search test';
|
|
97
98
|
const textValue = 'I am a text';
|
|
@@ -99,7 +100,10 @@ describe('AdvancedFields', () => {
|
|
|
99
100
|
const textField = screen.getByLabelText('adv plain hidden', {
|
|
100
101
|
selector: 'textarea',
|
|
101
102
|
});
|
|
102
|
-
const selectField = screen.
|
|
103
|
+
const selectField = screen.getByLabelText('adv plain select toggle');
|
|
104
|
+
const resourceSelectField = screen.getByLabelText(
|
|
105
|
+
'adv resource select toggle'
|
|
106
|
+
);
|
|
103
107
|
const searchField = screen.getByPlaceholderText('Filter...');
|
|
104
108
|
const dateField = screen.getByLabelText('adv date', {
|
|
105
109
|
selector: 'input',
|
|
@@ -108,7 +112,11 @@ describe('AdvancedFields', () => {
|
|
|
108
112
|
fireEvent.click(selectField);
|
|
109
113
|
await act(async () => {
|
|
110
114
|
await fireEvent.click(screen.getByText('option 2'));
|
|
111
|
-
fireEvent.click(screen.getAllByText(
|
|
115
|
+
fireEvent.click(screen.getAllByText(WIZARD_TITLES.advanced)[0]); // to remove focus
|
|
116
|
+
|
|
117
|
+
fireEvent.click(resourceSelectField);
|
|
118
|
+
await fireEvent.click(screen.getByText('resource2'));
|
|
119
|
+
|
|
112
120
|
await fireEvent.change(textField, {
|
|
113
121
|
target: { value: textValue },
|
|
114
122
|
});
|
|
@@ -128,9 +136,11 @@ describe('AdvancedFields', () => {
|
|
|
128
136
|
expect(searchField.value).toBe(searchValue);
|
|
129
137
|
expect(dateField.value).toBe(dateValue);
|
|
130
138
|
await act(async () => {
|
|
131
|
-
fireEvent.click(screen.getByText(
|
|
139
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.categoryAndTemplate));
|
|
132
140
|
});
|
|
133
|
-
expect(screen.getAllByText(
|
|
141
|
+
expect(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)).toHaveLength(
|
|
142
|
+
3
|
|
143
|
+
);
|
|
134
144
|
|
|
135
145
|
await act(async () => {
|
|
136
146
|
fireEvent.click(screen.getByText('Advanced Fields'));
|
|
@@ -140,5 +150,214 @@ describe('AdvancedFields', () => {
|
|
|
140
150
|
expect(dateField.value).toBe(dateValue);
|
|
141
151
|
expect(screen.queryAllByText('option 1')).toHaveLength(0);
|
|
142
152
|
expect(screen.queryAllByText('option 2')).toHaveLength(1);
|
|
153
|
+
expect(screen.queryAllByDisplayValue('resource1')).toHaveLength(0);
|
|
154
|
+
expect(screen.queryAllByDisplayValue('resource2')).toHaveLength(1);
|
|
155
|
+
});
|
|
156
|
+
it('fill defaults into fields', async () => {
|
|
157
|
+
render(
|
|
158
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
159
|
+
<Provider store={store}>
|
|
160
|
+
<JobWizard />
|
|
161
|
+
</Provider>
|
|
162
|
+
</MockedProvider>
|
|
163
|
+
);
|
|
164
|
+
await act(async () => {
|
|
165
|
+
fireEvent.click(screen.getByText('Advanced Fields'));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(
|
|
169
|
+
screen.getByLabelText('effective user', {
|
|
170
|
+
selector: 'input',
|
|
171
|
+
}).value
|
|
172
|
+
).toBe('default effective user');
|
|
173
|
+
expect(
|
|
174
|
+
screen.getByLabelText('timeout to kill', {
|
|
175
|
+
selector: 'input',
|
|
176
|
+
}).value
|
|
177
|
+
).toBe('2');
|
|
178
|
+
|
|
179
|
+
expect(
|
|
180
|
+
screen.getByLabelText('description preview', {
|
|
181
|
+
selector: 'input',
|
|
182
|
+
}).value
|
|
183
|
+
).toBe(
|
|
184
|
+
'template1 with inputs adv plain hidden="Default val" adv plain select="" adv resource select="" adv search="" adv date="" plain hidden="Default val"'
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
it('DescriptionField', async () => {
|
|
188
|
+
render(
|
|
189
|
+
<Provider store={store}>
|
|
190
|
+
<JobWizard />
|
|
191
|
+
</Provider>
|
|
192
|
+
);
|
|
193
|
+
await act(async () => {
|
|
194
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const textField = screen.getByLabelText('adv plain hidden', {
|
|
198
|
+
selector: 'textarea',
|
|
199
|
+
});
|
|
200
|
+
await act(async () => {
|
|
201
|
+
await fireEvent.change(textField, {
|
|
202
|
+
target: { value: 'test command' },
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
const descriptionValue = 'Run %{adv plain hidden} %{wrong command name}';
|
|
206
|
+
|
|
207
|
+
await act(async () => {
|
|
208
|
+
fireEvent.click(screen.getByText('Edit job description template'));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const editText = screen.getByLabelText('description edit', {
|
|
212
|
+
selector: 'input',
|
|
213
|
+
});
|
|
214
|
+
await fireEvent.change(editText, {
|
|
215
|
+
target: { value: descriptionValue },
|
|
216
|
+
});
|
|
217
|
+
await act(async () => {
|
|
218
|
+
fireEvent.click(screen.getByText('Preview job description'));
|
|
219
|
+
});
|
|
220
|
+
expect(
|
|
221
|
+
screen.getByLabelText('description preview', {
|
|
222
|
+
selector: 'input',
|
|
223
|
+
}).value
|
|
224
|
+
).toBe('Run test command %{wrong command name}');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('DescriptionField with no inputs', async () => {
|
|
228
|
+
jest.spyOn(api, 'get');
|
|
229
|
+
|
|
230
|
+
jest.spyOn(selectors, 'selectTemplateInputs');
|
|
231
|
+
jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
|
|
232
|
+
selectors.selectTemplateInputs.mockImplementation(() => []);
|
|
233
|
+
selectors.selectAdvancedTemplateInputs.mockImplementation(() => []);
|
|
234
|
+
api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
|
235
|
+
if (action.key === 'JOB_CATEGORIES') {
|
|
236
|
+
handleSuccess &&
|
|
237
|
+
handleSuccess({ data: { job_categories: jobCategories } });
|
|
238
|
+
} else if (action.key === 'JOB_TEMPLATE') {
|
|
239
|
+
handleSuccess &&
|
|
240
|
+
handleSuccess({
|
|
241
|
+
data: {
|
|
242
|
+
...jobTemplateResponse,
|
|
243
|
+
advanced_template_inputs: [],
|
|
244
|
+
template_inputs: [],
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
} else if (action.key === 'JOB_TEMPLATES') {
|
|
248
|
+
handleSuccess &&
|
|
249
|
+
handleSuccess({
|
|
250
|
+
data: { results: [jobTemplate] },
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return { type: 'get', ...action };
|
|
254
|
+
});
|
|
255
|
+
render(
|
|
256
|
+
<Provider store={store}>
|
|
257
|
+
<JobWizard />
|
|
258
|
+
</Provider>
|
|
259
|
+
);
|
|
260
|
+
await act(async () => {
|
|
261
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
262
|
+
});
|
|
263
|
+
expect(
|
|
264
|
+
screen.getByLabelText('description preview', {
|
|
265
|
+
selector: 'input',
|
|
266
|
+
}).value
|
|
267
|
+
).toBe('template1');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('DescriptionField with description_format', async () => {
|
|
271
|
+
jest.spyOn(api, 'get');
|
|
272
|
+
jest.spyOn(selectors, 'selectTemplateInputs');
|
|
273
|
+
selectors.selectTemplateInputs.mockImplementation(() => [
|
|
274
|
+
{
|
|
275
|
+
name: 'command',
|
|
276
|
+
required: true,
|
|
277
|
+
input_type: 'user',
|
|
278
|
+
description: 'some Description',
|
|
279
|
+
advanced: true,
|
|
280
|
+
value_type: 'plain',
|
|
281
|
+
resource_type: 'ansible_roles',
|
|
282
|
+
default: 'Default val',
|
|
283
|
+
hidden_value: true,
|
|
284
|
+
},
|
|
285
|
+
]);
|
|
286
|
+
api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
|
287
|
+
if (action.key === 'JOB_CATEGORIES') {
|
|
288
|
+
handleSuccess &&
|
|
289
|
+
handleSuccess({ data: { job_categories: jobCategories } });
|
|
290
|
+
} else if (action.key === 'JOB_TEMPLATE') {
|
|
291
|
+
handleSuccess &&
|
|
292
|
+
handleSuccess({
|
|
293
|
+
data: {
|
|
294
|
+
...jobTemplateResponse,
|
|
295
|
+
job_template: {
|
|
296
|
+
...jobTemplateResponse.jobTemplate,
|
|
297
|
+
description_format: 'Run %{command}',
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
template_inputs: [
|
|
301
|
+
{
|
|
302
|
+
name: 'command',
|
|
303
|
+
required: true,
|
|
304
|
+
input_type: 'user',
|
|
305
|
+
description: 'some Description',
|
|
306
|
+
advanced: true,
|
|
307
|
+
value_type: 'plain',
|
|
308
|
+
resource_type: 'ansible_roles',
|
|
309
|
+
default: 'Default val',
|
|
310
|
+
hidden_value: true,
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
} else if (action.key === 'JOB_TEMPLATES') {
|
|
316
|
+
handleSuccess &&
|
|
317
|
+
handleSuccess({
|
|
318
|
+
data: { results: [jobTemplate] },
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return { type: 'get', ...action };
|
|
322
|
+
});
|
|
323
|
+
render(
|
|
324
|
+
<Provider store={store}>
|
|
325
|
+
<JobWizard />
|
|
326
|
+
</Provider>
|
|
327
|
+
);
|
|
328
|
+
await act(async () => {
|
|
329
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
330
|
+
});
|
|
331
|
+
expect(
|
|
332
|
+
screen.getByLabelText('description preview', {
|
|
333
|
+
selector: 'input',
|
|
334
|
+
}).value
|
|
335
|
+
).toBe('Run Default val');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('search resources action', async () => {
|
|
339
|
+
jest.useFakeTimers();
|
|
340
|
+
mockApi(api);
|
|
341
|
+
const newStore = testSetup(selectors, api);
|
|
342
|
+
render(
|
|
343
|
+
<Provider store={newStore}>
|
|
344
|
+
<JobWizard />
|
|
345
|
+
</Provider>
|
|
346
|
+
);
|
|
347
|
+
await act(async () => {
|
|
348
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
349
|
+
});
|
|
350
|
+
const resourceSelectField = screen.getByLabelText(
|
|
351
|
+
'adv resource select typeahead input'
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
await act(async () => {
|
|
355
|
+
await fireEvent.change(resourceSelectField, {
|
|
356
|
+
target: { value: 'some search' },
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
await jest.runAllTimers();
|
|
360
|
+
});
|
|
361
|
+
expect(newStore.getActions()).toMatchSnapshot('resource search');
|
|
143
362
|
});
|
|
144
363
|
});
|
data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`AdvancedFields search resources action: resource search 1`] = `
|
|
4
|
+
Array [
|
|
5
|
+
Object {
|
|
6
|
+
"key": "JOB_CATEGORIES",
|
|
7
|
+
"type": "get",
|
|
8
|
+
"url": "/ui_job_wizard/categories",
|
|
9
|
+
},
|
|
10
|
+
Object {
|
|
11
|
+
"key": "JOB_TEMPLATES",
|
|
12
|
+
"type": "get",
|
|
13
|
+
"url": URI {
|
|
14
|
+
"_deferred_build": true,
|
|
15
|
+
"_parts": Object {
|
|
16
|
+
"duplicateQueryParameters": false,
|
|
17
|
+
"escapeQuerySpace": true,
|
|
18
|
+
"fragment": null,
|
|
19
|
+
"hostname": null,
|
|
20
|
+
"password": null,
|
|
21
|
+
"path": "foreman/api/v2/job_templates",
|
|
22
|
+
"port": null,
|
|
23
|
+
"preventInvalidHostname": false,
|
|
24
|
+
"protocol": null,
|
|
25
|
+
"query": "search=job_category%3D%22Ansible+Commands%22&per_page=all",
|
|
26
|
+
"urn": null,
|
|
27
|
+
"username": null,
|
|
28
|
+
},
|
|
29
|
+
"_string": "",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
Object {
|
|
33
|
+
"key": "JOB_TEMPLATE",
|
|
34
|
+
"type": "get",
|
|
35
|
+
"url": "/ui_job_wizard/template/178",
|
|
36
|
+
},
|
|
37
|
+
Object {
|
|
38
|
+
"key": "ForemanTasksTask",
|
|
39
|
+
"type": "get",
|
|
40
|
+
"url": URI {
|
|
41
|
+
"_deferred_build": true,
|
|
42
|
+
"_parts": Object {
|
|
43
|
+
"duplicateQueryParameters": false,
|
|
44
|
+
"escapeQuerySpace": true,
|
|
45
|
+
"fragment": null,
|
|
46
|
+
"hostname": null,
|
|
47
|
+
"password": null,
|
|
48
|
+
"path": "/ui_job_wizard/resources",
|
|
49
|
+
"port": null,
|
|
50
|
+
"preventInvalidHostname": false,
|
|
51
|
+
"protocol": null,
|
|
52
|
+
"query": "resource=ForemanTasks%3A%3ATask&name=",
|
|
53
|
+
"urn": null,
|
|
54
|
+
"username": null,
|
|
55
|
+
},
|
|
56
|
+
"_string": "",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
Object {
|
|
60
|
+
"key": "ForemanTasksTask",
|
|
61
|
+
"type": "get",
|
|
62
|
+
"url": URI {
|
|
63
|
+
"_deferred_build": true,
|
|
64
|
+
"_parts": Object {
|
|
65
|
+
"duplicateQueryParameters": false,
|
|
66
|
+
"escapeQuerySpace": true,
|
|
67
|
+
"fragment": null,
|
|
68
|
+
"hostname": null,
|
|
69
|
+
"password": null,
|
|
70
|
+
"path": "/ui_job_wizard/resources",
|
|
71
|
+
"port": null,
|
|
72
|
+
"preventInvalidHostname": false,
|
|
73
|
+
"protocol": null,
|
|
74
|
+
"query": "resource=ForemanTasks%3A%3ATask&name=some+search",
|
|
75
|
+
"urn": null,
|
|
76
|
+
"username": null,
|
|
77
|
+
},
|
|
78
|
+
"_string": "",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
`;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
import {
|
|
3
|
+
import { Text, TextVariants, Form, Alert } from '@patternfly/react-core';
|
|
4
4
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
5
|
import { SelectField } from '../form/SelectField';
|
|
6
6
|
import { GroupedSelectField } from '../form/GroupedSelectField';
|
|
7
|
+
import { WizardTitle } from '../form/WizardTitle';
|
|
8
|
+
import { WIZARD_TITLES } from '../../JobWizardConstants';
|
|
7
9
|
|
|
8
10
|
export const CategoryAndTemplate = ({
|
|
9
11
|
jobCategories,
|
|
@@ -40,7 +42,7 @@ export const CategoryAndTemplate = ({
|
|
|
40
42
|
const isError = !!(categoryError || allTemplatesError || templateError);
|
|
41
43
|
return (
|
|
42
44
|
<>
|
|
43
|
-
<
|
|
45
|
+
<WizardTitle title={WIZARD_TITLES.categoryAndTemplate} />
|
|
44
46
|
<Text component={TextVariants.p}>{__('All fields are required.')}</Text>
|
|
45
47
|
<Form>
|
|
46
48
|
<SelectField
|
|
@@ -51,6 +53,7 @@ export const CategoryAndTemplate = ({
|
|
|
51
53
|
value={selectedCategory}
|
|
52
54
|
placeholderText={categoryError ? __('Error') : ''}
|
|
53
55
|
isDisabled={!!categoryError}
|
|
56
|
+
isRequired
|
|
54
57
|
/>
|
|
55
58
|
<GroupedSelectField
|
|
56
59
|
label={__('Job template')}
|