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.
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;