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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -12
  3. data/.rubocop_todo.yml +34 -116
  4. data/app/controllers/foreman_tasks/api/recurring_logics_controller.rb +20 -1
  5. data/app/controllers/foreman_tasks/api/tasks_controller.rb +29 -9
  6. data/app/controllers/foreman_tasks/concerns/hosts_controller_extension.rb +1 -1
  7. data/app/controllers/foreman_tasks/recurring_logics_controller.rb +19 -0
  8. data/app/helpers/foreman_tasks/foreman_tasks_helper.rb +1 -3
  9. data/app/lib/actions/helpers/humanizer.rb +1 -3
  10. data/app/lib/actions/proxy_action.rb +33 -12
  11. data/app/models/foreman_tasks/concerns/action_triggering.rb +1 -1
  12. data/app/models/foreman_tasks/recurring_logic.rb +1 -0
  13. data/app/models/foreman_tasks/remote_task.rb +1 -0
  14. data/app/models/foreman_tasks/task.rb +4 -0
  15. data/app/models/foreman_tasks/task/dynflow_task.rb +1 -1
  16. data/app/models/foreman_tasks/task/search.rb +11 -1
  17. data/app/services/foreman_tasks/troubleshooting_help_generator.rb +0 -4
  18. data/app/views/foreman_tasks/api/recurring_logics/base.json.rabl +2 -1
  19. data/app/views/foreman_tasks/api/tasks/details.json.rabl +1 -0
  20. data/app/views/foreman_tasks/api/tasks/show.json.rabl +1 -1
  21. data/app/views/foreman_tasks/recurring_logics/index.html.erb +30 -0
  22. data/app/views/foreman_tasks/tasks/show.html.erb +3 -0
  23. data/config/routes.rb +7 -0
  24. data/foreman-tasks.gemspec +4 -6
  25. data/lib/foreman_tasks/dynflow/console_authorizer.rb +2 -2
  26. data/lib/foreman_tasks/engine.rb +15 -13
  27. data/lib/foreman_tasks/tasks/cleanup.rake +1 -1
  28. data/lib/foreman_tasks/tasks/export_tasks.rake +2 -2
  29. data/lib/foreman_tasks/test_extensions.rb +1 -1
  30. data/lib/foreman_tasks/version.rb +1 -1
  31. data/locale/action_names.rb +1 -1
  32. data/package.json +1 -2
  33. data/script/rails +2 -2
  34. data/test/factories/task_factory.rb +34 -2
  35. data/test/foreman_tasks_test_helper.rb +4 -0
  36. data/test/unit/actions/action_with_sub_plans_test.rb +1 -1
  37. data/test/unit/task_test.rb +160 -74
  38. data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +4 -0
  39. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +3 -12
  40. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +1 -0
  41. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/Task.test.js.snap +3 -1
  42. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/TaskInfo.test.js.snap +2 -6
  43. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +4 -1
  44. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/__snapshots__/TaskDetails.test.js.snap +1 -0
  45. data/webpack/ForemanTasks/Components/TaskDetails/index.js +2 -0
  46. data/webpack/ForemanTasks/Components/TasksTable/SubTasksPage.js +3 -1
  47. data/webpack/ForemanTasks/Components/TasksTable/TasksTableActions.js +87 -21
  48. data/webpack/ForemanTasks/Components/TasksTable/TasksTableConstants.js +7 -7
  49. data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.js +31 -22
  50. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTable.fixtures.js +2 -1
  51. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTableActions.test.js +44 -46
  52. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/SubTasksPage.test.js.snap +3 -1
  53. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksIndexPage.test.js.snap +2 -1
  54. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTableActions.test.js.snap +61 -5
  55. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +6 -2
  56. metadata +10 -10
@@ -1,97 +1,133 @@
1
1
  require 'foreman_tasks_test_helper'
2
2
 
3
3
  class TasksTest < ActiveSupport::TestCase
4
- describe 'filtering by current user' do
5
- before do
6
- @original_current_user = User.current
7
- @user_one = FactoryBot.create(:user)
8
- @user_two = FactoryBot.create(:user)
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
- @task_one = FactoryBot.create(:some_task, :user => @user_one)
11
- FactoryBot.create(:some_task, :user => @user_two)
11
+ @task_one = FactoryBot.create(:some_task, :user => @user_one)
12
+ FactoryBot.create(:some_task, :user => @user_two)
12
13
 
13
- User.current = @user_one
14
- end
15
- after { User.current = @original_current_user }
14
+ User.current = @user_one
15
+ end
16
+ after { User.current = @original_current_user }
16
17
 
17
- test 'can search the tasks by current_user' do
18
- assert_equal [@task_one], ForemanTasks::Task.search_for('owner.id = current_user')
19
- end
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
- test 'can search by implicit search' do
22
- assert_equal [@task_one], ForemanTasks::Task.search_for(@task_one.label)
23
- end
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
- test 'can search the tasks by current_user in combination with implicit search' do
26
- assert_equal [@task_one], ForemanTasks::Task.search_for("owner.id = current_user AND #{@task_one.label}")
27
- end
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
- test 'can search the tasks by user' do
30
- assert_equal [@task_one], ForemanTasks::Task.search_for("user = #{@user_one.login}")
31
- end
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
- test 'cannot search by arbitrary key' do
34
- proc { ForemanTasks::Task.search_for('user.my_key ~ 5') }.must_raise(ScopedSearch::QueryNotSupported)
35
- proc { ForemanTasks::Task.search_for('user. = 5') }.must_raise(ScopedSearch::QueryNotSupported)
36
- end
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
- test 'can search the tasks by negated user' do
39
- assert_equal [@task_one], ForemanTasks::Task.search_for("user != #{@user_two.login}")
40
- assert_equal [@task_one], ForemanTasks::Task.search_for("user <> #{@user_two.login}")
41
- SecureRandom.stubs(:hex).returns('abc')
42
- assert_equal ForemanTasks::Task.search_for("user != #{@user_two.login}").to_sql,
43
- ForemanTasks::Task.search_for("user <> #{@user_two.login}").to_sql
44
- end
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
- test 'can search the tasks by user\'s id' do
47
- assert_equal [@task_one], ForemanTasks::Task.search_for("user.id = #{@user_one.id}")
48
- assert_equal [@task_one], ForemanTasks::Task.search_for("owner.id = #{@user_one.id}")
49
- assert_equal [@task_one], ForemanTasks::Task.search_for("user.id != #{@user_two.id}")
50
- assert_equal [@task_one], ForemanTasks::Task.search_for("owner.id != #{@user_two.id}")
51
- end
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
- test 'can search by array of user ids' do
54
- assert_equal [@task_one], ForemanTasks::Task.search_for("user.id ^ (#{@user_one.id})")
55
- assert_equal [@task_one], ForemanTasks::Task.search_for("owner.id ^ (#{@user_one.id})")
56
- assert_equal [@task_one], ForemanTasks::Task.search_for("user.id !^ (#{@user_two.id})")
57
- assert_equal [@task_one], ForemanTasks::Task.search_for("owner.id !^ (#{@user_two.id})")
58
- end
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
- test 'cannot glob on user\'s id' do
61
- proc { ForemanTasks::Task.search_for("user.id ~ something") }.must_raise(ScopedSearch::QueryNotSupported)
62
- proc { ForemanTasks::Task.search_for("user.id ~ 5") }.must_raise(ScopedSearch::QueryNotSupported)
63
- end
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
- test 'can search the tasks by user with wildcards' do
66
- part = @user_one.login[1..-1] # search for '*ser1' if login is 'user1'
67
- # The following two should be equivalent
68
- assert_equal [@task_one], ForemanTasks::Task.search_for("user ~ #{part}")
69
- assert_equal [@task_one], ForemanTasks::Task.search_for("user ~ *#{part}*")
70
- SecureRandom.stubs(:hex).returns('abc')
71
- assert_equal ForemanTasks::Task.search_for("user ~ #{part}").to_sql,
72
- ForemanTasks::Task.search_for("user ~ *#{part}*").to_sql
73
- end
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
- test 'can search the tasks by user with negated wildcards' do
76
- part = @user_two.login[1..-1] # search for '*ser1' if login is 'user1'
77
- # The following two should be equivalent
78
- assert_equal [@task_one], ForemanTasks::Task.search_for("user !~ #{part}")
79
- assert_equal [@task_one], ForemanTasks::Task.search_for("user !~ *#{part}*")
80
- SecureRandom.stubs(:hex).returns('abc')
81
- assert_equal ForemanTasks::Task.search_for("user !~ #{part}").to_sql,
82
- ForemanTasks::Task.search_for("user !~ *#{part}*").to_sql
83
- end
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
- test 'can search the tasks by array' do
86
- assert_equal [@task_one], ForemanTasks::Task.search_for("user ^ (this_user, #{@user_one.login}, that_user)")
87
- end
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
- test 'can search the tasks by negated array' do
90
- assert_equal [@task_one], ForemanTasks::Task.search_for("user !^ (this_user, #{@user_two.login}, that_user)")
94
+ test 'properly returns username' do
95
+ assert_equal @task_one.username, @user_one.login
96
+ end
91
97
  end
92
98
 
93
- test 'properly returns username' do
94
- assert_equal @task_one.username, @user_one.login
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
  )}
@@ -9,6 +9,7 @@ const fixtures = {
9
9
  state: 'paused',
10
10
  hasSubTasks: true,
11
11
  allowDangerousActions: true,
12
+ dynflowEnableConsole: true,
12
13
  },
13
14
  };
14
15
 
@@ -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={false}
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
- <EllipisWithTooltip>
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
- <EllipisWithTooltip>
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(4) * 100
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;
@@ -18,6 +18,7 @@ exports[`TaskDetails rendering render without Props 1`] = `
18
18
  allowDangerousActions={false}
19
19
  cancelTaskRequest={[Function]}
20
20
  cancellable={false}
21
+ dynflowEnableConsole={false}
21
22
  endedAt=""
22
23
  error={Array []}
23
24
  executionPlan={Object {}}
@@ -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 = (id, name, url, parentTaskID) => async dispatch => {
22
- await dispatch(cancelTaskRequest(id, name));
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 = (id, name, url, parentTaskID) => async dispatch => {
53
- await dispatch(resumeTaskRequest(id, name));
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 actionSelected = (
97
- actionType,
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 (actionType === RESUME && task.isResumeble) {
177
+ if (task.isCancellable) {
106
178
  someActionable = true;
107
- return dispatch(resumeTaskRequest(task.id, task.name, url));
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) {