foreman_remote_execution 4.3.1 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +16 -0
  3. data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_commands_controller_extensions.rb +19 -0
  4. data/app/helpers/remote_execution_helper.rb +27 -0
  5. data/app/lib/foreman_remote_execution/provider_input.rb +29 -0
  6. data/app/models/invocation_provider_input_value.rb +12 -0
  7. data/app/models/job_invocation.rb +4 -0
  8. data/app/models/job_invocation_composer.rb +13 -0
  9. data/app/models/remote_execution_provider.rb +17 -2
  10. data/app/models/setting/remote_execution.rb +10 -0
  11. data/app/models/template_invocation.rb +2 -0
  12. data/app/services/renderer_methods.rb +12 -0
  13. data/app/views/job_invocations/_form.html.erb +8 -0
  14. data/db/migrate/20210312074713_add_provider_inputs.rb +10 -0
  15. data/foreman_remote_execution.gemspec +1 -1
  16. data/lib/foreman_remote_execution/engine.rb +5 -6
  17. data/lib/foreman_remote_execution/version.rb +1 -1
  18. data/locale/action_names.rb +1 -0
  19. data/locale/de/foreman_remote_execution.po +77 -27
  20. data/locale/en/foreman_remote_execution.po +77 -27
  21. data/locale/en_GB/foreman_remote_execution.po +77 -27
  22. data/locale/es/foreman_remote_execution.po +77 -27
  23. data/locale/foreman_remote_execution.pot +241 -163
  24. data/locale/fr/foreman_remote_execution.po +77 -27
  25. data/locale/ja/foreman_remote_execution.po +77 -27
  26. data/locale/ko/foreman_remote_execution.po +77 -27
  27. data/locale/pt_BR/foreman_remote_execution.po +77 -27
  28. data/locale/ru/foreman_remote_execution.po +77 -27
  29. data/locale/zh_CN/foreman_remote_execution.po +77 -27
  30. data/locale/zh_TW/foreman_remote_execution.po +77 -27
  31. data/package.json +3 -2
  32. data/test/helpers/remote_execution_helper_test.rb +16 -0
  33. data/test/unit/job_invocation_composer_test.rb +41 -1
  34. data/test/unit/job_invocation_report_template_test.rb +57 -0
  35. data/webpack/JobWizard/JobWizard.js +30 -7
  36. data/webpack/JobWizard/JobWizard.scss +12 -0
  37. data/webpack/JobWizard/JobWizardConstants.js +5 -0
  38. data/webpack/JobWizard/JobWizardSelectors.js +21 -0
  39. data/webpack/JobWizard/__tests__/JobWizard.test.js +20 -0
  40. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +83 -0
  41. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +77 -0
  42. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +45 -0
  43. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +64 -0
  44. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +86 -0
  45. data/webpack/JobWizard/steps/form/GroupedSelectField.js +88 -0
  46. data/webpack/JobWizard/steps/form/SelectField.js +39 -0
  47. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +38 -0
  48. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +23 -0
  49. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +36 -0
  50. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +22 -0
  51. data/webpack/__mocks__/foremanReact/common/helpers.js +1 -0
  52. data/webpack/__mocks__/foremanReact/redux/API/index.js +5 -0
  53. data/webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js +10 -0
  54. data/webpack/fills_index.js +11 -0
  55. data/webpack/global_index.js +4 -0
  56. data/webpack/index.js +0 -4
  57. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +87 -0
  58. data/webpack/react_app/components/RecentJobsCard/constants.js +1 -0
  59. data/webpack/react_app/components/RecentJobsCard/index.js +1 -0
  60. data/webpack/react_app/components/RecentJobsCard/styles.css +15 -0
  61. data/webpack/react_app/components/RegistrationExtension/RexInterface.js +50 -0
  62. data/webpack/react_app/components/RegistrationExtension/__tests__/RexInterface.test.js +9 -0
  63. data/webpack/react_app/components/RegistrationExtension/__tests__/__snapshots__/RexInterface.test.js.snap +35 -0
  64. data/webpack/react_app/extend/fills.js +10 -0
  65. data/webpack/react_app/extend/reducers.js +4 -0
  66. metadata +39 -5
  67. data/app/views/api/v2/registration/_form.html.erb +0 -12
@@ -0,0 +1,12 @@
1
+ .job-wizard {
2
+ .pf-c-wizard__main {
3
+ overflow: visible;
4
+ z-index: calc(
5
+ var(--pf-c-wizard__footer--ZIndex) + 1
6
+ ); // So the select box can be shown above the wizard footer
7
+ }
8
+
9
+ .pf-c-wizard__main-body {
10
+ max-width: 500px;
11
+ }
12
+ }
@@ -0,0 +1,5 @@
1
+ import { foremanUrl } from 'foremanReact/common/helpers';
2
+
3
+ export const JOB_TEMPLATES = 'JOB_TEMPLATES';
4
+ export const JOB_CATEGORIES = 'JOB_CATEGORIES';
5
+ export const templatesUrl = foremanUrl('/api/v2/job_templates');
@@ -0,0 +1,21 @@
1
+ import {
2
+ selectAPIResponse,
3
+ selectAPIStatus,
4
+ } from 'foremanReact/redux/API/APISelectors';
5
+
6
+ import { JOB_TEMPLATES, JOB_CATEGORIES } from './JobWizardConstants';
7
+
8
+ export const selectJobTemplatesStatus = state =>
9
+ selectAPIStatus(state, JOB_TEMPLATES);
10
+
11
+ export const filterJobTemplates = templates =>
12
+ templates?.filter(template => !template.snippet) || [];
13
+
14
+ export const selectJobTemplates = state =>
15
+ filterJobTemplates(selectAPIResponse(state, JOB_TEMPLATES)?.results);
16
+
17
+ export const selectJobCategories = state =>
18
+ selectAPIResponse(state, JOB_CATEGORIES).job_categories || [];
19
+
20
+ export const selectJobCategoriesStatus = state =>
21
+ selectAPIStatus(state, JOB_CATEGORIES);
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import * as patternfly from '@patternfly/react-core';
3
+ import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
4
+ import JobWizardPage from '../index';
5
+ import { JobWizard } from '../JobWizard';
6
+
7
+ jest.spyOn(patternfly, 'Wizard');
8
+ patternfly.Wizard.mockImplementation(props => <div>{props}</div>);
9
+ const fixtures = {
10
+ 'renders ': {},
11
+ };
12
+ describe('JobWizardPage', () => {
13
+ describe('rendering', () =>
14
+ testComponentSnapshotsWithFixtures(JobWizardPage, fixtures));
15
+ });
16
+
17
+ describe('JobWizard', () => {
18
+ describe('rendering', () =>
19
+ testComponentSnapshotsWithFixtures(JobWizard, fixtures));
20
+ });
@@ -0,0 +1,83 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`JobWizard rendering renders 1`] = `
4
+ <mockConstructor
5
+ className="job-wizard"
6
+ height="70vh"
7
+ navAriaLabel="Run Job steps"
8
+ onClose={[Function]}
9
+ steps={
10
+ Array [
11
+ Object {
12
+ "component": <ConnectedCategoryAndTemplate
13
+ category=""
14
+ jobTemplate={null}
15
+ setCategory={[Function]}
16
+ setJobTemplate={[Function]}
17
+ />,
18
+ "name": "Category and template",
19
+ },
20
+ Object {
21
+ "canJumpTo": false,
22
+ "component": <p>
23
+ TargetHosts
24
+ </p>,
25
+ "name": "Target hosts",
26
+ },
27
+ Object {
28
+ "canJumpTo": false,
29
+ "component": <p>
30
+ AdvancedFields
31
+ </p>,
32
+ "name": "Advanced fields",
33
+ },
34
+ Object {
35
+ "canJumpTo": false,
36
+ "component": <p>
37
+ Schedule
38
+ </p>,
39
+ "name": "Schedule",
40
+ },
41
+ Object {
42
+ "canJumpTo": false,
43
+ "component": <p>
44
+ ReviewDetails
45
+ </p>,
46
+ "name": "Review details",
47
+ "nextButtonText": "Run",
48
+ },
49
+ ]
50
+ }
51
+ />
52
+ `;
53
+
54
+ exports[`JobWizardPage rendering renders 1`] = `
55
+ <PageLayout
56
+ breadcrumbOptions={
57
+ Object {
58
+ "breadcrumbItems": Array [
59
+ Object {
60
+ "caption": "Jobs",
61
+ "url": "/jobs",
62
+ },
63
+ Object {
64
+ "caption": "Run job",
65
+ },
66
+ ],
67
+ }
68
+ }
69
+ header="Run job"
70
+ searchable={false}
71
+ >
72
+ <Title
73
+ headingLevel="h2"
74
+ size="2xl"
75
+ >
76
+ Run job
77
+ </Title>
78
+ <Divider
79
+ component="div"
80
+ />
81
+ <JobWizard />
82
+ </PageLayout>
83
+ `;
@@ -0,0 +1,77 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Title, Text, TextVariants, Form } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { SelectField } from '../form/SelectField';
6
+ import { GroupedSelectField } from '../form/GroupedSelectField';
7
+
8
+ export const CategoryAndTemplate = ({
9
+ jobCategories,
10
+ jobTemplates,
11
+ setJobTemplate,
12
+ selectedTemplateID,
13
+ selectedCategory,
14
+ setCategory,
15
+ }) => {
16
+ const templatesGroups = {};
17
+ jobTemplates.forEach(template => {
18
+ if (templatesGroups[template.provider_type]?.options)
19
+ templatesGroups[template.provider_type].options.push({
20
+ label: template.name,
21
+ value: template.id,
22
+ });
23
+ else
24
+ templatesGroups[template.provider_type] = {
25
+ options: [{ label: template.name, value: template.id }],
26
+ groupLabel: template.provider_type,
27
+ };
28
+ });
29
+
30
+ const selectedTemplate = jobTemplates.find(
31
+ template => template.id === selectedTemplateID
32
+ )?.name;
33
+
34
+ const onSelectCategory = newCategory => {
35
+ setCategory(newCategory);
36
+ setJobTemplate(null);
37
+ };
38
+ return (
39
+ <>
40
+ <Title headingLevel="h2">{__('Category And Template')}</Title>
41
+ <Text component={TextVariants.p}>{__('All fields are required.')}</Text>
42
+ <Form>
43
+ <SelectField
44
+ label={__('Job category')}
45
+ fieldId="job_category"
46
+ options={jobCategories}
47
+ setValue={onSelectCategory}
48
+ value={selectedCategory}
49
+ />
50
+ <GroupedSelectField
51
+ label={__('Job template')}
52
+ fieldId="job_template"
53
+ groups={Object.values(templatesGroups)}
54
+ setSelected={setJobTemplate}
55
+ selected={selectedTemplate}
56
+ />
57
+ </Form>
58
+ </>
59
+ );
60
+ };
61
+
62
+ CategoryAndTemplate.propTypes = {
63
+ jobCategories: PropTypes.array,
64
+ jobTemplates: PropTypes.array,
65
+ setJobTemplate: PropTypes.func.isRequired,
66
+ selectedTemplateID: PropTypes.number,
67
+ setCategory: PropTypes.func.isRequired,
68
+ selectedCategory: PropTypes.string,
69
+ };
70
+ CategoryAndTemplate.defaultProps = {
71
+ jobCategories: [],
72
+ jobTemplates: [],
73
+ selectedTemplateID: null,
74
+ selectedCategory: null,
75
+ };
76
+
77
+ export default CategoryAndTemplate;
@@ -0,0 +1,45 @@
1
+ import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
2
+ import { CategoryAndTemplate } from './CategoryAndTemplate';
3
+
4
+ const fixtures = {
5
+ 'renders with props': {
6
+ jobCategories: [
7
+ 'Commands',
8
+ 'Ansible Playbook',
9
+ 'Ansible Galaxy',
10
+ 'Ansible Roles Installation',
11
+ ],
12
+ jobTemplates: [
13
+ {
14
+ id: 190,
15
+ name: 'ab Run Command - SSH Default clone',
16
+ job_category: 'Commands',
17
+ provider_type: 'SSH',
18
+ snippet: false,
19
+ },
20
+ {
21
+ id: 168,
22
+ name: 'Ansible Roles - Ansible Default',
23
+ job_category: 'Ansible Playbook',
24
+ provider_type: 'Ansible',
25
+ snippet: false,
26
+ },
27
+ {
28
+ id: 170,
29
+ name: 'Ansible Roles - Install from git',
30
+ job_category: 'Ansible Roles Installation',
31
+ provider_type: 'Ansible',
32
+ snippet: false,
33
+ },
34
+ ],
35
+ setJobTemplate: jest.fn(),
36
+ selectedTemplateID: 190,
37
+ setCategory: jest.fn(),
38
+ selectedCategory: 'I am a category',
39
+ },
40
+ };
41
+
42
+ describe('CategoryAndTemplate', () => {
43
+ describe('rendering', () =>
44
+ testComponentSnapshotsWithFixtures(CategoryAndTemplate, fixtures));
45
+ });
@@ -0,0 +1,64 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`CategoryAndTemplate rendering renders with props 1`] = `
4
+ <Fragment>
5
+ <Title
6
+ headingLevel="h2"
7
+ >
8
+ Category And Template
9
+ </Title>
10
+ <Text
11
+ component="p"
12
+ >
13
+ All fields are required.
14
+ </Text>
15
+ <Form>
16
+ <SelectField
17
+ fieldId="job_category"
18
+ label="Job category"
19
+ options={
20
+ Array [
21
+ "Commands",
22
+ "Ansible Playbook",
23
+ "Ansible Galaxy",
24
+ "Ansible Roles Installation",
25
+ ]
26
+ }
27
+ setValue={[Function]}
28
+ value="I am a category"
29
+ />
30
+ <GroupedSelectField
31
+ fieldId="job_template"
32
+ groups={
33
+ Array [
34
+ Object {
35
+ "groupLabel": "SSH",
36
+ "options": Array [
37
+ Object {
38
+ "label": "ab Run Command - SSH Default clone",
39
+ "value": 190,
40
+ },
41
+ ],
42
+ },
43
+ Object {
44
+ "groupLabel": "Ansible",
45
+ "options": Array [
46
+ Object {
47
+ "label": "Ansible Roles - Ansible Default",
48
+ "value": 168,
49
+ },
50
+ Object {
51
+ "label": "Ansible Roles - Install from git",
52
+ "value": 170,
53
+ },
54
+ ],
55
+ },
56
+ ]
57
+ }
58
+ label="Job template"
59
+ selected="ab Run Command - SSH Default clone"
60
+ setSelected={[MockFunction]}
61
+ />
62
+ </Form>
63
+ </Fragment>
64
+ `;
@@ -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
+ };