foreman_remote_execution 4.2.1 → 4.4.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 (90) 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 +17 -1
  9. data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_commands_controller_extensions.rb +19 -0
  10. data/app/controllers/ui_job_wizard_controller.rb +18 -0
  11. data/app/helpers/remote_execution_helper.rb +27 -0
  12. data/app/lib/actions/remote_execution/run_host_job.rb +38 -1
  13. data/app/lib/foreman_remote_execution/provider_input.rb +29 -0
  14. data/app/lib/foreman_remote_execution/renderer/scope/input.rb +1 -0
  15. data/app/models/foreign_input_set.rb +1 -1
  16. data/app/models/host_status/execution_status.rb +7 -0
  17. data/app/models/invocation_provider_input_value.rb +12 -0
  18. data/app/models/job_invocation.rb +5 -1
  19. data/app/models/job_invocation_composer.rb +13 -0
  20. data/app/models/remote_execution_provider.rb +17 -2
  21. data/app/models/setting/remote_execution.rb +10 -0
  22. data/app/models/template_invocation.rb +2 -0
  23. data/app/services/renderer_methods.rb +12 -0
  24. data/app/views/job_invocations/_form.html.erb +8 -0
  25. data/app/views/template_invocations/show.html.erb +30 -23
  26. data/config/routes.rb +4 -0
  27. data/db/migrate/20210312074713_add_provider_inputs.rb +10 -0
  28. data/foreman_remote_execution.gemspec +1 -2
  29. data/lib/foreman_remote_execution/engine.rb +17 -7
  30. data/lib/foreman_remote_execution/version.rb +1 -1
  31. data/lib/tasks/foreman_remote_execution_tasks.rake +1 -18
  32. data/locale/action_names.rb +1 -0
  33. data/locale/de/foreman_remote_execution.po +77 -27
  34. data/locale/en/foreman_remote_execution.po +77 -27
  35. data/locale/en_GB/foreman_remote_execution.po +77 -27
  36. data/locale/es/foreman_remote_execution.po +77 -27
  37. data/locale/foreman_remote_execution.pot +241 -163
  38. data/locale/fr/foreman_remote_execution.po +77 -27
  39. data/locale/ja/foreman_remote_execution.po +77 -27
  40. data/locale/ko/foreman_remote_execution.po +77 -27
  41. data/locale/pt_BR/foreman_remote_execution.po +77 -27
  42. data/locale/ru/foreman_remote_execution.po +77 -27
  43. data/locale/zh_CN/foreman_remote_execution.po +77 -27
  44. data/locale/zh_TW/foreman_remote_execution.po +77 -27
  45. data/package.json +3 -2
  46. data/test/functional/api/v2/job_invocations_controller_test.rb +24 -4
  47. data/test/functional/api/v2/registration_controller_test.rb +4 -13
  48. data/test/functional/ui_job_wizard_controller_test.rb +16 -0
  49. data/test/helpers/remote_execution_helper_test.rb +16 -0
  50. data/test/unit/job_invocation_composer_test.rb +41 -1
  51. data/test/unit/job_invocation_report_template_test.rb +57 -0
  52. data/webpack/JobWizard/JobWizard.js +55 -0
  53. data/webpack/JobWizard/JobWizard.scss +12 -0
  54. data/webpack/JobWizard/JobWizardConstants.js +5 -0
  55. data/webpack/JobWizard/JobWizardSelectors.js +21 -0
  56. data/webpack/JobWizard/__tests__/JobWizard.test.js +20 -0
  57. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +83 -0
  58. data/webpack/JobWizard/index.js +32 -0
  59. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +77 -0
  60. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +45 -0
  61. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +64 -0
  62. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +86 -0
  63. data/webpack/JobWizard/steps/form/GroupedSelectField.js +88 -0
  64. data/webpack/JobWizard/steps/form/SelectField.js +39 -0
  65. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +38 -0
  66. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +23 -0
  67. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +36 -0
  68. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +22 -0
  69. data/webpack/Routes/routes.js +12 -0
  70. data/webpack/__mocks__/foremanReact/common/helpers.js +1 -0
  71. data/webpack/__mocks__/foremanReact/history.js +1 -0
  72. data/webpack/__mocks__/foremanReact/redux/API/index.js +5 -0
  73. data/webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js +10 -0
  74. data/webpack/fills_index.js +11 -0
  75. data/webpack/global_index.js +8 -0
  76. data/webpack/index.js +0 -4
  77. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +87 -0
  78. data/webpack/react_app/components/RecentJobsCard/constants.js +1 -0
  79. data/webpack/react_app/components/RecentJobsCard/index.js +1 -0
  80. data/webpack/react_app/components/RecentJobsCard/styles.css +15 -0
  81. data/webpack/react_app/components/RegistrationExtension/RexInterface.js +50 -0
  82. data/webpack/react_app/components/RegistrationExtension/__tests__/RexInterface.test.js +9 -0
  83. data/webpack/react_app/components/RegistrationExtension/__tests__/__snapshots__/RexInterface.test.js.snap +35 -0
  84. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +1 -1
  85. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +0 -3
  86. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -1
  87. data/webpack/react_app/extend/fills.js +10 -0
  88. data/webpack/react_app/extend/reducers.js +4 -0
  89. metadata +54 -24
  90. data/app/views/api/v2/registration/_form.html.erb +0 -12
@@ -0,0 +1,86 @@
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
+ } from '../../JobWizardSelectors';
12
+ import { CategoryAndTemplate } from './CategoryAndTemplate';
13
+
14
+ import {
15
+ JOB_TEMPLATES,
16
+ JOB_CATEGORIES,
17
+ templatesUrl,
18
+ } from '../../JobWizardConstants';
19
+
20
+ const ConnectedCategoryAndTemplate = ({
21
+ jobTemplate,
22
+ setJobTemplate,
23
+ category,
24
+ setCategory,
25
+ }) => {
26
+ const dispatch = useDispatch();
27
+
28
+ const jobCategoriesStatus = useSelector(selectJobCategoriesStatus);
29
+ useEffect(() => {
30
+ if (!jobCategoriesStatus) {
31
+ // Don't reload categories if they are already loaded
32
+ dispatch(
33
+ get({
34
+ key: JOB_CATEGORIES,
35
+ url: '/ui_job_wizard/categories',
36
+ handleSuccess: response =>
37
+ setCategory(response.data.job_categories[0] || ''),
38
+ })
39
+ );
40
+ }
41
+ }, [jobCategoriesStatus, dispatch, setCategory]);
42
+
43
+ const jobCategories = useSelector(selectJobCategories);
44
+
45
+ useEffect(() => {
46
+ if (category) {
47
+ const templatesUrlObject = new URI(templatesUrl);
48
+ dispatch(
49
+ get({
50
+ key: JOB_TEMPLATES,
51
+ url: templatesUrlObject.addSearch({
52
+ search: `job_category="${category}"`,
53
+ per_page: 'all',
54
+ }),
55
+ handleSuccess: response =>
56
+ setJobTemplate(
57
+ Number(filterJobTemplates(response?.data?.results)[0]?.id) || null
58
+ ),
59
+ })
60
+ );
61
+ }
62
+ }, [category, dispatch, setJobTemplate]);
63
+
64
+ const jobTemplates = useSelector(selectJobTemplates);
65
+
66
+ return (
67
+ <CategoryAndTemplate
68
+ jobTemplates={jobTemplates}
69
+ jobCategories={jobCategories}
70
+ setJobTemplate={setJobTemplate}
71
+ selectedTemplateID={jobTemplate}
72
+ setCategory={setCategory}
73
+ selectedCategory={category}
74
+ />
75
+ );
76
+ };
77
+
78
+ ConnectedCategoryAndTemplate.propTypes = {
79
+ jobTemplate: PropTypes.number,
80
+ setJobTemplate: PropTypes.func.isRequired,
81
+ category: PropTypes.string.isRequired,
82
+ setCategory: PropTypes.func.isRequired,
83
+ };
84
+ ConnectedCategoryAndTemplate.defaultProps = { jobTemplate: null };
85
+
86
+ export default ConnectedCategoryAndTemplate;
@@ -0,0 +1,88 @@
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
+ }) => {
18
+ const [isOpen, setIsOpen] = useState(false);
19
+ const onSelect = selection => {
20
+ setIsOpen(false);
21
+ setSelected(selection);
22
+ };
23
+
24
+ const onClear = () => {
25
+ onSelect(null);
26
+ };
27
+
28
+ const options = groups.map((group, groupIndex) => (
29
+ <SelectGroup key={groupIndex} label={group.groupLabel}>
30
+ {group.options.map((option, optionIndex) => (
31
+ <SelectOption
32
+ key={optionIndex}
33
+ value={option.label}
34
+ onClick={() => onSelect(option.value)}
35
+ />
36
+ ))}
37
+ </SelectGroup>
38
+ ));
39
+
40
+ const onFilter = evt => {
41
+ const textInput = evt?.target?.value || '';
42
+ if (textInput === '') {
43
+ return options;
44
+ }
45
+ return options
46
+ .map(group => {
47
+ const filteredGroup = React.cloneElement(group, {
48
+ children: group.props.children.filter(item =>
49
+ item.props.value.toLowerCase().includes(textInput.toLowerCase())
50
+ ),
51
+ });
52
+ if (filteredGroup.props.children.length > 0) return filteredGroup;
53
+ return null;
54
+ })
55
+ .filter(newGroup => newGroup);
56
+ };
57
+
58
+ return (
59
+ <FormGroup label={label} fieldId={fieldId}>
60
+ <Select
61
+ isGrouped
62
+ variant={SelectVariant.typeahead}
63
+ onToggle={setIsOpen}
64
+ onFilter={onFilter}
65
+ isOpen={isOpen}
66
+ onSelect={() => null}
67
+ selections={selected}
68
+ className="without_select2"
69
+ onClear={onClear}
70
+ >
71
+ {options}
72
+ </Select>
73
+ </FormGroup>
74
+ );
75
+ };
76
+
77
+ GroupedSelectField.propTypes = {
78
+ label: PropTypes.string.isRequired,
79
+ fieldId: PropTypes.string.isRequired,
80
+ groups: PropTypes.array,
81
+ selected: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
82
+ setSelected: PropTypes.func.isRequired,
83
+ };
84
+
85
+ GroupedSelectField.defaultProps = {
86
+ groups: [],
87
+ selected: null,
88
+ };
@@ -0,0 +1,39 @@
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 = ({ label, fieldId, options, value, setValue }) => {
6
+ const onSelect = (event, selection) => {
7
+ setValue(selection);
8
+ setIsOpen(false);
9
+ };
10
+ const [isOpen, setIsOpen] = useState(false);
11
+ return (
12
+ <FormGroup label={label} fieldId={fieldId}>
13
+ <Select
14
+ selections={value}
15
+ onSelect={onSelect}
16
+ onToggle={setIsOpen}
17
+ isOpen={isOpen}
18
+ className="without_select2"
19
+ maxHeight="45vh"
20
+ >
21
+ {options.map((option, index) => (
22
+ <SelectOption key={index} value={option} />
23
+ ))}
24
+ </Select>
25
+ </FormGroup>
26
+ );
27
+ };
28
+ SelectField.propTypes = {
29
+ label: PropTypes.string.isRequired,
30
+ fieldId: PropTypes.string.isRequired,
31
+ options: PropTypes.array,
32
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
33
+ setValue: PropTypes.func.isRequired,
34
+ };
35
+
36
+ SelectField.defaultProps = {
37
+ options: [],
38
+ value: null,
39
+ };
@@ -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,36 @@
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
+ onClear={[Function]}
13
+ onFilter={[Function]}
14
+ onSelect={[Function]}
15
+ onToggle={[Function]}
16
+ selections={170}
17
+ variant="typeahead"
18
+ >
19
+ <SelectGroup
20
+ key="0"
21
+ label="Ansible"
22
+ >
23
+ <mockConstructor
24
+ key="0"
25
+ onClick={[Function]}
26
+ value="Ansible Roles - Ansible Default"
27
+ />
28
+ <mockConstructor
29
+ key="1"
30
+ onClick={[Function]}
31
+ value="Ansible Roles - Install from git"
32
+ />
33
+ </SelectGroup>
34
+ </mockConstructor>
35
+ </FormGroup>
36
+ `;
@@ -0,0 +1,22 @@
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
+ onSelect={[Function]}
13
+ onToggle={[Function]}
14
+ selections="Commands"
15
+ >
16
+ <mockConstructor
17
+ key="0"
18
+ value="Commands"
19
+ />
20
+ </mockConstructor>
21
+ </FormGroup>
22
+ `;
@@ -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 };
@@ -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;
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import { addGlobalFill } from 'foremanReact/components/common/Fill/GlobalFill';
3
+
4
+ import RexInterface from './react_app/components/RegistrationExtension/RexInterface';
5
+
6
+ addGlobalFill(
7
+ 'registrationAdvanced',
8
+ 'foreman-remote-exectuion-rex-interface',
9
+ <RexInterface key="registration-rex-interface" />,
10
+ 100
11
+ );
@@ -0,0 +1,8 @@
1
+ import { registerRoutes } from 'foremanReact/routes/RoutingService';
2
+ import routes from './Routes/routes';
3
+ import registerReducers from './react_app/extend/reducers';
4
+ import registerFills from './react_app/extend/fills';
5
+
6
+ registerRoutes('foreman_remote_execution', routes);
7
+ registerReducers();
8
+ registerFills();
data/webpack/index.js CHANGED
@@ -1,8 +1,6 @@
1
- import { registerReducer } from 'foremanReact/common/MountingService';
2
1
  import componentRegistry from 'foremanReact/components/componentRegistry';
3
2
  import JobInvocationContainer from './react_app/components/jobInvocations';
4
3
  import TargetingHosts from './react_app/components/TargetingHosts';
5
- import rootReducer from './react_app/redux/reducers';
6
4
 
7
5
  const components = [
8
6
  { name: 'JobInvocationContainer', type: JobInvocationContainer },
@@ -12,5 +10,3 @@ const components = [
12
10
  components.forEach(component => {
13
11
  componentRegistry.register(component);
14
12
  });
15
-
16
- registerReducer('foremanRemoteExecutionReducers', rootReducer);
@@ -0,0 +1,87 @@
1
+ /* eslint-disable camelcase */
2
+
3
+ import PropTypes from 'prop-types';
4
+ import React from 'react';
5
+ import Skeleton from 'react-loading-skeleton';
6
+ import ElipsisWithTooltip from 'react-ellipsis-with-tooltip';
7
+
8
+ import { Grid, GridItem } from '@patternfly/react-core';
9
+ import {
10
+ OkIcon,
11
+ ErrorCircleOIcon,
12
+ } from '@patternfly/react-icons/dist/js/icons';
13
+ import {
14
+ PropertiesSidePanel,
15
+ PropertyItem,
16
+ } from '@patternfly/react-catalog-view-extension';
17
+ import { ArrowIcon } from '@patternfly/react-icons';
18
+
19
+ import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
20
+ import CardItem from 'foremanReact/components/HostDetails/Templates/CardItem/CardTemplate';
21
+ import RelativeDateTime from 'foremanReact/components/common/dates/RelativeDateTime';
22
+ import { translate as __ } from 'foremanReact/common/I18n';
23
+ import './styles.css';
24
+
25
+ const RecentJobsCard = ({ hostDetails: { name } }) => {
26
+ const jobsUrl =
27
+ name && `/api/job_invocations?search=host%3D${name}&per_page=3`;
28
+ const {
29
+ response: { results: jobs },
30
+ } = useAPI('get', jobsUrl);
31
+
32
+ const iconMarkup = status => {
33
+ if (status === 1) return <ErrorCircleOIcon color="#C9190B" />;
34
+ return <OkIcon color="#3E8635" />;
35
+ };
36
+
37
+ return (
38
+ <CardItem
39
+ header={
40
+ <span>
41
+ {__('Recent Jobs')}{' '}
42
+ <a href={`/job_invocations?search=host+%3D+${name}`}>
43
+ <ArrowIcon />
44
+ </a>
45
+ </span>
46
+ }
47
+ >
48
+ <PropertiesSidePanel>
49
+ {jobs?.map(({ status, status_label, id, start_at, description }) => (
50
+ <PropertyItem
51
+ key={id}
52
+ label={
53
+ description ? (
54
+ <Grid>
55
+ <GridItem span={8}>
56
+ <ElipsisWithTooltip>{description}</ElipsisWithTooltip>
57
+ </GridItem>
58
+ <GridItem span={1}>{iconMarkup(status)}</GridItem>
59
+ <GridItem span={3}>{status_label}</GridItem>
60
+ </Grid>
61
+ ) : (
62
+ <Skeleton />
63
+ )
64
+ }
65
+ value={
66
+ start_at ? (
67
+ <a href={`/job_invocations/${id}`}>
68
+ <RelativeDateTime date={start_at} />
69
+ </a>
70
+ ) : (
71
+ <Skeleton />
72
+ )
73
+ }
74
+ />
75
+ ))}
76
+ </PropertiesSidePanel>
77
+ </CardItem>
78
+ );
79
+ };
80
+
81
+ export default RecentJobsCard;
82
+
83
+ RecentJobsCard.propTypes = {
84
+ hostDetails: PropTypes.shape({
85
+ name: PropTypes.string,
86
+ }).isRequired,
87
+ };