foreman-tasks 1.0.1 → 1.1.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +12 -12
- data/.rubocop_todo.yml +34 -116
- data/app/controllers/foreman_tasks/api/recurring_logics_controller.rb +20 -1
- data/app/controllers/foreman_tasks/api/tasks_controller.rb +29 -9
- data/app/controllers/foreman_tasks/concerns/hosts_controller_extension.rb +1 -1
- data/app/controllers/foreman_tasks/recurring_logics_controller.rb +19 -0
- data/app/helpers/foreman_tasks/foreman_tasks_helper.rb +1 -3
- data/app/lib/actions/helpers/humanizer.rb +1 -3
- data/app/lib/actions/proxy_action.rb +33 -12
- data/app/models/foreman_tasks/concerns/action_triggering.rb +1 -1
- data/app/models/foreman_tasks/recurring_logic.rb +1 -0
- data/app/models/foreman_tasks/remote_task.rb +1 -0
- data/app/models/foreman_tasks/task.rb +4 -0
- data/app/models/foreman_tasks/task/dynflow_task.rb +1 -1
- data/app/models/foreman_tasks/task/search.rb +11 -1
- data/app/services/foreman_tasks/troubleshooting_help_generator.rb +0 -4
- data/app/views/foreman_tasks/api/recurring_logics/base.json.rabl +2 -1
- data/app/views/foreman_tasks/api/tasks/details.json.rabl +1 -0
- data/app/views/foreman_tasks/api/tasks/show.json.rabl +1 -1
- data/app/views/foreman_tasks/recurring_logics/index.html.erb +30 -0
- data/app/views/foreman_tasks/tasks/show.html.erb +3 -0
- data/config/routes.rb +7 -0
- data/foreman-tasks.gemspec +4 -6
- data/lib/foreman_tasks/dynflow/console_authorizer.rb +2 -2
- data/lib/foreman_tasks/engine.rb +15 -13
- data/lib/foreman_tasks/tasks/cleanup.rake +1 -1
- data/lib/foreman_tasks/tasks/export_tasks.rake +2 -2
- data/lib/foreman_tasks/test_extensions.rb +1 -1
- data/lib/foreman_tasks/version.rb +1 -1
- data/locale/action_names.rb +1 -1
- data/package.json +1 -2
- data/script/rails +2 -2
- data/test/factories/task_factory.rb +34 -2
- data/test/foreman_tasks_test_helper.rb +4 -0
- data/test/unit/actions/action_with_sub_plans_test.rb +1 -1
- data/test/unit/task_test.rb +160 -74
- data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +4 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +3 -12
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +1 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/Task.test.js.snap +3 -1
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/TaskInfo.test.js.snap +2 -6
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +4 -1
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/__snapshots__/TaskDetails.test.js.snap +1 -0
- data/webpack/ForemanTasks/Components/TaskDetails/index.js +2 -0
- data/webpack/ForemanTasks/Components/TasksTable/SubTasksPage.js +3 -1
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableActions.js +87 -21
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableConstants.js +7 -7
- data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.js +31 -22
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTable.fixtures.js +2 -1
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTableActions.test.js +44 -46
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/SubTasksPage.test.js.snap +3 -1
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksIndexPage.test.js.snap +2 -1
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTableActions.test.js.snap +61 -5
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +6 -2
- metadata +10 -10
data/test/unit/task_test.rb
CHANGED
@@ -1,97 +1,133 @@
|
|
1
1
|
require 'foreman_tasks_test_helper'
|
2
2
|
|
3
3
|
class TasksTest < ActiveSupport::TestCase
|
4
|
-
describe '
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
describe 'search' do
|
5
|
+
describe 'by current user' do
|
6
|
+
before do
|
7
|
+
@original_current_user = User.current
|
8
|
+
@user_one = FactoryBot.create(:user)
|
9
|
+
@user_two = FactoryBot.create(:user)
|
9
10
|
|
10
|
-
|
11
|
-
|
11
|
+
@task_one = FactoryBot.create(:some_task, :user => @user_one)
|
12
|
+
FactoryBot.create(:some_task, :user => @user_two)
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
User.current = @user_one
|
15
|
+
end
|
16
|
+
after { User.current = @original_current_user }
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
test 'can search the tasks by current_user' do
|
19
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for('owner.id = current_user')
|
20
|
+
end
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
22
|
+
test 'can search by implicit search' do
|
23
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for(@task_one.label)
|
24
|
+
end
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
26
|
+
test 'can search the tasks by current_user in combination with implicit search' do
|
27
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("owner.id = current_user AND #{@task_one.label}")
|
28
|
+
end
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
30
|
+
test 'can search the tasks by user' do
|
31
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("user = #{@user_one.login}")
|
32
|
+
end
|
32
33
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
34
|
+
test 'cannot search by arbitrary key' do
|
35
|
+
proc { ForemanTasks::Task.search_for('user.my_key ~ 5') }.must_raise(ScopedSearch::QueryNotSupported)
|
36
|
+
proc { ForemanTasks::Task.search_for('user. = 5') }.must_raise(ScopedSearch::QueryNotSupported)
|
37
|
+
end
|
37
38
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
39
|
+
test 'can search the tasks by negated user' do
|
40
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("user != #{@user_two.login}")
|
41
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("user <> #{@user_two.login}")
|
42
|
+
SecureRandom.stubs(:hex).returns('abc')
|
43
|
+
assert_equal ForemanTasks::Task.search_for("user != #{@user_two.login}").to_sql,
|
44
|
+
ForemanTasks::Task.search_for("user <> #{@user_two.login}").to_sql
|
45
|
+
end
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
47
|
+
test 'can search the tasks by user\'s id' do
|
48
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("user.id = #{@user_one.id}")
|
49
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("owner.id = #{@user_one.id}")
|
50
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("user.id != #{@user_two.id}")
|
51
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("owner.id != #{@user_two.id}")
|
52
|
+
end
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
54
|
+
test 'can search by array of user ids' do
|
55
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("user.id ^ (#{@user_one.id})")
|
56
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("owner.id ^ (#{@user_one.id})")
|
57
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("user.id !^ (#{@user_two.id})")
|
58
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("owner.id !^ (#{@user_two.id})")
|
59
|
+
end
|
59
60
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
61
|
+
test 'cannot glob on user\'s id' do
|
62
|
+
proc { ForemanTasks::Task.search_for("user.id ~ something") }.must_raise(ScopedSearch::QueryNotSupported)
|
63
|
+
proc { ForemanTasks::Task.search_for("user.id ~ 5") }.must_raise(ScopedSearch::QueryNotSupported)
|
64
|
+
end
|
64
65
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
66
|
+
test 'can search the tasks by user with wildcards' do
|
67
|
+
part = @user_one.login[1..-1] # search for '*ser1' if login is 'user1'
|
68
|
+
# The following two should be equivalent
|
69
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("user ~ #{part}")
|
70
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("user ~ *#{part}*")
|
71
|
+
SecureRandom.stubs(:hex).returns('abc')
|
72
|
+
assert_equal ForemanTasks::Task.search_for("user ~ #{part}").to_sql,
|
73
|
+
ForemanTasks::Task.search_for("user ~ *#{part}*").to_sql
|
74
|
+
end
|
74
75
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
76
|
+
test 'can search the tasks by user with negated wildcards' do
|
77
|
+
part = @user_two.login[1..-1] # search for '*ser1' if login is 'user1'
|
78
|
+
# The following two should be equivalent
|
79
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("user !~ #{part}")
|
80
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("user !~ *#{part}*")
|
81
|
+
SecureRandom.stubs(:hex).returns('abc')
|
82
|
+
assert_equal ForemanTasks::Task.search_for("user !~ #{part}").to_sql,
|
83
|
+
ForemanTasks::Task.search_for("user !~ *#{part}*").to_sql
|
84
|
+
end
|
84
85
|
|
85
|
-
|
86
|
-
|
87
|
-
|
86
|
+
test 'can search the tasks by array' do
|
87
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("user ^ (this_user, #{@user_one.login}, that_user)")
|
88
|
+
end
|
89
|
+
|
90
|
+
test 'can search the tasks by negated array' do
|
91
|
+
assert_equal [@task_one], ForemanTasks::Task.search_for("user !^ (this_user, #{@user_two.login}, that_user)")
|
92
|
+
end
|
88
93
|
|
89
|
-
|
90
|
-
|
94
|
+
test 'properly returns username' do
|
95
|
+
assert_equal @task_one.username, @user_one.login
|
96
|
+
end
|
91
97
|
end
|
92
98
|
|
93
|
-
|
94
|
-
|
99
|
+
describe 'by duration' do
|
100
|
+
before do
|
101
|
+
@task_one, @task_two = FactoryBot.create_list(:dynflow_task, 2).sort_by(&:id)
|
102
|
+
end
|
103
|
+
|
104
|
+
let(:scope) { ForemanTasks::Task.order(:id) }
|
105
|
+
|
106
|
+
it 'can search by seconds ' do
|
107
|
+
skip unless on_postgresql?
|
108
|
+
_(scope.search_for('duration < 2')).must_be :empty?
|
109
|
+
_(scope.search_for('duration = 2')).must_equal [@task_one, @task_two]
|
110
|
+
_(scope.search_for('duration < "2 seconds"')).must_be :empty?
|
111
|
+
_(scope.search_for('duration > "2 seconds"')).must_be :empty?
|
112
|
+
_(scope.search_for('duration = "2 seconds"')).must_equal [@task_one, @task_two]
|
113
|
+
_(scope.search_for('duration <= "2 seconds"')).must_equal [@task_one, @task_two]
|
114
|
+
_(scope.search_for('duration >= "2 seconds"')).must_equal [@task_one, @task_two]
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'can search by other time intervals' do
|
118
|
+
skip unless on_postgresql?
|
119
|
+
%w[minutes hours days months years].each do |interval|
|
120
|
+
_(scope.search_for("duration < \"2 #{interval}\"")).must_equal [@task_one, @task_two]
|
121
|
+
_(scope.search_for("duration > \"2 #{interval}\"")).must_be :empty?
|
122
|
+
_(scope.search_for("duration = \"2 #{interval}\"")).must_be :empty?
|
123
|
+
_(scope.search_for("duration <= \"2 #{interval}\"")).must_equal [@task_one, @task_two]
|
124
|
+
_(scope.search_for("duration >= \"2 #{interval}\"")).must_be :empty?
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'raises an exception if duration is unknown' do
|
129
|
+
proc { ForemanTasks::Task.search_for('duration = "25 potatoes"') }.must_raise ScopedSearch::QueryNotSupported
|
130
|
+
end
|
95
131
|
end
|
96
132
|
end
|
97
133
|
|
@@ -248,4 +284,54 @@ class TasksTest < ActiveSupport::TestCase
|
|
248
284
|
task.execution_type.must_equal 'Delayed'
|
249
285
|
end
|
250
286
|
end
|
287
|
+
|
288
|
+
describe 'search for resource_ids' do
|
289
|
+
it 'finds tasks' do
|
290
|
+
label = 'label1'
|
291
|
+
resource_ids = [1, 2]
|
292
|
+
resource_type = 'restype1'
|
293
|
+
|
294
|
+
task1_old = FactoryBot.create(
|
295
|
+
:task_with_locks,
|
296
|
+
started_at: '2019-10-01 11:15:55',
|
297
|
+
ended_at: '2019-10-01 11:15:57',
|
298
|
+
resource_id: 1,
|
299
|
+
label: label,
|
300
|
+
resource_type: resource_type
|
301
|
+
)
|
302
|
+
task1_new = FactoryBot.create(
|
303
|
+
:task_with_locks,
|
304
|
+
started_at: '2019-10-02 11:15:55',
|
305
|
+
ended_at: '2019-10-02 11:15:57',
|
306
|
+
resource_id: 1,
|
307
|
+
label: label,
|
308
|
+
resource_type: resource_type
|
309
|
+
)
|
310
|
+
task2 = FactoryBot.create(
|
311
|
+
:task_with_locks,
|
312
|
+
started_at: '2019-10-03 11:15:55',
|
313
|
+
ended_at: '2019-10-03 11:15:57',
|
314
|
+
resource_id: 2,
|
315
|
+
label: label,
|
316
|
+
resource_type: resource_type
|
317
|
+
)
|
318
|
+
task3 = FactoryBot.create(
|
319
|
+
:task_with_locks,
|
320
|
+
started_at: '2019-10-03 11:15:55',
|
321
|
+
ended_at: '2019-10-03 11:15:57',
|
322
|
+
resource_id: 3,
|
323
|
+
label: label,
|
324
|
+
resource_type: 'another_type'
|
325
|
+
)
|
326
|
+
|
327
|
+
result = ForemanTasks::Task.search_for(
|
328
|
+
"resource_id ^ (#{resource_ids.join(',')}) and resource_type = #{resource_type}"
|
329
|
+
).distinct
|
330
|
+
assert_equal 3, result.length
|
331
|
+
assert_includes result, task1_old
|
332
|
+
assert_includes result, task1_new
|
333
|
+
assert_includes result, task2
|
334
|
+
assert_not_includes result, task3
|
335
|
+
end
|
336
|
+
end
|
251
337
|
end
|
@@ -42,6 +42,7 @@ class Task extends Component {
|
|
42
42
|
cancelTaskRequest,
|
43
43
|
resumeTaskRequest,
|
44
44
|
action,
|
45
|
+
dynflowEnableConsole,
|
45
46
|
} = this.props;
|
46
47
|
const modalUnlock = (
|
47
48
|
<ClickConfirmation
|
@@ -99,6 +100,7 @@ class Task extends Component {
|
|
99
100
|
<Button
|
100
101
|
bsSize="small"
|
101
102
|
href={`/foreman_tasks/dynflow/${externalId}`}
|
103
|
+
disabled={!dynflowEnableConsole}
|
102
104
|
>
|
103
105
|
{__('Dynflow console')}
|
104
106
|
</Button>
|
@@ -188,6 +190,7 @@ Task.propTypes = {
|
|
188
190
|
toggleForceUnlockModal: PropTypes.func,
|
189
191
|
cancelTaskRequest: PropTypes.func,
|
190
192
|
resumeTaskRequest: PropTypes.func,
|
193
|
+
dynflowEnableConsole: PropTypes.bool,
|
191
194
|
};
|
192
195
|
|
193
196
|
Task.defaultProps = {
|
@@ -210,6 +213,7 @@ Task.defaultProps = {
|
|
210
213
|
toggleForceUnlockModal: () => null,
|
211
214
|
cancelTaskRequest: () => null,
|
212
215
|
resumeTaskRequest: () => null,
|
216
|
+
dynflowEnableConsole: false,
|
213
217
|
};
|
214
218
|
|
215
219
|
export default Task;
|
@@ -2,8 +2,8 @@ import React, { Component } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
3
3
|
import { Grid, Row, Col, ProgressBar } from 'patternfly-react';
|
4
4
|
import { translate as __ } from 'foremanReact/common/I18n';
|
5
|
-
import EllipsisWithTooltip from 'react-ellipsis-with-tooltip';
|
6
5
|
import RelativeDateTime from 'foremanReact/components/common/dates/RelativeDateTime';
|
6
|
+
import ReactHtmlParser from 'react-html-parser';
|
7
7
|
|
8
8
|
class TaskInfo extends Component {
|
9
9
|
isDelayed = () => {
|
@@ -69,9 +69,7 @@ class TaskInfo extends Component {
|
|
69
69
|
[
|
70
70
|
{
|
71
71
|
title: 'Name',
|
72
|
-
value: (
|
73
|
-
<EllipsisWithTooltip>{action || __('N/A')}</EllipsisWithTooltip>
|
74
|
-
),
|
72
|
+
value: action || __('N/A'),
|
75
73
|
},
|
76
74
|
{
|
77
75
|
title: 'Start at',
|
@@ -173,14 +171,7 @@ class TaskInfo extends Component {
|
|
173
171
|
<b>{__('Troubleshooting')}</b>
|
174
172
|
</span>
|
175
173
|
</p>
|
176
|
-
<p>
|
177
|
-
{help.split('\n').map((item, i) => (
|
178
|
-
<React.Fragment key={i}>
|
179
|
-
{item}
|
180
|
-
<br />
|
181
|
-
</React.Fragment>
|
182
|
-
))}
|
183
|
-
</p>
|
174
|
+
<p>{ReactHtmlParser(help)}</p>
|
184
175
|
</Col>
|
185
176
|
</Row>
|
186
177
|
)}
|
@@ -115,6 +115,7 @@ exports[`Task rendering render with some Props 1`] = `
|
|
115
115
|
allowDangerousActions={true}
|
116
116
|
cancelTaskRequest={[Function]}
|
117
117
|
cancellable={false}
|
118
|
+
dynflowEnableConsole={true}
|
118
119
|
endedAt=""
|
119
120
|
error={Array []}
|
120
121
|
externalId=""
|
@@ -205,7 +206,7 @@ exports[`Task rendering render without Props 1`] = `
|
|
205
206
|
bsClass="btn"
|
206
207
|
bsSize="small"
|
207
208
|
bsStyle="default"
|
208
|
-
disabled={
|
209
|
+
disabled={true}
|
209
210
|
href="/foreman_tasks/dynflow/"
|
210
211
|
>
|
211
212
|
Dynflow console
|
@@ -229,6 +230,7 @@ exports[`Task rendering render without Props 1`] = `
|
|
229
230
|
allowDangerousActions={false}
|
230
231
|
cancelTaskRequest={[Function]}
|
231
232
|
cancellable={false}
|
233
|
+
dynflowEnableConsole={false}
|
232
234
|
endedAt=""
|
233
235
|
error={Array []}
|
234
236
|
externalId=""
|
@@ -32,9 +32,7 @@ exports[`TaskInfo rendering render with Props 1`] = `
|
|
32
32
|
sm={6}
|
33
33
|
>
|
34
34
|
<span>
|
35
|
-
|
36
|
-
Monitor Event Queue
|
37
|
-
</EllipisWithTooltip>
|
35
|
+
Monitor Event Queue
|
38
36
|
</span>
|
39
37
|
</Col>
|
40
38
|
<Col
|
@@ -335,9 +333,7 @@ exports[`TaskInfo rendering render without Props 1`] = `
|
|
335
333
|
sm={6}
|
336
334
|
>
|
337
335
|
<span>
|
338
|
-
|
339
|
-
N/A
|
340
|
-
</EllipisWithTooltip>
|
336
|
+
N/A
|
341
337
|
</span>
|
342
338
|
</Col>
|
343
339
|
<Col
|
@@ -30,7 +30,7 @@ export const selectErrors = state => {
|
|
30
30
|
|
31
31
|
export const selectProgress = state =>
|
32
32
|
selectTaskDetails(state).progress
|
33
|
-
? selectTaskDetails(state).progress.toFixed(
|
33
|
+
? parseFloat((selectTaskDetails(state).progress * 100).toFixed(2))
|
34
34
|
: 0;
|
35
35
|
|
36
36
|
export const selectUsername = state =>
|
@@ -83,3 +83,6 @@ export const selectShowForceUnlockModal = state =>
|
|
83
83
|
|
84
84
|
export const selectExternalId = state =>
|
85
85
|
selectTaskDetails(state).external_id || null;
|
86
|
+
|
87
|
+
export const selectDynflowEnableConsole = state =>
|
88
|
+
selectTaskDetails(state).dynflow_enable_console || false;
|
@@ -33,6 +33,7 @@ import {
|
|
33
33
|
selectShowUnlockModal,
|
34
34
|
selectShowForceUnlockModal,
|
35
35
|
selectExternalId,
|
36
|
+
selectDynflowEnableConsole,
|
36
37
|
} from './TaskDetailsSelectors';
|
37
38
|
|
38
39
|
const mapStateToProps = state => ({
|
@@ -65,6 +66,7 @@ const mapStateToProps = state => ({
|
|
65
66
|
showUnlockModal: selectShowUnlockModal(state),
|
66
67
|
showForceUnlockModal: selectShowForceUnlockModal(state),
|
67
68
|
externalId: selectExternalId(state),
|
69
|
+
dynflowEnableConsole: selectDynflowEnableConsole(state),
|
68
70
|
});
|
69
71
|
|
70
72
|
const mapDispatchToProps = dispatch => bindActionCreators(actions, dispatch);
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import PropTypes from 'prop-types';
|
3
|
-
import { translate as __ } from 'foremanReact/common/I18n';
|
3
|
+
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
4
4
|
import TasksTablePage from './';
|
5
5
|
|
6
6
|
export const SubTasksPage = props => {
|
@@ -15,10 +15,12 @@ export const SubTasksPage = props => {
|
|
15
15
|
{ caption: __('Sub tasks') },
|
16
16
|
],
|
17
17
|
});
|
18
|
+
const createHeader = actionName => actionName ? sprintf(__('Sub tasks of %s'), actionName) : __('Sub tasks');
|
18
19
|
return (
|
19
20
|
<TasksTablePage
|
20
21
|
getBreadcrumbs={getBreadcrumbs}
|
21
22
|
parentTaskID={parentTaskID}
|
23
|
+
createHeader={createHeader}
|
22
24
|
{...props}
|
23
25
|
/>
|
24
26
|
);
|
@@ -2,15 +2,17 @@ import { getURIQuery } from 'foremanReact/common/helpers';
|
|
2
2
|
import { getTableItemsAction } from 'foremanReact/components/common/table';
|
3
3
|
import API from 'foremanReact/API';
|
4
4
|
import { addToast } from 'foremanReact/redux/actions/toasts';
|
5
|
-
import { translate as __ } from 'foremanReact/common/I18n';
|
5
|
+
import { translate as __, sprintf } from 'foremanReact/common/I18n';
|
6
|
+
import URI from 'urijs';
|
6
7
|
import {
|
7
8
|
TASKS_TABLE_ID,
|
8
9
|
SELECT_ROWS,
|
9
10
|
UNSELECT_ALL_ROWS,
|
10
11
|
UNSELECT_ROWS,
|
11
|
-
RESUME,
|
12
|
-
CANCEL,
|
13
12
|
UPDATE_CLICKED,
|
13
|
+
TASKS_RESUME_REQUEST,
|
14
|
+
TASKS_RESUME_SUCCESS,
|
15
|
+
TASKS_RESUME_FAILURE,
|
14
16
|
} from './TasksTableConstants';
|
15
17
|
import { getApiPathname } from './TasksTableHelpers';
|
16
18
|
import { fetchTasksSummary } from '../TasksDashboard/TasksDashboardActions';
|
@@ -18,8 +20,13 @@ import { fetchTasksSummary } from '../TasksDashboard/TasksDashboardActions';
|
|
18
20
|
export const getTableItems = url =>
|
19
21
|
getTableItemsAction(TASKS_TABLE_ID, getURIQuery(url), getApiPathname(url));
|
20
22
|
|
21
|
-
export const cancelTask = (
|
22
|
-
|
23
|
+
export const cancelTask = ({
|
24
|
+
taskId,
|
25
|
+
taskName,
|
26
|
+
url,
|
27
|
+
parentTaskID,
|
28
|
+
}) => async dispatch => {
|
29
|
+
await dispatch(cancelTaskRequest(taskId, taskName));
|
23
30
|
dispatch(getTableItems(url));
|
24
31
|
dispatch(fetchTasksSummary(getURIQuery(url).time, parentTaskID));
|
25
32
|
};
|
@@ -49,8 +56,13 @@ export const cancelTaskRequest = (id, name) => async dispatch => {
|
|
49
56
|
}
|
50
57
|
};
|
51
58
|
|
52
|
-
export const resumeTask = (
|
53
|
-
|
59
|
+
export const resumeTask = ({
|
60
|
+
taskId,
|
61
|
+
taskName,
|
62
|
+
url,
|
63
|
+
parentTaskID,
|
64
|
+
}) => async dispatch => {
|
65
|
+
await dispatch(resumeTaskRequest(taskId, taskName));
|
54
66
|
dispatch(getTableItems(url));
|
55
67
|
dispatch(fetchTasksSummary(getURIQuery(url).time), parentTaskID);
|
56
68
|
};
|
@@ -93,34 +105,88 @@ export const unselectRow = id => ({
|
|
93
105
|
payload: id,
|
94
106
|
});
|
95
107
|
|
96
|
-
export const
|
97
|
-
|
108
|
+
export const bulkResumeRequest = resumeTasks => {
|
109
|
+
const ids = resumeTasks.map(task => task.id);
|
110
|
+
const url = new URI('/foreman_tasks/api/tasks/bulk_resume');
|
111
|
+
url.setSearch('task_ids[]', ids);
|
112
|
+
return API.post(url);
|
113
|
+
};
|
114
|
+
|
115
|
+
export const bulkResume = ({
|
116
|
+
selected,
|
117
|
+
url,
|
118
|
+
parentTaskID,
|
119
|
+
}) => async dispatch => {
|
120
|
+
const resumeTasks = selected.filter(task => task.isResumable);
|
121
|
+
if (resumeTasks.length < selected.length)
|
122
|
+
dispatch(
|
123
|
+
addToast({
|
124
|
+
type: 'warning',
|
125
|
+
message: __('Not all the selected tasks can be resumed'),
|
126
|
+
})
|
127
|
+
);
|
128
|
+
if (resumeTasks.length) {
|
129
|
+
try {
|
130
|
+
dispatch({ type: TASKS_RESUME_REQUEST });
|
131
|
+
const { data } = await bulkResumeRequest(resumeTasks);
|
132
|
+
dispatch({ type: TASKS_RESUME_SUCCESS });
|
133
|
+
const toastInfo = {
|
134
|
+
resumed: { type: 'success', text: 'was resumed' },
|
135
|
+
failed: { type: 'error', text: 'could not be resumed' },
|
136
|
+
};
|
137
|
+
const toastDispatch = (type, name) =>
|
138
|
+
dispatch(
|
139
|
+
addToast({
|
140
|
+
type: toastInfo[type].type,
|
141
|
+
message: sprintf(__('%(name)s Task execution %(type)s'), {
|
142
|
+
name,
|
143
|
+
type: toastInfo[type].text,
|
144
|
+
}),
|
145
|
+
})
|
146
|
+
);
|
147
|
+
|
148
|
+
['resumed', 'failed'].forEach(type => {
|
149
|
+
data[type].forEach(task => {
|
150
|
+
toastDispatch(type, task.action);
|
151
|
+
});
|
152
|
+
});
|
153
|
+
if (data.resumed.length) {
|
154
|
+
dispatch(getTableItems(url));
|
155
|
+
dispatch(fetchTasksSummary(getURIQuery(url).time, parentTaskID));
|
156
|
+
}
|
157
|
+
} catch (error) {
|
158
|
+
dispatch({ type: TASKS_RESUME_FAILURE, error });
|
159
|
+
dispatch(
|
160
|
+
addToast({
|
161
|
+
type: 'error',
|
162
|
+
message: `${__(`Cannot resume tasks at the moment`)} ${error}`,
|
163
|
+
})
|
164
|
+
);
|
165
|
+
}
|
166
|
+
}
|
167
|
+
};
|
168
|
+
|
169
|
+
export const bulkCancel = ({
|
98
170
|
selected,
|
99
171
|
url,
|
100
|
-
parentTaskID
|
101
|
-
) => async dispatch => {
|
172
|
+
parentTaskID,
|
173
|
+
}) => async dispatch => {
|
102
174
|
let notAllActionable = false;
|
103
175
|
let someActionable = false;
|
104
176
|
const promises = selected.map(task => {
|
105
|
-
if (
|
177
|
+
if (task.isCancellable) {
|
106
178
|
someActionable = true;
|
107
|
-
return dispatch(
|
108
|
-
} else if (actionType === CANCEL && task.isCancelleble) {
|
109
|
-
someActionable = true;
|
110
|
-
return dispatch(cancelTaskRequest(task.id, task.name, url));
|
179
|
+
return dispatch(cancelTaskRequest(task.id, task.name));
|
111
180
|
}
|
112
181
|
notAllActionable = true;
|
113
182
|
return null;
|
114
183
|
});
|
184
|
+
|
115
185
|
if (notAllActionable)
|
116
186
|
dispatch(
|
117
187
|
addToast({
|
118
188
|
type: 'warning',
|
119
|
-
message: __(
|
120
|
-
`Not all the selected tasks can be ${
|
121
|
-
actionType === RESUME ? 'resumed' : 'canceled'
|
122
|
-
}`
|
123
|
-
),
|
189
|
+
message: __('Not all the selected tasks can be canceled'),
|
124
190
|
})
|
125
191
|
);
|
126
192
|
if (someActionable) {
|