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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/ui_job_wizard_controller.rb +7 -0
  3. data/app/helpers/remote_execution_helper.rb +5 -1
  4. data/app/views/templates/ssh/module_action.erb +1 -0
  5. data/app/views/templates/ssh/puppet_run_once.erb +1 -0
  6. data/lib/foreman_remote_execution/version.rb +1 -1
  7. data/webpack/JobWizard/JobWizard.js +28 -8
  8. data/webpack/JobWizard/JobWizard.scss +39 -0
  9. data/webpack/JobWizard/JobWizardConstants.js +10 -0
  10. data/webpack/JobWizard/JobWizardSelectors.js +9 -0
  11. data/webpack/JobWizard/__tests__/fixtures.js +104 -2
  12. data/webpack/JobWizard/__tests__/integration.test.js +13 -85
  13. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +21 -4
  14. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +67 -0
  15. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +73 -59
  16. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +135 -16
  17. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +23 -0
  18. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +122 -51
  19. data/webpack/JobWizard/steps/Schedule/QueryType.js +48 -0
  20. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +61 -0
  21. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +25 -0
  22. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +51 -0
  23. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +22 -0
  24. data/webpack/JobWizard/steps/Schedule/index.js +41 -0
  25. data/webpack/JobWizard/steps/form/FormHelpers.js +1 -0
  26. data/webpack/JobWizard/steps/form/Formatter.js +149 -0
  27. data/webpack/JobWizard/steps/form/NumberInput.js +33 -0
  28. data/webpack/JobWizard/steps/form/SelectField.js +14 -2
  29. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +76 -0
  30. data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
  31. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
  32. metadata +13 -10
  33. data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -13
  34. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -32
  35. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +0 -249
  36. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -113
  37. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
  38. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
  39. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -37
  40. 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 { selectJobTemplate } from '../../JobWizardSelectors';
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 jobTemplate = useSelector(selectJobTemplate);
19
- const effectiveUser = jobTemplate.effective_user;
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
- <FormGroup
30
- label={__('Timeout to kill')}
31
- labelIcon={helpLabel(
32
- __(
33
- 'Time in seconds from the start on the remote host after which the job should be killed.'
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
- fieldId="timeout-to-kill"
38
- >
39
- <TextInput
40
- type="number"
41
- value={value}
42
- placeholder={__('For example: 1, 2, 3, 4, 5...')}
43
- autoComplete="timeout-to-kill"
44
- id="timeout-to-kill"
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
- <FormGroup
118
- label={__('Concurrency level')}
119
- labelIcon={helpLabel(
120
- __(
121
- '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.'
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
- fieldId="concurrency-level"
126
- >
127
- <TextInput
128
- min={1}
129
- type="number"
130
- autoComplete="concurrency-level"
131
- id="concurrency-level"
132
- placeholder={__('For example: 1, 2, 3, 4, 5...')}
133
- value={value}
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
- <FormGroup
141
- label={__('Time span')}
142
- labelIcon={helpLabel(
143
- __(
144
- '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.'
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
- fieldId="time-span"
149
- >
150
- <TextInput
151
- min={1}
152
- type="number"
153
- autoComplete="time-span"
154
- id="time-span"
155
- placeholder={__('For example: 1, 2, 3, 4, 5...')}
156
- value={value}
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 { AdvancedFields } from '../AdvancedFields';
7
-
8
- jest.spyOn(patternfly, 'FormGroup');
9
- patternfly.FormGroup.mockImplementation(props => (
10
- <div>{props.navAriaLabel}</div>
11
- ));
12
- const mockStore = configureMockStore([]);
13
- const store = mockStore({
14
- JOB_TEMPLATE: { response: { effective_user: { overridable: true } } },
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('rendring', () => {
18
- const component = mount(
32
+ it('should save data between steps for advanced fields', async () => {
33
+ const wrapper = mount(
19
34
  <Provider store={store}>
20
- <AdvancedFields advancedValues={{}} setAdvancedValues={jest.fn()} />
35
+ <JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
21
36
  </Provider>
22
37
  );
23
- expect(component).toMatchSnapshot();
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
  });