foreman-tasks 3.0.4 → 4.1.1
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 +5 -3
- data/app/controllers/foreman_tasks/api/tasks_controller.rb +2 -5
- data/app/lib/actions/entry_action.rb +8 -4
- data/app/lib/actions/helpers/lock.rb +11 -5
- data/app/lib/actions/middleware/keep_current_request_id.rb +4 -1
- data/app/lib/actions/middleware/keep_current_user.rb +11 -1
- data/app/lib/actions/observable_action.rb +80 -0
- data/app/lib/actions/proxy_action.rb +2 -4
- data/app/models/foreman_tasks/concerns/action_subject.rb +0 -6
- data/app/models/foreman_tasks/link.rb +60 -0
- data/app/models/foreman_tasks/lock.rb +30 -128
- data/app/models/foreman_tasks/task.rb +20 -7
- data/app/models/foreman_tasks/task/search.rb +7 -6
- data/app/views/foreman_tasks/api/locks/show.json.rabl +4 -0
- data/app/views/foreman_tasks/api/tasks/details.json.rabl +5 -3
- data/app/views/foreman_tasks/tasks/_lock_card.html.erb +10 -0
- data/db/migrate/20180927120509_add_user_id.foreman_tasks.rb +4 -2
- data/db/migrate/20181206123910_create_foreman_tasks_links.foreman_tasks.rb +26 -0
- data/db/migrate/20181206124952_migrate_non_exclusive_locks_to_links.foreman_tasks.rb +14 -0
- data/db/migrate/20181206131436_drop_old_locks.foreman_tasks.rb +20 -0
- data/db/migrate/20181206131627_make_locks_exclusive.foreman_tasks.rb +25 -0
- data/lib/foreman_tasks/cleaner.rb +10 -0
- data/lib/foreman_tasks/engine.rb +5 -2
- data/lib/foreman_tasks/tasks/export_tasks.rake +2 -2
- data/lib/foreman_tasks/version.rb +1 -1
- data/package.json +6 -6
- data/test/controllers/api/tasks_controller_test.rb +1 -1
- data/test/controllers/tasks_controller_test.rb +3 -3
- data/test/core/unit/runner_test.rb +4 -17
- data/test/factories/task_factory.rb +31 -4
- data/test/unit/actions/action_with_sub_plans_test.rb +5 -2
- data/test/unit/actions/proxy_action_test.rb +4 -1
- data/test/unit/cleaner_test.rb +4 -4
- data/test/unit/locking_test.rb +85 -0
- data/test/unit/task_test.rb +15 -13
- data/test/unit/triggering_test.rb +2 -2
- data/webpack/ForemanTasks/Components/TaskDetails/Components/Locks.js +2 -2
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +3 -2
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/Locks.test.js.snap +4 -4
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/TaskInfo.test.js.snap +2 -0
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +4 -1
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +5 -1
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +3 -0
- data/webpack/ForemanTasks/Components/TaskDetails/index.js +2 -0
- data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.scss +4 -3
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +2 -2
- data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/actionNameCellFormatter.test.js.snap +2 -3
- data/webpack/ForemanTasks/Components/TasksTable/formatters/actionNameCellFormatter.js +2 -3
- metadata +12 -4
- data/test/unit/lock_test.rb +0 -22
@@ -34,8 +34,9 @@ module ForemanTasks
|
|
34
34
|
describe Actions::ActionWithSubPlans do
|
35
35
|
include ForemanTasks::TestHelpers::WithInThreadExecutor
|
36
36
|
|
37
|
+
let(:user) { FactoryBot.create(:user) }
|
38
|
+
|
37
39
|
let(:task) do
|
38
|
-
user = FactoryBot.create(:user)
|
39
40
|
triggered = ForemanTasks.trigger(ParentAction, user)
|
40
41
|
raise triggered.error if triggered.respond_to?(:error)
|
41
42
|
triggered.finished.wait(30)
|
@@ -49,7 +50,9 @@ module ForemanTasks
|
|
49
50
|
|
50
51
|
specify "the locks of the sub-plan don't colide with the locks of its parent" do
|
51
52
|
child_task = task.sub_tasks.first
|
52
|
-
|
53
|
+
assert_not(child_task.locks.any?, "the lock is ensured by the parent")
|
54
|
+
found = ForemanTasks::Link.for_resource(user).where(:task_id => child_task.id).any?
|
55
|
+
assert(found, "the action is linked properly")
|
53
56
|
end
|
54
57
|
end
|
55
58
|
end
|
@@ -14,6 +14,9 @@ module ForemanTasks
|
|
14
14
|
Support::DummyProxyAction.any_instance.stubs(:with_batch_triggering?).returns(batch_triggering)
|
15
15
|
Support::DummyProxyAction.reset
|
16
16
|
RemoteTask.any_instance.stubs(:proxy).returns(Support::DummyProxyAction.proxy)
|
17
|
+
Setting.stubs(:[]).with('foreman_tasks_proxy_action_retry_interval')
|
18
|
+
Setting.stubs(:[]).with('foreman_tasks_proxy_action_retry_count')
|
19
|
+
Setting.stubs(:[]).with('foreman_tasks_proxy_batch_trigger')
|
17
20
|
@action = create_and_plan_action(Support::DummyProxyAction,
|
18
21
|
Support::DummyProxyAction.proxy,
|
19
22
|
'Proxy::DummyAction',
|
@@ -107,7 +110,7 @@ module ForemanTasks
|
|
107
110
|
_(action.world.clock.pending_pings.length).must_equal 1
|
108
111
|
_(action.output[:metadata][:failed_proxy_tasks].length).must_equal 1
|
109
112
|
2.times { action.output[:metadata][:failed_proxy_tasks] << {} }
|
110
|
-
_
|
113
|
+
_(proc { action = run_stubbed_action.call action }).must_raise(Errno::ECONNREFUSED)
|
111
114
|
_(action.state).must_equal :error
|
112
115
|
end
|
113
116
|
|
data/test/unit/cleaner_test.rb
CHANGED
@@ -60,18 +60,18 @@ class TasksTest < ActiveSupport::TestCase
|
|
60
60
|
task.started_at = task.ended_at = Time.zone.now
|
61
61
|
task.save
|
62
62
|
end]
|
63
|
-
|
63
|
+
link_to_delete = tasks_to_delete.first.links.create(:resource => User.current)
|
64
64
|
|
65
65
|
tasks_to_keep = [FactoryBot.create(:dynflow_task, :product_create_task)]
|
66
|
-
|
66
|
+
link_to_keep = tasks_to_keep.first.links.create(:resource => User.current)
|
67
67
|
|
68
68
|
cleaner.expects(:tasks_to_csv)
|
69
69
|
cleaner.delete
|
70
70
|
_(ForemanTasks::Task.where(id: tasks_to_delete)).must_be_empty
|
71
71
|
_(ForemanTasks::Task.where(id: tasks_to_keep)).must_equal tasks_to_keep
|
72
72
|
|
73
|
-
_(ForemanTasks::
|
74
|
-
_(ForemanTasks::
|
73
|
+
_(ForemanTasks::Link.find_by(id: link_to_delete.id)).must_be_nil
|
74
|
+
_(ForemanTasks::Link.find_by(id: link_to_keep.id)).wont_be_nil
|
75
75
|
end
|
76
76
|
|
77
77
|
it 'supports passing empty filter (just delete all)' do
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'foreman_tasks_test_helper'
|
3
|
+
|
4
|
+
module ForemanTasks
|
5
|
+
class LockTest < ::ActiveSupport::TestCase
|
6
|
+
describe ::ForemanTasks::Lock::LockConflict do
|
7
|
+
class FakeLockConflict < ForemanTasks::Lock::LockConflict
|
8
|
+
def _(val)
|
9
|
+
val.freeze
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'does not modify frozen strings' do
|
14
|
+
required_lock = OpenStruct.new(:name => 'my_lock')
|
15
|
+
# Before #21770 the next line would raise
|
16
|
+
# RuntimeError: can't modify frozen String
|
17
|
+
conflict = FakeLockConflict.new(required_lock, [])
|
18
|
+
assert conflict._('this should be frozen').frozen?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe 'locking and linking' do
|
23
|
+
before { [Lock, Link].each(&:destroy_all) }
|
24
|
+
let(:task1) { FactoryBot.create(:some_task) }
|
25
|
+
let(:task2) { FactoryBot.create(:some_task) }
|
26
|
+
let(:resource) { FactoryBot.create(:user) }
|
27
|
+
|
28
|
+
describe Lock do
|
29
|
+
it 'can lock a resource for a single task' do
|
30
|
+
Lock.lock!(resource, task1)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'can lock a resource for a single task only once' do
|
34
|
+
Lock.lock!(resource, task1)
|
35
|
+
_(Lock.for_resource(resource).count).must_equal 1
|
36
|
+
Lock.lock!(resource, task1)
|
37
|
+
_(Lock.for_resource(resource).count).must_equal 1
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'cannot lock a resource for multiple tasks' do
|
41
|
+
lock = Lock.lock!(resource, task1)
|
42
|
+
_(Lock.colliding_locks(resource, task2)).must_equal [lock]
|
43
|
+
assert_raises Lock::LockConflict do
|
44
|
+
Lock.lock!(resource, task2)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'raises LockConflict when enforced by db' do
|
49
|
+
lock = Lock.lock!(resource, task1)
|
50
|
+
Lock.any_instance
|
51
|
+
.expects(:colliding_locks).twice.returns([], [lock])
|
52
|
+
exception = assert_raises Lock::LockConflict do
|
53
|
+
Lock.lock!(resource, task2)
|
54
|
+
end
|
55
|
+
_(exception.message).must_match(/#{lock.task_id}/)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'creates a link when creating a lock for a resource' do
|
59
|
+
Lock.lock!(resource, task1)
|
60
|
+
link = Link.for_resource(resource).first
|
61
|
+
_(link.task_id).must_equal task1.id
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe Link do
|
66
|
+
it 'can link a resource to a single task' do
|
67
|
+
Link.link!(resource, task1)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'can link a resource for a single task only once' do
|
71
|
+
Link.link!(resource, task1)
|
72
|
+
_(Link.for_resource(resource).count).must_equal 1
|
73
|
+
Link.link!(resource, task1)
|
74
|
+
_(Link.for_resource(resource).count).must_equal 1
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'can link a resource to multiple tasks' do
|
78
|
+
Link.link!(resource, task1)
|
79
|
+
Link.link!(resource, task2)
|
80
|
+
_(Link.for_resource(resource).count).must_equal 2
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/test/unit/task_test.rb
CHANGED
@@ -31,9 +31,9 @@ class TasksTest < ActiveSupport::TestCase
|
|
31
31
|
assert_equal [@task_one], ForemanTasks::Task.search_for("user = #{@user_one.login}")
|
32
32
|
end
|
33
33
|
|
34
|
-
|
35
|
-
_
|
36
|
-
_
|
34
|
+
it '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
37
|
end
|
38
38
|
|
39
39
|
test 'can search the tasks by negated user' do
|
@@ -59,8 +59,8 @@ class TasksTest < ActiveSupport::TestCase
|
|
59
59
|
end
|
60
60
|
|
61
61
|
test 'cannot glob on user\'s id' do
|
62
|
-
_
|
63
|
-
_
|
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
64
|
end
|
65
65
|
|
66
66
|
test 'can search the tasks by user with wildcards' do
|
@@ -126,15 +126,17 @@ class TasksTest < ActiveSupport::TestCase
|
|
126
126
|
end
|
127
127
|
|
128
128
|
it 'raises an exception if duration is unknown' do
|
129
|
-
_
|
129
|
+
_(proc { ForemanTasks::Task.search_for('duration = "25 potatoes"') }).must_raise ScopedSearch::QueryNotSupported
|
130
130
|
end
|
131
131
|
end
|
132
132
|
|
133
133
|
context 'by taxonomies' do
|
134
134
|
test 'can search by taxonomies using IN' do
|
135
|
-
assert_nothing_raised
|
136
|
-
assert_nothing_raised
|
137
|
-
assert_nothing_raised
|
135
|
+
assert_nothing_raised { ForemanTasks::Task.search_for('location_id ^ (1)').first }
|
136
|
+
assert_nothing_raised { ForemanTasks::Task.search_for('organization_id ^ (1)').first }
|
137
|
+
assert_nothing_raised { ForemanTasks::Task.search_for('location_id ^ (1,2)').first }
|
138
|
+
assert_nothing_raised { ForemanTasks::Task.search_for('organization_id ^ (1,2)').first }
|
139
|
+
assert_nothing_raised { ForemanTasks::Task.search_for('organization_id = 1').first }
|
138
140
|
end
|
139
141
|
end
|
140
142
|
end
|
@@ -300,7 +302,7 @@ class TasksTest < ActiveSupport::TestCase
|
|
300
302
|
resource_type = 'restype1'
|
301
303
|
|
302
304
|
task1_old = FactoryBot.create(
|
303
|
-
:
|
305
|
+
:task_with_links,
|
304
306
|
started_at: '2019-10-01 11:15:55',
|
305
307
|
ended_at: '2019-10-01 11:15:57',
|
306
308
|
resource_id: 1,
|
@@ -308,7 +310,7 @@ class TasksTest < ActiveSupport::TestCase
|
|
308
310
|
resource_type: resource_type
|
309
311
|
)
|
310
312
|
task1_new = FactoryBot.create(
|
311
|
-
:
|
313
|
+
:task_with_links,
|
312
314
|
started_at: '2019-10-02 11:15:55',
|
313
315
|
ended_at: '2019-10-02 11:15:57',
|
314
316
|
resource_id: 1,
|
@@ -316,7 +318,7 @@ class TasksTest < ActiveSupport::TestCase
|
|
316
318
|
resource_type: resource_type
|
317
319
|
)
|
318
320
|
task2 = FactoryBot.create(
|
319
|
-
:
|
321
|
+
:task_with_links,
|
320
322
|
started_at: '2019-10-03 11:15:55',
|
321
323
|
ended_at: '2019-10-03 11:15:57',
|
322
324
|
resource_id: 2,
|
@@ -324,7 +326,7 @@ class TasksTest < ActiveSupport::TestCase
|
|
324
326
|
resource_type: resource_type
|
325
327
|
)
|
326
328
|
task3 = FactoryBot.create(
|
327
|
-
:
|
329
|
+
:task_with_links,
|
328
330
|
started_at: '2019-10-03 11:15:55',
|
329
331
|
ended_at: '2019-10-03 11:15:57',
|
330
332
|
resource_id: 3,
|
@@ -24,7 +24,7 @@ class TriggeringTest < ActiveSupport::TestCase
|
|
24
24
|
it 'cannot have mode set to arbitrary value' do
|
25
25
|
triggering = FactoryBot.build(:triggering)
|
26
26
|
_(triggering).must_be :valid?
|
27
|
-
_
|
28
|
-
_
|
27
|
+
_(proc { triggering.mode = 'bogus' }).must_raise ArgumentError
|
28
|
+
_(proc { triggering.mode = 27 }).must_raise ArgumentError
|
29
29
|
end
|
30
30
|
end
|
@@ -21,10 +21,10 @@ const Locks = ({ locks }) => (
|
|
21
21
|
lock.exclusive ? 'fa-lock' : 'fa-unlock-alt'
|
22
22
|
}`}
|
23
23
|
/>
|
24
|
-
{lock.
|
24
|
+
{lock.resource_type}
|
25
25
|
</Card.Title>
|
26
26
|
<Card.Body>
|
27
|
-
{
|
27
|
+
{`id:${lock.resource_id}`}
|
28
28
|
<br />
|
29
29
|
</Card.Body>
|
30
30
|
</Card>
|
@@ -69,6 +69,7 @@ class TaskInfo extends Component {
|
|
69
69
|
{
|
70
70
|
title: 'Name',
|
71
71
|
value: action || __('N/A'),
|
72
|
+
className: 'details-name',
|
72
73
|
},
|
73
74
|
{
|
74
75
|
title: 'Start at',
|
@@ -129,7 +130,7 @@ class TaskInfo extends Component {
|
|
129
130
|
{__(items[0].title)}:
|
130
131
|
</span>
|
131
132
|
</Col>
|
132
|
-
<Col md={5} sm={6}>
|
133
|
+
<Col md={5} sm={6} className={items[0].className}>
|
133
134
|
<span>{items[0].value}</span>
|
134
135
|
</Col>
|
135
136
|
<Col md={2} sm={6}>
|
@@ -137,7 +138,7 @@ class TaskInfo extends Component {
|
|
137
138
|
{__(items[1].title)}:
|
138
139
|
</span>
|
139
140
|
</Col>
|
140
|
-
<Col md={3} sm={6}>
|
141
|
+
<Col md={3} sm={6} className={items[1].className}>
|
141
142
|
{items[1].value}
|
142
143
|
</Col>
|
143
144
|
</Row>
|
@@ -39,12 +39,12 @@ exports[`Locks rendering render with Props 1`] = `
|
|
39
39
|
<span
|
40
40
|
className="fa fa-unlock-alt"
|
41
41
|
/>
|
42
|
-
|
42
|
+
User
|
43
43
|
</CardTitle>
|
44
44
|
<CardBody
|
45
45
|
className=""
|
46
46
|
>
|
47
|
-
|
47
|
+
id:4
|
48
48
|
<br />
|
49
49
|
</CardBody>
|
50
50
|
</Card>
|
@@ -71,12 +71,12 @@ exports[`Locks rendering render with Props 1`] = `
|
|
71
71
|
<span
|
72
72
|
className="fa fa-unlock-alt"
|
73
73
|
/>
|
74
|
-
|
74
|
+
User
|
75
75
|
</CardTitle>
|
76
76
|
<CardBody
|
77
77
|
className=""
|
78
78
|
>
|
79
|
-
|
79
|
+
id:2
|
80
80
|
<br />
|
81
81
|
</CardBody>
|
82
82
|
</Card>
|
@@ -27,6 +27,7 @@ exports[`TaskInfo rendering render with Props 1`] = `
|
|
27
27
|
</Col>
|
28
28
|
<Col
|
29
29
|
bsClass="col"
|
30
|
+
className="details-name"
|
30
31
|
componentClass="div"
|
31
32
|
md={5}
|
32
33
|
sm={6}
|
@@ -330,6 +331,7 @@ exports[`TaskInfo rendering render without Props 1`] = `
|
|
330
331
|
</Col>
|
331
332
|
<Col
|
332
333
|
bsClass="col"
|
334
|
+
className="details-name"
|
333
335
|
componentClass="div"
|
334
336
|
md={5}
|
335
337
|
sm={6}
|
@@ -19,6 +19,7 @@ const TaskDetails = ({
|
|
19
19
|
failedSteps,
|
20
20
|
runningSteps,
|
21
21
|
locks,
|
22
|
+
links,
|
22
23
|
cancelStep,
|
23
24
|
taskReloadStart,
|
24
25
|
taskReloadStop,
|
@@ -87,7 +88,7 @@ const TaskDetails = ({
|
|
87
88
|
<Errors executionPlan={executionPlan} failedSteps={failedSteps} />
|
88
89
|
</Tab>
|
89
90
|
<Tab eventKey={4} disabled={isLoading} title={__('Locks')}>
|
90
|
-
<Locks locks={locks} />
|
91
|
+
<Locks locks={locks.concat(links)} />
|
91
92
|
</Tab>
|
92
93
|
<Tab eventKey={5} disabled={isLoading} title={__('Raw')}>
|
93
94
|
<Raw
|
@@ -114,6 +115,7 @@ TaskDetails.propTypes = {
|
|
114
115
|
APIerror: PropTypes.object,
|
115
116
|
taskReloadStop: PropTypes.func.isRequired,
|
116
117
|
taskReloadStart: PropTypes.func.isRequired,
|
118
|
+
links: PropTypes.array,
|
117
119
|
...Task.propTypes,
|
118
120
|
...Errors.propTypes,
|
119
121
|
...Locks.propTypes,
|
@@ -124,6 +126,7 @@ TaskDetails.defaultProps = {
|
|
124
126
|
runningSteps: [],
|
125
127
|
APIerror: null,
|
126
128
|
status: STATUS.PENDING,
|
129
|
+
links: [],
|
127
130
|
...Task.defaultProps,
|
128
131
|
...RunningSteps.defaultProps,
|
129
132
|
...Errors.defaultProps,
|
@@ -70,6 +70,9 @@ export const selectHasSubTasks = state =>
|
|
70
70
|
export const selectLocks = state =>
|
71
71
|
selectTaskDetailsResponse(state).locks || [];
|
72
72
|
|
73
|
+
export const selectLinks = state =>
|
74
|
+
selectTaskDetailsResponse(state).links || [];
|
75
|
+
|
73
76
|
export const selectUsernamePath = state =>
|
74
77
|
selectTaskDetailsResponse(state)?.username_path;
|
75
78
|
|
@@ -9,6 +9,7 @@ import {
|
|
9
9
|
selectStartBefore,
|
10
10
|
selectStartedAt,
|
11
11
|
selectLocks,
|
12
|
+
selectLinks,
|
12
13
|
selectInput,
|
13
14
|
selectOutput,
|
14
15
|
selectResumable,
|
@@ -56,6 +57,7 @@ const mapStateToProps = state => ({
|
|
56
57
|
help: selectHelp(state),
|
57
58
|
hasSubTasks: selectHasSubTasks(state),
|
58
59
|
locks: selectLocks(state),
|
60
|
+
links: selectLinks(state),
|
59
61
|
usernamePath: selectUsernamePath(state),
|
60
62
|
action: selectAction(state),
|
61
63
|
state: selectState(state),
|
data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap
CHANGED
@@ -17,7 +17,7 @@ exports[`TasksTablePage rendering render with Breadcrubs and edit permissions 1`
|
|
17
17
|
/>
|
18
18
|
<PageLayout
|
19
19
|
beforeToolbarComponent={
|
20
|
-
<
|
20
|
+
<Memo(Connect(TasksDashboard))
|
21
21
|
history={
|
22
22
|
Object {
|
23
23
|
"location": Object {
|
@@ -169,7 +169,7 @@ exports[`TasksTablePage rendering render with minimal props 1`] = `
|
|
169
169
|
/>
|
170
170
|
<PageLayout
|
171
171
|
beforeToolbarComponent={
|
172
|
-
<
|
172
|
+
<Memo(Connect(TasksDashboard))
|
173
173
|
history={
|
174
174
|
Object {
|
175
175
|
"location": Object {
|