foreman_remote_execution 4.5.5 → 4.5.6
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/app/models/job_invocation_composer.rb +1 -1
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/webpack/JobWizard/JobWizard.js +42 -19
- data/webpack/JobWizard/JobWizard.scss +19 -0
- data/webpack/JobWizard/JobWizardConstants.js +8 -0
- data/webpack/JobWizard/__tests__/fixtures.js +8 -0
- data/webpack/JobWizard/__tests__/integration.test.js +3 -7
- data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +16 -5
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +29 -14
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +4 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
- data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +25 -0
- data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +37 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +50 -0
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +66 -0
- data/webpack/JobWizard/steps/Schedule/index.js +25 -22
- data/webpack/JobWizard/steps/form/Formatter.js +10 -9
- data/webpack/JobWizard/steps/form/NumberInput.js +2 -0
- data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f14506c337ce696f794a63b0c30b59f6d301edc4d55b9d654e2b5360215beb9
|
4
|
+
data.tar.gz: 0b9d86b4a422b84d507dfd977dea5dd9f2dc8b30e464b3e4383c8ea421e0615b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 250ec14568078576a42664e40e2d96b974282765e6cfcffca8ec58bdff9e72c8948f2e28e7ba5fdafe36fb45457e2f38c03bd3c39842ba0bebaa303970ea5f09
|
7
|
+
data.tar.gz: 4f6469e5ad8535b7996d1a8c78d266edbba4c03e1e3eb9b52e0588c653351e153b87b4d6daaf082e039d7af47207fdf5e3e6267920cde4d2a233c4e11ac5ca57
|
@@ -636,7 +636,7 @@ class JobInvocationComposer
|
|
636
636
|
setting_value = Setting['remote_execution_form_job_template']
|
637
637
|
return default_value unless setting_value
|
638
638
|
|
639
|
-
form_template = JobTemplate.find_by :name => setting_value
|
639
|
+
form_template = JobTemplate.authorized(:view_job_templates).find_by :name => setting_value
|
640
640
|
return default_value unless form_template
|
641
641
|
|
642
642
|
if block_given?
|
@@ -3,43 +3,59 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|
3
3
|
import { useDispatch, useSelector } from 'react-redux';
|
4
4
|
import { Wizard } from '@patternfly/react-core';
|
5
5
|
import { get } from 'foremanReact/redux/API';
|
6
|
-
import { translate as __ } from 'foremanReact/common/I18n';
|
7
6
|
import history from 'foremanReact/history';
|
8
7
|
import CategoryAndTemplate from './steps/CategoryAndTemplate/';
|
9
8
|
import { AdvancedFields } from './steps/AdvancedFields/AdvancedFields';
|
10
|
-
import { JOB_TEMPLATE } from './JobWizardConstants';
|
9
|
+
import { JOB_TEMPLATE, WIZARD_TITLES } from './JobWizardConstants';
|
11
10
|
import { selectTemplateError } from './JobWizardSelectors';
|
12
11
|
import Schedule from './steps/Schedule/';
|
12
|
+
import HostsAndInputs from './steps/HostsAndInputs/';
|
13
13
|
import './JobWizard.scss';
|
14
14
|
|
15
15
|
export const JobWizard = () => {
|
16
16
|
const [jobTemplateID, setJobTemplateID] = useState(null);
|
17
17
|
const [category, setCategory] = useState('');
|
18
18
|
const [advancedValues, setAdvancedValues] = useState({});
|
19
|
+
const [templateValues, setTemplateValues] = useState({}); // TODO use templateValues in advanced fields - description https://github.com/theforeman/foreman_remote_execution/pull/605
|
20
|
+
const [selectedHosts, setSelectedHosts] = useState(['host1', 'host2']);
|
19
21
|
const dispatch = useDispatch();
|
20
22
|
|
21
23
|
const setDefaults = useCallback(
|
22
24
|
({
|
23
25
|
data: {
|
26
|
+
template_inputs,
|
24
27
|
advanced_template_inputs,
|
25
28
|
effective_user,
|
26
|
-
job_template: {
|
29
|
+
job_template: { execution_timeout_interval, description_format },
|
27
30
|
},
|
28
31
|
}) => {
|
29
32
|
const advancedTemplateValues = {};
|
33
|
+
const defaultTemplateValues = {};
|
34
|
+
const inputs = template_inputs;
|
30
35
|
const advancedInputs = advanced_template_inputs;
|
31
|
-
if (
|
32
|
-
|
33
|
-
|
36
|
+
if (inputs) {
|
37
|
+
setTemplateValues(() => {
|
38
|
+
inputs.forEach(input => {
|
39
|
+
defaultTemplateValues[input.name] = input?.default || '';
|
40
|
+
});
|
41
|
+
return defaultTemplateValues;
|
34
42
|
});
|
35
43
|
}
|
36
|
-
setAdvancedValues(currentAdvancedValues =>
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
44
|
+
setAdvancedValues(currentAdvancedValues => {
|
45
|
+
if (advancedInputs) {
|
46
|
+
advancedInputs.forEach(input => {
|
47
|
+
advancedTemplateValues[input.name] = input?.default || '';
|
48
|
+
});
|
49
|
+
}
|
50
|
+
return {
|
51
|
+
...currentAdvancedValues,
|
52
|
+
effectiveUserValue: effective_user?.value || '',
|
53
|
+
timeoutToKill: execution_timeout_interval || '',
|
54
|
+
templateValues: advancedTemplateValues,
|
55
|
+
description: description_format || '',
|
56
|
+
isRandomizedOrdering: false,
|
57
|
+
};
|
58
|
+
});
|
43
59
|
},
|
44
60
|
[]
|
45
61
|
);
|
@@ -59,7 +75,7 @@ export const JobWizard = () => {
|
|
59
75
|
const isTemplate = !templateError && !!jobTemplateID;
|
60
76
|
const steps = [
|
61
77
|
{
|
62
|
-
name:
|
78
|
+
name: WIZARD_TITLES.categoryAndTemplate,
|
63
79
|
component: (
|
64
80
|
<CategoryAndTemplate
|
65
81
|
jobTemplate={jobTemplateID}
|
@@ -70,12 +86,19 @@ export const JobWizard = () => {
|
|
70
86
|
),
|
71
87
|
},
|
72
88
|
{
|
73
|
-
name:
|
74
|
-
component:
|
89
|
+
name: WIZARD_TITLES.hostsAndInputs,
|
90
|
+
component: (
|
91
|
+
<HostsAndInputs
|
92
|
+
templateValues={templateValues}
|
93
|
+
setTemplateValues={setTemplateValues}
|
94
|
+
selectedHosts={selectedHosts}
|
95
|
+
setSelectedHosts={setSelectedHosts}
|
96
|
+
/>
|
97
|
+
),
|
75
98
|
canJumpTo: isTemplate,
|
76
99
|
},
|
77
100
|
{
|
78
|
-
name:
|
101
|
+
name: WIZARD_TITLES.advanced,
|
79
102
|
component: (
|
80
103
|
<AdvancedFields
|
81
104
|
advancedValues={advancedValues}
|
@@ -91,12 +114,12 @@ export const JobWizard = () => {
|
|
91
114
|
canJumpTo: isTemplate,
|
92
115
|
},
|
93
116
|
{
|
94
|
-
name:
|
117
|
+
name: WIZARD_TITLES.schedule,
|
95
118
|
component: <Schedule />,
|
96
119
|
canJumpTo: isTemplate,
|
97
120
|
},
|
98
121
|
{
|
99
|
-
name:
|
122
|
+
name: WIZARD_TITLES.review,
|
100
123
|
component: <p>Review Details</p>,
|
101
124
|
nextButtonText: 'Run',
|
102
125
|
canJumpTo: isTemplate,
|
@@ -1,5 +1,10 @@
|
|
1
1
|
.job-wizard {
|
2
|
+
.wizard-title {
|
3
|
+
margin-bottom: 25px;
|
4
|
+
}
|
5
|
+
|
2
6
|
.pf-c-wizard__main {
|
7
|
+
overflow: visible;
|
3
8
|
z-index: calc(
|
4
9
|
var(--pf-c-wizard__footer--ZIndex) + 1
|
5
10
|
); // So the select box can be shown above the wizard footer
|
@@ -12,6 +17,9 @@
|
|
12
17
|
}
|
13
18
|
#advanced-fields-job-template {
|
14
19
|
.foreman-search-field {
|
20
|
+
.rbt-input-hint input{
|
21
|
+
display: none;
|
22
|
+
}
|
15
23
|
// Giving pf3 search bar a pf4 look
|
16
24
|
.search-bar {
|
17
25
|
display: block;
|
@@ -41,6 +49,13 @@
|
|
41
49
|
}
|
42
50
|
}
|
43
51
|
|
52
|
+
.hosts-chip-group {
|
53
|
+
margin-top: 8px;
|
54
|
+
}
|
55
|
+
input[type='radio'],
|
56
|
+
input[type='checkbox'] {
|
57
|
+
margin: 0;
|
58
|
+
}
|
44
59
|
.schedule-tab {
|
45
60
|
input[type='radio'],
|
46
61
|
input[type='checkbox'] {
|
@@ -50,4 +65,8 @@
|
|
50
65
|
text-align: start;
|
51
66
|
}
|
52
67
|
}
|
68
|
+
textarea {
|
69
|
+
min-height: 40px;
|
70
|
+
min-width: 100px;
|
71
|
+
}
|
53
72
|
}
|
@@ -14,3 +14,11 @@ export const repeatTypes = {
|
|
14
14
|
daily: __('Daily'),
|
15
15
|
hourly: __('Hourly'),
|
16
16
|
};
|
17
|
+
|
18
|
+
export const WIZARD_TITLES = {
|
19
|
+
categoryAndTemplate: __('Category and Template'),
|
20
|
+
hostsAndInputs: __('Target hosts and inputs'),
|
21
|
+
advanced: __('Advanced Fields'),
|
22
|
+
schedule: __('Schedule'),
|
23
|
+
review: __('Review Details'),
|
24
|
+
};
|
@@ -93,6 +93,14 @@ export const testSetup = (selectors, api) => {
|
|
93
93
|
jest.spyOn(selectors, 'selectJobCategories');
|
94
94
|
jest.spyOn(selectors, 'selectJobCategoriesStatus');
|
95
95
|
|
96
|
+
jest.spyOn(selectors, 'selectTemplateInputs');
|
97
|
+
jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
|
98
|
+
selectors.selectTemplateInputs.mockImplementation(
|
99
|
+
() => jobTemplateResponse.template_inputs
|
100
|
+
);
|
101
|
+
selectors.selectAdvancedTemplateInputs.mockImplementation(
|
102
|
+
() => jobTemplateResponse.advanced_template_inputs
|
103
|
+
);
|
96
104
|
selectors.selectJobCategories.mockImplementation(() => jobCategories);
|
97
105
|
selectors.selectJobTemplates.mockImplementation(() => [
|
98
106
|
jobTemplate,
|
@@ -5,6 +5,7 @@ import { render, fireEvent, screen, act } from '@testing-library/react';
|
|
5
5
|
import * as api from 'foremanReact/redux/API';
|
6
6
|
import { JobWizard } from '../JobWizard';
|
7
7
|
import * as selectors from '../JobWizardSelectors';
|
8
|
+
import { WIZARD_TITLES } from '../JobWizardConstants';
|
8
9
|
import {
|
9
10
|
testSetup,
|
10
11
|
mockApi,
|
@@ -62,13 +63,8 @@ describe('Job wizard fill', () => {
|
|
62
63
|
<JobWizard />
|
63
64
|
</Provider>
|
64
65
|
);
|
65
|
-
const
|
66
|
-
|
67
|
-
'Advanced Fields',
|
68
|
-
'Schedule',
|
69
|
-
'Review Details',
|
70
|
-
'Category and Template',
|
71
|
-
];
|
66
|
+
const titles = Object.values(WIZARD_TITLES);
|
67
|
+
const steps = [titles[1], titles[0], ...titles.slice(2)]; // the first title is selected at the beggining
|
72
68
|
// eslint-disable-next-line no-unused-vars
|
73
69
|
for await (const step of steps) {
|
74
70
|
const stepSelector = screen.getByText(step);
|
@@ -1,8 +1,7 @@
|
|
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,
|
@@ -17,8 +16,11 @@ import {
|
|
17
16
|
ConcurrencyLevelField,
|
18
17
|
TimeSpanLevelField,
|
19
18
|
TemplateInputsFields,
|
19
|
+
ExecutionOrderingField,
|
20
20
|
} from './Fields';
|
21
21
|
import { DescriptionField } from './DescriptionField';
|
22
|
+
import { WIZARD_TITLES } from '../../JobWizardConstants';
|
23
|
+
import { WizardTitle } from '../form/WizardTitle';
|
22
24
|
|
23
25
|
export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
|
24
26
|
const effectiveUser = useSelector(selectEffectiveUser);
|
@@ -26,9 +28,10 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
|
|
26
28
|
const templateInputs = useSelector(selectTemplateInputs);
|
27
29
|
return (
|
28
30
|
<>
|
29
|
-
<
|
30
|
-
{
|
31
|
-
|
31
|
+
<WizardTitle
|
32
|
+
title={WIZARD_TITLES.advanced}
|
33
|
+
className="advanced-fields-title"
|
34
|
+
/>
|
32
35
|
<Form id="advanced-fields-job-template" autoComplete="off">
|
33
36
|
<TemplateInputsFields
|
34
37
|
inputs={advancedTemplateInputs}
|
@@ -98,6 +101,14 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
|
|
98
101
|
})
|
99
102
|
}
|
100
103
|
/>
|
104
|
+
<ExecutionOrderingField
|
105
|
+
isRandomizedOrdering={advancedValues.isRandomizedOrdering}
|
106
|
+
setValue={newValue =>
|
107
|
+
setAdvancedValues({
|
108
|
+
isRandomizedOrdering: newValue,
|
109
|
+
})
|
110
|
+
}
|
111
|
+
/>
|
101
112
|
</Form>
|
102
113
|
</>
|
103
114
|
);
|
@@ -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,
|
@@ -10,24 +10,16 @@ import {
|
|
10
10
|
testSetup,
|
11
11
|
mockApi,
|
12
12
|
} from '../../../__tests__/fixtures';
|
13
|
+
import { WIZARD_TITLES } from '../../../JobWizardConstants';
|
13
14
|
|
14
15
|
const store = testSetup(selectors, api);
|
15
16
|
mockApi(api);
|
16
17
|
|
17
18
|
jest.spyOn(selectors, 'selectEffectiveUser');
|
18
|
-
jest.spyOn(selectors, 'selectTemplateInputs');
|
19
|
-
jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
|
20
19
|
|
21
20
|
selectors.selectEffectiveUser.mockImplementation(
|
22
21
|
() => jobTemplate.effective_user
|
23
22
|
);
|
24
|
-
selectors.selectTemplateInputs.mockImplementation(
|
25
|
-
() => jobTemplate.template_inputs
|
26
|
-
);
|
27
|
-
|
28
|
-
selectors.selectAdvancedTemplateInputs.mockImplementation(
|
29
|
-
() => jobTemplate.advanced_template_inputs
|
30
|
-
);
|
31
23
|
describe('AdvancedFields', () => {
|
32
24
|
it('should save data between steps for advanced fields', async () => {
|
33
25
|
const wrapper = mount(
|
@@ -72,7 +64,7 @@ describe('AdvancedFields', () => {
|
|
72
64
|
.simulate('click');
|
73
65
|
|
74
66
|
expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-current').text()).toEqual(
|
75
|
-
'Target
|
67
|
+
'Target hosts and inputs'
|
76
68
|
);
|
77
69
|
wrapper
|
78
70
|
.find('.pf-c-wizard__nav-link')
|
@@ -91,7 +83,7 @@ describe('AdvancedFields', () => {
|
|
91
83
|
</Provider>
|
92
84
|
);
|
93
85
|
await act(async () => {
|
94
|
-
fireEvent.click(screen.getByText(
|
86
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
95
87
|
});
|
96
88
|
const searchValue = 'search test';
|
97
89
|
const textValue = 'I am a text';
|
@@ -108,7 +100,7 @@ describe('AdvancedFields', () => {
|
|
108
100
|
fireEvent.click(selectField);
|
109
101
|
await act(async () => {
|
110
102
|
await fireEvent.click(screen.getByText('option 2'));
|
111
|
-
fireEvent.click(screen.getAllByText(
|
103
|
+
fireEvent.click(screen.getAllByText(WIZARD_TITLES.advanced)[0]); // to remove focus
|
112
104
|
await fireEvent.change(textField, {
|
113
105
|
target: { value: textValue },
|
114
106
|
});
|
@@ -128,9 +120,11 @@ describe('AdvancedFields', () => {
|
|
128
120
|
expect(searchField.value).toBe(searchValue);
|
129
121
|
expect(dateField.value).toBe(dateValue);
|
130
122
|
await act(async () => {
|
131
|
-
fireEvent.click(screen.getByText(
|
123
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.categoryAndTemplate));
|
132
124
|
});
|
133
|
-
expect(screen.getAllByText(
|
125
|
+
expect(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)).toHaveLength(
|
126
|
+
3
|
127
|
+
);
|
134
128
|
|
135
129
|
await act(async () => {
|
136
130
|
fireEvent.click(screen.getByText('Advanced Fields'));
|
@@ -141,4 +135,25 @@ describe('AdvancedFields', () => {
|
|
141
135
|
expect(screen.queryAllByText('option 1')).toHaveLength(0);
|
142
136
|
expect(screen.queryAllByText('option 2')).toHaveLength(1);
|
143
137
|
});
|
138
|
+
it('fill defaults into fields', async () => {
|
139
|
+
render(
|
140
|
+
<Provider store={store}>
|
141
|
+
<JobWizard />
|
142
|
+
</Provider>
|
143
|
+
);
|
144
|
+
await act(async () => {
|
145
|
+
fireEvent.click(screen.getByText('Advanced Fields'));
|
146
|
+
});
|
147
|
+
|
148
|
+
expect(
|
149
|
+
screen.getByLabelText('effective user', {
|
150
|
+
selector: 'input',
|
151
|
+
}).value
|
152
|
+
).toBe('default effective user');
|
153
|
+
expect(
|
154
|
+
screen.getByLabelText('timeout to kill', {
|
155
|
+
selector: 'input',
|
156
|
+
}).value
|
157
|
+
).toBe('2');
|
158
|
+
});
|
144
159
|
});
|
@@ -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
|
@@ -5,6 +5,7 @@ import * as api from 'foremanReact/redux/API';
|
|
5
5
|
import { JobWizard } from '../../JobWizard';
|
6
6
|
import * as selectors from '../../JobWizardSelectors';
|
7
7
|
import { testSetup, mockApi } from '../../__tests__/fixtures';
|
8
|
+
import { WIZARD_TITLES } from '../../JobWizardConstants';
|
8
9
|
|
9
10
|
const store = testSetup(selectors, api);
|
10
11
|
mockApi(api);
|
@@ -32,7 +33,7 @@ describe('Category And Template', () => {
|
|
32
33
|
await act(async () => {
|
33
34
|
await fireEvent.click(screen.getByText('Puppet'));
|
34
35
|
});
|
35
|
-
fireEvent.click(screen.getAllByText(
|
36
|
+
fireEvent.click(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)[0]); // to remove focus
|
36
37
|
expect(
|
37
38
|
screen.queryAllByLabelText('Ansible Commands', { selector: 'button' })
|
38
39
|
).toHaveLength(0);
|
@@ -47,7 +48,7 @@ describe('Category And Template', () => {
|
|
47
48
|
await act(async () => {
|
48
49
|
await fireEvent.click(screen.getByText('template2'));
|
49
50
|
});
|
50
|
-
fireEvent.click(screen.getAllByText(
|
51
|
+
fireEvent.click(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)[0]); // to remove focus
|
51
52
|
expect(
|
52
53
|
screen.queryAllByDisplayValue('template1', { selector: 'button' })
|
53
54
|
).toHaveLength(0);
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { Chip, ChipGroup } from '@patternfly/react-core';
|
4
|
+
|
5
|
+
export const SelectedChips = ({ selected, setSelected }) => {
|
6
|
+
const deleteItem = itemToRemove => {
|
7
|
+
setSelected(oldSelected =>
|
8
|
+
oldSelected.filter(item => item !== itemToRemove)
|
9
|
+
);
|
10
|
+
};
|
11
|
+
return (
|
12
|
+
<ChipGroup className="hosts-chip-group">
|
13
|
+
{selected.map(chip => (
|
14
|
+
<Chip key={chip} id={chip} onClick={() => deleteItem(chip)}>
|
15
|
+
{chip}
|
16
|
+
</Chip>
|
17
|
+
))}
|
18
|
+
</ChipGroup>
|
19
|
+
);
|
20
|
+
};
|
21
|
+
|
22
|
+
SelectedChips.propTypes = {
|
23
|
+
selected: PropTypes.array.isRequired,
|
24
|
+
setSelected: PropTypes.func.isRequired,
|
25
|
+
};
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
4
|
+
import { formatter } from '../form/Formatter';
|
5
|
+
|
6
|
+
export const TemplateInputs = ({ inputs, value, setValue }) => {
|
7
|
+
if (inputs.length)
|
8
|
+
return inputs.map(input => formatter(input, value, setValue));
|
9
|
+
return (
|
10
|
+
<p className="gray-text">
|
11
|
+
{__('There are no available input fields for the selected template.')}
|
12
|
+
</p>
|
13
|
+
);
|
14
|
+
};
|
15
|
+
TemplateInputs.propTypes = {
|
16
|
+
inputs: PropTypes.array.isRequired,
|
17
|
+
value: PropTypes.object,
|
18
|
+
setValue: PropTypes.func.isRequired,
|
19
|
+
};
|
20
|
+
|
21
|
+
TemplateInputs.defaultProps = {
|
22
|
+
value: {},
|
23
|
+
};
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Provider } from 'react-redux';
|
3
|
+
import { fireEvent, screen, render, act } from '@testing-library/react';
|
4
|
+
import * as api from 'foremanReact/redux/API';
|
5
|
+
import { JobWizard } from '../../../JobWizard';
|
6
|
+
import * as selectors from '../../../JobWizardSelectors';
|
7
|
+
import { testSetup, mockApi } from '../../../__tests__/fixtures';
|
8
|
+
|
9
|
+
const store = testSetup(selectors, api);
|
10
|
+
mockApi(api);
|
11
|
+
|
12
|
+
describe('TemplateInputs', () => {
|
13
|
+
it('should save data between steps for template input fields', async () => {
|
14
|
+
render(
|
15
|
+
<Provider store={store}>
|
16
|
+
<JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
|
17
|
+
</Provider>
|
18
|
+
);
|
19
|
+
await act(async () => {
|
20
|
+
await fireEvent.click(
|
21
|
+
screen.getByText('Target hosts and inputs', { selector: 'button' })
|
22
|
+
);
|
23
|
+
});
|
24
|
+
|
25
|
+
expect(
|
26
|
+
screen.getAllByLabelText('host2', { selector: 'button' })
|
27
|
+
).toHaveLength(1);
|
28
|
+
const chip1 = screen.getByLabelText('host1', { selector: 'button' });
|
29
|
+
fireEvent.click(chip1);
|
30
|
+
expect(
|
31
|
+
screen.queryAllByLabelText('host1', { selector: 'button' })
|
32
|
+
).toHaveLength(0);
|
33
|
+
expect(
|
34
|
+
screen.queryAllByLabelText('host2', { selector: 'button' })
|
35
|
+
).toHaveLength(1);
|
36
|
+
});
|
37
|
+
});
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Provider } from 'react-redux';
|
3
|
+
import { fireEvent, screen, render, act } from '@testing-library/react';
|
4
|
+
import * as api from 'foremanReact/redux/API';
|
5
|
+
import { JobWizard } from '../../../JobWizard';
|
6
|
+
import * as selectors from '../../../JobWizardSelectors';
|
7
|
+
import { testSetup, mockApi } from '../../../__tests__/fixtures';
|
8
|
+
import { WIZARD_TITLES } from '../../../JobWizardConstants';
|
9
|
+
|
10
|
+
const store = testSetup(selectors, api);
|
11
|
+
mockApi(api);
|
12
|
+
|
13
|
+
describe('TemplateInputs', () => {
|
14
|
+
it('should save data between steps for template input fields', async () => {
|
15
|
+
render(
|
16
|
+
<Provider store={store}>
|
17
|
+
<JobWizard />
|
18
|
+
</Provider>
|
19
|
+
);
|
20
|
+
await act(async () => {
|
21
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.hostsAndInputs));
|
22
|
+
});
|
23
|
+
const textValue = 'I am a plain text';
|
24
|
+
const textField = screen.getByLabelText('plain hidden', {
|
25
|
+
selector: 'textarea',
|
26
|
+
});
|
27
|
+
|
28
|
+
await act(async () => {
|
29
|
+
await fireEvent.change(textField, {
|
30
|
+
target: { value: textValue },
|
31
|
+
});
|
32
|
+
});
|
33
|
+
expect(
|
34
|
+
screen.getByLabelText('plain hidden', {
|
35
|
+
selector: 'textarea',
|
36
|
+
}).value
|
37
|
+
).toBe(textValue);
|
38
|
+
await act(async () => {
|
39
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.categoryAndTemplate));
|
40
|
+
});
|
41
|
+
expect(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)).toHaveLength(
|
42
|
+
3
|
43
|
+
);
|
44
|
+
|
45
|
+
await act(async () => {
|
46
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.hostsAndInputs));
|
47
|
+
});
|
48
|
+
expect(textField.value).toBe(textValue);
|
49
|
+
});
|
50
|
+
});
|
@@ -0,0 +1,66 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import { Button, Form, FormGroup } from '@patternfly/react-core';
|
3
|
+
import PropTypes from 'prop-types';
|
4
|
+
import { useSelector } from 'react-redux';
|
5
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
6
|
+
import { selectTemplateInputs } from '../../JobWizardSelectors';
|
7
|
+
import { SelectField } from '../form/SelectField';
|
8
|
+
import { SelectedChips } from './SelectedChips';
|
9
|
+
import { TemplateInputs } from './TemplateInputs';
|
10
|
+
import { WIZARD_TITLES } from '../../JobWizardConstants';
|
11
|
+
import { WizardTitle } from '../form/WizardTitle';
|
12
|
+
|
13
|
+
const HostsAndInputs = ({
|
14
|
+
templateValues,
|
15
|
+
setTemplateValues,
|
16
|
+
selectedHosts,
|
17
|
+
setSelectedHosts,
|
18
|
+
}) => {
|
19
|
+
const templateInputs = useSelector(selectTemplateInputs);
|
20
|
+
const hostMethods = [
|
21
|
+
__('Hosts'),
|
22
|
+
__('Host collection'),
|
23
|
+
__('Host group'),
|
24
|
+
__('Search query'),
|
25
|
+
];
|
26
|
+
const [hostMethod, setHostMethod] = useState(hostMethods[0]);
|
27
|
+
return (
|
28
|
+
<>
|
29
|
+
<WizardTitle title={WIZARD_TITLES.hostsAndInputs} />
|
30
|
+
<Form>
|
31
|
+
<FormGroup fieldId="host_selection">
|
32
|
+
<SelectField
|
33
|
+
fieldId="host_methods"
|
34
|
+
options={hostMethods}
|
35
|
+
setValue={setHostMethod}
|
36
|
+
value={hostMethod}
|
37
|
+
/>
|
38
|
+
<SelectedChips
|
39
|
+
selected={selectedHosts}
|
40
|
+
setSelected={setSelectedHosts}
|
41
|
+
/>
|
42
|
+
</FormGroup>
|
43
|
+
<span>
|
44
|
+
{__('Apply to')}{' '}
|
45
|
+
<Button variant="link" isInline>
|
46
|
+
{selectedHosts.length} {__('hosts')}
|
47
|
+
</Button>
|
48
|
+
</span>
|
49
|
+
<TemplateInputs
|
50
|
+
inputs={templateInputs}
|
51
|
+
value={templateValues}
|
52
|
+
setValue={setTemplateValues}
|
53
|
+
/>
|
54
|
+
</Form>
|
55
|
+
</>
|
56
|
+
);
|
57
|
+
};
|
58
|
+
|
59
|
+
HostsAndInputs.propTypes = {
|
60
|
+
templateValues: PropTypes.object.isRequired,
|
61
|
+
setTemplateValues: PropTypes.func.isRequired,
|
62
|
+
selectedHosts: PropTypes.array.isRequired,
|
63
|
+
setSelectedHosts: PropTypes.func.isRequired,
|
64
|
+
};
|
65
|
+
|
66
|
+
export default HostsAndInputs;
|
@@ -1,11 +1,12 @@
|
|
1
1
|
import React, { useState } from 'react';
|
2
|
-
import {
|
2
|
+
import { Button, Form } from '@patternfly/react-core';
|
3
3
|
import { translate as __ } from 'foremanReact/common/I18n';
|
4
4
|
import { ScheduleType } from './ScheduleType';
|
5
5
|
import { RepeatOn } from './RepeatOn';
|
6
6
|
import { QueryType } from './QueryType';
|
7
7
|
import { StartEndDates } from './StartEndDates';
|
8
|
-
import { repeatTypes } from '../../JobWizardConstants';
|
8
|
+
import { repeatTypes, WIZARD_TITLES } from '../../JobWizardConstants';
|
9
|
+
import { WizardTitle } from '../form/WizardTitle';
|
9
10
|
|
10
11
|
const Schedule = () => {
|
11
12
|
const [repeatType, setRepeatType] = useState(repeatTypes.noRepeat);
|
@@ -14,27 +15,29 @@ const Schedule = () => {
|
|
14
15
|
const [ends, setEnds] = useState('');
|
15
16
|
|
16
17
|
return (
|
17
|
-
|
18
|
-
<
|
19
|
-
<
|
18
|
+
<>
|
19
|
+
<WizardTitle title={WIZARD_TITLES.schedule} />
|
20
|
+
<Form className="schedule-tab">
|
21
|
+
<ScheduleType />
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
23
|
+
<RepeatOn
|
24
|
+
repeatType={repeatType}
|
25
|
+
setRepeatType={setRepeatType}
|
26
|
+
repeatAmount={repeatAmount}
|
27
|
+
setRepeatAmount={setRepeatAmount}
|
28
|
+
/>
|
29
|
+
<StartEndDates
|
30
|
+
starts={starts}
|
31
|
+
setStarts={setStarts}
|
32
|
+
ends={ends}
|
33
|
+
setEnds={setEnds}
|
34
|
+
/>
|
35
|
+
<Button variant="link" className="advanced-scheduling-button" isInline>
|
36
|
+
{__('Advanced scheduling')}
|
37
|
+
</Button>
|
38
|
+
<QueryType />
|
39
|
+
</Form>
|
40
|
+
</>
|
38
41
|
);
|
39
42
|
};
|
40
43
|
|
@@ -22,11 +22,12 @@ const TemplateSearchField = ({
|
|
22
22
|
setValue({ ...values, [name]: searchQuery });
|
23
23
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
24
24
|
}, [searchQuery]);
|
25
|
+
const id = name.replace(/ /g, '-');
|
25
26
|
return (
|
26
27
|
<FormGroup
|
27
28
|
label={name}
|
28
29
|
labelIcon={helpLabel(labelText, name)}
|
29
|
-
fieldId={
|
30
|
+
fieldId={id}
|
30
31
|
isRequired={required}
|
31
32
|
className="foreman-search-field"
|
32
33
|
>
|
@@ -54,16 +55,16 @@ export const formatter = (input, values, setValue) => {
|
|
54
55
|
const { name, required, hidden_value: hidden } = input;
|
55
56
|
const labelText = input.description;
|
56
57
|
const value = values[name];
|
57
|
-
|
58
|
+
const id = name.replace(/ /g, '-');
|
58
59
|
if (isSelectType) {
|
59
60
|
const options = input.options.split(/\r?\n/).map(option => option.trim());
|
60
61
|
return (
|
61
62
|
<SelectField
|
62
63
|
aria-label={name}
|
63
|
-
key={
|
64
|
+
key={id}
|
64
65
|
isRequired={required}
|
65
66
|
label={name}
|
66
|
-
fieldId={
|
67
|
+
fieldId={id}
|
67
68
|
options={options}
|
68
69
|
labelIcon={helpLabel(labelText, name)}
|
69
70
|
value={value}
|
@@ -77,7 +78,7 @@ export const formatter = (input, values, setValue) => {
|
|
77
78
|
key={name}
|
78
79
|
label={name}
|
79
80
|
labelIcon={helpLabel(labelText, name)}
|
80
|
-
fieldId={
|
81
|
+
fieldId={id}
|
81
82
|
isRequired={required}
|
82
83
|
>
|
83
84
|
<TextArea
|
@@ -85,7 +86,7 @@ export const formatter = (input, values, setValue) => {
|
|
85
86
|
className={hidden ? 'masked-input' : null}
|
86
87
|
required={required}
|
87
88
|
rows={2}
|
88
|
-
id={
|
89
|
+
id={id}
|
89
90
|
value={value}
|
90
91
|
onChange={newValue => setValue({ ...values, [name]: newValue })}
|
91
92
|
/>
|
@@ -98,7 +99,7 @@ export const formatter = (input, values, setValue) => {
|
|
98
99
|
key={name}
|
99
100
|
label={name}
|
100
101
|
labelIcon={helpLabel(labelText, name)}
|
101
|
-
fieldId={
|
102
|
+
fieldId={id}
|
102
103
|
isRequired={required}
|
103
104
|
>
|
104
105
|
<TextInput
|
@@ -106,7 +107,7 @@ export const formatter = (input, values, setValue) => {
|
|
106
107
|
placeholder="YYYY-mm-dd HH:MM"
|
107
108
|
className={hidden ? 'masked-input' : null}
|
108
109
|
required={required}
|
109
|
-
id={
|
110
|
+
id={id}
|
110
111
|
type="text"
|
111
112
|
value={value}
|
112
113
|
onChange={newValue => setValue({ ...values, [name]: newValue })}
|
@@ -119,7 +120,7 @@ export const formatter = (input, values, setValue) => {
|
|
119
120
|
// TODO: get text from redux autocomplete
|
120
121
|
return (
|
121
122
|
<TemplateSearchField
|
122
|
-
key={
|
123
|
+
key={id}
|
123
124
|
name={name}
|
124
125
|
defaultValue={value}
|
125
126
|
controller={controller}
|
@@ -5,6 +5,7 @@ import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
5
|
|
6
6
|
export const NumberInput = ({ formProps, inputProps }) => {
|
7
7
|
const [validated, setValidated] = useState();
|
8
|
+
const name = inputProps.id.replace(/-/g, ' ');
|
8
9
|
return (
|
9
10
|
<FormGroup
|
10
11
|
{...formProps}
|
@@ -12,6 +13,7 @@ export const NumberInput = ({ formProps, inputProps }) => {
|
|
12
13
|
validated={validated}
|
13
14
|
>
|
14
15
|
<TextInput
|
16
|
+
aria-label={name}
|
15
17
|
type="text"
|
16
18
|
{...inputProps}
|
17
19
|
onChange={newValue => {
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { Title } from '@patternfly/react-core';
|
4
|
+
|
5
|
+
export const WizardTitle = ({ title, ...props }) => (
|
6
|
+
<Title headingLevel="h2" className="wizard-title" {...props}>
|
7
|
+
{title}
|
8
|
+
</Title>
|
9
|
+
);
|
10
|
+
|
11
|
+
WizardTitle.propTypes = {
|
12
|
+
title: PropTypes.string.isRequired,
|
13
|
+
};
|
14
|
+
export default WizardTitle;
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foreman_remote_execution
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.5.
|
4
|
+
version: 4.5.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Foreman Remote Execution team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-08-
|
11
|
+
date: 2021-08-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deface
|
@@ -416,6 +416,11 @@ files:
|
|
416
416
|
- webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js
|
417
417
|
- webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js
|
418
418
|
- webpack/JobWizard/steps/CategoryAndTemplate/index.js
|
419
|
+
- webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js
|
420
|
+
- webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js
|
421
|
+
- webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js
|
422
|
+
- webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js
|
423
|
+
- webpack/JobWizard/steps/HostsAndInputs/index.js
|
419
424
|
- webpack/JobWizard/steps/Schedule/QueryType.js
|
420
425
|
- webpack/JobWizard/steps/Schedule/RepeatOn.js
|
421
426
|
- webpack/JobWizard/steps/Schedule/ScheduleType.js
|
@@ -427,6 +432,7 @@ files:
|
|
427
432
|
- webpack/JobWizard/steps/form/GroupedSelectField.js
|
428
433
|
- webpack/JobWizard/steps/form/NumberInput.js
|
429
434
|
- webpack/JobWizard/steps/form/SelectField.js
|
435
|
+
- webpack/JobWizard/steps/form/WizardTitle.js
|
430
436
|
- webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example
|
431
437
|
- webpack/Routes/routes.js
|
432
438
|
- webpack/__mocks__/foremanReact/common/I18n.js
|