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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby_tests.yml +1 -0
- data/app/controllers/foreman_tasks/api/tasks_controller.rb +4 -19
- data/app/controllers/foreman_tasks/tasks_controller.rb +4 -5
- data/app/models/foreman_tasks/task.rb +1 -1
- data/app/views/foreman_tasks/api/tasks/dependency_summary.json.rabl +2 -0
- data/app/views/foreman_tasks/api/tasks/details.json.rabl +20 -0
- data/app/views/foreman_tasks/api/tasks/show.json.rabl +4 -1
- data/config/routes.rb +3 -3
- data/foreman-tasks.gemspec +3 -1
- data/lib/foreman_tasks/engine.rb +7 -2
- data/lib/foreman_tasks/tasks/export_tasks.rake +1 -1
- data/lib/foreman_tasks/triggers.rb +4 -0
- data/lib/foreman_tasks/version.rb +1 -1
- data/lib/foreman_tasks.rb +24 -0
- data/test/controllers/api/tasks_controller_test.rb +29 -3
- data/test/controllers/tasks_controller_test.rb +46 -2
- data/test/unit/chaining_test.rb +62 -0
- data/webpack/ForemanTasks/Components/TaskActions/TaskAction.test.js +8 -0
- data/webpack/ForemanTasks/Components/TaskActions/TaskActionHelpers.js +8 -2
- data/webpack/ForemanTasks/Components/TaskActions/TaskActionHelpers.test.js +25 -33
- data/webpack/ForemanTasks/Components/TaskActions/index.js +24 -3
- data/webpack/ForemanTasks/Components/TaskDetails/Components/Dependencies.js +93 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Dependencies.test.js +92 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/TaskInfo.test.js.snap +6 -6
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +13 -1
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +6 -0
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetailsActions.test.js +9 -0
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/__snapshots__/TaskDetails.test.js.snap +19 -0
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/__snapshots__/TaskDetailsActions.test.js.snap +16 -11
- data/webpack/ForemanTasks/Components/TaskDetails/index.js +4 -0
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksTimeRow/TasksTimeRow.js +2 -3
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksTimeRow/__snapshots__/TasksTimeRow.test.js.snap +4 -8
- data/webpack/ForemanTasks/Components/TasksDashboard/TasksDashboard.js +2 -4
- data/webpack/ForemanTasks/Components/TasksDashboard/TasksDashboard.scss +0 -3
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/__snapshots__/TasksDashboard.test.js.snap +2 -5
- data/webpack/ForemanTasks/Components/TasksTable/Components/ActionSelectButton.js +31 -30
- data/webpack/ForemanTasks/Components/{common/ActionButtons/ActionButton.js → TasksTable/Components/CellActionButton.js} +36 -21
- data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/__test__/index.test.js +32 -29
- data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/createBulkTaskModal.js +14 -18
- data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/createTaskModal.js +13 -15
- data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/index.js +7 -7
- data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/ActionSelectButton.test.js +78 -11
- data/webpack/ForemanTasks/Components/TasksTable/TasksBulkActions.js +16 -23
- data/webpack/ForemanTasks/Components/TasksTable/TasksColumns.js +56 -0
- data/webpack/ForemanTasks/Components/TasksTable/TasksIndexPage.js +277 -3
- data/webpack/ForemanTasks/Components/TasksTable/TasksModals.js +96 -0
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableConstants.js +10 -18
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableHelpers.js +3 -3
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksBulkActions.test.js +130 -63
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksIndexPage.test.js +315 -8
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTable.fixtures.js +214 -41
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTableHelpers.test.js +20 -12
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksBulkActions.test.js.snap +18 -35
- data/webpack/ForemanTasks/ForemanTasksReducers.js +0 -4
- data/webpack/Routes/routes.js +22 -0
- data/webpack/Routes/routes.test.js +95 -0
- data/webpack/global_index.js +10 -0
- data/webpack/index.js +0 -18
- data/webpack/test_setup.js +1 -0
- metadata +20 -90
- data/app/controllers/foreman_tasks/react_controller.rb +0 -17
- data/app/views/foreman_tasks/layouts/react.html.erb +0 -13
- data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/ConfirmModalSelectors.js +0 -30
- data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/__test__/ConfirmModalSelectors.test.js +0 -66
- data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/__test__/__snapshots__/ConfirmModalSelectors.test.js.snap +0 -33
- data/webpack/ForemanTasks/Components/TasksTable/Components/SelectAllAlert.js +0 -49
- data/webpack/ForemanTasks/Components/TasksTable/Components/TableSelectionCell.js +0 -32
- data/webpack/ForemanTasks/Components/TasksTable/Components/TableSelectionHeaderCell.js +0 -38
- data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/SelectAllAlert.test.js +0 -29
- data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/TableSelectionCell.test.js +0 -15
- data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/TableSelectionHeaderCell.test.js +0 -15
- data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/__snapshots__/ActionSelectButton.test.js.snap +0 -43
- data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/__snapshots__/SelectAllAlert.test.js.snap +0 -81
- data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/__snapshots__/TableSelectionCell.test.js.snap +0 -14
- data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/__snapshots__/TableSelectionHeaderCell.test.js.snap +0 -15
- data/webpack/ForemanTasks/Components/TasksTable/SubTasksPage.js +0 -40
- data/webpack/ForemanTasks/Components/TasksTable/TasksTable.js +0 -163
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableActions.js +0 -108
- data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.js +0 -215
- data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.scss +0 -20
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableReducer.js +0 -68
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableSchema.js +0 -85
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableSelectors.js +0 -63
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/SubTasksPage.test.js +0 -20
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTable.test.js +0 -9
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTableActions.test.js +0 -65
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTablePage.test.js +0 -35
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTableReducer.test.js +0 -87
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/SubTasksPage.test.js.snap +0 -48
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksIndexPage.test.js.snap +0 -39
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTable.test.js.snap +0 -52
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTableActions.test.js.snap +0 -40
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +0 -366
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTableReducer.test.js.snap +0 -116
- data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/actionCellFormatter.test.js.snap +0 -15
- data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/actionNameCellFormatter.test.js.snap +0 -10
- data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/dateCellFormmatter.test.js.snap +0 -9
- data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/durationCellFormmatter.test.js.snap +0 -18
- data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/selectionCellFormatter.test.js.snap +0 -12
- data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/selectionHeaderCellFormatter.test.js.snap +0 -11
- data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/actionCellFormatter.test.js +0 -11
- data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/actionNameCellFormatter.test.js +0 -8
- data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/dateCellFormmatter.test.js +0 -7
- data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/durationCellFormmatter.test.js +0 -12
- data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/selectionCellFormatter.test.js +0 -12
- data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/selectionHeaderCellFormatter.test.js +0 -12
- data/webpack/ForemanTasks/Components/TasksTable/formatters/actionCellFormatter.js +0 -19
- data/webpack/ForemanTasks/Components/TasksTable/formatters/actionNameCellFormatter.js +0 -9
- data/webpack/ForemanTasks/Components/TasksTable/formatters/dateCellFormmatter.js +0 -7
- data/webpack/ForemanTasks/Components/TasksTable/formatters/durationCellFormmatter.js +0 -7
- data/webpack/ForemanTasks/Components/TasksTable/formatters/index.js +0 -7
- data/webpack/ForemanTasks/Components/TasksTable/formatters/selectionCellFormatter.js +0 -17
- data/webpack/ForemanTasks/Components/TasksTable/formatters/selectionHeaderCellFormatter.js +0 -11
- data/webpack/ForemanTasks/Components/TasksTable/index.js +0 -42
- data/webpack/ForemanTasks/Components/common/ActionButtons/ActionButton.test.js +0 -101
- data/webpack/ForemanTasks/Components/common/ActionButtons/__snapshots__/ActionButton.test.js.snap +0 -95
- data/webpack/ForemanTasks/ForemanTasks.js +0 -11
- data/webpack/ForemanTasks/ForemanTasks.test.js +0 -10
- data/webpack/ForemanTasks/Routes/ForemanTasksRouter.js +0 -14
- data/webpack/ForemanTasks/Routes/ForemanTasksRouter.test.js +0 -26
- data/webpack/ForemanTasks/Routes/ForemanTasksRoutes.js +0 -23
- data/webpack/ForemanTasks/Routes/ForemanTasksRoutes.test.js +0 -16
- data/webpack/ForemanTasks/Routes/__snapshots__/ForemanTasksRouter.test.js.snap +0 -16
- data/webpack/ForemanTasks/Routes/__snapshots__/ForemanTasksRoutes.test.js.snap +0 -37
- data/webpack/ForemanTasks/__snapshots__/ForemanTasks.test.js.snap +0 -7
- data/webpack/ForemanTasks/index.js +0 -1
- data/webpack/__mocks__/foremanReact/common/I18n.js +0 -7
- data/webpack/__mocks__/foremanReact/common/helpers.js +0 -6
- data/webpack/__mocks__/foremanReact/common/urlHelpers.js +0 -1
- data/webpack/__mocks__/foremanReact/components/Layout/LayoutActions.js +0 -2
- data/webpack/__mocks__/foremanReact/components/Pagination/index.js +0 -2
- data/webpack/__mocks__/foremanReact/components/ToastsList/index.js +0 -8
- data/webpack/__mocks__/foremanReact/components/common/ActionButtons/ActionButtons.js +0 -3
- data/webpack/__mocks__/foremanReact/components/common/MessageBox.js +0 -4
- data/webpack/__mocks__/foremanReact/components/common/dates/LongDateTime.js +0 -5
- data/webpack/__mocks__/foremanReact/components/common/dates/RelativeDateTime.js +0 -3
- data/webpack/__mocks__/foremanReact/components/common/table/actionsHelpers/actionTypeCreator.js +0 -7
- data/webpack/__mocks__/foremanReact/components/common/table.js +0 -5
- data/webpack/__mocks__/foremanReact/constants.js +0 -24
- data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +0 -10
- data/webpack/__mocks__/foremanReact/redux/API/index.js +0 -10
- data/webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware.js +0 -5
- data/webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js +0 -10
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: faba9c8433d1b6c6faa616d7f6d58bfade42692f3e0afa6de35d67a0f1740bf2
|
|
4
|
+
data.tar.gz: 912a3a297fde3eddbbef8fca0e3da6e2bec6e6ca5882eb69a37eafe00a443ec9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bb4842d2685129d098bb05d37a423ce7817294cf523d2dd7f4e867618d80584135564d9df7c24a460bdad68372ac663dde160189d60cdd580bce9b825d3d89f8
|
|
7
|
+
data.tar.gz: e11dd77bbe8d94f347ed31d5b67dfec2924fba89207007fa370f7053ec5549c46a5d16e1fc34ea4e4f23ad1607abddb40db87aa74413d9b8d51f214aecab9485
|
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
54
|
+
render json: { statusText: 'OK' }
|
|
55
55
|
else
|
|
56
|
-
|
|
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).
|
|
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
|
-
|
|
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'
|
|
@@ -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, :
|
|
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'
|
|
38
|
-
match '/tasks/:id/sub_tasks'
|
|
39
|
-
match '/ex_tasks/:id'
|
|
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
|
data/foreman-tasks.gemspec
CHANGED
|
@@ -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", '>=
|
|
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
|
data/lib/foreman_tasks/engine.rb
CHANGED
|
@@ -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.
|
|
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).
|
|
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}")
|
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: { :
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
} =
|
|
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
|
-
|
|
24
|
-
|
|
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(
|
|
31
|
+
expect(convertDashboardQuery()).toEqual({ search: expected });
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
53
|
-
expect(convertDashboardQuery(
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
expect(convertDashboardQuery(
|
|
55
|
+
mockLocation('?result=other');
|
|
56
|
+
expect(convertDashboardQuery()).toEqual({
|
|
65
57
|
search: '(result ^ (pending, cancelled))',
|
|
66
58
|
});
|
|
67
59
|
});
|