mirror of
https://github.com/ansible/awx.git
synced 2024-10-31 23:51:09 +03:00
Add Inventory Host list and unit tests
* Add Inventory Host Add route * Fix host disabled loading switch bug
This commit is contained in:
parent
ea4e98c52a
commit
fa144aa98f
@ -11,6 +11,10 @@ class Inventories extends Base {
|
||||
readAccessList(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/access_list/`, { params });
|
||||
}
|
||||
|
||||
readHosts(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/hosts/`, { params });
|
||||
}
|
||||
}
|
||||
|
||||
export default Inventories;
|
||||
|
@ -35,7 +35,7 @@ class HostsList extends Component {
|
||||
itemCount: 0,
|
||||
actions: null,
|
||||
toggleError: false,
|
||||
toggleLoading: false,
|
||||
toggleLoading: null,
|
||||
};
|
||||
|
||||
this.handleSelectAll = this.handleSelectAll.bind(this);
|
||||
@ -101,7 +101,7 @@ class HostsList extends Component {
|
||||
|
||||
async handleHostToggle(hostToToggle) {
|
||||
const { hosts } = this.state;
|
||||
this.setState({ toggleLoading: true });
|
||||
this.setState({ toggleLoading: hostToToggle.id });
|
||||
try {
|
||||
const { data: updatedHost } = await HostsAPI.update(hostToToggle.id, {
|
||||
enabled: !hostToToggle.enabled,
|
||||
@ -114,7 +114,7 @@ class HostsList extends Component {
|
||||
} catch (err) {
|
||||
this.setState({ toggleError: true });
|
||||
} finally {
|
||||
this.setState({ toggleLoading: false });
|
||||
this.setState({ toggleLoading: null });
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,7 +237,7 @@ class HostsList extends Component {
|
||||
isSelected={selected.some(row => row.id === o.id)}
|
||||
onSelect={() => this.handleSelect(o)}
|
||||
toggleHost={this.handleHostToggle}
|
||||
toggleLoading={toggleLoading}
|
||||
toggleLoading={toggleLoading === o.id}
|
||||
/>
|
||||
)}
|
||||
emptyStateControls={
|
||||
|
@ -0,0 +1 @@
|
||||
export { default } from './HostList';
|
@ -6,8 +6,8 @@ import { t } from '@lingui/macro';
|
||||
import { Config } from '@contexts/Config';
|
||||
import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs';
|
||||
|
||||
import HostsList from './HostList/HostList';
|
||||
import HostAdd from './HostAdd/HostAdd';
|
||||
import HostList from './HostList';
|
||||
import HostAdd from './HostAdd';
|
||||
import Host from './Host';
|
||||
|
||||
class Hosts extends Component {
|
||||
@ -69,7 +69,7 @@ class Hosts extends Component {
|
||||
</Config>
|
||||
)}
|
||||
/>
|
||||
<Route path={`${match.path}`} render={() => <HostsList />} />
|
||||
<Route path={`${match.path}`} render={() => <HostList />} />
|
||||
</Switch>
|
||||
</Fragment>
|
||||
);
|
||||
|
@ -52,6 +52,9 @@ class Inventories extends Component {
|
||||
t`Completed Jobs`
|
||||
),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts`]: i18n._(t`Hosts`),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts/add`]: i18n._(
|
||||
t`Create New Host`
|
||||
),
|
||||
[`/inventories/inventory/${inventory.id}/sources`]: i18n._(t`Sources`),
|
||||
[`/inventories/inventory/${inventory.id}/groups`]: i18n._(t`Groups`),
|
||||
};
|
||||
|
@ -9,6 +9,7 @@ import RoutedTabs from '@components/RoutedTabs';
|
||||
import { ResourceAccessList } from '@components/ResourceAccessList';
|
||||
import InventoryDetail from './InventoryDetail';
|
||||
import InventoryHosts from './InventoryHosts';
|
||||
import InventoryHostAdd from './InventoryHostAdd';
|
||||
import InventoryGroups from './InventoryGroups';
|
||||
import InventoryCompletedJobs from './InventoryCompletedJobs';
|
||||
import InventorySources from './InventorySources';
|
||||
@ -84,7 +85,10 @@ class Inventory extends Component {
|
||||
</CardHeader>
|
||||
);
|
||||
|
||||
if (location.pathname.endsWith('edit')) {
|
||||
if (
|
||||
location.pathname.endsWith('edit') ||
|
||||
location.pathname.endsWith('add')
|
||||
) {
|
||||
cardHeader = null;
|
||||
}
|
||||
|
||||
@ -134,6 +138,11 @@ class Inventory extends Component {
|
||||
path="/inventories/inventory/:id/edit"
|
||||
render={() => <InventoryEdit inventory={inventory} />}
|
||||
/>,
|
||||
<Route
|
||||
key="host-add"
|
||||
path="/inventories/inventory/:id/hosts/add"
|
||||
render={() => <InventoryHostAdd />}
|
||||
/>,
|
||||
<Route
|
||||
key="access"
|
||||
path="/inventories/inventory/:id/access"
|
||||
|
@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
|
||||
function InventoryHostAdd() {
|
||||
return <CardBody>Coming soon :)</CardBody>;
|
||||
}
|
||||
|
||||
export default InventoryHostAdd;
|
@ -0,0 +1 @@
|
||||
export { default } from './InventoryHostAdd';
|
@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import { string, bool, func } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
DataListItem,
|
||||
DataListItemRow,
|
||||
DataListItemCells,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { PencilAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
import ActionButtonCell from '@components/ActionButtonCell';
|
||||
import DataListCell from '@components/DataListCell';
|
||||
import DataListCheck from '@components/DataListCheck';
|
||||
import ListActionButton from '@components/ListActionButton';
|
||||
import { Sparkline } from '@components/Sparkline';
|
||||
import Switch from '@components/Switch';
|
||||
import VerticalSeparator from '@components/VerticalSeparator';
|
||||
import { Host } from '@types';
|
||||
|
||||
function InventoryHostItem(props) {
|
||||
const {
|
||||
detailUrl,
|
||||
host,
|
||||
i18n,
|
||||
isSelected,
|
||||
onSelect,
|
||||
toggleHost,
|
||||
toggleLoading,
|
||||
} = props;
|
||||
|
||||
const labelId = `check-action-${host.id}`;
|
||||
|
||||
return (
|
||||
<DataListItem key={host.id} aria-labelledby={labelId}>
|
||||
<DataListItemRow>
|
||||
<DataListCheck
|
||||
id={`select-host-${host.id}`}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
aria-labelledby={labelId}
|
||||
/>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key="divider">
|
||||
<VerticalSeparator />
|
||||
<Link to={`${detailUrl}`}>
|
||||
<b>{host.name}</b>
|
||||
</Link>
|
||||
</DataListCell>,
|
||||
<DataListCell key="recentJobs">
|
||||
<Sparkline jobs={host.summary_fields.recent_jobs} />
|
||||
</DataListCell>,
|
||||
<ActionButtonCell lastcolumn="true" key="action">
|
||||
<Tooltip
|
||||
content={i18n._(
|
||||
t`Indicates if a host is available and should be included
|
||||
in running jobs. For hosts that are part of an external
|
||||
inventory, this may be reset by the inventory sync process.`
|
||||
)}
|
||||
position="top"
|
||||
>
|
||||
<Switch
|
||||
id={`host-${host.id}-toggle`}
|
||||
label={i18n._(t`On`)}
|
||||
labelOff={i18n._(t`Off`)}
|
||||
isChecked={host.enabled}
|
||||
isDisabled={
|
||||
toggleLoading || !host.summary_fields.user_capabilities.edit
|
||||
}
|
||||
onChange={() => toggleHost(host)}
|
||||
aria-label={i18n._(t`Toggle host`)}
|
||||
/>
|
||||
</Tooltip>
|
||||
{host.summary_fields.user_capabilities.edit && (
|
||||
<Tooltip content={i18n._(t`Edit Host`)} position="top">
|
||||
<ListActionButton
|
||||
variant="plain"
|
||||
component={Link}
|
||||
to={`/hosts/${host.id}/edit`}
|
||||
>
|
||||
<PencilAltIcon />
|
||||
</ListActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ActionButtonCell>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
);
|
||||
}
|
||||
|
||||
InventoryHostItem.propTypes = {
|
||||
detailUrl: string.isRequired,
|
||||
host: Host.isRequired,
|
||||
isSelected: bool.isRequired,
|
||||
onSelect: func.isRequired,
|
||||
toggleHost: func.isRequired,
|
||||
toggleLoading: bool.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(InventoryHostItem);
|
@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import InventoryHostItem from './InventoryHostItem';
|
||||
|
||||
let toggleHost;
|
||||
|
||||
const mockHost = {
|
||||
id: 1,
|
||||
name: 'Host 1',
|
||||
url: '/api/v2/hosts/1',
|
||||
inventory: 1,
|
||||
summary_fields: {
|
||||
inventory: {
|
||||
id: 1,
|
||||
name: 'Inv 1',
|
||||
},
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe.only('<InventoryHostItem />', () => {
|
||||
beforeEach(() => {
|
||||
toggleHost = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('edit button shown to users with edit capabilities', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<InventoryHostItem
|
||||
isSelected={false}
|
||||
detailUrl="/host/1"
|
||||
onSelect={() => {}}
|
||||
host={mockHost}
|
||||
toggleHost={toggleHost}
|
||||
toggleLoading={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('edit button hidden from users without edit capabilities', () => {
|
||||
const copyMockHost = Object.assign({}, mockHost);
|
||||
copyMockHost.summary_fields.user_capabilities.edit = false;
|
||||
const wrapper = mountWithContexts(
|
||||
<InventoryHostItem
|
||||
isSelected={false}
|
||||
detailUrl="/host/1"
|
||||
onSelect={() => {}}
|
||||
host={copyMockHost}
|
||||
toggleHost={toggleHost}
|
||||
toggleLoading={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('handles toggle click when host is enabled', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<InventoryHostItem
|
||||
isSelected={false}
|
||||
detailUrl="/host/1"
|
||||
onSelect={() => {}}
|
||||
host={mockHost}
|
||||
toggleHost={toggleHost}
|
||||
toggleLoading={false}
|
||||
/>
|
||||
);
|
||||
wrapper
|
||||
.find('Switch')
|
||||
.first()
|
||||
.find('input')
|
||||
.simulate('change');
|
||||
expect(toggleHost).toHaveBeenCalledWith(mockHost);
|
||||
});
|
||||
});
|
@ -1,10 +1,225 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import { InventoriesAPI, HostsAPI } from '@api';
|
||||
|
||||
class InventoryHosts extends Component {
|
||||
render() {
|
||||
return <CardBody>Coming soon :)</CardBody>;
|
||||
}
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import DataListToolbar from '@components/DataListToolbar';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
import PaginatedDataList, {
|
||||
ToolbarAddButton,
|
||||
ToolbarDeleteButton,
|
||||
} from '@components/PaginatedDataList';
|
||||
import InventoryHostItem from './InventoryHostItem';
|
||||
|
||||
const QS_CONFIG = getQSConfig('host', {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
function InventoryHosts({ i18n, location, match, inventory }) {
|
||||
const [actions, setActions] = useState(null);
|
||||
const [contentError, setContentError] = useState(null);
|
||||
const [deletionError, setDeletionError] = useState(null);
|
||||
const [hostCount, setHostCount] = useState(0);
|
||||
const [hosts, setHosts] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [toggleError, setToggleError] = useState(null);
|
||||
const [toggleLoading, setToggleLoading] = useState(null);
|
||||
|
||||
const fetchHosts = (id, queryString) => {
|
||||
const params = parseQueryString(QS_CONFIG, queryString);
|
||||
return InventoriesAPI.readHosts(id, params);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [
|
||||
{
|
||||
data: { count, results },
|
||||
},
|
||||
{
|
||||
data: { actions: optionActions },
|
||||
},
|
||||
] = await Promise.all([
|
||||
fetchHosts(inventory.id, location.search),
|
||||
InventoriesAPI.readOptions(),
|
||||
]);
|
||||
|
||||
setHosts(results);
|
||||
setHostCount(count);
|
||||
setActions(optionActions);
|
||||
} catch (error) {
|
||||
setContentError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, [inventory, location]);
|
||||
|
||||
const handleSelectAll = isSelected => {
|
||||
setSelected(isSelected ? [...hosts] : []);
|
||||
};
|
||||
|
||||
const handleSelect = row => {
|
||||
if (selected.some(s => s.id === row.id)) {
|
||||
setSelected(selected.filter(s => s.id !== row.id));
|
||||
} else {
|
||||
setSelected(selected.concat(row));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await Promise.all(selected.map(host => HostsAPI.destroy(host.id)));
|
||||
} catch (error) {
|
||||
setDeletionError(error);
|
||||
} finally {
|
||||
setSelected([]);
|
||||
try {
|
||||
const {
|
||||
data: { count, results },
|
||||
} = await fetchHosts(inventory.id, location.search);
|
||||
|
||||
setHosts(results);
|
||||
setHostCount(count);
|
||||
} catch (error) {
|
||||
setContentError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle = async hostToToggle => {
|
||||
setToggleLoading(hostToToggle.id);
|
||||
|
||||
try {
|
||||
const { data: updatedHost } = await HostsAPI.update(hostToToggle.id, {
|
||||
enabled: !hostToToggle.enabled,
|
||||
});
|
||||
|
||||
setHosts(
|
||||
hosts.map(host => (host.id === updatedHost.id ? updatedHost : host))
|
||||
);
|
||||
} catch (error) {
|
||||
setToggleError(error);
|
||||
} finally {
|
||||
setToggleLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const canAdd =
|
||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||
const isAllSelected = selected.length > 0 && selected.length === hosts.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading}
|
||||
items={hosts}
|
||||
itemCount={hostCount}
|
||||
pluralizedItemName={i18n._(t`Hosts`)}
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isSortable: true,
|
||||
isSearchable: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified`),
|
||||
key: 'modified',
|
||||
isSortable: true,
|
||||
isNumeric: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created`),
|
||||
key: 'created',
|
||||
isSortable: true,
|
||||
isNumeric: true,
|
||||
},
|
||||
]}
|
||||
renderToolbar={props => (
|
||||
<DataListToolbar
|
||||
{...props}
|
||||
showSelectAll
|
||||
isAllSelected={isAllSelected}
|
||||
onSelectAll={handleSelectAll}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={[
|
||||
<ToolbarDeleteButton
|
||||
key="delete"
|
||||
onDelete={handleDelete}
|
||||
itemsToDelete={selected}
|
||||
pluralizedItemName={i18n._(t`Hosts`)}
|
||||
/>,
|
||||
canAdd ? (
|
||||
<ToolbarAddButton
|
||||
key="add"
|
||||
linkTo={`/inventories/inventory/${match.params.id}/hosts/add`}
|
||||
/>
|
||||
) : null,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
renderItem={o => (
|
||||
<InventoryHostItem
|
||||
key={o.id}
|
||||
host={o}
|
||||
detailUrl={`/hosts/${o.id}/details`}
|
||||
isSelected={selected.some(row => row.id === o.id)}
|
||||
onSelect={() => handleSelect(o)}
|
||||
toggleHost={handleToggle}
|
||||
toggleLoading={toggleLoading === o.id}
|
||||
/>
|
||||
)}
|
||||
emptyStateControls={
|
||||
canAdd ? (
|
||||
<ToolbarAddButton
|
||||
key="add"
|
||||
linkTo={`/inventories/inventory/${match.params.id}/add`}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
|
||||
{toggleError && !toggleLoading && (
|
||||
<AlertModal
|
||||
variant="danger"
|
||||
title={i18n._(t`Error!`)}
|
||||
isOpen={toggleError && !toggleLoading}
|
||||
onClose={() => setToggleError(false)}
|
||||
>
|
||||
{i18n._(t`Failed to toggle host.`)}
|
||||
<ErrorDetail error={toggleError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
|
||||
{deletionError && (
|
||||
<AlertModal
|
||||
isOpen={deletionError}
|
||||
variant="danger"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={() => setDeletionError(null)}
|
||||
>
|
||||
{i18n._(t`Failed to delete one or more hosts.`)}
|
||||
<ErrorDetail error={deletionError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default InventoryHosts;
|
||||
export default withI18n()(withRouter(InventoryHosts));
|
||||
|
@ -0,0 +1,279 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { InventoriesAPI, HostsAPI } from '@api';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import InventoryHosts from './InventoryHosts';
|
||||
import mockInventory from '../shared/data.inventory.json';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
const mockHosts = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Host 1',
|
||||
url: '/api/v2/hosts/1',
|
||||
inventory: 1,
|
||||
enabled: true,
|
||||
summary_fields: {
|
||||
inventory: {
|
||||
id: 1,
|
||||
name: 'inv 1',
|
||||
},
|
||||
user_capabilities: {
|
||||
delete: true,
|
||||
update: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Host 2',
|
||||
url: '/api/v2/hosts/2',
|
||||
inventory: 1,
|
||||
enabled: true,
|
||||
summary_fields: {
|
||||
inventory: {
|
||||
id: 1,
|
||||
name: 'inv 1',
|
||||
},
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
delete: true,
|
||||
update: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Host 3',
|
||||
url: '/api/v2/hosts/3',
|
||||
inventory: 1,
|
||||
enabled: true,
|
||||
summary_fields: {
|
||||
inventory: {
|
||||
id: 1,
|
||||
name: 'inv 1',
|
||||
},
|
||||
user_capabilities: {
|
||||
delete: false,
|
||||
update: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('<InventoryHosts />', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
InventoriesAPI.readHosts.mockResolvedValue({
|
||||
data: {
|
||||
count: mockHosts.length,
|
||||
results: mockHosts,
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {},
|
||||
POST: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryHosts inventory={mockInventory} />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('initially renders successfully', () => {
|
||||
expect(wrapper.find('InventoryHosts').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should fetch hosts from api and render them in the list', async () => {
|
||||
expect(InventoriesAPI.readHosts).toHaveBeenCalled();
|
||||
expect(wrapper.find('InventoryHostItem').length).toBe(3);
|
||||
});
|
||||
|
||||
test('should check and uncheck the row item', async () => {
|
||||
expect(
|
||||
wrapper.find('PFDataListCheck[id="select-host-1"]').props().checked
|
||||
).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('PFDataListCheck[id="select-host-1"]').invoke('onChange')(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('PFDataListCheck[id="select-host-1"]').props().checked
|
||||
).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('PFDataListCheck[id="select-host-1"]').invoke('onChange')(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('PFDataListCheck[id="select-host-1"]').props().checked
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('should check all row items when select all is checked', async () => {
|
||||
wrapper.find('PFDataListCheck').forEach(el => {
|
||||
expect(el.props().checked).toBe(false);
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('Checkbox#select-all').invoke('onChange')(true);
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.find('PFDataListCheck').forEach(el => {
|
||||
expect(el.props().checked).toBe(true);
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('Checkbox#select-all').invoke('onChange')(false);
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.find('PFDataListCheck').forEach(el => {
|
||||
expect(el.props().checked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('should call api if host toggle is clicked', async () => {
|
||||
HostsAPI.update.mockResolvedValueOnce({
|
||||
data: { ...mockHosts[1], enabled: false },
|
||||
});
|
||||
expect(wrapper.find('PFSwitch[id="host-2-toggle"]').props().isChecked).toBe(
|
||||
true
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper.find('PFSwitch[id="host-2-toggle"]').invoke('onChange')();
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('PFSwitch[id="host-2-toggle"]').props().isChecked).toBe(
|
||||
false
|
||||
);
|
||||
expect(HostsAPI.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should show error modal if host is not successfully toggled', async () => {
|
||||
HostsAPI.update.mockImplementationOnce(() => Promise.reject(new Error()));
|
||||
await act(async () => {
|
||||
wrapper.find('PFSwitch[id="host-2-toggle"]').invoke('onChange')();
|
||||
});
|
||||
wrapper.update();
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'Modal',
|
||||
el => el.props().isOpen === true && el.props().title === 'Error!'
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper.find('ModalBoxCloseButton').invoke('onClose')();
|
||||
});
|
||||
await waitForElement(wrapper, 'Modal', el => el.length === 0);
|
||||
});
|
||||
|
||||
test('delete button is disabled if user does not have delete capabilities on a selected host', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('PFDataListCheck[id="select-host-3"]').invoke('onChange')();
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ToolbarDeleteButton button').props().disabled).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('should call api delete hosts for each selected host', async () => {
|
||||
HostsAPI.destroy = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper.find('PFDataListCheck[id="select-host-1"]').invoke('onChange')();
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
|
||||
});
|
||||
wrapper.update();
|
||||
expect(HostsAPI.destroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should show error modal when host is not successfully deleted from api', async () => {
|
||||
HostsAPI.destroy.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'delete',
|
||||
url: '/api/v2/hosts/1',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
},
|
||||
})
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper.find('PFDataListCheck[id="select-host-1"]').invoke('onChange')();
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
|
||||
});
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'Modal',
|
||||
el => el.props().isOpen === true && el.props().title === 'Error!'
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper.find('ModalBoxCloseButton').invoke('onClose')();
|
||||
});
|
||||
await waitForElement(wrapper, 'Modal', el => el.length === 0);
|
||||
});
|
||||
|
||||
test('should show content error if hosts are not successfully fetched from api', async () => {
|
||||
InventoriesAPI.readHosts.mockImplementation(() =>
|
||||
Promise.reject(new Error())
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper.find('PFDataListCheck[id="select-host-1"]').invoke('onChange')();
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
||||
|
||||
test('should show Add button for users with ability to POST', async () => {
|
||||
expect(wrapper.find('ToolbarAddButton').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should hide Add button for users without ability to POST', async () => {
|
||||
InventoriesAPI.readOptions.mockResolvedValueOnce({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryHosts inventory={mockInventory} />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(wrapper.find('ToolbarAddButton').length).toBe(0);
|
||||
});
|
||||
|
||||
test('should show content error when api throws error on initial render', async () => {
|
||||
InventoriesAPI.readOptions.mockImplementation(() =>
|
||||
Promise.reject(new Error())
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryHosts inventory={mockInventory} />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user