foreman_remote_execution 3.3.7 → 4.0.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/ci.yml +1 -1
- data/app/controllers/job_invocations_controller.rb +22 -8
- data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
- data/app/lib/actions/remote_execution/run_hosts_job.rb +1 -1
- data/app/lib/foreman_remote_execution/renderer/scope/input.rb +35 -0
- data/app/models/job_invocation.rb +6 -3
- data/app/models/job_invocation_composer.rb +2 -2
- data/app/models/remote_execution_provider.rb +2 -2
- data/app/models/setting/remote_execution.rb +2 -2
- data/app/models/ssh_execution_provider.rb +1 -1
- 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/show.html.erb +3 -0
- data/app/views/job_invocations/show.json.erb +2 -1
- data/db/migrate/20200623073022_rename_sudo_password_to_effective_user_password.rb +34 -0
- data/db/seeds.d/20-permissions.rb +9 -0
- data/lib/foreman_remote_execution/engine.rb +4 -10
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/functional/api/v2/job_invocations_controller_test.rb +64 -1
- 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 +1 -1
- 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 +21 -15
- data/webpack/react_app/components/TargetingHosts/TargetingHostsHelpers.js +10 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +62 -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 +17 -3
- data/webpack/react_app/components/TargetingHosts/TargetingHostsActions.js +0 -8
| @@ -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
         | 
| @@ -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,7 +51,7 @@ const TargetingHosts = ({ status, items }) => { | |
| 45 51 | 
             
            };
         | 
| 46 52 |  | 
| 47 53 | 
             
            TargetingHosts.propTypes = {
         | 
| 48 | 
            -
               | 
| 54 | 
            +
              apiStatus: PropTypes.string.isRequired,
         | 
| 49 55 | 
             
              items: PropTypes.array.isRequired,
         | 
| 50 56 | 
             
            };
         | 
| 51 57 |  | 
| @@ -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,62 @@ | |
| 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.isRequired,
         | 
| 56 | 
            +
              items: PropTypes.array.isRequired,
         | 
| 57 | 
            +
              totalHosts: PropTypes.number.isRequired,
         | 
| 58 | 
            +
              pagination: PropTypes.object.isRequired,
         | 
| 59 | 
            +
              handlePagination: PropTypes.func.isRequired,
         | 
| 60 | 
            +
            };
         | 
| 61 | 
            +
             | 
| 62 | 
            +
            export default TargetingHostsPage;
         | 
| @@ -2,11 +2,19 @@ import { | |
| 2 2 | 
             
              selectAPIStatus,
         | 
| 3 3 | 
             
              selectAPIResponse,
         | 
| 4 4 | 
             
            } from 'foremanReact/redux/API/APISelectors';
         | 
| 5 | 
            +
            import { selectDoesIntervalExist } from 'foremanReact/redux/middlewares/IntervalMiddleware/IntervalSelectors';
         | 
| 6 | 
            +
             | 
| 5 7 | 
             
            import { TARGETING_HOSTS } from './TargetingHostsConsts';
         | 
| 6 8 |  | 
| 7 9 | 
             
            export const selectItems = state =>
         | 
| 8 10 | 
             
              selectAPIResponse(state, TARGETING_HOSTS).hosts || [];
         | 
| 9 11 |  | 
| 10 12 | 
             
            export const selectAutoRefresh = state =>
         | 
| 11 | 
            -
              selectAPIResponse(state, TARGETING_HOSTS).autoRefresh;
         | 
| 12 | 
            -
             | 
| 13 | 
            +
              selectAPIResponse(state, TARGETING_HOSTS).autoRefresh || '';
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            export const selectApiStatus = state => selectAPIStatus(state, TARGETING_HOSTS);
         | 
| 16 | 
            +
            export const selectTotalHosts = state =>
         | 
| 17 | 
            +
              selectAPIResponse(state, TARGETING_HOSTS).total_hosts || 0;
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            export const selectIntervalExists = state =>
         | 
| 20 | 
            +
              selectDoesIntervalExist(state, TARGETING_HOSTS);
         | 
| @@ -0,0 +1,9 @@ | |
| 1 | 
            +
            import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
         | 
| 2 | 
            +
            import TargetingHostsPage from '../TargetingHostsPage';
         | 
| 3 | 
            +
            import { TargetingHostsPageFixtures } from './fixtures';
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            describe('TargetingHostsPage', () =>
         | 
| 6 | 
            +
              testComponentSnapshotsWithFixtures(
         | 
| 7 | 
            +
                TargetingHostsPage,
         | 
| 8 | 
            +
                TargetingHostsPageFixtures
         | 
| 9 | 
            +
              ));
         | 
| @@ -0,0 +1,26 @@ | |
| 1 | 
            +
            import { testSelectorsSnapshotWithFixtures } from '@theforeman/test';
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            import {
         | 
| 4 | 
            +
              selectItems,
         | 
| 5 | 
            +
              selectAutoRefresh,
         | 
| 6 | 
            +
              selectApiStatus,
         | 
| 7 | 
            +
              selectTotalHosts,
         | 
| 8 | 
            +
              selectIntervalExists,
         | 
| 9 | 
            +
            } from '../TargetingHostsSelectors';
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            const state = {
         | 
| 12 | 
            +
              hosts: [],
         | 
| 13 | 
            +
              autoRefresh: 'true',
         | 
| 14 | 
            +
              total_hosts: 0,
         | 
| 15 | 
            +
            };
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            const fixtures = {
         | 
| 18 | 
            +
              'should return hosts': () => selectItems(state),
         | 
| 19 | 
            +
              'should return autoRefresh': () => selectAutoRefresh(state),
         | 
| 20 | 
            +
              'should return apiStatus': () => selectApiStatus(state),
         | 
| 21 | 
            +
              'should return totalHosts': () => selectTotalHosts(state),
         | 
| 22 | 
            +
              'should return intervalExists': () => selectIntervalExists(state),
         | 
| 23 | 
            +
            };
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            describe('TargetingHostsSelectors', () =>
         | 
| 26 | 
            +
              testSelectorsSnapshotWithFixtures(fixtures));
         | 
    
        data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap
    CHANGED
    
    | @@ -33,6 +33,13 @@ exports[`TargetingHosts renders 1`] = ` | |
| 33 33 | 
             
                      name="host"
         | 
| 34 34 | 
             
                      status="success"
         | 
| 35 35 | 
             
                    />
         | 
| 36 | 
            +
                    <HostItem
         | 
| 37 | 
            +
                      actions={Array []}
         | 
| 38 | 
            +
                      key="host2"
         | 
| 39 | 
            +
                      link="/link2"
         | 
| 40 | 
            +
                      name="host2"
         | 
| 41 | 
            +
                      status="success"
         | 
| 42 | 
            +
                    />
         | 
| 36 43 | 
             
                  </tbody>
         | 
| 37 44 | 
             
                </table>
         | 
| 38 45 | 
             
              </div>
         | 
| @@ -52,7 +59,7 @@ exports[`TargetingHosts renders with error 1`] = ` | |
| 52 59 | 
             
            exports[`TargetingHosts renders with loading 1`] = `
         | 
| 53 60 | 
             
            <LoadingState
         | 
| 54 61 | 
             
              additionalClasses=""
         | 
| 55 | 
            -
              loading={ | 
| 62 | 
            +
              loading={true}
         | 
| 56 63 | 
             
              loadingText="Loading"
         | 
| 57 64 | 
             
              size="lg"
         | 
| 58 65 | 
             
              timeout={300}
         | 
| @@ -74,7 +81,15 @@ exports[`TargetingHosts renders with loading 1`] = ` | |
| 74 81 | 
             
                      </th>
         | 
| 75 82 | 
             
                    </tr>
         | 
| 76 83 | 
             
                  </thead>
         | 
| 77 | 
            -
                  <tbody | 
| 84 | 
            +
                  <tbody>
         | 
| 85 | 
            +
                    <tr>
         | 
| 86 | 
            +
                      <td
         | 
| 87 | 
            +
                        colSpan="3"
         | 
| 88 | 
            +
                      >
         | 
| 89 | 
            +
                        No hosts found.
         | 
| 90 | 
            +
                      </td>
         | 
| 91 | 
            +
                    </tr>
         | 
| 92 | 
            +
                  </tbody>
         | 
| 78 93 | 
             
                </table>
         | 
| 79 94 | 
             
              </div>
         | 
| 80 95 | 
             
            </LoadingState>
         |