foreman_remote_execution 4.4.0 → 4.5.3

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +13 -24
  3. data/app/controllers/job_invocations_controller.rb +1 -1
  4. data/app/controllers/job_templates_controller.rb +4 -4
  5. data/app/controllers/ui_job_wizard_controller.rb +19 -0
  6. data/app/helpers/job_invocations_helper.rb +2 -2
  7. data/app/helpers/remote_execution_helper.rb +13 -9
  8. data/app/lib/actions/remote_execution/run_host_job.rb +36 -6
  9. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +7 -5
  10. data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +6 -0
  11. data/app/models/host_proxy_invocation.rb +4 -0
  12. data/app/models/host_status/execution_status.rb +5 -5
  13. data/app/models/job_invocation.rb +31 -12
  14. data/app/models/job_invocation_composer.rb +61 -19
  15. data/app/models/remote_execution_provider.rb +1 -1
  16. data/app/models/setting/remote_execution.rb +2 -2
  17. data/app/models/ssh_execution_provider.rb +4 -4
  18. data/app/models/targeting.rb +5 -1
  19. data/app/overrides/execution_interface.rb +8 -8
  20. data/app/overrides/subnet_proxies.rb +6 -6
  21. data/app/views/job_invocations/index.html.erb +1 -1
  22. data/app/views/templates/ssh/module_action.erb +1 -0
  23. data/app/views/templates/ssh/puppet_run_once.erb +1 -0
  24. data/config/routes.rb +1 -0
  25. data/db/migrate/20180110104432_rename_template_invocation_permission.rb +1 -1
  26. data/db/migrate/20190111153330_remove_remote_execution_without_proxy_setting.rb +4 -4
  27. data/db/migrate/2021051713291621250977_add_host_proxy_invocations.rb +12 -0
  28. data/extra/cockpit/foreman-cockpit-session +6 -6
  29. data/lib/foreman_remote_execution/engine.rb +11 -8
  30. data/lib/foreman_remote_execution/version.rb +1 -1
  31. data/package.json +2 -1
  32. data/test/functional/api/v2/job_invocations_controller_test.rb +14 -1
  33. data/test/unit/job_invocation_composer_test.rb +59 -2
  34. data/test/unit/job_invocation_test.rb +1 -1
  35. data/webpack/JobWizard/JobWizard.js +80 -19
  36. data/webpack/JobWizard/JobWizard.scss +42 -1
  37. data/webpack/JobWizard/JobWizardConstants.js +11 -0
  38. data/webpack/JobWizard/JobWizardSelectors.js +27 -1
  39. data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +43 -0
  40. data/webpack/JobWizard/__tests__/fixtures.js +128 -0
  41. data/webpack/JobWizard/__tests__/integration.test.js +84 -0
  42. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +110 -0
  43. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +67 -0
  44. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +195 -0
  45. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +144 -0
  46. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +23 -0
  47. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +34 -2
  48. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +122 -44
  49. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +9 -1
  50. data/webpack/JobWizard/steps/Schedule/QueryType.js +48 -0
  51. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +61 -0
  52. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +25 -0
  53. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +51 -0
  54. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +22 -0
  55. data/webpack/JobWizard/steps/Schedule/index.js +41 -0
  56. data/webpack/JobWizard/steps/form/FormHelpers.js +20 -0
  57. data/webpack/JobWizard/steps/form/Formatter.js +149 -0
  58. data/webpack/JobWizard/steps/form/GroupedSelectField.js +3 -0
  59. data/webpack/JobWizard/steps/form/NumberInput.js +33 -0
  60. data/webpack/JobWizard/steps/form/SelectField.js +24 -3
  61. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +76 -0
  62. data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
  63. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +21 -2
  64. data/webpack/global_index.js +5 -3
  65. data/webpack/index.js +3 -0
  66. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +1 -5
  67. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +8 -3
  68. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
  69. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +7 -2
  70. data/webpack/react_app/extend/{fills.js → fillRecentJobsCard.js} +7 -6
  71. data/webpack/react_app/extend/fillregistrationAdvanced.js +11 -0
  72. data/webpack/react_app/extend/reducers.js +2 -1
  73. metadata +24 -14
  74. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +0 -70
  75. data/test/models/orchestration/ssh_test.rb +0 -56
  76. data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -20
  77. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -83
  78. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -64
  79. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
  80. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
  81. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -36
  82. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -22
  83. data/webpack/fills_index.js +0 -11
@@ -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
+ });
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { Title, Text, TextVariants, Form } from '@patternfly/react-core';
3
+ import { Title, Text, TextVariants, Form, Alert } from '@patternfly/react-core';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
5
  import { SelectField } from '../form/SelectField';
6
6
  import { GroupedSelectField } from '../form/GroupedSelectField';
@@ -12,6 +12,7 @@ export const CategoryAndTemplate = ({
12
12
  selectedTemplateID,
13
13
  selectedCategory,
14
14
  setCategory,
15
+ errors,
15
16
  }) => {
16
17
  const templatesGroups = {};
17
18
  jobTemplates.forEach(template => {
@@ -35,9 +36,11 @@ export const CategoryAndTemplate = ({
35
36
  setCategory(newCategory);
36
37
  setJobTemplate(null);
37
38
  };
39
+ const { categoryError, allTemplatesError, templateError } = errors;
40
+ const isError = !!(categoryError || allTemplatesError || templateError);
38
41
  return (
39
42
  <>
40
- <Title headingLevel="h2">{__('Category And Template')}</Title>
43
+ <Title headingLevel="h2">{__('Category and Template')}</Title>
41
44
  <Text component={TextVariants.p}>{__('All fields are required.')}</Text>
42
45
  <Form>
43
46
  <SelectField
@@ -46,6 +49,8 @@ export const CategoryAndTemplate = ({
46
49
  options={jobCategories}
47
50
  setValue={onSelectCategory}
48
51
  value={selectedCategory}
52
+ placeholderText={categoryError ? __('Error') : ''}
53
+ isDisabled={!!categoryError}
49
54
  />
50
55
  <GroupedSelectField
51
56
  label={__('Job template')}
@@ -53,7 +58,28 @@ export const CategoryAndTemplate = ({
53
58
  groups={Object.values(templatesGroups)}
54
59
  setSelected={setJobTemplate}
55
60
  selected={selectedTemplate}
61
+ isDisabled={!!(categoryError || allTemplatesError)}
62
+ placeholderText={allTemplatesError ? __('Error') : ''}
56
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
+ )}
57
83
  </Form>
58
84
  </>
59
85
  );
@@ -66,12 +92,18 @@ CategoryAndTemplate.propTypes = {
66
92
  selectedTemplateID: PropTypes.number,
67
93
  setCategory: PropTypes.func.isRequired,
68
94
  selectedCategory: PropTypes.string,
95
+ errors: PropTypes.shape({
96
+ categoryError: PropTypes.string,
97
+ allTemplatesError: PropTypes.string,
98
+ templateError: PropTypes.string,
99
+ }),
69
100
  };
70
101
  CategoryAndTemplate.defaultProps = {
71
102
  jobCategories: [],
72
103
  jobTemplates: [],
73
104
  selectedTemplateID: null,
74
105
  selectedCategory: null,
106
+ errors: {},
75
107
  };
76
108
 
77
109
  export default CategoryAndTemplate;
@@ -1,45 +1,123 @@
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));
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
+ });
45
123
  });
@@ -8,6 +8,9 @@ import {
8
8
  selectJobTemplates,
9
9
  selectJobCategoriesStatus,
10
10
  filterJobTemplates,
11
+ selectCategoryError,
12
+ selectAllTemplatesError,
13
+ selectTemplateError,
11
14
  } from '../../JobWizardSelectors';
12
15
  import { CategoryAndTemplate } from './CategoryAndTemplate';
13
16
 
@@ -41,7 +44,6 @@ const ConnectedCategoryAndTemplate = ({
41
44
  }, [jobCategoriesStatus, dispatch, setCategory]);
42
45
 
43
46
  const jobCategories = useSelector(selectJobCategories);
44
-
45
47
  useEffect(() => {
46
48
  if (category) {
47
49
  const templatesUrlObject = new URI(templatesUrl);
@@ -63,6 +65,11 @@ const ConnectedCategoryAndTemplate = ({
63
65
 
64
66
  const jobTemplates = useSelector(selectJobTemplates);
65
67
 
68
+ const errors = {
69
+ categoryError: useSelector(selectCategoryError),
70
+ allTemplatesError: useSelector(selectAllTemplatesError),
71
+ templateError: useSelector(selectTemplateError),
72
+ };
66
73
  return (
67
74
  <CategoryAndTemplate
68
75
  jobTemplates={jobTemplates}
@@ -71,6 +78,7 @@ const ConnectedCategoryAndTemplate = ({
71
78
  selectedTemplateID={jobTemplate}
72
79
  setCategory={setCategory}
73
80
  selectedCategory={category}
81
+ errors={errors}
74
82
  />
75
83
  );
76
84
  };