foreman_remote_execution 4.5.1 → 4.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/ui_job_wizard_controller.rb +7 -0
- data/app/helpers/remote_execution_helper.rb +5 -1
- data/app/views/templates/ssh/module_action.erb +1 -0
- data/app/views/templates/ssh/puppet_run_once.erb +1 -0
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/webpack/JobWizard/JobWizard.js +28 -8
- data/webpack/JobWizard/JobWizard.scss +39 -0
- data/webpack/JobWizard/JobWizardConstants.js +10 -0
- data/webpack/JobWizard/JobWizardSelectors.js +9 -0
- data/webpack/JobWizard/__tests__/fixtures.js +104 -2
- data/webpack/JobWizard/__tests__/integration.test.js +13 -85
- data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +21 -4
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +67 -0
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +73 -59
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +135 -16
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +23 -0
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +122 -51
- data/webpack/JobWizard/steps/Schedule/QueryType.js +48 -0
- data/webpack/JobWizard/steps/Schedule/RepeatOn.js +61 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +25 -0
- data/webpack/JobWizard/steps/Schedule/StartEndDates.js +51 -0
- data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +22 -0
- data/webpack/JobWizard/steps/Schedule/index.js +41 -0
- data/webpack/JobWizard/steps/form/FormHelpers.js +1 -0
- data/webpack/JobWizard/steps/form/Formatter.js +149 -0
- data/webpack/JobWizard/steps/form/NumberInput.js +33 -0
- data/webpack/JobWizard/steps/form/SelectField.js +14 -2
- data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +76 -0
- data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
- metadata +13 -10
- data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -13
- data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -32
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +0 -249
- data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -113
- data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
- data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
- data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -37
- data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -23
@@ -3,7 +3,11 @@ import PropTypes from 'prop-types';
|
|
3
3
|
import { useSelector } from 'react-redux';
|
4
4
|
import { Title, Form } from '@patternfly/react-core';
|
5
5
|
import { translate as __ } from 'foremanReact/common/I18n';
|
6
|
-
import {
|
6
|
+
import {
|
7
|
+
selectEffectiveUser,
|
8
|
+
selectAdvancedTemplateInputs,
|
9
|
+
selectTemplateInputs,
|
10
|
+
} from '../../JobWizardSelectors';
|
7
11
|
import {
|
8
12
|
EffectiveUserField,
|
9
13
|
TimeoutToKillField,
|
@@ -12,17 +16,25 @@ import {
|
|
12
16
|
EffectiveUserPasswordField,
|
13
17
|
ConcurrencyLevelField,
|
14
18
|
TimeSpanLevelField,
|
19
|
+
TemplateInputsFields,
|
15
20
|
} from './Fields';
|
21
|
+
import { DescriptionField } from './DescriptionField';
|
16
22
|
|
17
23
|
export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
|
18
|
-
const
|
19
|
-
const
|
24
|
+
const effectiveUser = useSelector(selectEffectiveUser);
|
25
|
+
const advancedTemplateInputs = useSelector(selectAdvancedTemplateInputs);
|
26
|
+
const templateInputs = useSelector(selectTemplateInputs);
|
20
27
|
return (
|
21
28
|
<>
|
22
29
|
<Title headingLevel="h2" className="advanced-fields-title">
|
23
30
|
{__('Advanced Fields')}
|
24
31
|
</Title>
|
25
|
-
<Form>
|
32
|
+
<Form id="advanced-fields-job-template" autoComplete="off">
|
33
|
+
<TemplateInputsFields
|
34
|
+
inputs={advancedTemplateInputs}
|
35
|
+
value={advancedValues.templateValues}
|
36
|
+
setValue={newValue => setAdvancedValues({ templateValues: newValue })}
|
37
|
+
/>
|
26
38
|
{effectiveUser?.overridable && (
|
27
39
|
<EffectiveUserField
|
28
40
|
value={advancedValues.effectiveUserValue}
|
@@ -33,6 +45,11 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
|
|
33
45
|
}
|
34
46
|
/>
|
35
47
|
)}
|
48
|
+
<DescriptionField
|
49
|
+
inputs={templateInputs}
|
50
|
+
value={advancedValues.description}
|
51
|
+
setValue={newValue => setAdvancedValues({ description: newValue })}
|
52
|
+
/>
|
36
53
|
<TimeoutToKillField
|
37
54
|
value={advancedValues.timeoutToKill}
|
38
55
|
setValue={newValue =>
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { FormGroup, TextInput, Button } from '@patternfly/react-core';
|
4
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
5
|
+
|
6
|
+
export const DescriptionField = ({ inputs, value, setValue }) => {
|
7
|
+
const generateDesc = () => {
|
8
|
+
let newDesc = value;
|
9
|
+
if (value) {
|
10
|
+
const re = new RegExp('%\\{([^\\}]+)\\}', 'gm');
|
11
|
+
const results = [...newDesc.matchAll(re)].map(result => ({
|
12
|
+
name: result[1],
|
13
|
+
text: result[0],
|
14
|
+
}));
|
15
|
+
results.forEach(result => {
|
16
|
+
newDesc = newDesc.replace(
|
17
|
+
result.text,
|
18
|
+
// TODO: Replace with the value of the input from Target Hosts step
|
19
|
+
inputs.find(input => input.name === result.name)?.name || result.text
|
20
|
+
);
|
21
|
+
});
|
22
|
+
}
|
23
|
+
return newDesc;
|
24
|
+
};
|
25
|
+
const [generatedDesc, setGeneratedDesc] = useState(generateDesc());
|
26
|
+
const [isPreview, setIsPreview] = useState(true);
|
27
|
+
|
28
|
+
const togglePreview = () => {
|
29
|
+
setGeneratedDesc(generateDesc());
|
30
|
+
setIsPreview(v => !v);
|
31
|
+
};
|
32
|
+
|
33
|
+
return (
|
34
|
+
<FormGroup
|
35
|
+
label={__('Description')}
|
36
|
+
fieldId="description"
|
37
|
+
helperText={
|
38
|
+
<Button variant="link" isInline onClick={togglePreview}>
|
39
|
+
{isPreview
|
40
|
+
? __('Edit job description template')
|
41
|
+
: __('Preview job description')}
|
42
|
+
</Button>
|
43
|
+
}
|
44
|
+
>
|
45
|
+
{isPreview ? (
|
46
|
+
<TextInput id="description-preview" value={generatedDesc} isDisabled />
|
47
|
+
) : (
|
48
|
+
<TextInput
|
49
|
+
type="text"
|
50
|
+
autoComplete="description"
|
51
|
+
id="description"
|
52
|
+
value={value}
|
53
|
+
onChange={newValue => setValue(newValue)}
|
54
|
+
/>
|
55
|
+
)}
|
56
|
+
</FormGroup>
|
57
|
+
);
|
58
|
+
};
|
59
|
+
|
60
|
+
DescriptionField.propTypes = {
|
61
|
+
inputs: PropTypes.array.isRequired,
|
62
|
+
value: PropTypes.string,
|
63
|
+
setValue: PropTypes.func.isRequired,
|
64
|
+
};
|
65
|
+
DescriptionField.defaultProps = {
|
66
|
+
value: '',
|
67
|
+
};
|
@@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
|
|
3
3
|
import { FormGroup, TextInput } from '@patternfly/react-core';
|
4
4
|
import { translate as __ } from 'foremanReact/common/I18n';
|
5
5
|
import { helpLabel } from '../form/FormHelpers';
|
6
|
+
import { formatter } from '../form/Formatter';
|
7
|
+
import { NumberInput } from '../form/NumberInput';
|
6
8
|
|
7
9
|
export const EffectiveUserField = ({ value, setValue }) => (
|
8
10
|
<FormGroup
|
@@ -26,25 +28,25 @@ export const EffectiveUserField = ({ value, setValue }) => (
|
|
26
28
|
);
|
27
29
|
|
28
30
|
export const TimeoutToKillField = ({ value, setValue }) => (
|
29
|
-
<
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
31
|
+
<NumberInput
|
32
|
+
formProps={{
|
33
|
+
label: __('Timeout to kill'),
|
34
|
+
labelIcon: helpLabel(
|
35
|
+
__(
|
36
|
+
'Time in seconds from the start on the remote host after which the job should be killed.'
|
37
|
+
),
|
38
|
+
'timeout-to-kill'
|
34
39
|
),
|
35
|
-
'timeout-to-kill'
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
onChange={newValue => setValue(newValue)}
|
46
|
-
/>
|
47
|
-
</FormGroup>
|
40
|
+
fieldId: 'timeout-to-kill',
|
41
|
+
}}
|
42
|
+
inputProps={{
|
43
|
+
value,
|
44
|
+
placeholder: __('For example: 1, 2, 3, 4, 5...'),
|
45
|
+
autoComplete: 'timeout-to-kill',
|
46
|
+
id: 'timeout-to-kill',
|
47
|
+
onChange: newValue => setValue(newValue),
|
48
|
+
}}
|
49
|
+
/>
|
48
50
|
);
|
49
51
|
|
50
52
|
export const PasswordField = ({ value, setValue }) => (
|
@@ -56,11 +58,11 @@ export const PasswordField = ({ value, setValue }) => (
|
|
56
58
|
),
|
57
59
|
'password'
|
58
60
|
)}
|
59
|
-
fieldId="password"
|
61
|
+
fieldId="job-password"
|
60
62
|
>
|
61
63
|
<TextInput
|
62
|
-
autoComplete="password"
|
63
|
-
id="password"
|
64
|
+
autoComplete="new-password" // to prevent firefox from autofilling the user password
|
65
|
+
id="job-password"
|
64
66
|
type="password"
|
65
67
|
placeholder="*****"
|
66
68
|
value={value}
|
@@ -114,51 +116,54 @@ export const EffectiveUserPasswordField = ({ value, setValue }) => (
|
|
114
116
|
);
|
115
117
|
|
116
118
|
export const ConcurrencyLevelField = ({ value, setValue }) => (
|
117
|
-
<
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
119
|
+
<NumberInput
|
120
|
+
formProps={{
|
121
|
+
label: __('Concurrency level'),
|
122
|
+
labelIcon: helpLabel(
|
123
|
+
__(
|
124
|
+
'Run at most N tasks at a time. If this is set and proxy batch triggering is enabled, then tasks are triggered on the smart proxy in batches of size 1.'
|
125
|
+
),
|
126
|
+
'concurrency-level'
|
122
127
|
),
|
123
|
-
'concurrency-level'
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
onChange={newValue => setValue(newValue)}
|
135
|
-
/>
|
136
|
-
</FormGroup>
|
128
|
+
fieldId: 'concurrency-level',
|
129
|
+
}}
|
130
|
+
inputProps={{
|
131
|
+
min: 1,
|
132
|
+
autoComplete: 'concurrency-level',
|
133
|
+
id: 'concurrency-level',
|
134
|
+
placeholder: __('For example: 1, 2, 3, 4, 5...'),
|
135
|
+
value,
|
136
|
+
onChange: newValue => setValue(newValue),
|
137
|
+
}}
|
138
|
+
/>
|
137
139
|
);
|
138
140
|
|
139
141
|
export const TimeSpanLevelField = ({ value, setValue }) => (
|
140
|
-
<
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
142
|
+
<NumberInput
|
143
|
+
formProps={{
|
144
|
+
label: __('Time span'),
|
145
|
+
labelIcon: helpLabel(
|
146
|
+
__(
|
147
|
+
'Distribute execution over N seconds. If this is set and proxy batch triggering is enabled, then tasks are triggered on the smart proxy in batches of size 1.'
|
148
|
+
),
|
149
|
+
'time-span'
|
145
150
|
),
|
146
|
-
'time-span'
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
onChange={newValue => setValue(newValue)}
|
158
|
-
/>
|
159
|
-
</FormGroup>
|
151
|
+
fieldId: 'time-span',
|
152
|
+
}}
|
153
|
+
inputProps={{
|
154
|
+
min: 1,
|
155
|
+
autoComplete: 'time-span',
|
156
|
+
id: 'time-span',
|
157
|
+
placeholder: __('For example: 1, 2, 3, 4, 5...'),
|
158
|
+
value,
|
159
|
+
onChange: newValue => setValue(newValue),
|
160
|
+
}}
|
161
|
+
/>
|
160
162
|
);
|
161
163
|
|
164
|
+
export const TemplateInputsFields = ({ inputs, value, setValue }) => (
|
165
|
+
<>{inputs?.map(input => formatter(input, value, setValue))}</>
|
166
|
+
);
|
162
167
|
EffectiveUserField.propTypes = {
|
163
168
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
164
169
|
setValue: PropTypes.func.isRequired,
|
@@ -179,3 +184,12 @@ ConcurrencyLevelField.propTypes = EffectiveUserField.propTypes;
|
|
179
184
|
ConcurrencyLevelField.defaultProps = EffectiveUserField.defaultProps;
|
180
185
|
TimeSpanLevelField.propTypes = EffectiveUserField.propTypes;
|
181
186
|
TimeSpanLevelField.defaultProps = EffectiveUserField.defaultProps;
|
187
|
+
TemplateInputsFields.propTypes = {
|
188
|
+
inputs: PropTypes.array.isRequired,
|
189
|
+
value: PropTypes.object,
|
190
|
+
setValue: PropTypes.func.isRequired,
|
191
|
+
};
|
192
|
+
|
193
|
+
TemplateInputsFields.defaultProps = {
|
194
|
+
value: {},
|
195
|
+
};
|
@@ -1,25 +1,144 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import { Provider } from 'react-redux';
|
3
|
-
import configureMockStore from 'redux-mock-store';
|
4
|
-
import * as patternfly from '@patternfly/react-core';
|
5
3
|
import { mount } from '@theforeman/test';
|
6
|
-
import {
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
4
|
+
import { fireEvent, screen, render, act } from '@testing-library/react';
|
5
|
+
import * as api from 'foremanReact/redux/API';
|
6
|
+
import { JobWizard } from '../../../JobWizard';
|
7
|
+
import * as selectors from '../../../JobWizardSelectors';
|
8
|
+
import {
|
9
|
+
jobTemplateResponse as jobTemplate,
|
10
|
+
testSetup,
|
11
|
+
mockApi,
|
12
|
+
} from '../../../__tests__/fixtures';
|
13
|
+
|
14
|
+
const store = testSetup(selectors, api);
|
15
|
+
mockApi(api);
|
16
|
+
|
17
|
+
jest.spyOn(selectors, 'selectEffectiveUser');
|
18
|
+
jest.spyOn(selectors, 'selectTemplateInputs');
|
19
|
+
jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
|
20
|
+
|
21
|
+
selectors.selectEffectiveUser.mockImplementation(
|
22
|
+
() => jobTemplate.effective_user
|
23
|
+
);
|
24
|
+
selectors.selectTemplateInputs.mockImplementation(
|
25
|
+
() => jobTemplate.template_inputs
|
26
|
+
);
|
27
|
+
|
28
|
+
selectors.selectAdvancedTemplateInputs.mockImplementation(
|
29
|
+
() => jobTemplate.advanced_template_inputs
|
30
|
+
);
|
16
31
|
describe('AdvancedFields', () => {
|
17
|
-
it('
|
18
|
-
const
|
32
|
+
it('should save data between steps for advanced fields', async () => {
|
33
|
+
const wrapper = mount(
|
19
34
|
<Provider store={store}>
|
20
|
-
<
|
35
|
+
<JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
|
21
36
|
</Provider>
|
22
37
|
);
|
23
|
-
|
38
|
+
// setup
|
39
|
+
wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
|
40
|
+
wrapper
|
41
|
+
.find('.pf-c-select__menu-item')
|
42
|
+
.first()
|
43
|
+
.simulate('click');
|
44
|
+
|
45
|
+
// test
|
46
|
+
expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
|
47
|
+
0
|
48
|
+
);
|
49
|
+
wrapper
|
50
|
+
.find('.pf-c-wizard__nav-link')
|
51
|
+
.at(2)
|
52
|
+
.simulate('click'); // Advanced step
|
53
|
+
const effectiveUserInput = () => wrapper.find('input#effective-user');
|
54
|
+
const advancedTemplateInput = () =>
|
55
|
+
wrapper.find('.pf-c-form__group-control textarea');
|
56
|
+
const effectiveUesrValue = 'effective user new value';
|
57
|
+
const advancedTemplateInputValue = 'advanced input new value';
|
58
|
+
effectiveUserInput().getDOMNode().value = effectiveUesrValue;
|
59
|
+
|
60
|
+
effectiveUserInput().simulate('change');
|
61
|
+
wrapper.update();
|
62
|
+
advancedTemplateInput().getDOMNode().value = advancedTemplateInputValue;
|
63
|
+
advancedTemplateInput().simulate('change');
|
64
|
+
expect(effectiveUserInput().prop('value')).toEqual(effectiveUesrValue);
|
65
|
+
expect(advancedTemplateInput().prop('value')).toEqual(
|
66
|
+
advancedTemplateInputValue
|
67
|
+
);
|
68
|
+
|
69
|
+
wrapper
|
70
|
+
.find('.pf-c-wizard__nav-link')
|
71
|
+
.at(1)
|
72
|
+
.simulate('click');
|
73
|
+
|
74
|
+
expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-current').text()).toEqual(
|
75
|
+
'Target Hosts'
|
76
|
+
);
|
77
|
+
wrapper
|
78
|
+
.find('.pf-c-wizard__nav-link')
|
79
|
+
.at(2)
|
80
|
+
.simulate('click'); // Advanced step
|
81
|
+
|
82
|
+
expect(effectiveUserInput().prop('value')).toEqual(effectiveUesrValue);
|
83
|
+
expect(advancedTemplateInput().prop('value')).toEqual(
|
84
|
+
advancedTemplateInputValue
|
85
|
+
);
|
86
|
+
});
|
87
|
+
it('fill template fields', async () => {
|
88
|
+
render(
|
89
|
+
<Provider store={store}>
|
90
|
+
<JobWizard />
|
91
|
+
</Provider>
|
92
|
+
);
|
93
|
+
await act(async () => {
|
94
|
+
fireEvent.click(screen.getByText('Advanced Fields'));
|
95
|
+
});
|
96
|
+
const searchValue = 'search test';
|
97
|
+
const textValue = 'I am a text';
|
98
|
+
const dateValue = '08/07/2021';
|
99
|
+
const textField = screen.getByLabelText('adv plain hidden', {
|
100
|
+
selector: 'textarea',
|
101
|
+
});
|
102
|
+
const selectField = screen.getByText('option 1');
|
103
|
+
const searchField = screen.getByPlaceholderText('Filter...');
|
104
|
+
const dateField = screen.getByLabelText('adv date', {
|
105
|
+
selector: 'input',
|
106
|
+
});
|
107
|
+
|
108
|
+
fireEvent.click(selectField);
|
109
|
+
await act(async () => {
|
110
|
+
await fireEvent.click(screen.getByText('option 2'));
|
111
|
+
fireEvent.click(screen.getAllByText('Advanced Fields')[0]); // to remove focus
|
112
|
+
await fireEvent.change(textField, {
|
113
|
+
target: { value: textValue },
|
114
|
+
});
|
115
|
+
|
116
|
+
await fireEvent.change(searchField, {
|
117
|
+
target: { value: searchValue },
|
118
|
+
});
|
119
|
+
await fireEvent.change(dateField, {
|
120
|
+
target: { value: dateValue },
|
121
|
+
});
|
122
|
+
});
|
123
|
+
expect(
|
124
|
+
screen.getByLabelText('adv plain hidden', {
|
125
|
+
selector: 'textarea',
|
126
|
+
}).value
|
127
|
+
).toBe(textValue);
|
128
|
+
expect(searchField.value).toBe(searchValue);
|
129
|
+
expect(dateField.value).toBe(dateValue);
|
130
|
+
await act(async () => {
|
131
|
+
fireEvent.click(screen.getByText('Category and Template'));
|
132
|
+
});
|
133
|
+
expect(screen.getAllByText('Category and Template')).toHaveLength(3);
|
134
|
+
|
135
|
+
await act(async () => {
|
136
|
+
fireEvent.click(screen.getByText('Advanced Fields'));
|
137
|
+
});
|
138
|
+
expect(textField.value).toBe(textValue);
|
139
|
+
expect(searchField.value).toBe(searchValue);
|
140
|
+
expect(dateField.value).toBe(dateValue);
|
141
|
+
expect(screen.queryAllByText('option 1')).toHaveLength(0);
|
142
|
+
expect(screen.queryAllByText('option 2')).toHaveLength(1);
|
24
143
|
});
|
25
144
|
});
|