foreman_remote_execution 4.4.0 → 4.5.3

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