foreman_remote_execution 3.3.6 → 4.2.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/ci.yml +1 -1
- data/app/controllers/api/v2/job_invocations_controller.rb +20 -2
- data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_controller_extensions.rb +26 -0
- data/app/controllers/foreman_remote_execution/concerns/api/v2/subnets_controller_extensions.rb +21 -0
- data/app/controllers/job_invocations_controller.rb +22 -8
- data/app/controllers/job_templates_controller.rb +1 -1
- data/app/helpers/job_invocations_helper.rb +3 -2
- data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
- data/app/lib/actions/remote_execution/run_hosts_job.rb +12 -3
- data/app/lib/foreman_remote_execution/renderer/scope/input.rb +35 -0
- data/app/models/concerns/api/v2/interfaces_controller_extensions.rb +13 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +38 -14
- data/app/models/job_invocation.rb +12 -4
- data/app/models/job_invocation_composer.rb +3 -3
- data/app/models/remote_execution_feature.rb +5 -2
- data/app/models/remote_execution_provider.rb +8 -3
- data/app/models/setting/remote_execution.rb +2 -2
- data/app/models/ssh_execution_provider.rb +1 -1
- data/app/services/remote_execution_proxy_selector.rb +3 -0
- data/app/views/api/v2/interfaces/execution_flag.json.rabl +1 -0
- data/app/views/api/v2/job_invocations/base.json.rabl +1 -0
- data/app/views/api/v2/job_invocations/main.json.rabl +1 -1
- data/app/views/api/v2/registration/_form.html.erb +12 -0
- data/app/views/api/v2/subnets/remote_execution_proxies.json.rabl +3 -0
- data/app/views/job_invocations/_form.html.erb +1 -1
- data/app/views/job_invocations/_tab_hosts.html.erb +1 -20
- data/app/views/job_invocations/_tab_overview.html.erb +13 -1
- data/app/views/job_invocations/show.html.erb +3 -0
- data/app/views/job_invocations/show.json.erb +2 -1
- data/app/views/template_invocations/_output_line_set.html.erb +1 -1
- data/app/views/templates/ssh/package_action.erb +1 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20200623073022_rename_sudo_password_to_effective_user_password.rb +34 -0
- data/db/migrate/20200820122057_add_proxy_selector_override_to_remote_execution_feature.rb +5 -0
- data/db/seeds.d/20-permissions.rb +9 -0
- data/lib/foreman_remote_execution/engine.rb +28 -2
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +6 -6
- data/test/functional/api/v2/job_invocations_controller_test.rb +83 -1
- data/test/functional/api/v2/registration_controller_test.rb +82 -0
- data/test/functional/job_invocations_controller_test.rb +71 -0
- data/test/support/remote_execution_helper.rb +5 -0
- data/test/unit/actions/run_host_job_test.rb +3 -3
- data/test/unit/actions/run_hosts_job_test.rb +3 -2
- data/test/unit/job_invocation_composer_test.rb +5 -5
- data/test/unit/remote_execution_provider_test.rb +6 -6
- data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +2 -0
- data/webpack/__mocks__/foremanReact/components/SearchBar.js +2 -0
- data/webpack/__mocks__/foremanReact/constants.js +21 -0
- data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +2 -0
- data/webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware/IntervalSelectors.js +1 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +25 -15
- data/webpack/react_app/components/TargetingHosts/TargetingHostsHelpers.js +10 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +66 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +6 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +10 -2
- data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsPage.test.js +9 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +26 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap +17 -2
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +68 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +11 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +35 -19
- data/webpack/react_app/components/TargetingHosts/index.js +73 -13
- metadata +26 -3
- data/webpack/react_app/components/TargetingHosts/TargetingHostsActions.js +0 -8
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'test_plugin_helper'
|
2
|
+
|
3
|
+
module Api
|
4
|
+
module V2
|
5
|
+
# Tests for the extra methods to play roles on a Host
|
6
|
+
class RegistrationControllerTest < ActionController::TestCase
|
7
|
+
describe 'host registration' do
|
8
|
+
let(:organization) { FactoryBot.create(:organization) }
|
9
|
+
let(:tax_location) { FactoryBot.create(:location) }
|
10
|
+
let(:template_kind) { template_kinds(:registration) }
|
11
|
+
let(:registration_template) do
|
12
|
+
FactoryBot.create(
|
13
|
+
:provisioning_template,
|
14
|
+
template_kind: template_kind,
|
15
|
+
template: 'template content <%= @host.name %>',
|
16
|
+
locations: [tax_location],
|
17
|
+
organizations: [organization]
|
18
|
+
)
|
19
|
+
end
|
20
|
+
let(:os) do
|
21
|
+
FactoryBot.create(
|
22
|
+
:operatingsystem,
|
23
|
+
:with_associations,
|
24
|
+
family: 'Redhat',
|
25
|
+
provisioning_templates: [
|
26
|
+
registration_template,
|
27
|
+
]
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
let(:host_params) do
|
32
|
+
{ host: { name: 'centos-test.example.com',
|
33
|
+
managed: false, build: false,
|
34
|
+
organization_id: organization.id,
|
35
|
+
location_id: tax_location.id,
|
36
|
+
operatingsystem_id: os.id } }
|
37
|
+
end
|
38
|
+
|
39
|
+
setup do
|
40
|
+
FactoryBot.create(
|
41
|
+
:os_default_template,
|
42
|
+
template_kind: template_kind,
|
43
|
+
provisioning_template: registration_template,
|
44
|
+
operatingsystem: os
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'remote_execution_interface' do
|
49
|
+
setup do
|
50
|
+
@host = Host.create(host_params[:host])
|
51
|
+
@interface0 = FactoryBot.create(:nic_managed, host: @host, identifier: 'dummy0', execution: false)
|
52
|
+
end
|
53
|
+
|
54
|
+
test 'with existing interface' do
|
55
|
+
params = host_params.merge(remote_execution_interface: @interface0.identifier)
|
56
|
+
|
57
|
+
post :host, params: params, session: set_session_user
|
58
|
+
assert_response :success
|
59
|
+
assert @interface0.reload.execution
|
60
|
+
end
|
61
|
+
|
62
|
+
test 'with not-existing interface' do
|
63
|
+
params = host_params.merge(remote_execution_interface: 'dummy999')
|
64
|
+
|
65
|
+
post :host, params: params, session: set_session_user
|
66
|
+
assert_response :not_found
|
67
|
+
end
|
68
|
+
|
69
|
+
test 'with multiple interfaces' do
|
70
|
+
interface1 = FactoryBot.create(:nic_managed, host: @host, identifier: 'dummy1', execution: false)
|
71
|
+
params = host_params.merge(remote_execution_interface: interface1.identifier)
|
72
|
+
|
73
|
+
post :host, params: params, session: set_session_user
|
74
|
+
assert_response :success
|
75
|
+
refute @interface0.reload.execution
|
76
|
+
assert interface1.reload.execution
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'test_plugin_helper'
|
4
|
+
require_relative '../support/remote_execution_helper'
|
4
5
|
|
5
6
|
class JobInvocationsControllerTest < ActionController::TestCase
|
6
7
|
test 'should parse inputs coming from the URL params' do
|
@@ -58,4 +59,74 @@ class JobInvocationsControllerTest < ActionController::TestCase
|
|
58
59
|
post :new, params: params, session: set_session_user
|
59
60
|
assert_response :success
|
60
61
|
end
|
62
|
+
|
63
|
+
context 'restricted access' do
|
64
|
+
setup do
|
65
|
+
@admin = users(:admin)
|
66
|
+
@user = FactoryBot.create(:user, mail: 'test23@test.foreman.com', admin: false)
|
67
|
+
@invocation = FactoryBot.create(:job_invocation, :with_template, :with_task)
|
68
|
+
@invocation2 = FactoryBot.create(:job_invocation, :with_template, :with_task)
|
69
|
+
|
70
|
+
@invocation.task.update(user: @admin)
|
71
|
+
@invocation2.task.update(user: @user)
|
72
|
+
|
73
|
+
setup_user 'view', 'hosts', nil, @user
|
74
|
+
setup_user 'view', 'job_invocations', 'user = current_user', @user
|
75
|
+
setup_user 'create', 'job_invocations', 'user = current_user', @user
|
76
|
+
setup_user 'cancel', 'job_invocations', 'user = current_user', @user
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'without user filter' do
|
80
|
+
test '#index' do
|
81
|
+
get :index, session: prepare_user(@admin)
|
82
|
+
assert_response :success
|
83
|
+
assert 2, assigns(:job_invocations).size
|
84
|
+
end
|
85
|
+
|
86
|
+
test '#show' do
|
87
|
+
get :show, params: { id: @invocation2.id }, session: prepare_user(@admin)
|
88
|
+
assert_response :success
|
89
|
+
end
|
90
|
+
|
91
|
+
test '#rerun' do
|
92
|
+
get :rerun, params: { id: @invocation2.id }, session: prepare_user(@admin)
|
93
|
+
assert_response :success
|
94
|
+
end
|
95
|
+
|
96
|
+
test '#cancel' do
|
97
|
+
ForemanTasks::Task.any_instance.expects(:cancel).returns(true)
|
98
|
+
post :cancel, params: { id: @invocation2.id }, session: prepare_user(@admin)
|
99
|
+
assert_response :redirect
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'with user filter' do
|
104
|
+
test '#index' do
|
105
|
+
get :index, session: prepare_user(@user)
|
106
|
+
assert_response :success
|
107
|
+
assert_equal 1, assigns(:job_invocations).size
|
108
|
+
assert_equal @invocation2, assigns(:job_invocations)[0]
|
109
|
+
end
|
110
|
+
|
111
|
+
test '#show' do
|
112
|
+
get :show, params: { id: @invocation.id }, session: prepare_user(@user)
|
113
|
+
assert_response :not_found
|
114
|
+
end
|
115
|
+
|
116
|
+
test '#rerun' do
|
117
|
+
get :rerun, params: { id: @invocation.id }, session: prepare_user(@user)
|
118
|
+
assert_response :not_found
|
119
|
+
end
|
120
|
+
|
121
|
+
test 'cancel' do
|
122
|
+
post :cancel, params: { id: @invocation.id }, session: prepare_user(@user)
|
123
|
+
assert_response :not_found
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def prepare_user(user)
|
129
|
+
User.current = user
|
130
|
+
set_session_user(user)
|
131
|
+
end
|
61
132
|
end
|
@@ -12,7 +12,7 @@ module ForemanRemoteExecution
|
|
12
12
|
let(:provider) do
|
13
13
|
provider = ::SSHExecutionProvider
|
14
14
|
provider.expects(:ssh_password).with(host).returns('sshpass')
|
15
|
-
provider.expects(:
|
15
|
+
provider.expects(:effective_user_password).with(host).returns('sudopass')
|
16
16
|
provider.expects(:ssh_key_passphrase).with(host).returns('keypass')
|
17
17
|
provider
|
18
18
|
end
|
@@ -21,7 +21,7 @@ module ForemanRemoteExecution
|
|
21
21
|
secrets = subject.secrets(host, job_invocation, provider)
|
22
22
|
|
23
23
|
assert_equal 'sshpass', secrets[:ssh_password]
|
24
|
-
assert_equal 'sudopass', secrets[:
|
24
|
+
assert_equal 'sudopass', secrets[:effective_user_password]
|
25
25
|
assert_equal 'keypass', secrets[:key_passphrase]
|
26
26
|
end
|
27
27
|
|
@@ -31,7 +31,7 @@ module ForemanRemoteExecution
|
|
31
31
|
secrets = subject.secrets(host, job_invocation, provider)
|
32
32
|
|
33
33
|
assert_equal 'jobsshpass', secrets[:ssh_password]
|
34
|
-
assert_equal 'sudopass', secrets[:
|
34
|
+
assert_equal 'sudopass', secrets[:effective_user_password]
|
35
35
|
assert_equal 'jobkeypass', secrets[:key_passphrase]
|
36
36
|
end
|
37
37
|
end
|
@@ -14,7 +14,7 @@ module ForemanRemoteExecution
|
|
14
14
|
invocation.description = 'Some short description'
|
15
15
|
invocation.password = 'changeme'
|
16
16
|
invocation.key_passphrase = 'changemetoo'
|
17
|
-
invocation.
|
17
|
+
invocation.effective_user_password = 'sudopassword'
|
18
18
|
invocation.save
|
19
19
|
end
|
20
20
|
end
|
@@ -24,6 +24,7 @@ module ForemanRemoteExecution
|
|
24
24
|
OpenStruct.new(:id => uuid).tap do |o|
|
25
25
|
o.stubs(:add_missing_task_groups)
|
26
26
|
o.stubs(:task_groups).returns([])
|
27
|
+
o.stubs(:pending?).returns(true)
|
27
28
|
end
|
28
29
|
end
|
29
30
|
let(:action) do
|
@@ -73,7 +74,7 @@ module ForemanRemoteExecution
|
|
73
74
|
end
|
74
75
|
|
75
76
|
it 'triggers the RunHostJob actions on the resolved hosts in run phase' do
|
76
|
-
planned.expects(:output).returns(:planned_count => 0)
|
77
|
+
planned.expects(:output).at_most(5).returns(:planned_count => 0)
|
77
78
|
planned.expects(:trigger).with { |*args| args[0] == Actions::RemoteExecution::RunHostJob }
|
78
79
|
planned.create_sub_plans
|
79
80
|
end
|
@@ -523,15 +523,15 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
|
|
523
523
|
end
|
524
524
|
end
|
525
525
|
|
526
|
-
describe '#
|
527
|
-
let(:
|
526
|
+
describe '#effective_user_password' do
|
527
|
+
let(:effective_user_password) { 'password' }
|
528
528
|
let(:params) do
|
529
|
-
{ :job_invocation => { :
|
529
|
+
{ :job_invocation => { :effective_user_password => effective_user_password }}
|
530
530
|
end
|
531
531
|
|
532
|
-
it 'sets the
|
532
|
+
it 'sets the effective_user_password password properly' do
|
533
533
|
composer
|
534
|
-
_(composer.job_invocation.
|
534
|
+
_(composer.job_invocation.effective_user_password).must_equal effective_user_password
|
535
535
|
end
|
536
536
|
end
|
537
537
|
|
@@ -78,12 +78,12 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
|
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
|
-
describe '
|
82
|
-
it 'uses the
|
83
|
-
host.params['
|
84
|
-
host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => '
|
85
|
-
assert_not proxy_options.key?(:
|
86
|
-
_(secrets[:
|
81
|
+
describe 'effective user password' do
|
82
|
+
it 'uses the remote_execution_effective_user_password on the host param' do
|
83
|
+
host.params['remote_execution_effective_user_password'] = 'mypassword'
|
84
|
+
host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_effective_user_password', :value => 'mypassword')
|
85
|
+
assert_not proxy_options.key?(:effective_user_password)
|
86
|
+
_(secrets[:effective_user_password]).must_equal 'mypassword'
|
87
87
|
end
|
88
88
|
end
|
89
89
|
|
@@ -1,3 +1,24 @@
|
|
1
1
|
export const STATUS = {
|
2
|
+
PENDING: 'PENDING',
|
3
|
+
RESOLVED: 'RESOLVED',
|
2
4
|
ERROR: 'ERROR',
|
3
5
|
};
|
6
|
+
|
7
|
+
export const getControllerSearchProps = (
|
8
|
+
controller,
|
9
|
+
id = 'searchBar',
|
10
|
+
canCreate = true
|
11
|
+
) => ({
|
12
|
+
controller,
|
13
|
+
autocomplete: {
|
14
|
+
id,
|
15
|
+
searchQuery: '',
|
16
|
+
url: `${controller}/auto_complete_search`,
|
17
|
+
useKeyShortcuts: true,
|
18
|
+
},
|
19
|
+
bookmarks: {
|
20
|
+
url: '/api/bookmarks',
|
21
|
+
canCreate,
|
22
|
+
documentationUrl: `4.1.5Searching`,
|
23
|
+
},
|
24
|
+
});
|
@@ -0,0 +1 @@
|
|
1
|
+
export const selectDoesIntervalExist = () => false;
|
@@ -5,8 +5,8 @@ import { LoadingState, Alert } from 'patternfly-react';
|
|
5
5
|
import { STATUS } from 'foremanReact/constants';
|
6
6
|
import HostItem from './components/HostItem';
|
7
7
|
|
8
|
-
const TargetingHosts = ({
|
9
|
-
if (
|
8
|
+
const TargetingHosts = ({ apiStatus, items }) => {
|
9
|
+
if (apiStatus === STATUS.ERROR) {
|
10
10
|
return (
|
11
11
|
<Alert type="error">
|
12
12
|
{__(
|
@@ -16,8 +16,24 @@ const TargetingHosts = ({ status, items }) => {
|
|
16
16
|
);
|
17
17
|
}
|
18
18
|
|
19
|
+
const tableBodyRows = items.length ? (
|
20
|
+
items.map(({ name, link, status, actions }) => (
|
21
|
+
<HostItem
|
22
|
+
key={name}
|
23
|
+
name={name}
|
24
|
+
link={link}
|
25
|
+
status={status}
|
26
|
+
actions={actions}
|
27
|
+
/>
|
28
|
+
))
|
29
|
+
) : (
|
30
|
+
<tr>
|
31
|
+
<td colSpan="3">{__('No hosts found.')}</td>
|
32
|
+
</tr>
|
33
|
+
);
|
34
|
+
|
19
35
|
return (
|
20
|
-
<LoadingState loading={!items.length &&
|
36
|
+
<LoadingState loading={!items.length && apiStatus === STATUS.PENDING}>
|
21
37
|
<div>
|
22
38
|
<table className="table table-bordered table-striped table-hover">
|
23
39
|
<thead>
|
@@ -27,17 +43,7 @@ const TargetingHosts = ({ status, items }) => {
|
|
27
43
|
<th>{__('Actions')}</th>
|
28
44
|
</tr>
|
29
45
|
</thead>
|
30
|
-
<tbody>
|
31
|
-
{items.map(host => (
|
32
|
-
<HostItem
|
33
|
-
key={host.name}
|
34
|
-
name={host.name}
|
35
|
-
link={host.link}
|
36
|
-
status={host.status}
|
37
|
-
actions={host.actions}
|
38
|
-
/>
|
39
|
-
))}
|
40
|
-
</tbody>
|
46
|
+
<tbody>{tableBodyRows}</tbody>
|
41
47
|
</table>
|
42
48
|
</div>
|
43
49
|
</LoadingState>
|
@@ -45,8 +51,12 @@ const TargetingHosts = ({ status, items }) => {
|
|
45
51
|
};
|
46
52
|
|
47
53
|
TargetingHosts.propTypes = {
|
48
|
-
|
54
|
+
apiStatus: PropTypes.string,
|
49
55
|
items: PropTypes.array.isRequired,
|
50
56
|
};
|
51
57
|
|
58
|
+
TargetingHosts.defaultProps = {
|
59
|
+
apiStatus: null,
|
60
|
+
};
|
61
|
+
|
52
62
|
export default TargetingHosts;
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { getURI } from 'foremanReact/common/urlHelpers';
|
2
|
+
|
3
|
+
export const getApiUrl = (searchQuery, pagination) => {
|
4
|
+
const baseUrl = getURI()
|
5
|
+
.search('')
|
6
|
+
.addQuery('page', pagination.page)
|
7
|
+
.addQuery('per_page', pagination.perPage);
|
8
|
+
|
9
|
+
return searchQuery ? baseUrl.addQuery('search', searchQuery) : baseUrl;
|
10
|
+
};
|
@@ -0,0 +1,66 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { Grid } from 'patternfly-react';
|
4
|
+
|
5
|
+
import SearchBar from 'foremanReact/components/SearchBar';
|
6
|
+
import Pagination from 'foremanReact/components/Pagination/PaginationWrapper';
|
7
|
+
import { getControllerSearchProps } from 'foremanReact/constants';
|
8
|
+
|
9
|
+
import TargetingHosts from './TargetingHosts';
|
10
|
+
import './TargetingHostsPage.scss';
|
11
|
+
|
12
|
+
const TargetingHostsPage = ({
|
13
|
+
handleSearch,
|
14
|
+
searchQuery,
|
15
|
+
apiStatus,
|
16
|
+
items,
|
17
|
+
totalHosts,
|
18
|
+
pagination,
|
19
|
+
handlePagination,
|
20
|
+
}) => (
|
21
|
+
<div id="targeting_hosts">
|
22
|
+
<Grid.Row>
|
23
|
+
<Grid.Col md={6} className="title_filter">
|
24
|
+
<SearchBar
|
25
|
+
onSearch={query => handleSearch(query)}
|
26
|
+
data={{
|
27
|
+
...getControllerSearchProps('hosts'),
|
28
|
+
autocomplete: {
|
29
|
+
id: 'targeting_hosts_search',
|
30
|
+
searchQuery,
|
31
|
+
url: '/hosts/auto_complete_search',
|
32
|
+
useKeyShortcuts: true,
|
33
|
+
},
|
34
|
+
bookmarks: {},
|
35
|
+
}}
|
36
|
+
/>
|
37
|
+
</Grid.Col>
|
38
|
+
</Grid.Row>
|
39
|
+
<br />
|
40
|
+
<TargetingHosts apiStatus={apiStatus} items={items} />
|
41
|
+
<Pagination
|
42
|
+
viewType="list"
|
43
|
+
itemCount={totalHosts}
|
44
|
+
pagination={pagination}
|
45
|
+
onChange={args => handlePagination(args)}
|
46
|
+
dropdownButtonId="targeting-hosts-pagination-dropdown"
|
47
|
+
className="targeting-hosts-pagination"
|
48
|
+
/>
|
49
|
+
</div>
|
50
|
+
);
|
51
|
+
|
52
|
+
TargetingHostsPage.propTypes = {
|
53
|
+
handleSearch: PropTypes.func.isRequired,
|
54
|
+
searchQuery: PropTypes.string.isRequired,
|
55
|
+
apiStatus: PropTypes.string,
|
56
|
+
items: PropTypes.array.isRequired,
|
57
|
+
totalHosts: PropTypes.number.isRequired,
|
58
|
+
pagination: PropTypes.object.isRequired,
|
59
|
+
handlePagination: PropTypes.func.isRequired,
|
60
|
+
};
|
61
|
+
|
62
|
+
TargetingHostsPage.defaultProps = {
|
63
|
+
apiStatus: null,
|
64
|
+
};
|
65
|
+
|
66
|
+
export default TargetingHostsPage;
|