foreman_remote_execution 4.5.1 → 4.5.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/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
@@ -0,0 +1,23 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { mount } from '@theforeman/test';
|
3
|
+
import { DescriptionField } from '../DescriptionField';
|
4
|
+
|
5
|
+
describe('DescriptionField', () => {
|
6
|
+
it('rendring', () => {
|
7
|
+
const component = mount(
|
8
|
+
<DescriptionField
|
9
|
+
inputs={[{ name: 'command' }]}
|
10
|
+
value="Run %{command}"
|
11
|
+
setValue={jest.fn()}
|
12
|
+
/>
|
13
|
+
);
|
14
|
+
const preview = component.find('#description-preview').hostNodes();
|
15
|
+
const findLink = () => component.find('.pf-m-link.pf-m-inline');
|
16
|
+
expect(findLink().text()).toEqual('Edit job description template');
|
17
|
+
expect(preview.props().value).toEqual('Run command');
|
18
|
+
findLink().simulate('click');
|
19
|
+
const description = component.find('#description').hostNodes();
|
20
|
+
expect(description.props().value).toEqual('Run %{command}');
|
21
|
+
expect(findLink().text()).toEqual('Preview job description');
|
22
|
+
});
|
23
|
+
});
|
@@ -1,52 +1,123 @@
|
|
1
|
-
import
|
2
|
-
import {
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
const
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
+
jest.spyOn(selectors, 'selectCategoryError');
|
12
|
+
jest.spyOn(selectors, 'selectAllTemplatesError');
|
13
|
+
jest.spyOn(selectors, 'selectTemplateError');
|
14
|
+
|
15
|
+
describe('Category And Template', () => {
|
16
|
+
it('should select ', async () => {
|
17
|
+
selectors.selectCategoryError.mockImplementation(() => null);
|
18
|
+
selectors.selectAllTemplatesError.mockImplementation(() => null);
|
19
|
+
selectors.selectTemplateError.mockImplementation(() => null);
|
20
|
+
render(
|
21
|
+
<Provider store={store}>
|
22
|
+
<JobWizard />
|
23
|
+
</Provider>
|
24
|
+
);
|
25
|
+
|
26
|
+
expect(screen.queryAllByLabelText('Error')).toHaveLength(0);
|
27
|
+
expect(screen.queryAllByLabelText('failed')).toHaveLength(0);
|
28
|
+
// Category
|
29
|
+
fireEvent.click(
|
30
|
+
screen.getByLabelText('Ansible Commands', { selector: 'button' })
|
31
|
+
);
|
32
|
+
await act(async () => {
|
33
|
+
await fireEvent.click(screen.getByText('Puppet'));
|
34
|
+
});
|
35
|
+
fireEvent.click(screen.getAllByText('Category and Template')[0]); // to remove focus
|
36
|
+
expect(
|
37
|
+
screen.queryAllByLabelText('Ansible Commands', { selector: 'button' })
|
38
|
+
).toHaveLength(0);
|
39
|
+
expect(
|
40
|
+
screen.queryAllByLabelText('Puppet', { selector: 'button' })
|
41
|
+
).toHaveLength(1);
|
42
|
+
|
43
|
+
// Template
|
44
|
+
fireEvent.click(
|
45
|
+
screen.getByDisplayValue('template1', { selector: 'button' })
|
46
|
+
);
|
47
|
+
await act(async () => {
|
48
|
+
await fireEvent.click(screen.getByText('template2'));
|
49
|
+
});
|
50
|
+
fireEvent.click(screen.getAllByText('Category and Template')[0]); // to remove focus
|
51
|
+
expect(
|
52
|
+
screen.queryAllByDisplayValue('template1', { selector: 'button' })
|
53
|
+
).toHaveLength(0);
|
54
|
+
expect(
|
55
|
+
screen.queryAllByDisplayValue('template2', { selector: 'button' })
|
56
|
+
).toHaveLength(1);
|
57
|
+
});
|
58
|
+
it('category error ', async () => {
|
59
|
+
selectors.selectCategoryError.mockImplementation(() => 'category error');
|
60
|
+
selectors.selectAllTemplatesError.mockImplementation(() => null);
|
61
|
+
selectors.selectTemplateError.mockImplementation(() => null);
|
62
|
+
render(
|
63
|
+
<Provider store={store}>
|
64
|
+
<JobWizard />
|
65
|
+
</Provider>
|
66
|
+
);
|
67
|
+
|
68
|
+
expect(
|
69
|
+
screen.queryAllByText('Categories list failed with:', { exact: false })
|
70
|
+
).toHaveLength(1);
|
71
|
+
|
72
|
+
expect(
|
73
|
+
screen.queryAllByText('Templates list failed with:', { exact: false })
|
74
|
+
).toHaveLength(0);
|
75
|
+
expect(
|
76
|
+
screen.queryAllByText('Template failed with:', { exact: false })
|
77
|
+
).toHaveLength(0);
|
78
|
+
});
|
79
|
+
it('templates error ', async () => {
|
80
|
+
selectors.selectCategoryError.mockImplementation(() => null);
|
81
|
+
selectors.selectAllTemplatesError.mockImplementation(
|
82
|
+
() => 'templates error'
|
83
|
+
);
|
84
|
+
selectors.selectTemplateError.mockImplementation(() => null);
|
85
|
+
render(
|
86
|
+
<Provider store={store}>
|
87
|
+
<JobWizard />
|
88
|
+
</Provider>
|
89
|
+
);
|
90
|
+
|
91
|
+
expect(
|
92
|
+
screen.queryAllByText('Categories list failed with:', { exact: false })
|
93
|
+
).toHaveLength(0);
|
94
|
+
|
95
|
+
expect(
|
96
|
+
screen.queryAllByText('Templates list failed with:', { exact: false })
|
97
|
+
).toHaveLength(1);
|
98
|
+
expect(
|
99
|
+
screen.queryAllByText('Template failed with:', { exact: false })
|
100
|
+
).toHaveLength(0);
|
101
|
+
});
|
102
|
+
it('template error ', async () => {
|
103
|
+
selectors.selectCategoryError.mockImplementation(() => null);
|
104
|
+
selectors.selectAllTemplatesError.mockImplementation(() => null);
|
105
|
+
selectors.selectTemplateError.mockImplementation(() => 'template error');
|
106
|
+
render(
|
107
|
+
<Provider store={store}>
|
108
|
+
<JobWizard />
|
109
|
+
</Provider>
|
110
|
+
);
|
111
|
+
|
112
|
+
expect(
|
113
|
+
screen.queryAllByText('Categories list failed with:', { exact: false })
|
114
|
+
).toHaveLength(0);
|
115
|
+
|
116
|
+
expect(
|
117
|
+
screen.queryAllByText('Templates list failed with:', { exact: false })
|
118
|
+
).toHaveLength(0);
|
119
|
+
expect(
|
120
|
+
screen.queryAllByText('Template failed with:', { exact: false })
|
121
|
+
).toHaveLength(1);
|
122
|
+
});
|
52
123
|
});
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import { FormGroup, Radio } from '@patternfly/react-core';
|
3
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
4
|
+
import { helpLabel } from '../form/FormHelpers';
|
5
|
+
|
6
|
+
export const QueryType = () => {
|
7
|
+
const [isTypeStatic, setIsTypeStatic] = useState(true);
|
8
|
+
return (
|
9
|
+
<FormGroup
|
10
|
+
label={__('Query type')}
|
11
|
+
fieldId="query-type"
|
12
|
+
labelIcon={helpLabel(
|
13
|
+
<p>
|
14
|
+
{__('Type has impact on when is the query evaluated to hosts.')}
|
15
|
+
<br />
|
16
|
+
<ul>
|
17
|
+
<li>
|
18
|
+
<b>{__('Static')}</b> -{' '}
|
19
|
+
{__('evaluates just after you submit this form')}
|
20
|
+
</li>
|
21
|
+
<li>
|
22
|
+
<b>{__('Dynamic')}</b> -{' '}
|
23
|
+
{__(
|
24
|
+
"evaluates just before the execution is started, so if it's planed in future, targeted hosts set may change before it"
|
25
|
+
)}
|
26
|
+
</li>
|
27
|
+
</ul>
|
28
|
+
</p>,
|
29
|
+
'query-type'
|
30
|
+
)}
|
31
|
+
>
|
32
|
+
<Radio
|
33
|
+
isChecked={isTypeStatic}
|
34
|
+
name="query-type"
|
35
|
+
onChange={() => setIsTypeStatic(true)}
|
36
|
+
id="query-type-static"
|
37
|
+
label={__('Static query')}
|
38
|
+
/>
|
39
|
+
<Radio
|
40
|
+
isChecked={!isTypeStatic}
|
41
|
+
name="query-type"
|
42
|
+
onChange={() => setIsTypeStatic(false)}
|
43
|
+
id="query-type-dynamic"
|
44
|
+
label={__('Dynamic query')}
|
45
|
+
/>
|
46
|
+
</FormGroup>
|
47
|
+
);
|
48
|
+
};
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { TextInput, Grid, GridItem, FormGroup } from '@patternfly/react-core';
|
4
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
5
|
+
import { SelectField } from '../form/SelectField';
|
6
|
+
import { repeatTypes } from '../../JobWizardConstants';
|
7
|
+
|
8
|
+
export const RepeatOn = ({
|
9
|
+
repeatType,
|
10
|
+
setRepeatType,
|
11
|
+
repeatAmount,
|
12
|
+
setRepeatAmount,
|
13
|
+
}) => {
|
14
|
+
const [repeatValidated, setRepeatValidated] = useState('default');
|
15
|
+
const handleRepeatInputChange = newValue => {
|
16
|
+
setRepeatValidated(newValue >= 1 ? 'default' : 'error');
|
17
|
+
setRepeatAmount(newValue);
|
18
|
+
};
|
19
|
+
return (
|
20
|
+
<Grid>
|
21
|
+
<GridItem span={6}>
|
22
|
+
<SelectField
|
23
|
+
fieldId="repeat-select"
|
24
|
+
options={Object.values(repeatTypes)}
|
25
|
+
setValue={newValue => {
|
26
|
+
setRepeatType(newValue);
|
27
|
+
if (newValue === repeatTypes.noRepeat) {
|
28
|
+
setRepeatValidated('default');
|
29
|
+
}
|
30
|
+
}}
|
31
|
+
value={repeatType}
|
32
|
+
/>
|
33
|
+
</GridItem>
|
34
|
+
<GridItem span={1} />
|
35
|
+
<GridItem span={5}>
|
36
|
+
<FormGroup
|
37
|
+
isInline
|
38
|
+
helperTextInvalid={__('Repeat amount can only be a positive number')}
|
39
|
+
validated={repeatValidated}
|
40
|
+
>
|
41
|
+
<TextInput
|
42
|
+
isDisabled={repeatType === repeatTypes.noRepeat}
|
43
|
+
id="repeat-amount"
|
44
|
+
value={repeatAmount}
|
45
|
+
type="text"
|
46
|
+
onChange={newValue => handleRepeatInputChange(newValue)}
|
47
|
+
placeholder={__('Repeat N times')}
|
48
|
+
/>
|
49
|
+
</FormGroup>
|
50
|
+
</GridItem>
|
51
|
+
</Grid>
|
52
|
+
);
|
53
|
+
};
|
54
|
+
|
55
|
+
RepeatOn.propTypes = {
|
56
|
+
repeatType: PropTypes.oneOf(Object.values(repeatTypes)).isRequired,
|
57
|
+
setRepeatType: PropTypes.func.isRequired,
|
58
|
+
repeatAmount: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
59
|
+
.isRequired,
|
60
|
+
setRepeatAmount: PropTypes.func.isRequired,
|
61
|
+
};
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import { FormGroup, Radio } from '@patternfly/react-core';
|
3
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
4
|
+
|
5
|
+
export const ScheduleType = () => {
|
6
|
+
const [isFuture, setIsFuture] = useState(false);
|
7
|
+
return (
|
8
|
+
<FormGroup label={__('Schedule type')} fieldId="schedule-type">
|
9
|
+
<Radio
|
10
|
+
isChecked={!isFuture}
|
11
|
+
name="schedule-type"
|
12
|
+
onChange={() => setIsFuture(false)}
|
13
|
+
id="schedule-type-now"
|
14
|
+
label={__('Execute now')}
|
15
|
+
/>
|
16
|
+
<Radio
|
17
|
+
isChecked={isFuture}
|
18
|
+
name="schedule-type"
|
19
|
+
onChange={() => setIsFuture(true)}
|
20
|
+
id="schedule-type-future"
|
21
|
+
label={__('Schedule for future execution')}
|
22
|
+
/>
|
23
|
+
</FormGroup>
|
24
|
+
);
|
25
|
+
};
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { FormGroup, TextInput, Checkbox } from '@patternfly/react-core';
|
4
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
5
|
+
|
6
|
+
// TODO: change to datepicker
|
7
|
+
export const StartEndDates = ({ starts, setStarts, ends, setEnds }) => {
|
8
|
+
const [isNeverEnds, setIsNeverEnds] = useState(false);
|
9
|
+
const toggleIsNeverEnds = (checked, event) => {
|
10
|
+
const value = event?.target?.checked;
|
11
|
+
setIsNeverEnds(value);
|
12
|
+
setEnds('');
|
13
|
+
};
|
14
|
+
return (
|
15
|
+
<>
|
16
|
+
<FormGroup label={__('Starts')} fieldId="start-date">
|
17
|
+
<TextInput
|
18
|
+
id="start-date"
|
19
|
+
value={starts}
|
20
|
+
type="text"
|
21
|
+
onChange={newValue => setStarts(newValue)}
|
22
|
+
placeholder="mm/dd/yy, hh:mm UTC"
|
23
|
+
/>
|
24
|
+
</FormGroup>
|
25
|
+
<FormGroup label={__('Ends')} fieldId="end-date">
|
26
|
+
<TextInput
|
27
|
+
isDisabled={isNeverEnds}
|
28
|
+
id="end-date"
|
29
|
+
value={ends}
|
30
|
+
type="text"
|
31
|
+
onChange={newValue => setEnds(newValue)}
|
32
|
+
placeholder="mm/dd/yy, hh:mm UTC"
|
33
|
+
/>
|
34
|
+
<Checkbox
|
35
|
+
label={__('Never ends')}
|
36
|
+
isChecked={isNeverEnds}
|
37
|
+
onChange={toggleIsNeverEnds}
|
38
|
+
id="never-ends"
|
39
|
+
name="never-ends"
|
40
|
+
/>
|
41
|
+
</FormGroup>
|
42
|
+
</>
|
43
|
+
);
|
44
|
+
};
|
45
|
+
|
46
|
+
StartEndDates.propTypes = {
|
47
|
+
starts: PropTypes.string.isRequired,
|
48
|
+
setStarts: PropTypes.func.isRequired,
|
49
|
+
ends: PropTypes.string.isRequired,
|
50
|
+
setEnds: PropTypes.func.isRequired,
|
51
|
+
};
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { render, fireEvent, screen } from '@testing-library/react';
|
3
|
+
import { StartEndDates } from '../StartEndDates';
|
4
|
+
|
5
|
+
const setEnds = jest.fn();
|
6
|
+
const props = {
|
7
|
+
starts: '',
|
8
|
+
setStarts: jest.fn(),
|
9
|
+
ends: 'some-end-date',
|
10
|
+
setEnds,
|
11
|
+
};
|
12
|
+
|
13
|
+
describe('StartEndDates', () => {
|
14
|
+
it('never ends', () => {
|
15
|
+
render(<StartEndDates {...props} />);
|
16
|
+
const neverEnds = screen.getByLabelText('Never ends', {
|
17
|
+
selector: 'input',
|
18
|
+
});
|
19
|
+
fireEvent.click(neverEnds);
|
20
|
+
expect(setEnds).toBeCalledWith('');
|
21
|
+
});
|
22
|
+
});
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import { Title, Button, Form } from '@patternfly/react-core';
|
3
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
4
|
+
import { ScheduleType } from './ScheduleType';
|
5
|
+
import { RepeatOn } from './RepeatOn';
|
6
|
+
import { QueryType } from './QueryType';
|
7
|
+
import { StartEndDates } from './StartEndDates';
|
8
|
+
import { repeatTypes } from '../../JobWizardConstants';
|
9
|
+
|
10
|
+
const Schedule = () => {
|
11
|
+
const [repeatType, setRepeatType] = useState(repeatTypes.noRepeat);
|
12
|
+
const [repeatAmount, setRepeatAmount] = useState('');
|
13
|
+
const [starts, setStarts] = useState('');
|
14
|
+
const [ends, setEnds] = useState('');
|
15
|
+
|
16
|
+
return (
|
17
|
+
<Form className="schedule-tab">
|
18
|
+
<Title headingLevel="h2">{__('Schedule')}</Title>
|
19
|
+
<ScheduleType />
|
20
|
+
|
21
|
+
<RepeatOn
|
22
|
+
repeatType={repeatType}
|
23
|
+
setRepeatType={setRepeatType}
|
24
|
+
repeatAmount={repeatAmount}
|
25
|
+
setRepeatAmount={setRepeatAmount}
|
26
|
+
/>
|
27
|
+
<StartEndDates
|
28
|
+
starts={starts}
|
29
|
+
setStarts={setStarts}
|
30
|
+
ends={ends}
|
31
|
+
setEnds={setEnds}
|
32
|
+
/>
|
33
|
+
<Button variant="link" className="advanced-scheduling-button" isInline>
|
34
|
+
{__('Advanced scheduling')}
|
35
|
+
</Button>
|
36
|
+
<QueryType />
|
37
|
+
</Form>
|
38
|
+
);
|
39
|
+
};
|
40
|
+
|
41
|
+
export default Schedule;
|