mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
add association and disassociation of groups on invhostgroups/hostgroups lists
This commit is contained in:
parent
ab8726dafa
commit
cc4c514103
@ -1,4 +1,5 @@
|
||||
import Base from '../Base';
|
||||
import { TintSlashIcon } from '@patternfly/react-icons';
|
||||
|
||||
class Hosts extends Base {
|
||||
constructor(http) {
|
||||
@ -8,6 +9,8 @@ class Hosts extends Base {
|
||||
this.readFacts = this.readFacts.bind(this);
|
||||
this.readGroups = this.readGroups.bind(this);
|
||||
this.readGroupsOptions = this.readGroupsOptions.bind(this);
|
||||
this.associateGroup = this.associateGroup.bind(this);
|
||||
this.disassociateGroup = this.disassociateGroup.bind(this);
|
||||
}
|
||||
|
||||
readFacts(id) {
|
||||
@ -21,6 +24,17 @@ class Hosts extends Base {
|
||||
readGroupsOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/groups/`);
|
||||
}
|
||||
|
||||
associateGroup(id, groupId) {
|
||||
return this.http.post(`${this.baseUrl}${id}/groups/`, { id: groupId });
|
||||
}
|
||||
|
||||
disassociateGroup(id, group) {
|
||||
return this.http.post(`${this.baseUrl}${id}/groups/`, {
|
||||
id: group.id,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Hosts;
|
||||
|
||||
@ -5,14 +5,16 @@ import { Switch, Route, withRouter } from 'react-router-dom';
|
||||
|
||||
import HostGroupsList from './HostGroupsList';
|
||||
|
||||
function HostGroups({ location, match }) {
|
||||
function HostGroups({ location, match, host }) {
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
key="list"
|
||||
path="/hosts/:id/groups"
|
||||
render={() => {
|
||||
return <HostGroupsList location={location} match={match} />;
|
||||
return (
|
||||
<HostGroupsList host={host} location={location} match={match} />
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
|
||||
@ -12,7 +12,11 @@ describe('<HostGroups />', () => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/hosts/1/groups'],
|
||||
});
|
||||
const host = { id: 1, name: 'Foo' };
|
||||
const host = {
|
||||
id: 1,
|
||||
name: 'Foo',
|
||||
summary_fields: { inventory: { id: 1 } },
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
|
||||
@ -2,11 +2,21 @@ import React, { useState, useEffect, useCallback } 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 useRequest from '@util/useRequest';
|
||||
import { HostsAPI } from '@api';
|
||||
import { getQSConfig, parseQueryString, mergeParams } from '@util/qs';
|
||||
import useRequest, {
|
||||
useDismissableError,
|
||||
useDeleteItems,
|
||||
} from '@util/useRequest';
|
||||
import useSelected from '@util/useSelected';
|
||||
import { HostsAPI, InventoriesAPI } from '@api';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
import PaginatedDataList, {
|
||||
ToolbarAddButton,
|
||||
} from '@components/PaginatedDataList';
|
||||
import AssociateModal from '@components/AssociateModal';
|
||||
import DisassociateButton from '@components/DisassociateButton';
|
||||
import DataListToolbar from '@components/DataListToolbar';
|
||||
import PaginatedDataList from '@components/PaginatedDataList';
|
||||
import HostGroupItem from './HostGroupItem';
|
||||
|
||||
const QS_CONFIG = getQSConfig('group', {
|
||||
@ -15,13 +25,14 @@ const QS_CONFIG = getQSConfig('group', {
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
function HostGroupsList({ i18n, location, match }) {
|
||||
const [selected, setSelected] = useState([]);
|
||||
function HostGroupsList({ i18n, location, match, host }) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const hostId = match.params.id;
|
||||
const invId = host.summary_fields.inventory.id;
|
||||
|
||||
const {
|
||||
result: { groups, itemCount },
|
||||
result: { groups, itemCount, actions },
|
||||
error: contentError,
|
||||
isLoading,
|
||||
request: fetchGroups,
|
||||
@ -29,13 +40,20 @@ function HostGroupsList({ i18n, location, match }) {
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
|
||||
const {
|
||||
data: { count, results },
|
||||
} = await HostsAPI.readGroups(hostId, params);
|
||||
const [
|
||||
{
|
||||
data: { count, results },
|
||||
},
|
||||
actionsResponse,
|
||||
] = await Promise.all([
|
||||
HostsAPI.readGroups(hostId, params),
|
||||
HostsAPI.readGroupsOptions(hostId),
|
||||
]);
|
||||
|
||||
return {
|
||||
itemCount: count,
|
||||
groups: results,
|
||||
itemCount: count,
|
||||
actions: actionsResponse.data.actions,
|
||||
};
|
||||
}, [hostId, location]), // eslint-disable-line react-hooks/exhaustive-deps
|
||||
{
|
||||
@ -48,26 +66,68 @@ function HostGroupsList({ i18n, location, match }) {
|
||||
fetchGroups();
|
||||
}, [fetchGroups]);
|
||||
|
||||
const handleSelectAll = isSelected => {
|
||||
setSelected(isSelected ? [...groups] : []);
|
||||
};
|
||||
const { selected, isAllSelected, handleSelect, setSelected } = useSelected(
|
||||
groups
|
||||
);
|
||||
|
||||
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 {
|
||||
isLoading: isDisassociateLoading,
|
||||
deleteItems: disassociateHosts,
|
||||
deletionError: disassociateError,
|
||||
} = useDeleteItems(
|
||||
useCallback(async () => {
|
||||
return Promise.all(
|
||||
selected.map(group => HostsAPI.disassociateGroup(hostId, group))
|
||||
);
|
||||
}, [hostId, selected]),
|
||||
{
|
||||
qsConfig: QS_CONFIG,
|
||||
allItemsSelected: isAllSelected,
|
||||
fetchItems: fetchGroups,
|
||||
}
|
||||
);
|
||||
|
||||
const handleDisassociate = async () => {
|
||||
await disassociateHosts();
|
||||
setSelected([]);
|
||||
};
|
||||
|
||||
const isAllSelected =
|
||||
selected.length > 0 && selected.length === groups.length;
|
||||
const fetchGroupsToAssociate = useCallback(
|
||||
params => {
|
||||
return InventoriesAPI.readGroups(
|
||||
invId,
|
||||
mergeParams(params, { not__hosts: hostId })
|
||||
);
|
||||
},
|
||||
[invId, hostId]
|
||||
);
|
||||
|
||||
const { request: handleAssociate, error: associateError } = useRequest(
|
||||
useCallback(
|
||||
async groupsToAssociate => {
|
||||
await Promise.all(
|
||||
groupsToAssociate.map(group =>
|
||||
HostsAPI.associateGroup(hostId, group.id)
|
||||
)
|
||||
);
|
||||
fetchGroups();
|
||||
},
|
||||
[hostId, fetchGroups]
|
||||
)
|
||||
);
|
||||
|
||||
const { error, dismissError } = useDismissableError(
|
||||
associateError || disassociateError
|
||||
);
|
||||
|
||||
const canAdd =
|
||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading}
|
||||
hasContentLoading={isLoading || isDisassociateLoading}
|
||||
items={groups}
|
||||
itemCount={itemCount}
|
||||
qsConfig={QS_CONFIG}
|
||||
@ -108,11 +168,57 @@ function HostGroupsList({ i18n, location, match }) {
|
||||
{...props}
|
||||
showSelectAll
|
||||
isAllSelected={isAllSelected}
|
||||
onSelectAll={handleSelectAll}
|
||||
onSelectAll={isSelected =>
|
||||
setSelected(isSelected ? [...groups] : [])
|
||||
}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={[
|
||||
...(canAdd
|
||||
? [
|
||||
<ToolbarAddButton
|
||||
key="add"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<DisassociateButton
|
||||
key="disassociate"
|
||||
onDisassociate={handleDisassociate}
|
||||
itemsToDisassociate={selected}
|
||||
modalTitle={i18n._(t`Disassociate group from host?`)}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
emptyStateControls={
|
||||
canAdd ? (
|
||||
<ToolbarAddButton key="add" onClick={() => setIsModalOpen(true)} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
{isModalOpen && (
|
||||
<AssociateModal
|
||||
header={i18n._(t`Groups`)}
|
||||
fetchRequest={fetchGroupsToAssociate}
|
||||
isModalOpen={isModalOpen}
|
||||
onAssociate={handleAssociate}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
title={i18n._(t`Select Groups`)}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<AlertModal
|
||||
isOpen={error}
|
||||
onClose={dismissError}
|
||||
title={i18n._(t`Error!`)}
|
||||
variant="error"
|
||||
>
|
||||
{associateError
|
||||
? i18n._(t`Failed to associate.`)
|
||||
: i18n._(t`Failed to disassociate one or more groups.`)}
|
||||
<ErrorDetail error={error} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,11 +3,19 @@ import { act } from 'react-dom/test-utils';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import { HostsAPI } from '@api';
|
||||
import { HostsAPI, InventoriesAPI } from '@api';
|
||||
import HostGroupsList from './HostGroupsList';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
const host = {
|
||||
summary_fields: {
|
||||
inventory: {
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockGroups = [
|
||||
{
|
||||
id: 1,
|
||||
@ -52,7 +60,7 @@ const mockGroups = [
|
||||
id: 1,
|
||||
},
|
||||
user_capabilities: {
|
||||
delete: false,
|
||||
delete: true,
|
||||
edit: false,
|
||||
},
|
||||
},
|
||||
@ -82,7 +90,10 @@ describe('<HostGroupsList />', () => {
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Route path="/hosts/:id/groups" component={() => <HostGroupsList />} />,
|
||||
<Route
|
||||
path="/hosts/:id/groups"
|
||||
component={() => <HostGroupsList host={host} />}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
router: { history, route: { location: history.location } },
|
||||
@ -93,6 +104,11 @@ describe('<HostGroupsList />', () => {
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('initially renders successfully', () => {
|
||||
expect(wrapper.find('HostGroupsList').length).toBe(1);
|
||||
});
|
||||
@ -151,8 +167,104 @@ describe('<HostGroupsList />', () => {
|
||||
test('should show content error when api throws error on initial render', async () => {
|
||||
HostsAPI.readGroups.mockImplementation(() => Promise.reject(new Error()));
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<HostGroupsList />);
|
||||
wrapper = mountWithContexts(<HostGroupsList host={host} />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
||||
|
||||
test('should show add button according to permissions', async () => {
|
||||
expect(wrapper.find('ToolbarAddButton').length).toBe(1);
|
||||
HostsAPI.readGroupsOptions.mockResolvedValueOnce({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<HostGroupsList host={host} />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(wrapper.find('ToolbarAddButton').length).toBe(0);
|
||||
});
|
||||
|
||||
test('should show associate group modal when adding an existing group', () => {
|
||||
wrapper.find('ToolbarAddButton').simulate('click');
|
||||
expect(wrapper.find('AssociateModal').length).toBe(1);
|
||||
wrapper.find('ModalBoxCloseButton').simulate('click');
|
||||
expect(wrapper.find('AssociateModal').length).toBe(0);
|
||||
});
|
||||
|
||||
test('should make expected api request when associating groups', async () => {
|
||||
HostsAPI.associateGroup.mockResolvedValue();
|
||||
InventoriesAPI.readGroups.mockResolvedValue({
|
||||
data: {
|
||||
count: 1,
|
||||
results: [{ id: 123, name: 'foo', url: '/api/v2/groups/123/' }],
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('ToolbarAddButton').simulate('click');
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('CheckboxListItem')
|
||||
.first()
|
||||
.invoke('onSelect')();
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||
});
|
||||
await waitForElement(wrapper, 'AssociateModal', el => el.length === 0);
|
||||
expect(InventoriesAPI.readGroups).toHaveBeenCalledTimes(1);
|
||||
expect(HostsAPI.associateGroup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('expected api calls are made for multi-disassociation', async () => {
|
||||
expect(HostsAPI.disassociateGroup).toHaveBeenCalledTimes(0);
|
||||
expect(HostsAPI.readGroups).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.find('DataListCheck').length).toBe(3);
|
||||
wrapper.find('DataListCheck').forEach(el => {
|
||||
expect(el.props().checked).toBe(false);
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('Checkbox#select-all').invoke('onChange')(true);
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.find('DataListCheck').forEach(el => {
|
||||
expect(el.props().checked).toBe(true);
|
||||
});
|
||||
wrapper.find('button[aria-label="Disassociate"]').simulate('click');
|
||||
expect(wrapper.find('AlertModal Title').text()).toEqual(
|
||||
'Disassociate group from host?'
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('button[aria-label="confirm disassociate"]')
|
||||
.simulate('click');
|
||||
});
|
||||
expect(HostsAPI.disassociateGroup).toHaveBeenCalledTimes(3);
|
||||
expect(HostsAPI.readGroups).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('should show error modal for failed disassociation', async () => {
|
||||
HostsAPI.disassociateGroup.mockRejectedValue(new Error());
|
||||
await act(async () => {
|
||||
wrapper.find('Checkbox#select-all').invoke('onChange')(true);
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.find('button[aria-label="Disassociate"]').simulate('click');
|
||||
expect(wrapper.find('AlertModal Title').text()).toEqual(
|
||||
'Disassociate group from host?'
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('button[aria-label="confirm disassociate"]')
|
||||
.simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('AlertModal ErrorDetail').length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,11 +2,21 @@ import React, { useState, useEffect, useCallback } 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 useRequest from '@util/useRequest';
|
||||
import { HostsAPI } from '@api';
|
||||
import { getQSConfig, parseQueryString, mergeParams } from '@util/qs';
|
||||
import useRequest, {
|
||||
useDismissableError,
|
||||
useDeleteItems,
|
||||
} from '@util/useRequest';
|
||||
import useSelected from '@util/useSelected';
|
||||
import { HostsAPI, InventoriesAPI } from '@api';
|
||||
import DataListToolbar from '@components/DataListToolbar';
|
||||
import PaginatedDataList from '@components/PaginatedDataList';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
import PaginatedDataList, {
|
||||
ToolbarAddButton,
|
||||
} from '@components/PaginatedDataList';
|
||||
import AssociateModal from '@components/AssociateModal';
|
||||
import DisassociateButton from '@components/DisassociateButton';
|
||||
import InventoryHostGroupItem from './InventoryHostGroupItem';
|
||||
|
||||
const QS_CONFIG = getQSConfig('group', {
|
||||
@ -16,12 +26,12 @@ const QS_CONFIG = getQSConfig('group', {
|
||||
});
|
||||
|
||||
function InventoryHostGroupsList({ i18n, location, match }) {
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const { hostId } = match.params;
|
||||
const { hostId, id: invId } = match.params;
|
||||
|
||||
const {
|
||||
result: { groups, itemCount },
|
||||
result: { groups, itemCount, actions },
|
||||
error: contentError,
|
||||
isLoading,
|
||||
request: fetchGroups,
|
||||
@ -29,13 +39,20 @@ function InventoryHostGroupsList({ i18n, location, match }) {
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
|
||||
const {
|
||||
data: { count, results },
|
||||
} = await HostsAPI.readGroups(hostId, params);
|
||||
const [
|
||||
{
|
||||
data: { count, results },
|
||||
},
|
||||
actionsResponse,
|
||||
] = await Promise.all([
|
||||
HostsAPI.readGroups(hostId, params),
|
||||
HostsAPI.readGroupsOptions(hostId),
|
||||
]);
|
||||
|
||||
return {
|
||||
itemCount: count,
|
||||
groups: results,
|
||||
itemCount: count,
|
||||
actions: actionsResponse.data.actions,
|
||||
};
|
||||
}, [hostId, location]), // eslint-disable-line react-hooks/exhaustive-deps
|
||||
{
|
||||
@ -48,26 +65,68 @@ function InventoryHostGroupsList({ i18n, location, match }) {
|
||||
fetchGroups();
|
||||
}, [fetchGroups]);
|
||||
|
||||
const handleSelectAll = isSelected => {
|
||||
setSelected(isSelected ? [...groups] : []);
|
||||
};
|
||||
const { selected, isAllSelected, handleSelect, setSelected } = useSelected(
|
||||
groups
|
||||
);
|
||||
|
||||
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 {
|
||||
isLoading: isDisassociateLoading,
|
||||
deleteItems: disassociateHosts,
|
||||
deletionError: disassociateError,
|
||||
} = useDeleteItems(
|
||||
useCallback(async () => {
|
||||
return Promise.all(
|
||||
selected.map(group => HostsAPI.disassociateGroup(hostId, group))
|
||||
);
|
||||
}, [hostId, selected]),
|
||||
{
|
||||
qsConfig: QS_CONFIG,
|
||||
allItemsSelected: isAllSelected,
|
||||
fetchItems: fetchGroups,
|
||||
}
|
||||
);
|
||||
|
||||
const handleDisassociate = async () => {
|
||||
await disassociateHosts();
|
||||
setSelected([]);
|
||||
};
|
||||
|
||||
const isAllSelected =
|
||||
selected.length > 0 && selected.length === groups.length;
|
||||
const fetchGroupsToAssociate = useCallback(
|
||||
params => {
|
||||
return InventoriesAPI.readGroups(
|
||||
invId,
|
||||
mergeParams(params, { not__hosts: hostId })
|
||||
);
|
||||
},
|
||||
[invId, hostId]
|
||||
);
|
||||
|
||||
const { request: handleAssociate, error: associateError } = useRequest(
|
||||
useCallback(
|
||||
async groupsToAssociate => {
|
||||
await Promise.all(
|
||||
groupsToAssociate.map(group =>
|
||||
HostsAPI.associateGroup(hostId, group.id)
|
||||
)
|
||||
);
|
||||
fetchGroups();
|
||||
},
|
||||
[hostId, fetchGroups]
|
||||
)
|
||||
);
|
||||
|
||||
const { error, dismissError } = useDismissableError(
|
||||
associateError || disassociateError
|
||||
);
|
||||
|
||||
const canAdd =
|
||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading}
|
||||
hasContentLoading={isLoading || isDisassociateLoading}
|
||||
items={groups}
|
||||
itemCount={itemCount}
|
||||
qsConfig={QS_CONFIG}
|
||||
@ -107,11 +166,57 @@ function InventoryHostGroupsList({ i18n, location, match }) {
|
||||
{...props}
|
||||
showSelectAll
|
||||
isAllSelected={isAllSelected}
|
||||
onSelectAll={handleSelectAll}
|
||||
onSelectAll={isSelected =>
|
||||
setSelected(isSelected ? [...groups] : [])
|
||||
}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={[
|
||||
...(canAdd
|
||||
? [
|
||||
<ToolbarAddButton
|
||||
key="add"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<DisassociateButton
|
||||
key="disassociate"
|
||||
onDisassociate={handleDisassociate}
|
||||
itemsToDisassociate={selected}
|
||||
modalTitle={i18n._(t`Disassociate group from host?`)}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
emptyStateControls={
|
||||
canAdd ? (
|
||||
<ToolbarAddButton key="add" onClick={() => setIsModalOpen(true)} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
{isModalOpen && (
|
||||
<AssociateModal
|
||||
header={i18n._(t`Groups`)}
|
||||
fetchRequest={fetchGroupsToAssociate}
|
||||
isModalOpen={isModalOpen}
|
||||
onAssociate={handleAssociate}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
title={i18n._(t`Select Groups`)}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<AlertModal
|
||||
isOpen={error}
|
||||
onClose={dismissError}
|
||||
title={i18n._(t`Error!`)}
|
||||
variant="error"
|
||||
>
|
||||
{associateError
|
||||
? i18n._(t`Failed to associate.`)
|
||||
: i18n._(t`Failed to disassociate one or more groups.`)}
|
||||
<ErrorDetail error={error} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { act } from 'react-dom/test-utils';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import { HostsAPI } from '@api';
|
||||
import { HostsAPI, InventoriesAPI } from '@api';
|
||||
import InventoryHostGroupsList from './InventoryHostGroupsList';
|
||||
|
||||
jest.mock('@api');
|
||||
@ -52,7 +52,7 @@ const mockGroups = [
|
||||
id: 1,
|
||||
},
|
||||
user_capabilities: {
|
||||
delete: false,
|
||||
delete: true,
|
||||
edit: false,
|
||||
},
|
||||
},
|
||||
@ -96,6 +96,11 @@ describe('<InventoryHostGroupsList />', () => {
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('initially renders successfully', () => {
|
||||
expect(wrapper.find('InventoryHostGroupsList').length).toBe(1);
|
||||
});
|
||||
@ -158,4 +163,100 @@ describe('<InventoryHostGroupsList />', () => {
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
||||
|
||||
test('should show add button according to permissions', async () => {
|
||||
expect(wrapper.find('ToolbarAddButton').length).toBe(1);
|
||||
HostsAPI.readGroupsOptions.mockResolvedValueOnce({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryHostGroupsList />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(wrapper.find('ToolbarAddButton').length).toBe(0);
|
||||
});
|
||||
|
||||
test('should show associate group modal when adding an existing group', () => {
|
||||
wrapper.find('ToolbarAddButton').simulate('click');
|
||||
expect(wrapper.find('AssociateModal').length).toBe(1);
|
||||
wrapper.find('ModalBoxCloseButton').simulate('click');
|
||||
expect(wrapper.find('AssociateModal').length).toBe(0);
|
||||
});
|
||||
|
||||
test('should make expected api request when associating groups', async () => {
|
||||
HostsAPI.associateGroup.mockResolvedValue();
|
||||
InventoriesAPI.readGroups.mockResolvedValue({
|
||||
data: {
|
||||
count: 1,
|
||||
results: [{ id: 123, name: 'foo', url: '/api/v2/groups/123/' }],
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('ToolbarAddButton').simulate('click');
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('CheckboxListItem')
|
||||
.first()
|
||||
.invoke('onSelect')();
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||
});
|
||||
await waitForElement(wrapper, 'AssociateModal', el => el.length === 0);
|
||||
expect(InventoriesAPI.readGroups).toHaveBeenCalledTimes(1);
|
||||
expect(HostsAPI.associateGroup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('expected api calls are made for multi-disassociation', async () => {
|
||||
expect(HostsAPI.disassociateGroup).toHaveBeenCalledTimes(0);
|
||||
expect(HostsAPI.readGroups).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.find('DataListCheck').length).toBe(3);
|
||||
wrapper.find('DataListCheck').forEach(el => {
|
||||
expect(el.props().checked).toBe(false);
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('Checkbox#select-all').invoke('onChange')(true);
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.find('DataListCheck').forEach(el => {
|
||||
expect(el.props().checked).toBe(true);
|
||||
});
|
||||
wrapper.find('button[aria-label="Disassociate"]').simulate('click');
|
||||
expect(wrapper.find('AlertModal Title').text()).toEqual(
|
||||
'Disassociate group from host?'
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('button[aria-label="confirm disassociate"]')
|
||||
.simulate('click');
|
||||
});
|
||||
expect(HostsAPI.disassociateGroup).toHaveBeenCalledTimes(3);
|
||||
expect(HostsAPI.readGroups).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('should show error modal for failed disassociation', async () => {
|
||||
HostsAPI.disassociateGroup.mockRejectedValue(new Error());
|
||||
await act(async () => {
|
||||
wrapper.find('Checkbox#select-all').invoke('onChange')(true);
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.find('button[aria-label="Disassociate"]').simulate('click');
|
||||
expect(wrapper.find('AlertModal Title').text()).toEqual(
|
||||
'Disassociate group from host?'
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('button[aria-label="confirm disassociate"]')
|
||||
.simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('AlertModal ErrorDetail').length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user