foreman_remote_execution 4.2.2 → 4.5.0

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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/js_ci.yml +29 -0
  3. data/.github/workflows/{ci.yml → ruby_ci.yml} +22 -50
  4. data/.prettierrc +4 -0
  5. data/.rubocop.yml +13 -49
  6. data/.rubocop_todo.yml +326 -102
  7. data/Gemfile +1 -4
  8. data/app/controllers/api/v2/job_invocations_controller.rb +28 -23
  9. data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_commands_controller_extensions.rb +19 -0
  10. data/app/controllers/job_templates_controller.rb +4 -4
  11. data/app/controllers/ui_job_wizard_controller.rb +30 -0
  12. data/app/helpers/job_invocations_helper.rb +2 -2
  13. data/app/helpers/remote_execution_helper.rb +35 -8
  14. data/app/lib/actions/remote_execution/run_host_job.rb +68 -5
  15. data/app/lib/foreman_remote_execution/provider_input.rb +29 -0
  16. data/app/lib/foreman_remote_execution/renderer/scope/input.rb +1 -0
  17. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +5 -5
  18. data/app/models/host_status/execution_status.rb +12 -5
  19. data/app/models/invocation_provider_input_value.rb +12 -0
  20. data/app/models/job_invocation.rb +28 -8
  21. data/app/models/job_invocation_composer.rb +72 -17
  22. data/app/models/remote_execution_provider.rb +17 -2
  23. data/app/models/setting/remote_execution.rb +10 -0
  24. data/app/models/ssh_execution_provider.rb +4 -4
  25. data/app/models/template_invocation.rb +2 -0
  26. data/app/overrides/execution_interface.rb +8 -8
  27. data/app/overrides/subnet_proxies.rb +6 -6
  28. data/app/services/renderer_methods.rb +12 -0
  29. data/app/views/job_invocations/_form.html.erb +8 -0
  30. data/app/views/template_invocations/show.html.erb +30 -23
  31. data/config/routes.rb +5 -0
  32. data/db/migrate/20180110104432_rename_template_invocation_permission.rb +1 -1
  33. data/db/migrate/20190111153330_remove_remote_execution_without_proxy_setting.rb +4 -4
  34. data/db/migrate/20210312074713_add_provider_inputs.rb +10 -0
  35. data/extra/cockpit/foreman-cockpit-session +6 -6
  36. data/foreman_remote_execution.gemspec +1 -2
  37. data/lib/foreman_remote_execution/engine.rb +22 -7
  38. data/lib/foreman_remote_execution/version.rb +1 -1
  39. data/lib/tasks/foreman_remote_execution_tasks.rake +1 -18
  40. data/locale/action_names.rb +1 -0
  41. data/locale/de/foreman_remote_execution.po +77 -27
  42. data/locale/en/foreman_remote_execution.po +77 -27
  43. data/locale/en_GB/foreman_remote_execution.po +77 -27
  44. data/locale/es/foreman_remote_execution.po +77 -27
  45. data/locale/foreman_remote_execution.pot +241 -163
  46. data/locale/fr/foreman_remote_execution.po +77 -27
  47. data/locale/ja/foreman_remote_execution.po +77 -27
  48. data/locale/ko/foreman_remote_execution.po +77 -27
  49. data/locale/pt_BR/foreman_remote_execution.po +77 -27
  50. data/locale/ru/foreman_remote_execution.po +77 -27
  51. data/locale/zh_CN/foreman_remote_execution.po +77 -27
  52. data/locale/zh_TW/foreman_remote_execution.po +77 -27
  53. data/package.json +4 -2
  54. data/test/functional/api/v2/job_invocations_controller_test.rb +38 -5
  55. data/test/functional/api/v2/registration_controller_test.rb +4 -13
  56. data/test/functional/ui_job_wizard_controller_test.rb +16 -0
  57. data/test/helpers/remote_execution_helper_test.rb +16 -0
  58. data/test/unit/job_invocation_composer_test.rb +86 -2
  59. data/test/unit/job_invocation_report_template_test.rb +57 -0
  60. data/webpack/JobWizard/JobWizard.js +96 -0
  61. data/webpack/JobWizard/JobWizard.scss +14 -0
  62. data/webpack/JobWizard/JobWizardConstants.js +6 -0
  63. data/webpack/JobWizard/JobWizardSelectors.js +38 -0
  64. data/webpack/JobWizard/__tests__/JobWizard.test.js +13 -0
  65. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +32 -0
  66. data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +43 -0
  67. data/webpack/JobWizard/__tests__/fixtures.js +26 -0
  68. data/webpack/JobWizard/__tests__/integration.test.js +156 -0
  69. data/webpack/JobWizard/index.js +32 -0
  70. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +93 -0
  71. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +181 -0
  72. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +25 -0
  73. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +249 -0
  74. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +109 -0
  75. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +52 -0
  76. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +113 -0
  77. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +94 -0
  78. data/webpack/JobWizard/steps/form/FormHelpers.js +19 -0
  79. data/webpack/JobWizard/steps/form/GroupedSelectField.js +91 -0
  80. data/webpack/JobWizard/steps/form/SelectField.js +48 -0
  81. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +38 -0
  82. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +23 -0
  83. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +37 -0
  84. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +23 -0
  85. data/webpack/Routes/routes.js +12 -0
  86. data/webpack/__mocks__/foremanReact/common/helpers.js +1 -0
  87. data/webpack/__mocks__/foremanReact/history.js +1 -0
  88. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +21 -2
  89. data/webpack/__mocks__/foremanReact/redux/API/index.js +5 -0
  90. data/webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js +10 -0
  91. data/webpack/global_index.js +10 -0
  92. data/webpack/index.js +3 -4
  93. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +83 -0
  94. data/webpack/react_app/components/RecentJobsCard/constants.js +1 -0
  95. data/webpack/react_app/components/RecentJobsCard/index.js +1 -0
  96. data/webpack/react_app/components/RecentJobsCard/styles.css +15 -0
  97. data/webpack/react_app/components/RegistrationExtension/RexInterface.js +50 -0
  98. data/webpack/react_app/components/RegistrationExtension/__tests__/RexInterface.test.js +9 -0
  99. data/webpack/react_app/components/RegistrationExtension/__tests__/__snapshots__/RexInterface.test.js.snap +35 -0
  100. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +1 -1
  101. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +0 -3
  102. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +8 -3
  103. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -1
  104. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +7 -2
  105. data/webpack/react_app/extend/fillRecentJobsCard.js +11 -0
  106. data/webpack/react_app/extend/fillregistrationAdvanced.js +11 -0
  107. data/webpack/react_app/extend/reducers.js +5 -0
  108. metadata +58 -20
  109. data/app/views/api/v2/registration/_form.html.erb +0 -12
@@ -0,0 +1,94 @@
1
+ import React, { useEffect } from 'react';
2
+ import { useSelector, useDispatch } from 'react-redux';
3
+ import PropTypes from 'prop-types';
4
+ import URI from 'urijs';
5
+ import { get } from 'foremanReact/redux/API';
6
+ import {
7
+ selectJobCategories,
8
+ selectJobTemplates,
9
+ selectJobCategoriesStatus,
10
+ filterJobTemplates,
11
+ selectCategoryError,
12
+ selectAllTemplatesError,
13
+ selectTemplateError,
14
+ } from '../../JobWizardSelectors';
15
+ import { CategoryAndTemplate } from './CategoryAndTemplate';
16
+
17
+ import {
18
+ JOB_TEMPLATES,
19
+ JOB_CATEGORIES,
20
+ templatesUrl,
21
+ } from '../../JobWizardConstants';
22
+
23
+ const ConnectedCategoryAndTemplate = ({
24
+ jobTemplate,
25
+ setJobTemplate,
26
+ category,
27
+ setCategory,
28
+ }) => {
29
+ const dispatch = useDispatch();
30
+
31
+ const jobCategoriesStatus = useSelector(selectJobCategoriesStatus);
32
+ useEffect(() => {
33
+ if (!jobCategoriesStatus) {
34
+ // Don't reload categories if they are already loaded
35
+ dispatch(
36
+ get({
37
+ key: JOB_CATEGORIES,
38
+ url: '/ui_job_wizard/categories',
39
+ handleSuccess: response =>
40
+ setCategory(response.data.job_categories[0] || ''),
41
+ })
42
+ );
43
+ }
44
+ }, [jobCategoriesStatus, dispatch, setCategory]);
45
+
46
+ const jobCategories = useSelector(selectJobCategories);
47
+ useEffect(() => {
48
+ if (category) {
49
+ const templatesUrlObject = new URI(templatesUrl);
50
+ dispatch(
51
+ get({
52
+ key: JOB_TEMPLATES,
53
+ url: templatesUrlObject.addSearch({
54
+ search: `job_category="${category}"`,
55
+ per_page: 'all',
56
+ }),
57
+ handleSuccess: response =>
58
+ setJobTemplate(
59
+ Number(filterJobTemplates(response?.data?.results)[0]?.id) || null
60
+ ),
61
+ })
62
+ );
63
+ }
64
+ }, [category, dispatch, setJobTemplate]);
65
+
66
+ const jobTemplates = useSelector(selectJobTemplates);
67
+
68
+ const errors = {
69
+ categoryError: useSelector(selectCategoryError),
70
+ allTemplatesError: useSelector(selectAllTemplatesError),
71
+ templateError: useSelector(selectTemplateError),
72
+ };
73
+ return (
74
+ <CategoryAndTemplate
75
+ jobTemplates={jobTemplates}
76
+ jobCategories={jobCategories}
77
+ setJobTemplate={setJobTemplate}
78
+ selectedTemplateID={jobTemplate}
79
+ setCategory={setCategory}
80
+ selectedCategory={category}
81
+ errors={errors}
82
+ />
83
+ );
84
+ };
85
+
86
+ ConnectedCategoryAndTemplate.propTypes = {
87
+ jobTemplate: PropTypes.number,
88
+ setJobTemplate: PropTypes.func.isRequired,
89
+ category: PropTypes.string.isRequired,
90
+ setCategory: PropTypes.func.isRequired,
91
+ };
92
+ ConnectedCategoryAndTemplate.defaultProps = { jobTemplate: null };
93
+
94
+ export default ConnectedCategoryAndTemplate;
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { Popover } from '@patternfly/react-core';
3
+ import { HelpIcon } from '@patternfly/react-icons';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+
6
+ export const helpLabel = (text, id) => {
7
+ if (!text) return null;
8
+ return (
9
+ <Popover id={`${id}-help`} bodyContent={text} aria-label="help-text">
10
+ <button
11
+ aria-label={__('open-help-tooltip-button')}
12
+ onClick={e => e.preventDefault()}
13
+ className="pf-c-form__group-label-help"
14
+ >
15
+ <HelpIcon noVerticalAlign />
16
+ </button>
17
+ </Popover>
18
+ );
19
+ };
@@ -0,0 +1,91 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ SelectOption,
5
+ Select,
6
+ SelectGroup,
7
+ SelectVariant,
8
+ FormGroup,
9
+ } from '@patternfly/react-core';
10
+
11
+ export const GroupedSelectField = ({
12
+ label,
13
+ fieldId,
14
+ groups,
15
+ selected,
16
+ setSelected,
17
+ ...props
18
+ }) => {
19
+ const [isOpen, setIsOpen] = useState(false);
20
+ const onSelect = selection => {
21
+ setIsOpen(false);
22
+ setSelected(selection);
23
+ };
24
+
25
+ const onClear = () => {
26
+ onSelect(null);
27
+ };
28
+
29
+ const options = groups.map((group, groupIndex) => (
30
+ <SelectGroup key={groupIndex} label={group.groupLabel}>
31
+ {group.options.map((option, optionIndex) => (
32
+ <SelectOption
33
+ key={optionIndex}
34
+ value={option.label}
35
+ onClick={() => onSelect(option.value)}
36
+ />
37
+ ))}
38
+ </SelectGroup>
39
+ ));
40
+
41
+ const onFilter = evt => {
42
+ const textInput = evt?.target?.value || '';
43
+ if (textInput === '') {
44
+ return options;
45
+ }
46
+ return options
47
+ .map(group => {
48
+ const filteredGroup = React.cloneElement(group, {
49
+ children: group.props.children.filter(item =>
50
+ item.props.value.toLowerCase().includes(textInput.toLowerCase())
51
+ ),
52
+ });
53
+ if (filteredGroup.props.children.length > 0) return filteredGroup;
54
+ return null;
55
+ })
56
+ .filter(newGroup => newGroup);
57
+ };
58
+
59
+ return (
60
+ <FormGroup label={label} fieldId={fieldId}>
61
+ <Select
62
+ isGrouped
63
+ variant={SelectVariant.typeahead}
64
+ onToggle={setIsOpen}
65
+ onFilter={onFilter}
66
+ isOpen={isOpen}
67
+ onSelect={() => null}
68
+ selections={selected}
69
+ className="without_select2"
70
+ onClear={onClear}
71
+ menuAppendTo={() => document.body}
72
+ {...props}
73
+ >
74
+ {options}
75
+ </Select>
76
+ </FormGroup>
77
+ );
78
+ };
79
+
80
+ GroupedSelectField.propTypes = {
81
+ label: PropTypes.string.isRequired,
82
+ fieldId: PropTypes.string.isRequired,
83
+ groups: PropTypes.array,
84
+ selected: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
85
+ setSelected: PropTypes.func.isRequired,
86
+ };
87
+
88
+ GroupedSelectField.defaultProps = {
89
+ groups: [],
90
+ selected: null,
91
+ };
@@ -0,0 +1,48 @@
1
+ import React, { useState } from 'react';
2
+ import { FormGroup, Select, SelectOption } from '@patternfly/react-core';
3
+ import PropTypes from 'prop-types';
4
+
5
+ export const SelectField = ({
6
+ label,
7
+ fieldId,
8
+ options,
9
+ value,
10
+ setValue,
11
+ ...props
12
+ }) => {
13
+ const onSelect = (event, selection) => {
14
+ setValue(selection);
15
+ setIsOpen(false);
16
+ };
17
+ const [isOpen, setIsOpen] = useState(false);
18
+ return (
19
+ <FormGroup label={label} fieldId={fieldId}>
20
+ <Select
21
+ selections={value}
22
+ onSelect={onSelect}
23
+ onToggle={setIsOpen}
24
+ isOpen={isOpen}
25
+ className="without_select2"
26
+ maxHeight="45vh"
27
+ menuAppendTo={() => document.body}
28
+ {...props}
29
+ >
30
+ {options.map((option, index) => (
31
+ <SelectOption key={index} value={option} />
32
+ ))}
33
+ </Select>
34
+ </FormGroup>
35
+ );
36
+ };
37
+ SelectField.propTypes = {
38
+ label: PropTypes.string.isRequired,
39
+ fieldId: PropTypes.string.isRequired,
40
+ options: PropTypes.array,
41
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
42
+ setValue: PropTypes.func.isRequired,
43
+ };
44
+
45
+ SelectField.defaultProps = {
46
+ options: [],
47
+ value: null,
48
+ };
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import * as patternfly from '@patternfly/react-core';
3
+ import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
4
+ import { GroupedSelectField } from '../GroupedSelectField';
5
+
6
+ jest.spyOn(patternfly, 'Select');
7
+ jest.spyOn(patternfly, 'SelectOption');
8
+ patternfly.Select.mockImplementation(props => <div>{props}</div>);
9
+ patternfly.SelectOption.mockImplementation(props => <div>{props}</div>);
10
+
11
+ const fixtures = {
12
+ 'renders with props': {
13
+ label: 'grouped select',
14
+ fieldId: 'field-id',
15
+ groups: [
16
+ {
17
+ groupLabel: 'Ansible',
18
+ options: [
19
+ {
20
+ label: 'Ansible Roles - Ansible Default',
21
+ value: 168,
22
+ },
23
+ {
24
+ label: 'Ansible Roles - Install from git',
25
+ value: 170,
26
+ },
27
+ ],
28
+ },
29
+ ],
30
+ selected: 170,
31
+ setSelected: jest.fn(),
32
+ },
33
+ };
34
+
35
+ describe('GroupedSelectField', () => {
36
+ describe('rendering', () =>
37
+ testComponentSnapshotsWithFixtures(GroupedSelectField, fixtures));
38
+ });
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import * as patternfly from '@patternfly/react-core';
3
+ import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
4
+ import { SelectField } from '../SelectField';
5
+
6
+ jest.spyOn(patternfly, 'Select');
7
+ jest.spyOn(patternfly, 'SelectOption');
8
+ patternfly.Select.mockImplementation(props => <div>{props}</div>);
9
+ patternfly.SelectOption.mockImplementation(props => <div>{props}</div>);
10
+ const fixtures = {
11
+ 'renders with props': {
12
+ label: 'grouped select',
13
+ fieldId: 'field-id',
14
+ options: ['Commands'],
15
+ value: 'Commands',
16
+ setValue: jest.fn(),
17
+ },
18
+ };
19
+
20
+ describe('SelectField', () => {
21
+ describe('rendering', () =>
22
+ testComponentSnapshotsWithFixtures(SelectField, fixtures));
23
+ });
@@ -0,0 +1,37 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`GroupedSelectField rendering renders with props 1`] = `
4
+ <FormGroup
5
+ fieldId="field-id"
6
+ label="grouped select"
7
+ >
8
+ <mockConstructor
9
+ className="without_select2"
10
+ isGrouped={true}
11
+ isOpen={false}
12
+ menuAppendTo={[Function]}
13
+ onClear={[Function]}
14
+ onFilter={[Function]}
15
+ onSelect={[Function]}
16
+ onToggle={[Function]}
17
+ selections={170}
18
+ variant="typeahead"
19
+ >
20
+ <SelectGroup
21
+ key="0"
22
+ label="Ansible"
23
+ >
24
+ <mockConstructor
25
+ key="0"
26
+ onClick={[Function]}
27
+ value="Ansible Roles - Ansible Default"
28
+ />
29
+ <mockConstructor
30
+ key="1"
31
+ onClick={[Function]}
32
+ value="Ansible Roles - Install from git"
33
+ />
34
+ </SelectGroup>
35
+ </mockConstructor>
36
+ </FormGroup>
37
+ `;
@@ -0,0 +1,23 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`SelectField rendering renders with props 1`] = `
4
+ <FormGroup
5
+ fieldId="field-id"
6
+ label="grouped select"
7
+ >
8
+ <mockConstructor
9
+ className="without_select2"
10
+ isOpen={false}
11
+ maxHeight="45vh"
12
+ menuAppendTo={[Function]}
13
+ onSelect={[Function]}
14
+ onToggle={[Function]}
15
+ selections="Commands"
16
+ >
17
+ <mockConstructor
18
+ key="0"
19
+ value="Commands"
20
+ />
21
+ </mockConstructor>
22
+ </FormGroup>
23
+ `;
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import JobWizardPage from '../JobWizard';
3
+
4
+ const ForemanREXRoutes = [
5
+ {
6
+ path: '/experimental/job_wizard',
7
+ exact: true,
8
+ render: props => <JobWizardPage {...props} />,
9
+ },
10
+ ];
11
+
12
+ export default ForemanREXRoutes;
@@ -0,0 +1 @@
1
+ export const foremanUrl = path => `foreman${path}`;
@@ -0,0 +1 @@
1
+ export default { goBack: () => null };
@@ -1,2 +1,21 @@
1
- export const selectAPIStatus = () => 'RESOLVED';
2
- export const selectAPIResponse = state => state;
1
+ export const selectAPI = state => state;
2
+ export const selectAPIByKey = (state, key) => selectAPI(state)[key] || {};
3
+
4
+ export const selectAPIStatus = (state, key) =>
5
+ selectAPIByKey(state, key).status;
6
+
7
+ export const selectAPIPayload = (state, key) =>
8
+ selectAPIByKey(state, key).payload || {};
9
+
10
+ export const selectAPIResponse = (state, key) =>
11
+ selectAPIByKey(state, key).response || {};
12
+
13
+ export const selectAPIError = (state, key) =>
14
+ selectAPIStatus(state, key) === 'ERROR'
15
+ ? selectAPIResponse(state, key)
16
+ : null;
17
+
18
+ export const selectAPIErrorMessage = (state, key) => {
19
+ const error = selectAPIError(state, key);
20
+ return error && error.message;
21
+ };
@@ -0,0 +1,5 @@
1
+ export const API = {
2
+ get: jest.fn(),
3
+ };
4
+
5
+ export const get = data => ({ type: 'get-some-type', ...data });
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ const PageLayout = ({ children }) => <div>{children}</div>;
5
+
6
+ PageLayout.propTypes = {
7
+ children: PropTypes.node.isRequired,
8
+ };
9
+
10
+ export default PageLayout;