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
@@ -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 { testComponentSnapshotsWithFixtures } from '@theforeman/test';
2
- import { CategoryAndTemplate } from './CategoryAndTemplate';
3
-
4
- const baseProps = {
5
- setJobTemplate: jest.fn(),
6
- selectedTemplateID: 190,
7
- setCategory: jest.fn(),
8
- };
9
- const fixtures = {
10
- 'renders with props': {
11
- ...baseProps,
12
- jobCategories: [
13
- 'Commands',
14
- 'Ansible Playbook',
15
- 'Ansible Galaxy',
16
- 'Ansible Roles Installation',
17
- ],
18
- jobTemplates: [
19
- {
20
- id: 190,
21
- name: 'ab Run Command - SSH Default clone',
22
- job_category: 'Commands',
23
- provider_type: 'SSH',
24
- snippet: false,
25
- },
26
- {
27
- id: 168,
28
- name: 'Ansible Roles - Ansible Default',
29
- job_category: 'Ansible Playbook',
30
- provider_type: 'Ansible',
31
- snippet: false,
32
- },
33
- {
34
- id: 170,
35
- name: 'Ansible Roles - Install from git',
36
- job_category: 'Ansible Roles Installation',
37
- provider_type: 'Ansible',
38
- snippet: false,
39
- },
40
- ],
41
- selectedCategory: 'I am a category',
42
- },
43
- 'render with error': {
44
- ...baseProps,
45
- errors: { allTemplatesError: 'I have an error' },
46
- },
47
- };
48
-
49
- describe('CategoryAndTemplate', () => {
50
- describe('rendering', () =>
51
- testComponentSnapshotsWithFixtures(CategoryAndTemplate, fixtures));
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;