foreman_remote_execution 4.3.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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +27 -22
  3. data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_commands_controller_extensions.rb +19 -0
  4. data/app/controllers/job_invocations_controller.rb +1 -1
  5. data/app/controllers/job_templates_controller.rb +4 -4
  6. data/app/controllers/ui_job_wizard_controller.rb +19 -0
  7. data/app/helpers/job_invocations_helper.rb +2 -2
  8. data/app/helpers/remote_execution_helper.rb +40 -9
  9. data/app/lib/actions/remote_execution/run_host_job.rb +36 -6
  10. data/app/lib/foreman_remote_execution/provider_input.rb +29 -0
  11. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +7 -5
  12. data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +6 -0
  13. data/app/models/host_proxy_invocation.rb +4 -0
  14. data/app/models/host_status/execution_status.rb +5 -5
  15. data/app/models/invocation_provider_input_value.rb +12 -0
  16. data/app/models/job_invocation.rb +35 -12
  17. data/app/models/job_invocation_composer.rb +74 -19
  18. data/app/models/remote_execution_provider.rb +18 -3
  19. data/app/models/setting/remote_execution.rb +11 -1
  20. data/app/models/ssh_execution_provider.rb +4 -4
  21. data/app/models/targeting.rb +5 -1
  22. data/app/models/template_invocation.rb +2 -0
  23. data/app/overrides/execution_interface.rb +8 -8
  24. data/app/overrides/subnet_proxies.rb +6 -6
  25. data/app/services/renderer_methods.rb +12 -0
  26. data/app/views/job_invocations/_form.html.erb +8 -0
  27. data/app/views/job_invocations/index.html.erb +1 -1
  28. data/app/views/templates/ssh/module_action.erb +1 -0
  29. data/app/views/templates/ssh/puppet_run_once.erb +1 -0
  30. data/config/routes.rb +1 -0
  31. data/db/migrate/20180110104432_rename_template_invocation_permission.rb +1 -1
  32. data/db/migrate/20190111153330_remove_remote_execution_without_proxy_setting.rb +4 -4
  33. data/db/migrate/20210312074713_add_provider_inputs.rb +10 -0
  34. data/db/migrate/2021051713291621250977_add_host_proxy_invocations.rb +12 -0
  35. data/extra/cockpit/foreman-cockpit-session +6 -6
  36. data/foreman_remote_execution.gemspec +1 -1
  37. data/lib/foreman_remote_execution/engine.rb +14 -12
  38. data/lib/foreman_remote_execution/version.rb +1 -1
  39. data/locale/action_names.rb +1 -0
  40. data/locale/de/foreman_remote_execution.po +77 -27
  41. data/locale/en/foreman_remote_execution.po +77 -27
  42. data/locale/en_GB/foreman_remote_execution.po +77 -27
  43. data/locale/es/foreman_remote_execution.po +77 -27
  44. data/locale/foreman_remote_execution.pot +241 -163
  45. data/locale/fr/foreman_remote_execution.po +77 -27
  46. data/locale/ja/foreman_remote_execution.po +77 -27
  47. data/locale/ko/foreman_remote_execution.po +77 -27
  48. data/locale/pt_BR/foreman_remote_execution.po +77 -27
  49. data/locale/ru/foreman_remote_execution.po +77 -27
  50. data/locale/zh_CN/foreman_remote_execution.po +77 -27
  51. data/locale/zh_TW/foreman_remote_execution.po +77 -27
  52. data/package.json +4 -2
  53. data/test/functional/api/v2/job_invocations_controller_test.rb +14 -1
  54. data/test/helpers/remote_execution_helper_test.rb +16 -0
  55. data/test/unit/job_invocation_composer_test.rb +100 -3
  56. data/test/unit/job_invocation_report_template_test.rb +57 -0
  57. data/test/unit/job_invocation_test.rb +1 -1
  58. data/webpack/JobWizard/JobWizard.js +95 -11
  59. data/webpack/JobWizard/JobWizard.scss +53 -0
  60. data/webpack/JobWizard/JobWizardConstants.js +16 -0
  61. data/webpack/JobWizard/JobWizardSelectors.js +47 -0
  62. data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +43 -0
  63. data/webpack/JobWizard/__tests__/fixtures.js +128 -0
  64. data/webpack/JobWizard/__tests__/integration.test.js +84 -0
  65. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +110 -0
  66. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +67 -0
  67. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +195 -0
  68. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +144 -0
  69. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +23 -0
  70. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +109 -0
  71. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +123 -0
  72. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +94 -0
  73. data/webpack/JobWizard/steps/Schedule/QueryType.js +48 -0
  74. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +61 -0
  75. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +25 -0
  76. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +51 -0
  77. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +22 -0
  78. data/webpack/JobWizard/steps/Schedule/index.js +41 -0
  79. data/webpack/JobWizard/steps/form/FormHelpers.js +20 -0
  80. data/webpack/JobWizard/steps/form/Formatter.js +149 -0
  81. data/webpack/JobWizard/steps/form/GroupedSelectField.js +91 -0
  82. data/webpack/JobWizard/steps/form/NumberInput.js +33 -0
  83. data/webpack/JobWizard/steps/form/SelectField.js +60 -0
  84. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +76 -0
  85. data/webpack/__mocks__/foremanReact/common/helpers.js +1 -0
  86. data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
  87. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +21 -2
  88. data/webpack/__mocks__/foremanReact/redux/API/index.js +5 -0
  89. data/webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js +10 -0
  90. data/webpack/global_index.js +6 -0
  91. data/webpack/index.js +3 -4
  92. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +83 -0
  93. data/webpack/react_app/components/RecentJobsCard/constants.js +1 -0
  94. data/webpack/react_app/components/RecentJobsCard/index.js +1 -0
  95. data/webpack/react_app/components/RecentJobsCard/styles.css +15 -0
  96. data/webpack/react_app/components/RegistrationExtension/RexInterface.js +50 -0
  97. data/webpack/react_app/components/RegistrationExtension/__tests__/RexInterface.test.js +9 -0
  98. data/webpack/react_app/components/RegistrationExtension/__tests__/__snapshots__/RexInterface.test.js.snap +35 -0
  99. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +8 -3
  100. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
  101. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +7 -2
  102. data/webpack/react_app/extend/fillRecentJobsCard.js +11 -0
  103. data/webpack/react_app/extend/fillregistrationAdvanced.js +11 -0
  104. data/webpack/react_app/extend/reducers.js +5 -0
  105. metadata +52 -8
  106. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +0 -70
  107. data/app/views/api/v2/registration/_form.html.erb +0 -12
  108. data/test/models/orchestration/ssh_test.rb +0 -56
@@ -0,0 +1,144 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { mount } from '@theforeman/test';
4
+ import { fireEvent, screen, render, act } from '@testing-library/react';
5
+ import * as api from 'foremanReact/redux/API';
6
+ import { JobWizard } from '../../../JobWizard';
7
+ import * as selectors from '../../../JobWizardSelectors';
8
+ import {
9
+ jobTemplateResponse as jobTemplate,
10
+ testSetup,
11
+ mockApi,
12
+ } from '../../../__tests__/fixtures';
13
+
14
+ const store = testSetup(selectors, api);
15
+ mockApi(api);
16
+
17
+ jest.spyOn(selectors, 'selectEffectiveUser');
18
+ jest.spyOn(selectors, 'selectTemplateInputs');
19
+ jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
20
+
21
+ selectors.selectEffectiveUser.mockImplementation(
22
+ () => jobTemplate.effective_user
23
+ );
24
+ selectors.selectTemplateInputs.mockImplementation(
25
+ () => jobTemplate.template_inputs
26
+ );
27
+
28
+ selectors.selectAdvancedTemplateInputs.mockImplementation(
29
+ () => jobTemplate.advanced_template_inputs
30
+ );
31
+ describe('AdvancedFields', () => {
32
+ it('should save data between steps for advanced fields', async () => {
33
+ const wrapper = mount(
34
+ <Provider store={store}>
35
+ <JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
36
+ </Provider>
37
+ );
38
+ // setup
39
+ wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
40
+ wrapper
41
+ .find('.pf-c-select__menu-item')
42
+ .first()
43
+ .simulate('click');
44
+
45
+ // test
46
+ expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
47
+ 0
48
+ );
49
+ wrapper
50
+ .find('.pf-c-wizard__nav-link')
51
+ .at(2)
52
+ .simulate('click'); // Advanced step
53
+ const effectiveUserInput = () => wrapper.find('input#effective-user');
54
+ const advancedTemplateInput = () =>
55
+ wrapper.find('.pf-c-form__group-control textarea');
56
+ const effectiveUesrValue = 'effective user new value';
57
+ const advancedTemplateInputValue = 'advanced input new value';
58
+ effectiveUserInput().getDOMNode().value = effectiveUesrValue;
59
+
60
+ effectiveUserInput().simulate('change');
61
+ wrapper.update();
62
+ advancedTemplateInput().getDOMNode().value = advancedTemplateInputValue;
63
+ advancedTemplateInput().simulate('change');
64
+ expect(effectiveUserInput().prop('value')).toEqual(effectiveUesrValue);
65
+ expect(advancedTemplateInput().prop('value')).toEqual(
66
+ advancedTemplateInputValue
67
+ );
68
+
69
+ wrapper
70
+ .find('.pf-c-wizard__nav-link')
71
+ .at(1)
72
+ .simulate('click');
73
+
74
+ expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-current').text()).toEqual(
75
+ 'Target Hosts'
76
+ );
77
+ wrapper
78
+ .find('.pf-c-wizard__nav-link')
79
+ .at(2)
80
+ .simulate('click'); // Advanced step
81
+
82
+ expect(effectiveUserInput().prop('value')).toEqual(effectiveUesrValue);
83
+ expect(advancedTemplateInput().prop('value')).toEqual(
84
+ advancedTemplateInputValue
85
+ );
86
+ });
87
+ it('fill template fields', async () => {
88
+ render(
89
+ <Provider store={store}>
90
+ <JobWizard />
91
+ </Provider>
92
+ );
93
+ await act(async () => {
94
+ fireEvent.click(screen.getByText('Advanced Fields'));
95
+ });
96
+ const searchValue = 'search test';
97
+ const textValue = 'I am a text';
98
+ const dateValue = '08/07/2021';
99
+ const textField = screen.getByLabelText('adv plain hidden', {
100
+ selector: 'textarea',
101
+ });
102
+ const selectField = screen.getByText('option 1');
103
+ const searchField = screen.getByPlaceholderText('Filter...');
104
+ const dateField = screen.getByLabelText('adv date', {
105
+ selector: 'input',
106
+ });
107
+
108
+ fireEvent.click(selectField);
109
+ await act(async () => {
110
+ await fireEvent.click(screen.getByText('option 2'));
111
+ fireEvent.click(screen.getAllByText('Advanced Fields')[0]); // to remove focus
112
+ await fireEvent.change(textField, {
113
+ target: { value: textValue },
114
+ });
115
+
116
+ await fireEvent.change(searchField, {
117
+ target: { value: searchValue },
118
+ });
119
+ await fireEvent.change(dateField, {
120
+ target: { value: dateValue },
121
+ });
122
+ });
123
+ expect(
124
+ screen.getByLabelText('adv plain hidden', {
125
+ selector: 'textarea',
126
+ }).value
127
+ ).toBe(textValue);
128
+ expect(searchField.value).toBe(searchValue);
129
+ expect(dateField.value).toBe(dateValue);
130
+ await act(async () => {
131
+ fireEvent.click(screen.getByText('Category and Template'));
132
+ });
133
+ expect(screen.getAllByText('Category and Template')).toHaveLength(3);
134
+
135
+ await act(async () => {
136
+ fireEvent.click(screen.getByText('Advanced Fields'));
137
+ });
138
+ expect(textField.value).toBe(textValue);
139
+ expect(searchField.value).toBe(searchValue);
140
+ expect(dateField.value).toBe(dateValue);
141
+ expect(screen.queryAllByText('option 1')).toHaveLength(0);
142
+ expect(screen.queryAllByText('option 2')).toHaveLength(1);
143
+ });
144
+ });
@@ -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
+ });
@@ -0,0 +1,109 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Title, Text, TextVariants, Form, Alert } 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
+ errors,
16
+ }) => {
17
+ const templatesGroups = {};
18
+ jobTemplates.forEach(template => {
19
+ if (templatesGroups[template.provider_type]?.options)
20
+ templatesGroups[template.provider_type].options.push({
21
+ label: template.name,
22
+ value: template.id,
23
+ });
24
+ else
25
+ templatesGroups[template.provider_type] = {
26
+ options: [{ label: template.name, value: template.id }],
27
+ groupLabel: template.provider_type,
28
+ };
29
+ });
30
+
31
+ const selectedTemplate = jobTemplates.find(
32
+ template => template.id === selectedTemplateID
33
+ )?.name;
34
+
35
+ const onSelectCategory = newCategory => {
36
+ setCategory(newCategory);
37
+ setJobTemplate(null);
38
+ };
39
+ const { categoryError, allTemplatesError, templateError } = errors;
40
+ const isError = !!(categoryError || allTemplatesError || templateError);
41
+ return (
42
+ <>
43
+ <Title headingLevel="h2">{__('Category and Template')}</Title>
44
+ <Text component={TextVariants.p}>{__('All fields are required.')}</Text>
45
+ <Form>
46
+ <SelectField
47
+ label={__('Job category')}
48
+ fieldId="job_category"
49
+ options={jobCategories}
50
+ setValue={onSelectCategory}
51
+ value={selectedCategory}
52
+ placeholderText={categoryError ? __('Error') : ''}
53
+ isDisabled={!!categoryError}
54
+ />
55
+ <GroupedSelectField
56
+ label={__('Job template')}
57
+ fieldId="job_template"
58
+ groups={Object.values(templatesGroups)}
59
+ setSelected={setJobTemplate}
60
+ selected={selectedTemplate}
61
+ isDisabled={!!(categoryError || allTemplatesError)}
62
+ placeholderText={allTemplatesError ? __('Error') : ''}
63
+ />
64
+ {isError && (
65
+ <Alert variant="danger" title={__('Errors:')}>
66
+ {categoryError && (
67
+ <span>
68
+ {__('Categories list failed with:')} {categoryError}
69
+ </span>
70
+ )}
71
+ {allTemplatesError && (
72
+ <span>
73
+ {__('Templates list failed with:')} {allTemplatesError}
74
+ </span>
75
+ )}
76
+ {templateError && (
77
+ <span>
78
+ {__('Template failed with:')} {templateError}
79
+ </span>
80
+ )}
81
+ </Alert>
82
+ )}
83
+ </Form>
84
+ </>
85
+ );
86
+ };
87
+
88
+ CategoryAndTemplate.propTypes = {
89
+ jobCategories: PropTypes.array,
90
+ jobTemplates: PropTypes.array,
91
+ setJobTemplate: PropTypes.func.isRequired,
92
+ selectedTemplateID: PropTypes.number,
93
+ setCategory: PropTypes.func.isRequired,
94
+ selectedCategory: PropTypes.string,
95
+ errors: PropTypes.shape({
96
+ categoryError: PropTypes.string,
97
+ allTemplatesError: PropTypes.string,
98
+ templateError: PropTypes.string,
99
+ }),
100
+ };
101
+ CategoryAndTemplate.defaultProps = {
102
+ jobCategories: [],
103
+ jobTemplates: [],
104
+ selectedTemplateID: null,
105
+ selectedCategory: null,
106
+ errors: {},
107
+ };
108
+
109
+ export default CategoryAndTemplate;
@@ -0,0 +1,123 @@
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
+ });
123
+ });
@@ -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;