foreman-tasks 1.1.0 → 1.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/app/controllers/foreman_tasks/api/tasks_controller.rb +12 -5
- data/app/lib/foreman_tasks/concerns/polling_action_extensions.rb +12 -0
- data/app/models/setting/foreman_tasks.rb +6 -1
- data/app/services/ui_notifications/tasks/task_bulk_cancel.rb +36 -0
- data/app/services/ui_notifications/tasks/task_bulk_resume.rb +38 -0
- data/db/seeds.d/30-notification_blueprints.rb +14 -0
- data/foreman-tasks.gemspec +1 -0
- data/gemfile.d/foreman-tasks.rb +1 -0
- data/lib/foreman_tasks/engine.rb +1 -0
- data/lib/foreman_tasks/version.rb +1 -1
- data/script/travis_run_js_tests.sh +2 -2
- data/test/lib/concerns/polling_action_extensions_test.rb +34 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/TaskInfo.test.js.snap +1 -3
- data/webpack/ForemanTasks/Components/TasksDashboard/TasksDashboardConstants.js +5 -0
- data/webpack/ForemanTasks/Components/TasksDashboard/TasksDashboardHelper.js +3 -2
- data/webpack/ForemanTasks/Components/TasksTable/Components/SelectAllAlert.js +43 -0
- data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/SelectAllAlert.test.js +29 -0
- data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/__snapshots__/SelectAllAlert.test.js.snap +75 -0
- data/webpack/ForemanTasks/Components/TasksTable/SubTasksPage.js +2 -1
- data/webpack/ForemanTasks/Components/TasksTable/TasksBulkActions.js +164 -0
- data/webpack/ForemanTasks/Components/TasksTable/TasksTable.js +24 -10
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableActionHelpers.js +52 -0
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableActions.js +66 -128
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableConstants.js +11 -1
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableHelpers.js +4 -3
- data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.js +64 -12
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableReducer.js +21 -2
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableSelectors.js +6 -0
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksBulkActions.test.js +112 -0
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTable.fixtures.js +5 -3
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTableActionHelpers.test.js +46 -0
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTableActions.test.js +19 -41
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTableHelpers.test.js +17 -1
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTablePage.test.js +9 -1
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTableReducer.test.js +22 -1
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/SubTasksPage.test.js.snap +5 -3
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksBulkActions.test.js.snap +229 -0
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksIndexPage.test.js.snap +5 -3
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTableActions.test.js.snap +39 -124
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +40 -16
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTableReducer.test.js.snap +34 -0
- data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/selectionHeaderCellFormatter.test.js +1 -1
- data/webpack/ForemanTasks/Components/TasksTable/formatters/selectionHeaderCellFormatter.js +2 -2
- data/webpack/ForemanTasks/Components/TasksTable/index.js +8 -2
- data/webpack/ForemanTasks/Components/common/ToastTypesConstants.js +11 -0
- metadata +31 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c5c591217c85cb51b8bfa6372cac7b6eca2010a6b574da5f85c970ad83392641
|
4
|
+
data.tar.gz: 49e5c517005966476ee6ffca2d61998a8fb9ce89f5c621e8fcecb8ed5dc6917c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c31a093250bca2a31d50c5d9f85dd2c80cb35cfaa4d48a24c96a471c3d218cf0f6cf2b1511074b0bd5c1349d738e49a0cd5f0d5ffeba1d98035958eb02c39fbb
|
7
|
+
data.tar.gz: a548fb2f051c5173066e009539a17d768b353a52ddd66b43816b4d308b910d173b1264b525745621b18023a0111e9c2b25e4508668f406c4df1f82f6c0ec8801
|
@@ -95,7 +95,8 @@ module ForemanTasks
|
|
95
95
|
resumed = []
|
96
96
|
failed = []
|
97
97
|
skipped = []
|
98
|
-
|
98
|
+
filtered_scope = bulk_scope
|
99
|
+
filtered_scope.each do |task|
|
99
100
|
if task.resumable?
|
100
101
|
begin
|
101
102
|
ForemanTasks.dynflow.world.execute(task.execution_plan.id)
|
@@ -107,7 +108,10 @@ module ForemanTasks
|
|
107
108
|
skipped << task_hash(task)
|
108
109
|
end
|
109
110
|
end
|
110
|
-
|
111
|
+
if params[:search]
|
112
|
+
notification = UINotifications::Tasks::TaskBulkResume.new(filtered_scope.first, resumed.length, failed.length, skipped.length)
|
113
|
+
notification.deliver!
|
114
|
+
end
|
111
115
|
render :json => {
|
112
116
|
total: resumed.length + failed.length + skipped.length,
|
113
117
|
resumed: resumed,
|
@@ -123,10 +127,13 @@ module ForemanTasks
|
|
123
127
|
if params[:search].nil? && params[:task_ids].nil?
|
124
128
|
raise BadRequest, _('Please provide at least one of search or task_ids parameters in the request')
|
125
129
|
end
|
126
|
-
|
127
|
-
cancelled, skipped =
|
128
|
-
|
130
|
+
filtered_scope = bulk_scope
|
131
|
+
cancelled, skipped = filtered_scope.partition(&:cancellable?)
|
129
132
|
cancelled.each(&:cancel)
|
133
|
+
if params[:search]
|
134
|
+
notification = UINotifications::Tasks::TaskBulkCancel.new(filtered_scope.first, cancelled.length, skipped.length)
|
135
|
+
notification.deliver!
|
136
|
+
end
|
130
137
|
render :json => {
|
131
138
|
total: cancelled.length + skipped.length,
|
132
139
|
cancelled: cancelled,
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module ForemanTasks
|
2
|
+
module Concerns
|
3
|
+
module PollingActionExtensions
|
4
|
+
def poll_intervals
|
5
|
+
multiplier = Setting[:foreman_tasks_polling_multiplier] || 1
|
6
|
+
|
7
|
+
# Prevent the intervals from going below 0.5 seconds
|
8
|
+
super.map { |interval| [interval * multiplier, 0.5].max }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -13,7 +13,12 @@ class Setting::ForemanTasks < Setting
|
|
13
13
|
N_('Url pointing to the task troubleshooting documentation. '\
|
14
14
|
'It should contain %{label} placeholder, that will be replaced with normalized task label '\
|
15
15
|
'(restricted to only alphanumeric characters)). %{version} placeholder is also available.'),
|
16
|
-
nil)
|
16
|
+
nil),
|
17
|
+
set('foreman_tasks_polling_multiplier',
|
18
|
+
N_('Polling multiplier which is used to multiply the default polling intervals. '\
|
19
|
+
'This can be used to prevent polling too frequently for long running tasks.'),
|
20
|
+
1,
|
21
|
+
N_("Polling intervals multiplier"))
|
17
22
|
]
|
18
23
|
end
|
19
24
|
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module UINotifications
|
2
|
+
module Tasks
|
3
|
+
class TaskBulkCancel < ::UINotifications::Base
|
4
|
+
def initialize(task, cancelled_length, skipped_length)
|
5
|
+
@subject = task
|
6
|
+
@cancelled_length = cancelled_length
|
7
|
+
@skipped_length = skipped_length
|
8
|
+
end
|
9
|
+
|
10
|
+
def create
|
11
|
+
Notification.create!(
|
12
|
+
initiator: initiator,
|
13
|
+
audience: audience,
|
14
|
+
subject: subject,
|
15
|
+
notification_blueprint: blueprint,
|
16
|
+
message: message,
|
17
|
+
notification_recipients: [NotificationRecipient.create({ :user => User.current })]
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def audience
|
22
|
+
Notification::AUDIENCE_GLOBAL
|
23
|
+
end
|
24
|
+
|
25
|
+
def message
|
26
|
+
('%{cancelled} Tasks were cancelled. %{skipped} Tasks were skipped. ' %
|
27
|
+
{ cancelled: @cancelled_length,
|
28
|
+
skipped: @skipped_length })
|
29
|
+
end
|
30
|
+
|
31
|
+
def blueprint
|
32
|
+
@blueprint ||= NotificationBlueprint.find_by(name: 'tasks_bulk_cancel')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module UINotifications
|
2
|
+
module Tasks
|
3
|
+
class TaskBulkResume < ::UINotifications::Base
|
4
|
+
def initialize(task, resumed_length, failed_length, skipped_length)
|
5
|
+
@subject = task
|
6
|
+
@resumed_length = resumed_length
|
7
|
+
@failed_length = failed_length
|
8
|
+
@skipped_length = skipped_length
|
9
|
+
end
|
10
|
+
|
11
|
+
def create
|
12
|
+
Notification.create!(
|
13
|
+
initiator: initiator,
|
14
|
+
audience: audience,
|
15
|
+
subject: subject,
|
16
|
+
notification_blueprint: blueprint,
|
17
|
+
message: message,
|
18
|
+
notification_recipients: [NotificationRecipient.create({ :user => User.current })]
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def audience
|
23
|
+
Notification::AUDIENCE_USER
|
24
|
+
end
|
25
|
+
|
26
|
+
def message
|
27
|
+
('%{resumed} Tasks were resumed. %{failed} Tasks failed to resume. %{skipped} Tasks were skipped. ' %
|
28
|
+
{ resumed: @resumed_length,
|
29
|
+
failed: @failed_length,
|
30
|
+
skipped: @skipped_length })
|
31
|
+
end
|
32
|
+
|
33
|
+
def blueprint
|
34
|
+
@blueprint ||= NotificationBlueprint.find_by(name: 'tasks_bulk_resume')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -27,6 +27,20 @@ blueprints = [
|
|
27
27
|
title: N_('Task Details')
|
28
28
|
]
|
29
29
|
}
|
30
|
+
},
|
31
|
+
|
32
|
+
{
|
33
|
+
group: N_('Tasks'),
|
34
|
+
name: 'tasks_bulk_resume',
|
35
|
+
level: 'info',
|
36
|
+
message: "DYNAMIC",
|
37
|
+
},
|
38
|
+
|
39
|
+
{
|
40
|
+
group: N_('Tasks'),
|
41
|
+
name: 'tasks_bulk_cancel',
|
42
|
+
level: 'info',
|
43
|
+
message: "DYNAMIC",
|
30
44
|
}
|
31
45
|
]
|
32
46
|
|
data/foreman-tasks.gemspec
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
gem 'sqlite3'
|
data/lib/foreman_tasks/engine.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/bin/bash
|
2
2
|
set -ev
|
3
|
-
if [[ $( git diff --name-only HEAD~1..HEAD webpack/ .travis.yml .
|
3
|
+
if [[ $( git diff --name-only HEAD~1..HEAD webpack/ .travis.yml babel.config.js .eslintrc package.json | wc -l ) -ne 0 ]]; then
|
4
4
|
npm run test;
|
5
|
-
npm run
|
5
|
+
npm run publish-coverage;
|
6
6
|
npm run lint;
|
7
7
|
fi
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'foreman_tasks_test_helper'
|
2
|
+
|
3
|
+
module ForemanTasks
|
4
|
+
module Concerns
|
5
|
+
class PollingActionExtensionsTest < ::ActiveSupport::TestCase
|
6
|
+
class Action < ::Dynflow::Action
|
7
|
+
include ::Dynflow::Action::Polling
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'polling interval tuning' do
|
11
|
+
let(:default_intervals) { [0.5, 1, 2, 4, 8, 16] }
|
12
|
+
|
13
|
+
it 'is extends the polling action module' do
|
14
|
+
_(::Dynflow::Action::Polling.ancestors.first).must_equal ForemanTasks::Concerns::PollingActionExtensions
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'does not modify polling intervals by default' do
|
18
|
+
_(Action.allocate.poll_intervals).must_equal default_intervals
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'cannot make intervals shorter than 0.5 seconds' do
|
22
|
+
Setting.expects(:[]).with(:foreman_tasks_polling_multiplier).returns 0
|
23
|
+
_(Action.allocate.poll_intervals).must_equal(default_intervals.map { 0.5 })
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'can be used to make the intervals longer' do
|
27
|
+
value = 5
|
28
|
+
Setting.expects(:[]).with(:foreman_tasks_polling_multiplier).returns value
|
29
|
+
_(Action.allocate.poll_intervals).must_equal(default_intervals.map { |i| i * value })
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -292,9 +292,7 @@ exports[`TaskInfo rendering render with Props 1`] = `
|
|
292
292
|
</p>
|
293
293
|
<p>
|
294
294
|
A paused task represents a process that has not finished properly. Any task in paused state can lead to potential inconsistency and needs to be resolved.
|
295
|
-
|
296
|
-
The recommended approach is to investigate the error messages below and in 'errors' tab, address the primary cause of the issue and resume the task.
|
297
|
-
<br />
|
295
|
+
The recommended approach is to investigate the error messages below and in 'errors' tab, address the primary cause of the issue and resume the task.
|
298
296
|
</p>
|
299
297
|
</Col>
|
300
298
|
</Row>
|
@@ -18,6 +18,11 @@ export const TASKS_DASHBOARD_AVAILABLE_QUERY_MODES = {
|
|
18
18
|
OLDER: 'older',
|
19
19
|
};
|
20
20
|
|
21
|
+
export const TASKS_DASHBOARD_JS_QUERY_MODES = {
|
22
|
+
RECENT: 'recent',
|
23
|
+
OLDER: 'older',
|
24
|
+
};
|
25
|
+
|
21
26
|
export const TASKS_DASHBOARD_AVAILABLE_TIMES = {
|
22
27
|
H24: 'H24',
|
23
28
|
H12: 'H12',
|
@@ -4,6 +4,7 @@ import {
|
|
4
4
|
TASKS_DASHBOARD_AVAILABLE_TIMES,
|
5
5
|
TASKS_DASHBOARD_QUERY_KEYS_TEXT,
|
6
6
|
TASKS_DASHBOARD_QUERY_VALUES_TEXT,
|
7
|
+
TASKS_DASHBOARD_JS_QUERY_MODES,
|
7
8
|
} from './TasksDashboardConstants';
|
8
9
|
import { updateURlQuery } from '../TasksTable/TasksTableHelpers';
|
9
10
|
|
@@ -39,8 +40,8 @@ const queryFromUriQuery = uriQuery => {
|
|
39
40
|
if (uriQuery[uriField]) query[queryField] = uriQuery[uriField];
|
40
41
|
});
|
41
42
|
|
42
|
-
if (query.mode ===
|
43
|
-
query.mode =
|
43
|
+
if (query.mode === TASKS_DASHBOARD_JS_QUERY_MODES.RECENT) {
|
44
|
+
query.mode = TASKS_DASHBOARD_QUERY_VALUES_TEXT.LAST;
|
44
45
|
}
|
45
46
|
|
46
47
|
return query;
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { Alert, Button } from 'patternfly-react';
|
4
|
+
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
5
|
+
|
6
|
+
export const SelectAllAlert = ({
|
7
|
+
itemCount,
|
8
|
+
perPage,
|
9
|
+
selectAllRows,
|
10
|
+
unselectAllRows,
|
11
|
+
allRowsSelected,
|
12
|
+
}) => {
|
13
|
+
const selectAllText = (
|
14
|
+
<React.Fragment>
|
15
|
+
{sprintf(
|
16
|
+
'All %s tasks on this page are selected',
|
17
|
+
Math.min(itemCount, perPage)
|
18
|
+
)}
|
19
|
+
<Button bsStyle="link" onClick={selectAllRows}>
|
20
|
+
{__('Select All')}
|
21
|
+
<b> {itemCount} </b> {__('tasks.')}
|
22
|
+
</Button>
|
23
|
+
</React.Fragment>
|
24
|
+
);
|
25
|
+
const undoSelectText = (
|
26
|
+
<React.Fragment>
|
27
|
+
{sprintf(__(`All %s tasks are selected. `), itemCount)}
|
28
|
+
<Button bsStyle="link" onClick={unselectAllRows}>
|
29
|
+
{__('Undo selection')}
|
30
|
+
</Button>
|
31
|
+
</React.Fragment>
|
32
|
+
);
|
33
|
+
const selectAlertText = allRowsSelected ? undoSelectText : selectAllText;
|
34
|
+
return <Alert type="info">{selectAlertText}</Alert>;
|
35
|
+
};
|
36
|
+
|
37
|
+
SelectAllAlert.propTypes = {
|
38
|
+
allRowsSelected: PropTypes.bool.isRequired,
|
39
|
+
itemCount: PropTypes.number.isRequired,
|
40
|
+
perPage: PropTypes.number.isRequired,
|
41
|
+
selectAllRows: PropTypes.func.isRequired,
|
42
|
+
unselectAllRows: PropTypes.func.isRequired,
|
43
|
+
};
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
|
2
|
+
|
3
|
+
import { SelectAllAlert } from '../SelectAllAlert';
|
4
|
+
|
5
|
+
const baseProps = {
|
6
|
+
itemCount: 7,
|
7
|
+
perPage: 5,
|
8
|
+
selectAllRows: jest.fn(),
|
9
|
+
unselectAllRows: jest.fn(),
|
10
|
+
};
|
11
|
+
const fixtures = {
|
12
|
+
'renders SelectAllAlert with perPage > itemCout': {
|
13
|
+
allRowsSelected: false,
|
14
|
+
itemCount: 7,
|
15
|
+
perPage: 10,
|
16
|
+
...baseProps,
|
17
|
+
},
|
18
|
+
'renders SelectAllAlert without all rows selected': {
|
19
|
+
allRowsSelected: false,
|
20
|
+
...baseProps,
|
21
|
+
},
|
22
|
+
'renders SelectAllAlert with all rows selected': {
|
23
|
+
allRowsSelected: true,
|
24
|
+
...baseProps,
|
25
|
+
},
|
26
|
+
};
|
27
|
+
|
28
|
+
describe('SelectAllAlert', () =>
|
29
|
+
testComponentSnapshotsWithFixtures(SelectAllAlert, fixtures));
|
@@ -0,0 +1,75 @@
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
2
|
+
|
3
|
+
exports[`SelectAllAlert renders SelectAllAlert with all rows selected 1`] = `
|
4
|
+
<Alert
|
5
|
+
className=""
|
6
|
+
onDismiss={null}
|
7
|
+
type="info"
|
8
|
+
>
|
9
|
+
All 7 tasks are selected.
|
10
|
+
<Button
|
11
|
+
active={false}
|
12
|
+
block={false}
|
13
|
+
bsClass="btn"
|
14
|
+
bsStyle="link"
|
15
|
+
disabled={false}
|
16
|
+
onClick={[MockFunction]}
|
17
|
+
>
|
18
|
+
Undo selection
|
19
|
+
</Button>
|
20
|
+
</Alert>
|
21
|
+
`;
|
22
|
+
|
23
|
+
exports[`SelectAllAlert renders SelectAllAlert with perPage > itemCout 1`] = `
|
24
|
+
<Alert
|
25
|
+
className=""
|
26
|
+
onDismiss={null}
|
27
|
+
type="info"
|
28
|
+
>
|
29
|
+
All 5 tasks on this page are selected
|
30
|
+
<Button
|
31
|
+
active={false}
|
32
|
+
block={false}
|
33
|
+
bsClass="btn"
|
34
|
+
bsStyle="link"
|
35
|
+
disabled={false}
|
36
|
+
onClick={[MockFunction]}
|
37
|
+
>
|
38
|
+
Select All
|
39
|
+
<b>
|
40
|
+
|
41
|
+
7
|
42
|
+
|
43
|
+
</b>
|
44
|
+
|
45
|
+
tasks.
|
46
|
+
</Button>
|
47
|
+
</Alert>
|
48
|
+
`;
|
49
|
+
|
50
|
+
exports[`SelectAllAlert renders SelectAllAlert without all rows selected 1`] = `
|
51
|
+
<Alert
|
52
|
+
className=""
|
53
|
+
onDismiss={null}
|
54
|
+
type="info"
|
55
|
+
>
|
56
|
+
All 5 tasks on this page are selected
|
57
|
+
<Button
|
58
|
+
active={false}
|
59
|
+
block={false}
|
60
|
+
bsClass="btn"
|
61
|
+
bsStyle="link"
|
62
|
+
disabled={false}
|
63
|
+
onClick={[MockFunction]}
|
64
|
+
>
|
65
|
+
Select All
|
66
|
+
<b>
|
67
|
+
|
68
|
+
7
|
69
|
+
|
70
|
+
</b>
|
71
|
+
|
72
|
+
tasks.
|
73
|
+
</Button>
|
74
|
+
</Alert>
|
75
|
+
`;
|
@@ -15,7 +15,8 @@ export const SubTasksPage = props => {
|
|
15
15
|
{ caption: __('Sub tasks') },
|
16
16
|
],
|
17
17
|
});
|
18
|
-
const createHeader = actionName =>
|
18
|
+
const createHeader = actionName =>
|
19
|
+
actionName ? sprintf(__('Sub tasks of %s'), actionName) : __('Sub tasks');
|
19
20
|
return (
|
20
21
|
<TasksTablePage
|
21
22
|
getBreadcrumbs={getBreadcrumbs}
|