foreman_remote_execution 4.5.1 → 4.5.5
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.
- checksums.yaml +4 -4
- data/app/controllers/ui_job_wizard_controller.rb +7 -0
- data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +5 -1
- data/app/helpers/remote_execution_helper.rb +9 -3
- data/app/lib/actions/remote_execution/run_hosts_job.rb +1 -1
- data/app/models/job_invocation_composer.rb +2 -2
- data/app/models/remote_execution_feature.rb +5 -1
- data/app/models/remote_execution_provider.rb +2 -1
- data/app/views/templates/ssh/module_action.erb +1 -0
- data/app/views/templates/ssh/power_action.erb +2 -0
- data/app/views/templates/ssh/puppet_run_once.erb +1 -0
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/unit/remote_execution_provider_test.rb +12 -0
- data/webpack/JobWizard/JobWizard.js +28 -8
- data/webpack/JobWizard/JobWizard.scss +39 -0
- data/webpack/JobWizard/JobWizardConstants.js +10 -0
- data/webpack/JobWizard/JobWizardSelectors.js +9 -0
- data/webpack/JobWizard/__tests__/fixtures.js +104 -2
- data/webpack/JobWizard/__tests__/integration.test.js +13 -85
- data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +21 -4
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +67 -0
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +73 -59
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +135 -16
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +23 -0
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +122 -51
- data/webpack/JobWizard/steps/Schedule/QueryType.js +48 -0
- data/webpack/JobWizard/steps/Schedule/RepeatOn.js +61 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +25 -0
- data/webpack/JobWizard/steps/Schedule/StartEndDates.js +51 -0
- data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +22 -0
- data/webpack/JobWizard/steps/Schedule/index.js +41 -0
- data/webpack/JobWizard/steps/form/FormHelpers.js +1 -0
- data/webpack/JobWizard/steps/form/Formatter.js +149 -0
- data/webpack/JobWizard/steps/form/NumberInput.js +33 -0
- data/webpack/JobWizard/steps/form/SelectField.js +14 -2
- data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +76 -0
- data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
- metadata +13 -10
- data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -13
- data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -32
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +0 -249
- data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -113
- data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
- data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
- data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -37
- data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 99202dff05c36d40eac36950a23298ca1cf2ca4510e2506787bace85c72f0fd9
|
4
|
+
data.tar.gz: '059cd0dd0b427bb2fc1a80c59a47ec26e2fedb4d0e27f989c1ec859e52e1191a'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 19e978232f3ee68a30d65ceb7c1b799b180894c16f5060c80ed4c5b85870fbe1f0f7ed320b44924cbb7177e280deb32309a061ab2737ff9bc2cc944092a48b63
|
7
|
+
data.tar.gz: 9ac86095eb5c6cdf2bed666e911895462e44b0cbf96ddf40023c062604f0de947ebf4851e48d556332ed6624a59d6748608bdb472ce63c3e593ebcc3e5a21d52
|
@@ -10,9 +10,12 @@ class UiJobWizardController < ::Api::V2::BaseController
|
|
10
10
|
|
11
11
|
def template
|
12
12
|
job_template = JobTemplate.authorized.find(params[:id])
|
13
|
+
advanced_template_inputs, template_inputs = map_template_inputs(job_template.template_inputs_with_foreign).partition { |x| x["advanced"] }
|
13
14
|
render :json => {
|
14
15
|
:job_template => job_template,
|
15
16
|
:effective_user => job_template.effective_user,
|
17
|
+
:template_inputs => template_inputs,
|
18
|
+
:advanced_template_inputs => advanced_template_inputs,
|
16
19
|
}
|
17
20
|
end
|
18
21
|
|
@@ -20,6 +23,10 @@ class UiJobWizardController < ::Api::V2::BaseController
|
|
20
23
|
nested_resource || 'job_template'
|
21
24
|
end
|
22
25
|
|
26
|
+
def map_template_inputs(template_inputs_with_foreign)
|
27
|
+
template_inputs_with_foreign.map { |input| input.attributes.merge({:resource_type => input.resource_type&.tableize }) }
|
28
|
+
end
|
29
|
+
|
23
30
|
def resource_class
|
24
31
|
JobTemplate
|
25
32
|
end
|
@@ -7,7 +7,9 @@ module ForemanRemoteExecution
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def multiple_actions
|
10
|
-
|
10
|
+
res = super
|
11
|
+
res += [ [_('Schedule Remote Job'), new_job_invocation_path, false] ] if authorized_for(controller: :job_invocations, action: :new)
|
12
|
+
res
|
11
13
|
end
|
12
14
|
|
13
15
|
def schedule_job_multi_button(*args)
|
@@ -21,12 +23,14 @@ module ForemanRemoteExecution
|
|
21
23
|
end
|
22
24
|
|
23
25
|
def rex_host_features(*args)
|
26
|
+
return unless authorized_for(controller: :job_invocations, action: :create)
|
24
27
|
RemoteExecutionFeature.with_host_action_button.order(:label).map do |feature|
|
25
28
|
link_to(_('%s') % feature.name, job_invocations_path(:host_ids => [args.first.id], :feature => feature.label), :method => :post)
|
26
29
|
end
|
27
30
|
end
|
28
31
|
|
29
32
|
def schedule_job_button(*args)
|
33
|
+
return unless authorized_for(controller: :job_invocations, action: :new)
|
30
34
|
link_to(_('Schedule Remote Job'), new_job_invocation_path(:host_ids => [args.first.id]), :id => :run_button, :class => 'btn btn-default')
|
31
35
|
end
|
32
36
|
|
@@ -7,6 +7,10 @@ module RemoteExecutionHelper
|
|
7
7
|
@job_hosts_authorizer ||= Authorizer.new(User.current, :collection => @hosts)
|
8
8
|
end
|
9
9
|
|
10
|
+
def host_tasks_authorizer
|
11
|
+
@host_tasks_authorizer ||= Authorizer.new(User.current, :collection => @job_invocation.sub_tasks)
|
12
|
+
end
|
13
|
+
|
10
14
|
def host_counter(label, count)
|
11
15
|
content_tag(:div, :class => 'host_counter') do
|
12
16
|
content_tag(:div, label, :class => 'header') + content_tag(:div, count.to_s, :class => 'count')
|
@@ -36,7 +40,7 @@ module RemoteExecutionHelper
|
|
36
40
|
'data-method': 'get', id: "#{host.name}-actions-rerun" } }
|
37
41
|
end
|
38
42
|
|
39
|
-
if host_task.present? && authorized_for(hash_for_foreman_tasks_task_path(host_task).merge(auth_object: host_task, permission: :view_foreman_tasks))
|
43
|
+
if host_task.present? && authorized_for(hash_for_foreman_tasks_task_path(host_task).merge(auth_object: host_task, permission: :view_foreman_tasks, authorizer: host_tasks_authorizer))
|
40
44
|
links << { title: _('Host task'),
|
41
45
|
action: { href: foreman_tasks_task_path(host_task),
|
42
46
|
'data-method': 'get', id: "#{host.name}-actions-task" } }
|
@@ -109,12 +113,14 @@ module RemoteExecutionHelper
|
|
109
113
|
:class => 'btn btn-danger',
|
110
114
|
:title => _('Try to cancel the job on a host'),
|
111
115
|
:disabled => !task.cancellable?,
|
112
|
-
:method => :post
|
116
|
+
:method => :post,
|
117
|
+
:remote => true)
|
113
118
|
buttons << link_to(_('Abort Job'), abort_foreman_tasks_task_path(task),
|
114
119
|
:class => 'btn btn-danger',
|
115
120
|
:title => _('Try to abort the job on a host without waiting for its result'),
|
116
121
|
:disabled => !task.cancellable?,
|
117
|
-
:method => :post
|
122
|
+
:method => :post,
|
123
|
+
:remote => true)
|
118
124
|
end
|
119
125
|
buttons
|
120
126
|
end
|
@@ -72,7 +72,7 @@ module Actions
|
|
72
72
|
|
73
73
|
def total_count
|
74
74
|
# For compatibility with already existing tasks
|
75
|
-
return output[:total_count] unless output.has_key?(:host_count) || task.pending?
|
75
|
+
return output[:total_count] || hosts.count unless output.has_key?(:host_count) || task.pending?
|
76
76
|
|
77
77
|
output[:host_count] || hosts.count
|
78
78
|
end
|
@@ -209,7 +209,7 @@ class JobInvocationComposer
|
|
209
209
|
def format_datetime(datetime)
|
210
210
|
return datetime if datetime.blank?
|
211
211
|
|
212
|
-
Time.parse(datetime).strftime('%Y-%m-%d %H:%M')
|
212
|
+
Time.parse(datetime).utc.strftime('%Y-%m-%d %H:%M')
|
213
213
|
end
|
214
214
|
end
|
215
215
|
|
@@ -296,7 +296,7 @@ class JobInvocationComposer
|
|
296
296
|
attr_reader :feature_label, :feature, :provided_inputs
|
297
297
|
|
298
298
|
def initialize(feature_label, hosts, provided_inputs = {})
|
299
|
-
@feature = RemoteExecutionFeature.feature(feature_label)
|
299
|
+
@feature = RemoteExecutionFeature.feature!(feature_label)
|
300
300
|
@provided_inputs = provided_inputs
|
301
301
|
translator = HostIdsTranslator.new(hosts)
|
302
302
|
@host_bookmark = translator.bookmark
|
@@ -20,7 +20,11 @@ class RemoteExecutionFeature < ApplicationRecord
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def self.feature(label)
|
23
|
-
self.find_by(label: label)
|
23
|
+
self.find_by(label: label)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.feature!(label)
|
27
|
+
feature(label) || raise(::Foreman::Exception.new(N_('Unknown remote execution feature %s'), label))
|
24
28
|
end
|
25
29
|
|
26
30
|
def self.register(label, name, options = {})
|
@@ -89,7 +89,8 @@ class RemoteExecutionProvider
|
|
89
89
|
end
|
90
90
|
|
91
91
|
def host_setting(host, setting)
|
92
|
-
host.host_param(setting.to_s)
|
92
|
+
param_value = host.host_param(setting.to_s)
|
93
|
+
param_value.nil? ? Setting[setting] : param_value
|
93
94
|
end
|
94
95
|
|
95
96
|
def ssh_password(_host)
|
@@ -50,6 +50,18 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
|
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
53
|
+
describe '.host_setting' do
|
54
|
+
let(:host) { FactoryBot.create(:host) }
|
55
|
+
|
56
|
+
it 'honors falsey values set as a host parameter' do
|
57
|
+
key = 'remote_execution_connect_by_ip'
|
58
|
+
Setting[key] = true
|
59
|
+
host.parameters << HostParameter.new(name: key, value: false)
|
60
|
+
|
61
|
+
refute RemoteExecutionProvider.host_setting(host, key)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
53
65
|
describe SSHExecutionProvider do
|
54
66
|
before { User.current = FactoryBot.build(:user, :admin) }
|
55
67
|
after { User.current = nil }
|
@@ -1,3 +1,4 @@
|
|
1
|
+
/* eslint-disable camelcase */
|
1
2
|
import React, { useState, useEffect, useCallback } from 'react';
|
2
3
|
import { useDispatch, useSelector } from 'react-redux';
|
3
4
|
import { Wizard } from '@patternfly/react-core';
|
@@ -8,6 +9,7 @@ import CategoryAndTemplate from './steps/CategoryAndTemplate/';
|
|
8
9
|
import { AdvancedFields } from './steps/AdvancedFields/AdvancedFields';
|
9
10
|
import { JOB_TEMPLATE } from './JobWizardConstants';
|
10
11
|
import { selectTemplateError } from './JobWizardSelectors';
|
12
|
+
import Schedule from './steps/Schedule/';
|
11
13
|
import './JobWizard.scss';
|
12
14
|
|
13
15
|
export const JobWizard = () => {
|
@@ -16,13 +18,31 @@ export const JobWizard = () => {
|
|
16
18
|
const [advancedValues, setAdvancedValues] = useState({});
|
17
19
|
const dispatch = useDispatch();
|
18
20
|
|
19
|
-
const setDefaults = useCallback(
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
const setDefaults = useCallback(
|
22
|
+
({
|
23
|
+
data: {
|
24
|
+
advanced_template_inputs,
|
25
|
+
effective_user,
|
26
|
+
job_template: { executionTimeoutInterval, description_format },
|
27
|
+
},
|
28
|
+
}) => {
|
29
|
+
const advancedTemplateValues = {};
|
30
|
+
const advancedInputs = advanced_template_inputs;
|
31
|
+
if (advancedInputs) {
|
32
|
+
advancedInputs.forEach(input => {
|
33
|
+
advancedTemplateValues[input.name] = input?.default || '';
|
34
|
+
});
|
35
|
+
}
|
36
|
+
setAdvancedValues(currentAdvancedValues => ({
|
37
|
+
...currentAdvancedValues,
|
38
|
+
effectiveUserValue: effective_user?.value || '',
|
39
|
+
timeoutToKill: executionTimeoutInterval || '',
|
40
|
+
templateValues: advancedTemplateValues,
|
41
|
+
description: description_format || '',
|
42
|
+
}));
|
43
|
+
},
|
44
|
+
[]
|
45
|
+
);
|
26
46
|
useEffect(() => {
|
27
47
|
if (jobTemplateID) {
|
28
48
|
dispatch(
|
@@ -72,7 +92,7 @@ export const JobWizard = () => {
|
|
72
92
|
},
|
73
93
|
{
|
74
94
|
name: __('Schedule'),
|
75
|
-
component: <
|
95
|
+
component: <Schedule />,
|
76
96
|
canJumpTo: isTemplate,
|
77
97
|
},
|
78
98
|
{
|
@@ -10,5 +10,44 @@
|
|
10
10
|
.advanced-fields-title {
|
11
11
|
margin-bottom: 10px;
|
12
12
|
}
|
13
|
+
#advanced-fields-job-template {
|
14
|
+
.foreman-search-field {
|
15
|
+
// Giving pf3 search bar a pf4 look
|
16
|
+
.search-bar {
|
17
|
+
display: block;
|
18
|
+
}
|
19
|
+
.input-group-btn {
|
20
|
+
display: none;
|
21
|
+
}
|
22
|
+
li {
|
23
|
+
font-size: 16px;
|
24
|
+
}
|
25
|
+
input {
|
26
|
+
font-size: 16px;
|
27
|
+
height: 36px;
|
28
|
+
}
|
29
|
+
.foreman-autocomplete .autocomplete-focus-shortcut {
|
30
|
+
top: 8px;
|
31
|
+
font-size: 16px;
|
32
|
+
}
|
33
|
+
.foreman-autocomplete .autocomplete-aux {
|
34
|
+
top: 8px;
|
35
|
+
font-size: 16px;
|
36
|
+
.autocomplete-clear-button {
|
37
|
+
font-size: 16px;
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
.schedule-tab {
|
45
|
+
input[type='radio'],
|
46
|
+
input[type='checkbox'] {
|
47
|
+
margin: 0;
|
48
|
+
}
|
49
|
+
.advanced-scheduling-button {
|
50
|
+
text-align: start;
|
51
|
+
}
|
13
52
|
}
|
14
53
|
}
|
@@ -1,6 +1,16 @@
|
|
1
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
1
2
|
import { foremanUrl } from 'foremanReact/common/helpers';
|
2
3
|
|
3
4
|
export const JOB_TEMPLATES = 'JOB_TEMPLATES';
|
4
5
|
export const JOB_CATEGORIES = 'JOB_CATEGORIES';
|
5
6
|
export const JOB_TEMPLATE = 'JOB_TEMPLATE';
|
6
7
|
export const templatesUrl = foremanUrl('/api/v2/job_templates');
|
8
|
+
|
9
|
+
export const repeatTypes = {
|
10
|
+
noRepeat: __('Does not repeat'),
|
11
|
+
cronline: __('Cronline'),
|
12
|
+
monthly: __('Monthly'),
|
13
|
+
weekly: __('Weekly'),
|
14
|
+
daily: __('Daily'),
|
15
|
+
hourly: __('Hourly'),
|
16
|
+
};
|
@@ -36,3 +36,12 @@ export const selectTemplateError = state =>
|
|
36
36
|
|
37
37
|
export const selectJobTemplate = state =>
|
38
38
|
selectAPIResponse(state, JOB_TEMPLATE);
|
39
|
+
|
40
|
+
export const selectEffectiveUser = state =>
|
41
|
+
selectAPIResponse(state, JOB_TEMPLATE).effective_user;
|
42
|
+
|
43
|
+
export const selectAdvancedTemplateInputs = state =>
|
44
|
+
selectAPIResponse(state, JOB_TEMPLATE).advanced_template_inputs || [];
|
45
|
+
|
46
|
+
export const selectTemplateInputs = state =>
|
47
|
+
selectAPIResponse(state, JOB_TEMPLATE).template_inputs || [];
|
@@ -1,6 +1,8 @@
|
|
1
|
-
|
1
|
+
import configureMockStore from 'redux-mock-store';
|
2
|
+
|
3
|
+
export const jobTemplate = {
|
2
4
|
id: 178,
|
3
|
-
name: '
|
5
|
+
name: 'template1',
|
4
6
|
template:
|
5
7
|
"---\n- hosts: all\n tasks:\n - shell:\n cmd: |\n<%= indent(10) { input('command') } %>\n register: out\n - debug: var=out",
|
6
8
|
snippet: false,
|
@@ -23,4 +25,104 @@ export const jobTemplateResponse = {
|
|
23
25
|
overridable: true,
|
24
26
|
current_user: false,
|
25
27
|
},
|
28
|
+
advanced_template_inputs: [
|
29
|
+
{
|
30
|
+
name: 'adv plain hidden',
|
31
|
+
required: true,
|
32
|
+
input_type: 'user',
|
33
|
+
description: 'some Description',
|
34
|
+
advanced: true,
|
35
|
+
value_type: 'plain',
|
36
|
+
resource_type: 'ansible_roles',
|
37
|
+
default: 'Default val',
|
38
|
+
hidden_value: true,
|
39
|
+
},
|
40
|
+
{
|
41
|
+
name: 'adv plain select',
|
42
|
+
required: false,
|
43
|
+
input_type: 'user',
|
44
|
+
options: 'option 1\r\noption 2\r\noption 3\r\noption 4',
|
45
|
+
advanced: true,
|
46
|
+
value_type: 'plain',
|
47
|
+
resource_type: 'ansible_roles',
|
48
|
+
default: '',
|
49
|
+
hidden_value: false,
|
50
|
+
},
|
51
|
+
{
|
52
|
+
name: 'adv search',
|
53
|
+
required: false,
|
54
|
+
options: '',
|
55
|
+
advanced: true,
|
56
|
+
value_type: 'search',
|
57
|
+
resource_type: 'foreman_tasks/tasks',
|
58
|
+
default: '',
|
59
|
+
hidden_value: false,
|
60
|
+
},
|
61
|
+
{
|
62
|
+
name: 'adv date',
|
63
|
+
required: false,
|
64
|
+
options: '',
|
65
|
+
advanced: true,
|
66
|
+
value_type: 'date',
|
67
|
+
resource_type: 'ansible_roles',
|
68
|
+
default: '',
|
69
|
+
hidden_value: false,
|
70
|
+
},
|
71
|
+
],
|
72
|
+
template_inputs: [
|
73
|
+
{
|
74
|
+
name: 'plain hidden',
|
75
|
+
required: true,
|
76
|
+
input_type: 'user',
|
77
|
+
description: 'some Description',
|
78
|
+
advanced: false,
|
79
|
+
value_type: 'plain',
|
80
|
+
resource_type: 'ansible_roles',
|
81
|
+
default: 'Default val',
|
82
|
+
hidden_value: true,
|
83
|
+
},
|
84
|
+
],
|
85
|
+
};
|
86
|
+
|
87
|
+
export const jobCategories = ['Ansible Commands', 'Puppet', 'Services'];
|
88
|
+
|
89
|
+
export const testSetup = (selectors, api) => {
|
90
|
+
jest.spyOn(api, 'get');
|
91
|
+
jest.spyOn(selectors, 'selectJobTemplate');
|
92
|
+
jest.spyOn(selectors, 'selectJobTemplates');
|
93
|
+
jest.spyOn(selectors, 'selectJobCategories');
|
94
|
+
jest.spyOn(selectors, 'selectJobCategoriesStatus');
|
95
|
+
|
96
|
+
selectors.selectJobCategories.mockImplementation(() => jobCategories);
|
97
|
+
selectors.selectJobTemplates.mockImplementation(() => [
|
98
|
+
jobTemplate,
|
99
|
+
{ ...jobTemplate, id: 2, name: 'template2' },
|
100
|
+
]);
|
101
|
+
const mockStore = configureMockStore([]);
|
102
|
+
const store = mockStore({});
|
103
|
+
return store;
|
104
|
+
};
|
105
|
+
|
106
|
+
export const mockTemplate = selectors => {
|
107
|
+
selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
|
108
|
+
selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
|
109
|
+
};
|
110
|
+
export const mockApi = api => {
|
111
|
+
api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
112
|
+
if (action.key === 'JOB_CATEGORIES') {
|
113
|
+
handleSuccess &&
|
114
|
+
handleSuccess({ data: { job_categories: jobCategories } });
|
115
|
+
} else if (action.key === 'JOB_TEMPLATE') {
|
116
|
+
handleSuccess &&
|
117
|
+
handleSuccess({
|
118
|
+
data: jobTemplateResponse,
|
119
|
+
});
|
120
|
+
} else if (action.key === 'JOB_TEMPLATES') {
|
121
|
+
handleSuccess &&
|
122
|
+
handleSuccess({
|
123
|
+
data: { results: [jobTemplate] },
|
124
|
+
});
|
125
|
+
}
|
126
|
+
return { type: 'get', ...action };
|
127
|
+
});
|
26
128
|
};
|