foreman_remote_execution 4.2.1 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ };