foreman-tasks 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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) {
|