foreman-tasks 11.1.1 → 12.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.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_tests.yml +1 -0
  3. data/app/controllers/foreman_tasks/api/tasks_controller.rb +4 -19
  4. data/app/controllers/foreman_tasks/tasks_controller.rb +4 -5
  5. data/app/models/foreman_tasks/task.rb +1 -1
  6. data/app/views/foreman_tasks/api/tasks/dependency_summary.json.rabl +2 -0
  7. data/app/views/foreman_tasks/api/tasks/details.json.rabl +20 -0
  8. data/app/views/foreman_tasks/api/tasks/show.json.rabl +4 -1
  9. data/config/routes.rb +3 -3
  10. data/foreman-tasks.gemspec +3 -1
  11. data/lib/foreman_tasks/engine.rb +7 -2
  12. data/lib/foreman_tasks/tasks/export_tasks.rake +1 -1
  13. data/lib/foreman_tasks/triggers.rb +4 -0
  14. data/lib/foreman_tasks/version.rb +1 -1
  15. data/lib/foreman_tasks.rb +24 -0
  16. data/test/controllers/api/tasks_controller_test.rb +29 -3
  17. data/test/controllers/tasks_controller_test.rb +46 -2
  18. data/test/unit/chaining_test.rb +62 -0
  19. data/webpack/ForemanTasks/Components/TaskActions/TaskAction.test.js +8 -0
  20. data/webpack/ForemanTasks/Components/TaskActions/TaskActionHelpers.js +8 -2
  21. data/webpack/ForemanTasks/Components/TaskActions/TaskActionHelpers.test.js +25 -33
  22. data/webpack/ForemanTasks/Components/TaskActions/index.js +24 -3
  23. data/webpack/ForemanTasks/Components/TaskDetails/Components/Dependencies.js +93 -0
  24. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Dependencies.test.js +92 -0
  25. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/TaskInfo.test.js.snap +6 -6
  26. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +13 -1
  27. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +6 -0
  28. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetailsActions.test.js +9 -0
  29. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/__snapshots__/TaskDetails.test.js.snap +19 -0
  30. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/__snapshots__/TaskDetailsActions.test.js.snap +16 -11
  31. data/webpack/ForemanTasks/Components/TaskDetails/index.js +4 -0
  32. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksTimeRow/TasksTimeRow.js +2 -3
  33. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksTimeRow/__snapshots__/TasksTimeRow.test.js.snap +4 -8
  34. data/webpack/ForemanTasks/Components/TasksDashboard/TasksDashboard.js +2 -4
  35. data/webpack/ForemanTasks/Components/TasksDashboard/TasksDashboard.scss +0 -3
  36. data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/__snapshots__/TasksDashboard.test.js.snap +2 -5
  37. data/webpack/ForemanTasks/Components/TasksTable/Components/ActionSelectButton.js +31 -30
  38. data/webpack/ForemanTasks/Components/{common/ActionButtons/ActionButton.js → TasksTable/Components/CellActionButton.js} +36 -21
  39. data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/__test__/index.test.js +32 -29
  40. data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/createBulkTaskModal.js +14 -18
  41. data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/createTaskModal.js +13 -15
  42. data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/index.js +7 -7
  43. data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/ActionSelectButton.test.js +78 -11
  44. data/webpack/ForemanTasks/Components/TasksTable/TasksBulkActions.js +16 -23
  45. data/webpack/ForemanTasks/Components/TasksTable/TasksColumns.js +56 -0
  46. data/webpack/ForemanTasks/Components/TasksTable/TasksIndexPage.js +277 -3
  47. data/webpack/ForemanTasks/Components/TasksTable/TasksModals.js +96 -0
  48. data/webpack/ForemanTasks/Components/TasksTable/TasksTableConstants.js +10 -18
  49. data/webpack/ForemanTasks/Components/TasksTable/TasksTableHelpers.js +3 -3
  50. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksBulkActions.test.js +130 -63
  51. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksIndexPage.test.js +315 -8
  52. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTable.fixtures.js +214 -41
  53. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTableHelpers.test.js +20 -12
  54. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksBulkActions.test.js.snap +18 -35
  55. data/webpack/ForemanTasks/ForemanTasksReducers.js +0 -4
  56. data/webpack/Routes/routes.js +22 -0
  57. data/webpack/Routes/routes.test.js +95 -0
  58. data/webpack/global_index.js +10 -0
  59. data/webpack/index.js +0 -18
  60. data/webpack/test_setup.js +1 -0
  61. metadata +20 -90
  62. data/app/controllers/foreman_tasks/react_controller.rb +0 -17
  63. data/app/views/foreman_tasks/layouts/react.html.erb +0 -13
  64. data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/ConfirmModalSelectors.js +0 -30
  65. data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/__test__/ConfirmModalSelectors.test.js +0 -66
  66. data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/__test__/__snapshots__/ConfirmModalSelectors.test.js.snap +0 -33
  67. data/webpack/ForemanTasks/Components/TasksTable/Components/SelectAllAlert.js +0 -49
  68. data/webpack/ForemanTasks/Components/TasksTable/Components/TableSelectionCell.js +0 -32
  69. data/webpack/ForemanTasks/Components/TasksTable/Components/TableSelectionHeaderCell.js +0 -38
  70. data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/SelectAllAlert.test.js +0 -29
  71. data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/TableSelectionCell.test.js +0 -15
  72. data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/TableSelectionHeaderCell.test.js +0 -15
  73. data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/__snapshots__/ActionSelectButton.test.js.snap +0 -43
  74. data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/__snapshots__/SelectAllAlert.test.js.snap +0 -81
  75. data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/__snapshots__/TableSelectionCell.test.js.snap +0 -14
  76. data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/__snapshots__/TableSelectionHeaderCell.test.js.snap +0 -15
  77. data/webpack/ForemanTasks/Components/TasksTable/SubTasksPage.js +0 -40
  78. data/webpack/ForemanTasks/Components/TasksTable/TasksTable.js +0 -163
  79. data/webpack/ForemanTasks/Components/TasksTable/TasksTableActions.js +0 -108
  80. data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.js +0 -215
  81. data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.scss +0 -20
  82. data/webpack/ForemanTasks/Components/TasksTable/TasksTableReducer.js +0 -68
  83. data/webpack/ForemanTasks/Components/TasksTable/TasksTableSchema.js +0 -85
  84. data/webpack/ForemanTasks/Components/TasksTable/TasksTableSelectors.js +0 -63
  85. data/webpack/ForemanTasks/Components/TasksTable/__tests__/SubTasksPage.test.js +0 -20
  86. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTable.test.js +0 -9
  87. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTableActions.test.js +0 -65
  88. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTablePage.test.js +0 -35
  89. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTableReducer.test.js +0 -87
  90. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/SubTasksPage.test.js.snap +0 -48
  91. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksIndexPage.test.js.snap +0 -39
  92. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTable.test.js.snap +0 -52
  93. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTableActions.test.js.snap +0 -40
  94. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +0 -366
  95. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTableReducer.test.js.snap +0 -116
  96. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/actionCellFormatter.test.js.snap +0 -15
  97. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/actionNameCellFormatter.test.js.snap +0 -10
  98. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/dateCellFormmatter.test.js.snap +0 -9
  99. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/durationCellFormmatter.test.js.snap +0 -18
  100. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/selectionCellFormatter.test.js.snap +0 -12
  101. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/selectionHeaderCellFormatter.test.js.snap +0 -11
  102. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/actionCellFormatter.test.js +0 -11
  103. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/actionNameCellFormatter.test.js +0 -8
  104. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/dateCellFormmatter.test.js +0 -7
  105. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/durationCellFormmatter.test.js +0 -12
  106. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/selectionCellFormatter.test.js +0 -12
  107. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/selectionHeaderCellFormatter.test.js +0 -12
  108. data/webpack/ForemanTasks/Components/TasksTable/formatters/actionCellFormatter.js +0 -19
  109. data/webpack/ForemanTasks/Components/TasksTable/formatters/actionNameCellFormatter.js +0 -9
  110. data/webpack/ForemanTasks/Components/TasksTable/formatters/dateCellFormmatter.js +0 -7
  111. data/webpack/ForemanTasks/Components/TasksTable/formatters/durationCellFormmatter.js +0 -7
  112. data/webpack/ForemanTasks/Components/TasksTable/formatters/index.js +0 -7
  113. data/webpack/ForemanTasks/Components/TasksTable/formatters/selectionCellFormatter.js +0 -17
  114. data/webpack/ForemanTasks/Components/TasksTable/formatters/selectionHeaderCellFormatter.js +0 -11
  115. data/webpack/ForemanTasks/Components/TasksTable/index.js +0 -42
  116. data/webpack/ForemanTasks/Components/common/ActionButtons/ActionButton.test.js +0 -101
  117. data/webpack/ForemanTasks/Components/common/ActionButtons/__snapshots__/ActionButton.test.js.snap +0 -95
  118. data/webpack/ForemanTasks/ForemanTasks.js +0 -11
  119. data/webpack/ForemanTasks/ForemanTasks.test.js +0 -10
  120. data/webpack/ForemanTasks/Routes/ForemanTasksRouter.js +0 -14
  121. data/webpack/ForemanTasks/Routes/ForemanTasksRouter.test.js +0 -26
  122. data/webpack/ForemanTasks/Routes/ForemanTasksRoutes.js +0 -23
  123. data/webpack/ForemanTasks/Routes/ForemanTasksRoutes.test.js +0 -16
  124. data/webpack/ForemanTasks/Routes/__snapshots__/ForemanTasksRouter.test.js.snap +0 -16
  125. data/webpack/ForemanTasks/Routes/__snapshots__/ForemanTasksRoutes.test.js.snap +0 -37
  126. data/webpack/ForemanTasks/__snapshots__/ForemanTasks.test.js.snap +0 -7
  127. data/webpack/ForemanTasks/index.js +0 -1
  128. data/webpack/__mocks__/foremanReact/common/I18n.js +0 -7
  129. data/webpack/__mocks__/foremanReact/common/helpers.js +0 -6
  130. data/webpack/__mocks__/foremanReact/common/urlHelpers.js +0 -1
  131. data/webpack/__mocks__/foremanReact/components/Layout/LayoutActions.js +0 -2
  132. data/webpack/__mocks__/foremanReact/components/Pagination/index.js +0 -2
  133. data/webpack/__mocks__/foremanReact/components/ToastsList/index.js +0 -8
  134. data/webpack/__mocks__/foremanReact/components/common/ActionButtons/ActionButtons.js +0 -3
  135. data/webpack/__mocks__/foremanReact/components/common/MessageBox.js +0 -4
  136. data/webpack/__mocks__/foremanReact/components/common/dates/LongDateTime.js +0 -5
  137. data/webpack/__mocks__/foremanReact/components/common/dates/RelativeDateTime.js +0 -3
  138. data/webpack/__mocks__/foremanReact/components/common/table/actionsHelpers/actionTypeCreator.js +0 -7
  139. data/webpack/__mocks__/foremanReact/components/common/table.js +0 -5
  140. data/webpack/__mocks__/foremanReact/constants.js +0 -24
  141. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +0 -10
  142. data/webpack/__mocks__/foremanReact/redux/API/index.js +0 -10
  143. data/webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware.js +0 -5
  144. data/webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js +0 -10
  145. data/webpack/__mocks__/foremanReact/routes/common/PageLayout/components/ExportButton/ExportButton.js +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2aac0a5327f2c06d5a88126a179a411ed15b52d8e19e621eb6e6ef60b5c00d3
4
- data.tar.gz: 911be05d74f3274945bc5285205f977fdcc9894cb752f0c1aa921659b553b406
3
+ metadata.gz: faba9c8433d1b6c6faa616d7f6d58bfade42692f3e0afa6de35d67a0f1740bf2
4
+ data.tar.gz: 912a3a297fde3eddbbef8fca0e3da6e2bec6e6ca5882eb69a37eafe00a443ec9
5
5
  SHA512:
6
- metadata.gz: bdbdec27c38515a772e9ab1d177b07c5fa9f19d73ad412b1e45c0a4497c4a07539d8aeaceeb69cb75fccd1d05028e77058854a5544ec3f063c69890df965690f
7
- data.tar.gz: c99e51b91af6b3b67ce17ee83c58e22d3ffa64314755b0faa2791fc455d83adeb16017d6dee3fb5b1dcd0e4a6d927305443f4973e7a1102476b955807634dc46
6
+ metadata.gz: bb4842d2685129d098bb05d37a423ce7817294cf523d2dd7f4e867618d80584135564d9df7c24a460bdad68372ac663dde160189d60cdd580bce9b825d3d89f8
7
+ data.tar.gz: e11dd77bbe8d94f347ed31d5b67dfec2924fba89207007fa370f7053ec5549c46a5d16e1fc34ea4e4f23ad1607abddb40db87aa74413d9b8d51f214aecab9485
@@ -13,6 +13,7 @@ jobs:
13
13
  uses: theforeman/actions/.github/workflows/rubocop.yml@v0
14
14
  with:
15
15
  command: bundle exec rubocop --parallel --format github
16
+ ruby: '3.0'
16
17
 
17
18
  test:
18
19
  name: Ruby
@@ -179,24 +179,12 @@ module ForemanTasks
179
179
  param :parent_task_id, :identifier, desc: 'UUID of the task'
180
180
  param_group :search_and_pagination, ::Api::V2::BaseController
181
181
  def index
182
- if params[:sort_by] || params[:sort_order]
183
- Foreman::Deprecation.api_deprecation_warning(
184
- "The sort params sort_by and sort_order are deprecated.
185
- Please use the order param instead as one string 'order=started_at desc'"
186
- )
187
-
188
- ordering_params = {
189
- sort_by: params[:sort_by] || 'started_at',
190
- sort_order: params[:sort_order] || 'DESC',
191
- }
192
- params[:order] = "#{ordering_params[:sort_by]} #{ordering_params[:sort_order]}"
193
- end
194
182
  params[:order] ||= 'started_at DESC'
195
- @tasks = resource_scope_for_index.order(params[:order].to_s)
183
+ @tasks = resource_scope_for_index
196
184
  end
197
185
 
198
186
  def search_options
199
- [search_query, {}]
187
+ [search_query, { :order => params[:order] }]
200
188
  end
201
189
 
202
190
  def_param_group :callback_target do
@@ -320,19 +308,16 @@ module ForemanTasks
320
308
  end
321
309
 
322
310
  def find_task
323
- @task = resource_scope.with_duration.find(params[:id])
311
+ @task = resource_scope.select_duration.find(params[:id])
324
312
  end
325
313
 
326
314
  def resource_scope(_options = {})
327
315
  scope = ForemanTasks::Task.authorized("#{action_permission}_foreman_tasks")
328
316
  scope = scope.where(:parent_task_id => params[:parent_task_id]) if params[:parent_task_id]
317
+ scope = scope.select_duration if params[:action] == 'index'
329
318
  scope
330
319
  end
331
320
 
332
- def resource_scope_for_index(*args)
333
- super.with_duration.distinct
334
- end
335
-
336
321
  def controller_permission
337
322
  'foreman_tasks'
338
323
  end
@@ -4,7 +4,7 @@ module ForemanTasks
4
4
  include Foreman::Controller::CsvResponder
5
5
  include ForemanTasks::FindTasksCommon
6
6
 
7
- before_action :find_dynflow_task, only: [:unlock, :force_unlock, :cancel, :cancel_step, :resume]
7
+ before_action :find_dynflow_task, only: [:unlock, :force_unlock, :cancel, :abort, :cancel_step, :resume]
8
8
 
9
9
  def show
10
10
  @task = resource_base.find(params[:id])
@@ -51,11 +51,10 @@ module ForemanTasks
51
51
 
52
52
  def abort
53
53
  if @dynflow_task.abort
54
- flash[:info] = _('Trying to abort the task')
54
+ render json: { statusText: 'OK' }
55
55
  else
56
- flash[:warning] = _('The task cannot be aborted at the moment.')
56
+ render json: {}, status: :bad_request
57
57
  end
58
- redirect_back(:fallback_location => foreman_tasks_task_path(@dynflow_task))
59
58
  end
60
59
 
61
60
  def resume
@@ -98,7 +97,7 @@ module ForemanTasks
98
97
  private
99
98
 
100
99
  def respond_with_tasks(scope)
101
- @tasks = filter(scope, paginate: false).with_duration
100
+ @tasks = filter(scope, paginate: false).select_duration
102
101
  csv_response(@tasks, [:id, :action, :state, :result, 'started_at.in_time_zone', 'ended_at.in_time_zone', :duration, :username], ['Id', 'Action', 'State', 'Result', 'Started At', 'Ended At', 'Duration', 'User'])
103
102
  end
104
103
 
@@ -78,7 +78,7 @@ module ForemanTasks
78
78
  :"foreman_tasks_links.resource_type" => resource.class.name)
79
79
  end)
80
80
  scope :for_action_types, (->(action_types) { where('foreman_tasks_tasks.label IN (?)', Array(action_types)) })
81
- scope :with_duration, -> { select("foreman_tasks_tasks.*, coalesce(ended_at, current_timestamp) - coalesce(coalesce(started_at, ended_at), current_timestamp) as duration") }
81
+ virtual_column_scope :select_duration, -> { select("foreman_tasks_tasks.*, coalesce(ended_at, current_timestamp) - coalesce(coalesce(started_at, ended_at), current_timestamp) as duration") }
82
82
 
83
83
  apipie :class, "A class representing #{model_name.human} object" do
84
84
  name 'Task'
@@ -0,0 +1,2 @@
1
+ attributes :id, :action, :state, :result
2
+ node(:humanized, &:to_label)
@@ -18,3 +18,23 @@ node(:links) do
18
18
  end
19
19
  node(:username_path) { username_link_task(@task.owner, @task.username) }
20
20
  node(:dynflow_enable_console) { Setting['dynflow_enable_console'] }
21
+ node(:depends_on) do
22
+ if @task.execution_plan
23
+ dynflow_uuids = ForemanTasks.dynflow.world.persistence.find_execution_plan_dependencies(@task.execution_plan.id)
24
+ ForemanTasks::Task.where(external_id: dynflow_uuids).map do |task|
25
+ partial('foreman_tasks/api/tasks/dependency_summary', :object => task)
26
+ end
27
+ else
28
+ []
29
+ end
30
+ end
31
+ node(:blocks) do
32
+ if @task.execution_plan
33
+ dynflow_uuids = ForemanTasks.dynflow.world.persistence.find_blocked_execution_plans(@task.execution_plan.id)
34
+ ForemanTasks::Task.where(external_id: dynflow_uuids).map do |task|
35
+ partial('foreman_tasks/api/tasks/dependency_summary', :object => task)
36
+ end
37
+ else
38
+ []
39
+ end
40
+ end
@@ -3,6 +3,9 @@ object @task if @task
3
3
  extends 'api/v2/layouts/permissions'
4
4
 
5
5
  attributes :id, :label, :pending, :action
6
- attributes :username, :started_at, :ended_at, :duration, :state, :result, :progress
6
+ attributes :username, :started_at, :ended_at, :state, :result, :progress
7
+
8
+ # A workaround for https://github.com/ruby/json/issues/957
9
+ node(:duration) { |t| t.duration&.in_seconds&.to_s if t.respond_to?(:duration) }
7
10
  attributes :input, :output, :humanized, :cli_example, :start_at
8
11
  node(:available_actions) { |t| { cancellable: t.execution_plan&.cancellable?, resumable: t.resumable? } }
data/config/routes.rb CHANGED
@@ -34,9 +34,9 @@ Foreman::Application.routes.draw do
34
34
  end
35
35
  resources :tasks, :only => [:index], constraints: ->(req) { req.format == :csv }
36
36
 
37
- match '/tasks' => 'react#index', :via => [:get]
38
- match '/tasks/:id/sub_tasks' => 'react#index', :via => [:get]
39
- match '/ex_tasks/:id' => 'react#index', :via => [:get]
37
+ match '/tasks', to: '/react#index', via: :get
38
+ match '/tasks/:id/sub_tasks', to: '/react#index', via: :get
39
+ match '/ex_tasks/:id', to: '/react#index', via: :get
40
40
 
41
41
  namespace :api do
42
42
  resources :recurring_logics, :only => [:index, :show, :update] do
@@ -23,10 +23,12 @@ same resource. It also optionally provides Dynflow infrastructure for using it f
23
23
  file.end_with?("test.rake") || file == '.packit.yaml'
24
24
  end
25
25
 
26
+ s.required_ruby_version = '>= 3.0', '< 4'
27
+
26
28
  s.test_files = `git ls-files test`.split("\n")
27
29
  s.extra_rdoc_files = Dir['README*', 'LICENSE']
28
30
 
29
- s.add_dependency "dynflow", '>= 1.9.0'
31
+ s.add_dependency "dynflow", '>= 2.0.0'
30
32
  s.add_dependency 'fugit', '~> 1.8'
31
33
  s.add_dependency "get_process_mem" # for memory polling
32
34
  s.add_dependency "sinatra" # for Dynflow web console
@@ -17,8 +17,11 @@ module ForemanTasks
17
17
  end
18
18
 
19
19
  initializer 'foreman_tasks.register_plugin', :before => :finisher_hook do
20
+ require 'foreman/cron'
21
+
20
22
  Foreman::Plugin.register :"foreman-tasks" do
21
- requires_foreman '>= 3.15'
23
+ requires_foreman '>= 3.19'
24
+ register_global_js_file 'global'
22
25
  divider :top_menu, :parent => :monitor_menu, :last => true, :caption => N_('Foreman Tasks')
23
26
  menu :top_menu, :tasks,
24
27
  :url_hash => { :controller => 'foreman_tasks/tasks', :action => :index },
@@ -34,7 +37,6 @@ module ForemanTasks
34
37
 
35
38
  security_block :foreman_tasks do |_map|
36
39
  permission :view_foreman_tasks, { :'foreman_tasks/tasks' => [:auto_complete_search, :sub_tasks, :index, :summary, :summary_sub_tasks, :show],
37
- :'foreman_tasks/react' => [:index],
38
40
  :'foreman_tasks/api/tasks' => [:bulk_search, :show, :index, :summary, :summary_sub_tasks, :details, :sub_tasks] }, :resource_type => 'ForemanTasks::Task'
39
41
  permission :edit_foreman_tasks, { :'foreman_tasks/tasks' => [:resume, :unlock, :force_unlock, :cancel_step, :cancel, :abort],
40
42
  :'foreman_tasks/api/tasks' => [:bulk_resume, :bulk_cancel, :bulk_stop] }, :resource_type => 'ForemanTasks::Task'
@@ -124,6 +126,9 @@ module ForemanTasks
124
126
  widget 'foreman_tasks/tasks/dashboard/latest_tasks_in_error_warning', :sizex => 6, :sizey => 1, :name => N_('Latest Warning/Error Tasks')
125
127
 
126
128
  register_gettext domain: "foreman_tasks"
129
+
130
+ # Register recurring task with Foreman::Cron framework
131
+ Foreman::Cron.register(:daily, 'foreman_tasks:cleanup')
127
132
  end
128
133
  end
129
134
 
@@ -324,7 +324,7 @@ namespace :foreman_tasks do
324
324
  format = ENV['TASK_FORMAT'] || 'html'
325
325
  export_filename = ENV['TASK_FILE'] || generate_filename(format)
326
326
 
327
- task_scope = ForemanTasks::Task.search_for(filter).with_duration.order(:started_at => :desc)
327
+ task_scope = ForemanTasks::Task.search_for(filter).select_duration.order(:started_at => :desc)
328
328
  id_scope = task_scope.group(:id, :started_at)
329
329
 
330
330
  puts _("Exporting all tasks matching filter #{filter}")
@@ -25,5 +25,9 @@ module ForemanTasks
25
25
  def delay(action, delay_options, *args)
26
26
  foreman_tasks.delay(action, delay_options, *args)
27
27
  end
28
+
29
+ def chain(dependencies, action, *args)
30
+ foreman_tasks.chain(dependencies, action, *args)
31
+ end
28
32
  end
29
33
  end
@@ -1,3 +1,3 @@
1
1
  module ForemanTasks
2
- VERSION = '11.1.1'.freeze
2
+ VERSION = '12.1.0'.freeze
3
3
  end
data/lib/foreman_tasks.rb CHANGED
@@ -62,6 +62,30 @@ module ForemanTasks
62
62
  ForemanTasks::Task::DynflowTask.where(:external_id => result.id).first!
63
63
  end
64
64
 
65
+ # Chain a task to wait for dependency task(s) to finish before executing.
66
+ # The chained task remains 'scheduled' until all dependencies reach 'stopped' state.
67
+ #
68
+ # @param dependencies [ForemanTasks::Task, Array<ForemanTasks::Task>, ActiveRecord::Relation]
69
+ # Dependency ForemanTasks task object(s) or an ActiveRecord relation of tasks.
70
+ # @param action [Class] Action class to execute
71
+ # @param args Arguments to pass to the action
72
+ # @return [ForemanTasks::Task::DynflowTask] The chained task
73
+ def self.chain(dependencies, action, *args)
74
+ plan_uuids =
75
+ if dependencies.is_a?(ActiveRecord::Relation)
76
+ dependencies.pluck(:external_id)
77
+ else
78
+ Array(dependencies).map(&:external_id)
79
+ end
80
+
81
+ if plan_uuids.any?(&:blank?)
82
+ raise ArgumentError, 'All dependency tasks must have external_id set'
83
+ end
84
+
85
+ result = dynflow.world.chain(plan_uuids, action, *args)
86
+ ForemanTasks::Task::DynflowTask.where(:external_id => result.id).first!
87
+ end
88
+
65
89
  def self.register_scheduled_task(task_class, cronline)
66
90
  ForemanTasks::RecurringLogic.transaction(isolation: :serializable) do
67
91
  return if ForemanTasks::RecurringLogic.joins(:tasks)
@@ -37,7 +37,7 @@ module ForemanTasks
37
37
  end
38
38
 
39
39
  it 'supports ordering by duration' do
40
- get :index, params: { :sort_by => 'duration' }
40
+ get :index, params: { :order => 'duration' }
41
41
  assert_response :success
42
42
  data = JSON.parse(response.body)
43
43
  assert_equal 'duration', data.dig('sort', 'by')
@@ -104,7 +104,7 @@ module ForemanTasks
104
104
  end
105
105
 
106
106
  it 'shows duration column' do
107
- task = ForemanTasks::Task.with_duration.find(FactoryBot.create(:dynflow_task).id)
107
+ task = ForemanTasks::Task.select_duration.find(FactoryBot.create(:dynflow_task).id)
108
108
  get :show, params: { id: task.id }, session: set_session_user
109
109
  assert_response :success
110
110
  data = JSON.parse(response.body)
@@ -114,7 +114,7 @@ module ForemanTasks
114
114
 
115
115
  describe 'GET /api/tasks/index' do
116
116
  it 'shows duration column' do
117
- task = ForemanTasks::Task.with_duration.find(FactoryBot.create(:dynflow_task).id)
117
+ task = ForemanTasks::Task.select_duration.find(FactoryBot.create(:dynflow_task).id)
118
118
  get :index, session: set_session_user
119
119
  assert_response :success
120
120
  data = JSON.parse(response.body)
@@ -203,6 +203,32 @@ module ForemanTasks
203
203
  end
204
204
  end
205
205
 
206
+ describe 'GET /api/tasks/:id/details' do
207
+ it 'shows task dependencies with correct task names' do
208
+ # Tests https://projects.theforeman.org/issues/39130
209
+ dependency_task = FactoryBot.create(:dynflow_task, :user_create_task, label: 'Actions::User::Create')
210
+ blocking_task = FactoryBot.create(:dynflow_task, :user_create_task, label: 'Actions::User::Create')
211
+
212
+ ForemanTasks.dynflow.world.persistence.stubs(:find_execution_plan_dependencies)
213
+ .with(dependency_task.execution_plan.id)
214
+ .returns([])
215
+ ForemanTasks.dynflow.world.persistence.stubs(:find_blocked_execution_plans)
216
+ .with(dependency_task.execution_plan.id)
217
+ .returns([blocking_task.external_id])
218
+
219
+ get :details, params: { id: dependency_task.id }
220
+ assert_response :success
221
+
222
+ data = JSON.parse(response.body)
223
+ assert_kind_of Array, data['blocks']
224
+ assert_equal 1, data['blocks'].length
225
+
226
+ blocking_task_data = data['blocks'].first
227
+ assert_equal blocking_task.id, blocking_task_data['id']
228
+ assert_equal blocking_task.to_label, blocking_task_data['humanized']
229
+ end
230
+ end
231
+
206
232
  describe 'POST /api/tasks/bulk_stop' do
207
233
  it 'requires search or task_ids parameter' do
208
234
  post :bulk_stop
@@ -93,7 +93,7 @@ module ForemanTasks
93
93
 
94
94
  describe 'index' do
95
95
  it 'shows duration column' do
96
- task = ForemanTasks::Task.with_duration.find(FactoryBot.create(:some_task).id)
96
+ task = ForemanTasks::Task.select_duration.find(FactoryBot.create(:some_task).id)
97
97
  get(:index, params: {}, session: set_session_user)
98
98
  assert_response :success
99
99
  row = CSV.parse(response.body, headers: true).first
@@ -122,7 +122,7 @@ module ForemanTasks
122
122
 
123
123
  it 'shows duration column' do
124
124
  parent = ForemanTasks::Task.find(FactoryBot.create(:some_task).id)
125
- child = ForemanTasks::Task.with_duration.find(FactoryBot.create(:some_task).id)
125
+ child = ForemanTasks::Task.select_duration.find(FactoryBot.create(:some_task).id)
126
126
  child.parent_task_id = parent.id
127
127
  child.save!
128
128
  get(:sub_tasks, params: { id: parent.id }, session: set_session_user)
@@ -168,6 +168,50 @@ module ForemanTasks
168
168
  end
169
169
  end
170
170
  end
171
+
172
+ describe 'cancel' do
173
+ it 'finds the dynflow task and cancels it' do
174
+ task = FactoryBot.create(:dynflow_task)
175
+ ForemanTasks::Task::DynflowTask.any_instance.stubs(:cancel).returns(true)
176
+
177
+ post(:cancel, params: { id: task.id }, session: set_session_user)
178
+
179
+ assert_response :success
180
+ data = JSON.parse(response.body)
181
+ assert_equal 'OK', data['statusText']
182
+ end
183
+
184
+ it 'returns bad request when task cannot be cancelled' do
185
+ task = FactoryBot.create(:dynflow_task)
186
+ ForemanTasks::Task::DynflowTask.any_instance.stubs(:cancel).returns(false)
187
+
188
+ post(:cancel, params: { id: task.id }, session: set_session_user)
189
+
190
+ assert_response :bad_request
191
+ end
192
+ end
193
+
194
+ describe 'abort' do
195
+ it 'finds the dynflow task and aborts it' do
196
+ task = FactoryBot.create(:dynflow_task)
197
+ ForemanTasks::Task::DynflowTask.any_instance.stubs(:abort).returns(true)
198
+
199
+ post(:abort, params: { id: task.id }, session: set_session_user)
200
+
201
+ assert_response :success
202
+ data = JSON.parse(response.body)
203
+ assert_equal 'OK', data['statusText']
204
+ end
205
+
206
+ it 'returns bad request when the task cannot be aborted' do
207
+ task = FactoryBot.create(:dynflow_task)
208
+ ForemanTasks::Task::DynflowTask.any_instance.stubs(:abort).returns(false)
209
+
210
+ post(:abort, params: { id: task.id }, session: set_session_user)
211
+
212
+ assert_response :bad_request
213
+ end
214
+ end
171
215
  end
172
216
  end
173
217
  end
@@ -0,0 +1,62 @@
1
+ require 'foreman_tasks_test_helper'
2
+
3
+ module ForemanTasks
4
+ class ChainingTest < ActiveSupport::TestCase
5
+ include ForemanTasks::TestHelpers::WithInThreadExecutor
6
+
7
+ before do
8
+ User.current = User.where(:login => 'apiadmin').first
9
+ end
10
+
11
+ it 'creates a scheduled task chained to a dependency task' do
12
+ triggered = ForemanTasks.trigger(Support::DummyDynflowAction)
13
+ triggered.finished.wait(30)
14
+ dependency_task = ForemanTasks::Task::DynflowTask.find_by!(:external_id => triggered.id)
15
+
16
+ task = ForemanTasks.chain(dependency_task, Support::DummyDynflowAction)
17
+
18
+ assert_kind_of ForemanTasks::Task::DynflowTask, task
19
+ assert_predicate task, :scheduled?
20
+
21
+ dependencies = ForemanTasks.dynflow.world.persistence.find_execution_plan_dependencies(task.execution_plan.id)
22
+ assert_includes dependencies, dependency_task.external_id
23
+ end
24
+
25
+ it 'accepts multiple dependency tasks' do
26
+ triggered_1 = ForemanTasks.trigger(Support::DummyDynflowAction)
27
+ triggered_2 = ForemanTasks.trigger(Support::DummyDynflowAction)
28
+ triggered_1.finished.wait(30)
29
+ triggered_2.finished.wait(30)
30
+ dependency_task_1 = ForemanTasks::Task::DynflowTask.find_by!(:external_id => triggered_1.id)
31
+ dependency_task_2 = ForemanTasks::Task::DynflowTask.find_by!(:external_id => triggered_2.id)
32
+
33
+ task = ForemanTasks.chain([dependency_task_1, dependency_task_2], Support::DummyDynflowAction)
34
+
35
+ dependencies = ForemanTasks.dynflow.world.persistence.find_execution_plan_dependencies(task.execution_plan.id)
36
+ assert_includes dependencies, dependency_task_1.external_id
37
+ assert_includes dependencies, dependency_task_2.external_id
38
+ end
39
+
40
+ it 'accepts dependency task objects' do
41
+ triggered = ForemanTasks.trigger(Support::DummyDynflowAction)
42
+ triggered.finished.wait(30)
43
+ dependency_task = ForemanTasks::Task::DynflowTask.find_by!(:external_id => triggered.id)
44
+
45
+ task = ForemanTasks.chain(dependency_task, Support::DummyDynflowAction)
46
+
47
+ dependencies = ForemanTasks.dynflow.world.persistence.find_execution_plan_dependencies(task.execution_plan.id)
48
+ assert_includes dependencies, dependency_task.external_id
49
+ end
50
+
51
+ it 'accepts dependency tasks as a relation' do
52
+ triggered = ForemanTasks.trigger(Support::DummyDynflowAction)
53
+ triggered.finished.wait(30)
54
+ dependency_task = ForemanTasks::Task::DynflowTask.find_by!(:external_id => triggered.id)
55
+
56
+ task = ForemanTasks.chain(ForemanTasks::Task::DynflowTask.where(:id => dependency_task.id), Support::DummyDynflowAction)
57
+
58
+ dependencies = ForemanTasks.dynflow.world.persistence.find_execution_plan_dependencies(task.execution_plan.id)
59
+ assert_includes dependencies, dependency_task.external_id
60
+ end
61
+ end
62
+ end
@@ -12,6 +12,14 @@ jest.mock('foremanReact/components/common/table', () => ({
12
12
  }));
13
13
 
14
14
  jest.mock('foremanReact/redux/API');
15
+ jest.mock('foremanReact/components/ToastsList', () => ({
16
+ addToast: toast => ({
17
+ type: 'TOASTS_ADD',
18
+ payload: {
19
+ message: toast,
20
+ },
21
+ }),
22
+ }));
15
23
 
16
24
  const task = ['some-id', 'some-name'];
17
25
 
@@ -1,5 +1,6 @@
1
1
  import { translate as __, sprintf } from 'foremanReact/common/I18n';
2
2
  import { addToast } from 'foremanReact/components/ToastsList';
3
+ import { getURIQuery } from 'foremanReact/common/helpers';
3
4
  import { TASKS_DASHBOARD_JS_QUERY_MODES } from '../TasksDashboard/TasksDashboardConstants';
4
5
  import { timeToHoursNumber } from '../TasksDashboard/TasksDashboardHelper';
5
6
  import {
@@ -8,7 +9,12 @@ import {
8
9
  warningToastData,
9
10
  } from '../common/ToastsHelpers';
10
11
 
11
- export const convertDashboardQuery = query => {
12
+ const getTasksQuery = () => {
13
+ const url = window.location.pathname + window.location.search;
14
+ return getURIQuery(url);
15
+ };
16
+
17
+ export const convertDashboardQuery = () => {
12
18
  const {
13
19
  time_mode: timeMode,
14
20
  time_horizon: timeHorizon,
@@ -16,7 +22,7 @@ export const convertDashboardQuery = query => {
16
22
  result,
17
23
  search,
18
24
  ...rest
19
- } = query;
25
+ } = getTasksQuery();
20
26
 
21
27
  const hours = timeToHoursNumber(timeHorizon);
22
28
  const timestamp = new Date(new Date() - hours * 60 * 60 * 1000);
@@ -1,14 +1,19 @@
1
1
  import { convertDashboardQuery } from './TaskActionHelpers';
2
- import {
3
- TASKS_DASHBOARD_JS_QUERY_MODES,
4
- TASKS_DASHBOARD_AVAILABLE_TIMES,
5
- } from '../TasksDashboard/TasksDashboardConstants';
6
2
 
7
3
  let realDate;
8
4
 
9
5
  describe('convertDashboardQuery', () => {
6
+ const mockLocation = query => {
7
+ global.window = Object.create(window);
8
+ Object.defineProperty(window, 'location', {
9
+ value: {
10
+ pathname: '/foreman_tasks/tasks',
11
+ search: query,
12
+ },
13
+ writable: true,
14
+ });
15
+ };
10
16
  it('convertDashboardQuery should work with full query', () => {
11
- // Setup
12
17
  const currentDate = new Date('2020-05-08T11:01:58.135Z');
13
18
  realDate = Date;
14
19
  global.Date = class extends Date {
@@ -20,48 +25,35 @@ describe('convertDashboardQuery', () => {
20
25
  return currentDate;
21
26
  }
22
27
  };
23
- const query = {
24
- time_mode: TASKS_DASHBOARD_JS_QUERY_MODES.RECENT,
25
- time_horizon: TASKS_DASHBOARD_AVAILABLE_TIMES.WEEK,
26
- state: 'stopped',
27
- result: 'error',
28
- search: 'action~job',
29
- };
30
- const expected =
31
- '(state=stopped) and (result=error) and (action~job) and (state_updated_at>2020-05-01T11:01:58.135Z or null? state_updated_at)';
28
+ mockLocation('?state=stopped&result=error&search=action~job');
29
+ const expected = '(state=stopped) and (result=error) and (action~job)';
32
30
 
33
- expect(convertDashboardQuery(query)).toEqual({ search: expected });
31
+ expect(convertDashboardQuery()).toEqual({ search: expected });
34
32
 
35
- const query2 = {
36
- ...query,
37
- time_mode: TASKS_DASHBOARD_JS_QUERY_MODES.OLDER,
38
- };
39
- const expected2 =
40
- '(state=stopped) and (result=error) and (action~job) and (state_updated_at<=2020-05-01T11:01:58.135Z)';
41
- expect(convertDashboardQuery(query2)).toEqual({ search: expected2 });
42
- // Cleanup
33
+ mockLocation('?state=stopped&result=error&search=action~job');
34
+ const expected2 = '(state=stopped) and (result=error) and (action~job)';
35
+ expect(convertDashboardQuery()).toEqual({ search: expected2 });
43
36
  global.Date = realDate;
44
37
  });
45
38
  it('convertDashboardQuery should work with only search query', () => {
46
- const query = {
47
- search: 'action~job',
48
- };
49
- expect(convertDashboardQuery(query)).toEqual({ search: '(action~job)' });
39
+ mockLocation('?search=action~job');
40
+ expect(convertDashboardQuery()).toEqual({ search: '(action~job)' });
50
41
  });
51
42
  it('convertDashboardQuery should work with no query', () => {
52
- const query = {};
53
- expect(convertDashboardQuery(query)).toEqual({});
43
+ mockLocation('');
44
+ expect(convertDashboardQuery()).toEqual({});
54
45
  });
55
46
  it('convertDashboardQuery should not override unknown keys', () => {
56
- const query = { weather: 'nice', search: 'okay', number: 7 };
57
- expect(convertDashboardQuery(query)).toEqual({
47
+ const query = { weather: 'nice', search: 'okay', number: '7' };
48
+ mockLocation('?weather=nice&search=okay&number=7');
49
+ expect(convertDashboardQuery()).toEqual({
58
50
  ...query,
59
51
  search: '(okay)',
60
52
  });
61
53
  });
62
54
  it('convertDashboardQuery should expand other result', () => {
63
- const query = { result: 'other' };
64
- expect(convertDashboardQuery(query)).toEqual({
55
+ mockLocation('?result=other');
56
+ expect(convertDashboardQuery()).toEqual({
65
57
  search: '(result ^ (pending, cancelled))',
66
58
  });
67
59
  });