foreman_remote_execution 4.4.0 → 4.5.0

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +13 -24
  3. data/app/controllers/job_templates_controller.rb +4 -4
  4. data/app/controllers/ui_job_wizard_controller.rb +12 -0
  5. data/app/helpers/job_invocations_helper.rb +2 -2
  6. data/app/helpers/remote_execution_helper.rb +8 -8
  7. data/app/lib/actions/remote_execution/run_host_job.rb +31 -5
  8. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +5 -5
  9. data/app/models/host_status/execution_status.rb +5 -5
  10. data/app/models/job_invocation.rb +23 -7
  11. data/app/models/job_invocation_composer.rb +59 -17
  12. data/app/models/ssh_execution_provider.rb +4 -4
  13. data/app/overrides/execution_interface.rb +8 -8
  14. data/app/overrides/subnet_proxies.rb +6 -6
  15. data/config/routes.rb +1 -0
  16. data/db/migrate/20180110104432_rename_template_invocation_permission.rb +1 -1
  17. data/db/migrate/20190111153330_remove_remote_execution_without_proxy_setting.rb +4 -4
  18. data/extra/cockpit/foreman-cockpit-session +6 -6
  19. data/lib/foreman_remote_execution/engine.rb +11 -6
  20. data/lib/foreman_remote_execution/version.rb +1 -1
  21. data/package.json +2 -1
  22. data/test/functional/api/v2/job_invocations_controller_test.rb +14 -1
  23. data/test/unit/job_invocation_composer_test.rb +45 -1
  24. data/webpack/JobWizard/JobWizard.js +59 -18
  25. data/webpack/JobWizard/JobWizard.scss +3 -1
  26. data/webpack/JobWizard/JobWizardConstants.js +1 -0
  27. data/webpack/JobWizard/JobWizardSelectors.js +18 -1
  28. data/webpack/JobWizard/__tests__/JobWizard.test.js +4 -11
  29. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -51
  30. data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +43 -0
  31. data/webpack/JobWizard/__tests__/fixtures.js +26 -0
  32. data/webpack/JobWizard/__tests__/integration.test.js +156 -0
  33. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +93 -0
  34. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +181 -0
  35. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +25 -0
  36. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +249 -0
  37. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +34 -2
  38. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +10 -3
  39. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +50 -1
  40. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +9 -1
  41. data/webpack/JobWizard/steps/form/FormHelpers.js +19 -0
  42. data/webpack/JobWizard/steps/form/GroupedSelectField.js +3 -0
  43. data/webpack/JobWizard/steps/form/SelectField.js +10 -1
  44. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +1 -0
  45. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +1 -0
  46. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +21 -2
  47. data/webpack/global_index.js +5 -3
  48. data/webpack/index.js +3 -0
  49. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +1 -5
  50. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +8 -3
  51. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +7 -2
  52. data/webpack/react_app/extend/{fills.js → fillRecentJobsCard.js} +7 -6
  53. data/webpack/react_app/extend/fillregistrationAdvanced.js +11 -0
  54. data/webpack/react_app/extend/reducers.js +2 -1
  55. metadata +12 -4
  56. data/webpack/fills_index.js +0 -11
@@ -2,10 +2,10 @@ class SSHExecutionProvider < RemoteExecutionProvider
2
2
  class << self
3
3
  def proxy_command_options(template_invocation, host)
4
4
  super.merge(:ssh_user => ssh_user(host),
5
- :effective_user => effective_user(template_invocation),
6
- :effective_user_method => effective_user_method(host),
7
- :cleanup_working_dirs => cleanup_working_dirs?(host),
8
- :ssh_port => ssh_port(host))
5
+ :effective_user => effective_user(template_invocation),
6
+ :effective_user_method => effective_user_method(host),
7
+ :cleanup_working_dirs => cleanup_working_dirs?(host),
8
+ :ssh_port => ssh_port(host))
9
9
  end
10
10
 
11
11
  def humanized_name
@@ -1,9 +1,9 @@
1
- Deface::Override.new(:virtual_path => 'hosts/_form',
2
- :name => 'add_execution_interface_js',
3
- :insert_before => 'div#primary',
4
- :text => '<%= javascript "foreman_remote_execution/execution_interface" %>')
1
+ Deface::Override.new(:virtual_path => 'hosts/_form',
2
+ :name => 'add_execution_interface_js',
3
+ :insert_before => 'div#primary',
4
+ :text => '<%= javascript "foreman_remote_execution/execution_interface" %>')
5
5
 
6
- Deface::Override.new(:virtual_path => 'nic/_base_form',
7
- :name => 'add_execution_interface',
8
- :insert_after => 'erb[loud]:contains("interface_provision")',
9
- :partial => 'overrides/nics/execution_interface')
6
+ Deface::Override.new(:virtual_path => 'nic/_base_form',
7
+ :name => 'add_execution_interface',
8
+ :insert_after => 'erb[loud]:contains("interface_provision")',
9
+ :partial => 'overrides/nics/execution_interface')
@@ -1,9 +1,9 @@
1
1
  Deface::Override.new(:virtual_path => 'subnets/_form',
2
- :name => 'add_remote_execution_proxies_tab',
3
- :insert_after => 'li.active',
4
- :partial => 'overrides/subnets/rex_tab')
2
+ :name => 'add_remote_execution_proxies_tab',
3
+ :insert_after => 'li.active',
4
+ :partial => 'overrides/subnets/rex_tab')
5
5
 
6
6
  Deface::Override.new(:virtual_path => 'subnets/_form',
7
- :name => 'add_remote_execution_proxies_tab_pane',
8
- :insert_after => 'div#proxies',
9
- :partial => 'overrides/subnets/rex_tab_pane')
7
+ :name => 'add_remote_execution_proxies_tab_pane',
8
+ :insert_after => 'div#proxies',
9
+ :partial => 'overrides/subnets/rex_tab_pane')
data/config/routes.rb CHANGED
@@ -44,6 +44,7 @@ Rails.application.routes.draw do
44
44
  end
45
45
  get 'cockpit/redirect', to: 'cockpit#redirect'
46
46
  get 'ui_job_wizard/categories', to: 'ui_job_wizard#categories'
47
+ get 'ui_job_wizard/template/:id', to: 'ui_job_wizard#template'
47
48
 
48
49
  match '/experimental/job_wizard', to: 'react#index', :via => [:get]
49
50
 
@@ -16,7 +16,7 @@ class RenameTemplateInvocationPermission < ActiveRecord::Migration[4.2]
16
16
  return if old_permission.nil?
17
17
 
18
18
  new_permission = Permission.find_or_create_by(:name => new,
19
- :resource_type => 'TemplateInvocation')
19
+ :resource_type => 'TemplateInvocation')
20
20
  old_permission.filterings.each do |filtering|
21
21
  filtering.permission_id = new_permission.id
22
22
  filtering.save!
@@ -5,9 +5,9 @@ class RemoveRemoteExecutionWithoutProxySetting < ActiveRecord::Migration[5.2]
5
5
 
6
6
  def down
7
7
  Setting.create!(:name => 'remote_execution_without_proxy',
8
- :description => N_('When enabled, the remote execution will try to run '\
9
- 'the commands directly, when no proxy with remote execution '\
10
- 'feature is configured for the host.'),
11
- :default => false, :full_name => N_('Fallback Without Proxy'))
8
+ :description => N_('When enabled, the remote execution will try to run '\
9
+ 'the commands directly, when no proxy with remote execution '\
10
+ 'feature is configured for the host.'),
11
+ :default => false, :full_name => N_('Fallback Without Proxy'))
12
12
  end
13
13
  end
@@ -56,13 +56,13 @@ end
56
56
 
57
57
  def send_auth_challenge(challenge)
58
58
  send_control({ "command" => "authorize",
59
- "cookie" => "1234", # must be present, but value doesn't matter
60
- "challenge" => challenge})
59
+ "cookie" => "1234", # must be present, but value doesn't matter
60
+ "challenge" => challenge})
61
61
  end
62
62
 
63
63
  def send_auth_response(response)
64
64
  send_control({ "command" => "authorize",
65
- "response" => response})
65
+ "response" => response})
66
66
  end
67
67
 
68
68
  def read_auth_reply
@@ -76,9 +76,9 @@ end
76
76
  def exit_with_problem(problem, message, auth_methods)
77
77
  LOG.error("#{problem} - #{message}")
78
78
  send_control({ "command" => "init",
79
- "problem" => problem,
80
- "message" => message,
81
- "auth-method-results" => auth_methods})
79
+ "problem" => problem,
80
+ "message" => message,
81
+ "auth-method-results" => auth_methods})
82
82
  exit 1
83
83
  end
84
84
 
@@ -51,14 +51,13 @@ module ForemanRemoteExecution
51
51
 
52
52
  initializer 'foreman_remote_execution.register_plugin', before: :finisher_hook do |_app|
53
53
  Foreman::Plugin.register :foreman_remote_execution do
54
- register_global_js_file 'global'
55
54
  requires_foreman '>= 2.2'
55
+ register_global_js_file 'global'
56
56
 
57
57
  apipie_documented_controllers ["#{ForemanRemoteExecution::Engine.root}/app/controllers/api/v2/*.rb"]
58
58
  ApipieDSL.configuration.dsl_classes_matchers += [
59
59
  "#{ForemanRemoteExecution::Engine.root}/app/lib/foreman_remote_execution/renderer/**/*.rb",
60
60
  ]
61
- register_global_js_file 'global'
62
61
  automatic_assets(false)
63
62
  precompile_assets(*assets_to_precompile)
64
63
 
@@ -68,7 +67,7 @@ module ForemanRemoteExecution
68
67
  :'api/v2/job_templates' => [:index, :show, :revision, :export],
69
68
  :'api/v2/template_inputs' => [:index, :show],
70
69
  :'api/v2/foreign_input_sets' => [:index, :show],
71
- :ui_job_wizard => [:categories]}, :resource_type => 'JobTemplate'
70
+ :ui_job_wizard => [:categories, :template]}, :resource_type => 'JobTemplate'
72
71
  permission :create_job_templates, { :job_templates => [:new, :create, :clone_template, :import],
73
72
  :'api/v2/job_templates' => [:create, :clone, :import] }, :resource_type => 'JobTemplate'
74
73
  permission :edit_job_templates, { :job_templates => [:edit, :update],
@@ -141,7 +140,7 @@ module ForemanRemoteExecution
141
140
  url_hash: { controller: 'job_wizard', action: :index },
142
141
  caption: N_('Job wizard'),
143
142
  parent: :lab_features_menu,
144
- url: 'experimental/job_wizard',
143
+ url: '/experimental/job_wizard',
145
144
  after: :host_wizard
146
145
 
147
146
  register_custom_status HostStatus::ExecutionStatus
@@ -163,9 +162,15 @@ module ForemanRemoteExecution
163
162
 
164
163
  # Extend Registration module
165
164
  extend_allowed_registration_vars :remote_execution_interface
166
- register_global_js_file 'fills'
167
165
  ForemanTasks.dynflow.eager_load_actions!
168
- extend_observable_events(::Dynflow::Action.descendants.select { |klass| klass <= ::Actions::ObservableAction }.map(&:namespaced_event_names))
166
+ extend_observable_events(
167
+ ::Dynflow::Action.descendants.select do |klass|
168
+ klass <= ::Actions::ObservableAction
169
+ end.map(&:namespaced_event_names) +
170
+ RemoteExecutionFeature.all.pluck(:label).map do |label|
171
+ ::Actions::RemoteExecution::RunHostJob.feature_job_event_name(label)
172
+ end
173
+ )
169
174
  end
170
175
  end
171
176
 
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '4.4.0'.freeze
2
+ VERSION = '4.5.0'.freeze
3
3
  end
data/package.json CHANGED
@@ -29,7 +29,8 @@
29
29
  "babel-eslint": "^10.0.0",
30
30
  "eslint": "^6.8.0",
31
31
  "prettier": "^1.19.1",
32
- "@patternfly/react-catalog-view-extension": "^4.8.126"
32
+ "@patternfly/react-catalog-view-extension": "^4.8.126",
33
+ "redux-mock-store": "^1.2.2"
33
34
  },
34
35
  "peerDependencies": {
35
36
  "@theforeman/vendor": "^8.3.0"
@@ -114,11 +114,24 @@ module Api
114
114
  assert_response :success
115
115
  end
116
116
 
117
- test 'search_query' do
117
+ test 'host ids as search_query' do
118
118
  @attrs[:host_ids] = 'name = testfqdn'
119
119
  post :create, params: { job_invocation: @attrs }
120
120
  assert_response :success
121
121
  end
122
+
123
+ test 'with search_query param' do
124
+ @attrs[:targeting_type] = 'static_query'
125
+ @attrs[:search_query] = 'name = testfqdn'
126
+ post :create, params: { job_invocation: @attrs }
127
+ assert_response :success
128
+ end
129
+
130
+ test 'with job_template_id param' do
131
+ @attrs[:job_template_id] = 12_345
132
+ post :create, params: { job_invocation: @attrs }
133
+ assert_response :error
134
+ end
122
135
  end
123
136
  end
124
137
 
@@ -622,7 +622,7 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
622
622
  end
623
623
  end
624
624
 
625
- describe '#from_api_params' do
625
+ describe '.from_api_params' do
626
626
  let(:composer) do
627
627
  JobInvocationComposer.from_api_params(params)
628
628
  end
@@ -754,12 +754,23 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
754
754
  assert composer.save!
755
755
  _(composer.job_invocation.remote_execution_feature).must_equal feature
756
756
  end
757
+
758
+ it 'sets the remote execution_feature id based on `feature` param' do
759
+ params[:remote_execution_feature_id] = nil
760
+ params[:feature] = feature.label
761
+ params[:job_template_id] = trying_job_template_1.id
762
+ refute_equal feature.job_template, trying_job_template_1
763
+
764
+ assert composer.save!
765
+ _(composer.job_invocation.remote_execution_feature).must_equal feature
766
+ end
757
767
  end
758
768
 
759
769
  context 'with invalid targeting' do
760
770
  let(:params) do
761
771
  { :job_category => trying_job_template_1.job_category,
762
772
  :job_template_id => trying_job_template_1.id,
773
+ :targeting_type => 'fake',
763
774
  :search_query => 'some hosts',
764
775
  :inputs => {input1.name => 'some_value'}}
765
776
  end
@@ -865,6 +876,39 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
865
876
  end
866
877
  end
867
878
 
879
+ describe '.for_feature' do
880
+ let(:feature) { FactoryBot.create(:remote_execution_feature, job_template: trying_job_template_1) }
881
+ let(:host) { FactoryBot.create(:host) }
882
+ let(:bookmark) { Bookmark.create!(:query => 'b', :name => 'bookmark', :public => true, :controller => 'hosts') }
883
+
884
+ context 'specifying hosts' do
885
+ it 'takes a bookmarked search' do
886
+ composer = JobInvocationComposer.for_feature(feature.label, bookmark, {})
887
+ assert_equal bookmark.id, composer.params['targeting']['bookmark_id']
888
+ end
889
+
890
+ it 'takes an array of host ids' do
891
+ composer = JobInvocationComposer.for_feature(feature.label, [host.id], {})
892
+ assert_match(/#{host.name}/, composer.params['targeting']['search_query'])
893
+ end
894
+
895
+ it 'takes a single host object' do
896
+ composer = JobInvocationComposer.for_feature(feature.label, host, {})
897
+ assert_match(/#{host.name}/, composer.params['targeting']['search_query'])
898
+ end
899
+
900
+ it 'takes an array of host FQDNs' do
901
+ composer = JobInvocationComposer.for_feature(feature.label, [host.fqdn], {})
902
+ assert_match(/#{host.name}/, composer.params['targeting']['search_query'])
903
+ end
904
+
905
+ it 'takes a search query string' do
906
+ composer = JobInvocationComposer.for_feature(feature.label, 'host.example.com', {})
907
+ assert_equal 'host.example.com', composer.search_query
908
+ end
909
+ end
910
+ end
911
+
868
912
  describe '#resolve_job_category and #resolve job_templates' do
869
913
  let(:setting_template) { as_admin { FactoryBot.create(:job_template, :name => 'trying setting', :job_category => 'fluff') } }
870
914
  let(:other_template) { as_admin { FactoryBot.create(:job_template, :name => 'trying something', :job_category => 'fluff') } }
@@ -1,55 +1,96 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { useDispatch, useSelector } from 'react-redux';
2
3
  import { Wizard } from '@patternfly/react-core';
4
+ import { get } from 'foremanReact/redux/API';
3
5
  import { translate as __ } from 'foremanReact/common/I18n';
4
6
  import history from 'foremanReact/history';
5
7
  import CategoryAndTemplate from './steps/CategoryAndTemplate/';
8
+ import { AdvancedFields } from './steps/AdvancedFields/AdvancedFields';
9
+ import { JOB_TEMPLATE } from './JobWizardConstants';
10
+ import { selectTemplateError } from './JobWizardSelectors';
6
11
  import './JobWizard.scss';
7
12
 
8
13
  export const JobWizard = () => {
9
- const [jobTemplate, setJobTemplate] = useState(null);
14
+ const [jobTemplateID, setJobTemplateID] = useState(null);
10
15
  const [category, setCategory] = useState('');
16
+ const [advancedValues, setAdvancedValues] = useState({});
17
+ const dispatch = useDispatch();
18
+
19
+ const setDefaults = useCallback(response => {
20
+ const responseJob = response.data;
21
+ setAdvancedValues({
22
+ effectiveUserValue: responseJob.effective_user?.value || '',
23
+ timeoutToKill: responseJob.job_template.execution_timeout_interval || '',
24
+ });
25
+ }, []);
26
+ useEffect(() => {
27
+ if (jobTemplateID) {
28
+ dispatch(
29
+ get({
30
+ key: JOB_TEMPLATE,
31
+ url: `/ui_job_wizard/template/${jobTemplateID}`,
32
+ handleSuccess: setDefaults,
33
+ })
34
+ );
35
+ }
36
+ }, [jobTemplateID, setDefaults, dispatch]);
37
+
38
+ const templateError = !!useSelector(selectTemplateError);
39
+ const isTemplate = !templateError && !!jobTemplateID;
11
40
  const steps = [
12
41
  {
13
- name: __('Category and template'),
42
+ name: __('Category and Template'),
14
43
  component: (
15
44
  <CategoryAndTemplate
16
- jobTemplate={jobTemplate}
17
- setJobTemplate={setJobTemplate}
45
+ jobTemplate={jobTemplateID}
46
+ setJobTemplate={setJobTemplateID}
18
47
  category={category}
19
48
  setCategory={setCategory}
20
49
  />
21
50
  ),
22
51
  },
23
52
  {
24
- name: __('Target hosts'),
25
- component: <p>TargetHosts </p>,
26
- canJumpTo: !!jobTemplate,
53
+ name: __('Target Hosts'),
54
+ component: <p>Target Hosts</p>,
55
+ canJumpTo: isTemplate,
27
56
  },
28
57
  {
29
- name: __('Advanced fields'),
30
- component: <p> AdvancedFields </p>,
31
- canJumpTo: !!jobTemplate,
58
+ name: __('Advanced Fields'),
59
+ component: (
60
+ <AdvancedFields
61
+ advancedValues={advancedValues}
62
+ setAdvancedValues={newValues => {
63
+ setAdvancedValues(currentAdvancedValues => ({
64
+ ...currentAdvancedValues,
65
+ ...newValues,
66
+ }));
67
+ }}
68
+ jobTemplateID={jobTemplateID}
69
+ />
70
+ ),
71
+ canJumpTo: isTemplate,
32
72
  },
33
73
  {
34
74
  name: __('Schedule'),
35
75
  component: <p>Schedule</p>,
36
- canJumpTo: !!jobTemplate,
76
+ canJumpTo: isTemplate,
37
77
  },
38
78
  {
39
- name: __('Review details'),
40
- component: <p>ReviewDetails</p>,
79
+ name: __('Review Details'),
80
+ component: <p>Review Details</p>,
41
81
  nextButtonText: 'Run',
42
- canJumpTo: !!jobTemplate,
82
+ canJumpTo: isTemplate,
43
83
  },
44
84
  ];
45
- const title = __('Run Job');
46
85
  return (
47
86
  <Wizard
48
87
  onClose={() => history.goBack()}
49
- navAriaLabel={`${title} steps`}
88
+ navAriaLabel="Run Job steps"
50
89
  steps={steps}
51
- height="70vh"
90
+ height="100%"
52
91
  className="job-wizard"
53
92
  />
54
93
  );
55
94
  };
95
+
96
+ export default JobWizard;
@@ -1,6 +1,5 @@
1
1
  .job-wizard {
2
2
  .pf-c-wizard__main {
3
- overflow: visible;
4
3
  z-index: calc(
5
4
  var(--pf-c-wizard__footer--ZIndex) + 1
6
5
  ); // So the select box can be shown above the wizard footer
@@ -8,5 +7,8 @@
8
7
 
9
8
  .pf-c-wizard__main-body {
10
9
  max-width: 500px;
10
+ .advanced-fields-title {
11
+ margin-bottom: 10px;
12
+ }
11
13
  }
12
14
  }
@@ -2,4 +2,5 @@ import { foremanUrl } from 'foremanReact/common/helpers';
2
2
 
3
3
  export const JOB_TEMPLATES = 'JOB_TEMPLATES';
4
4
  export const JOB_CATEGORIES = 'JOB_CATEGORIES';
5
+ export const JOB_TEMPLATE = 'JOB_TEMPLATE';
5
6
  export const templatesUrl = foremanUrl('/api/v2/job_templates');
@@ -1,9 +1,14 @@
1
1
  import {
2
2
  selectAPIResponse,
3
3
  selectAPIStatus,
4
+ selectAPIErrorMessage,
4
5
  } from 'foremanReact/redux/API/APISelectors';
5
6
 
6
- import { JOB_TEMPLATES, JOB_CATEGORIES } from './JobWizardConstants';
7
+ import {
8
+ JOB_TEMPLATES,
9
+ JOB_CATEGORIES,
10
+ JOB_TEMPLATE,
11
+ } from './JobWizardConstants';
7
12
 
8
13
  export const selectJobTemplatesStatus = state =>
9
14
  selectAPIStatus(state, JOB_TEMPLATES);
@@ -19,3 +24,15 @@ export const selectJobCategories = state =>
19
24
 
20
25
  export const selectJobCategoriesStatus = state =>
21
26
  selectAPIStatus(state, JOB_CATEGORIES);
27
+
28
+ export const selectCategoryError = state =>
29
+ selectAPIErrorMessage(state, JOB_CATEGORIES);
30
+
31
+ export const selectAllTemplatesError = state =>
32
+ selectAPIErrorMessage(state, JOB_TEMPLATES);
33
+
34
+ export const selectTemplateError = state =>
35
+ selectAPIErrorMessage(state, JOB_TEMPLATE);
36
+
37
+ export const selectJobTemplate = state =>
38
+ selectAPIResponse(state, JOB_TEMPLATE);