mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 19:10:07 -03:30
Add Inventory Groups list
This commit is contained in:
parent
715483c669
commit
5d1f322cd1
@ -2,6 +2,7 @@ import AdHocCommands from './models/AdHocCommands';
|
||||
import Config from './models/Config';
|
||||
import CredentialTypes from './models/CredentialTypes';
|
||||
import Credentials from './models/Credentials';
|
||||
import Groups from './models/Groups';
|
||||
import Hosts from './models/Hosts';
|
||||
import InstanceGroups from './models/InstanceGroups';
|
||||
import Inventories from './models/Inventories';
|
||||
@ -28,6 +29,7 @@ const AdHocCommandsAPI = new AdHocCommands();
|
||||
const ConfigAPI = new Config();
|
||||
const CredentialsAPI = new Credentials();
|
||||
const CredentialTypesAPI = new CredentialTypes();
|
||||
const GroupsAPI = new Groups();
|
||||
const HostsAPI = new Hosts();
|
||||
const InstanceGroupsAPI = new InstanceGroups();
|
||||
const InventoriesAPI = new Inventories();
|
||||
@ -55,6 +57,7 @@ export {
|
||||
ConfigAPI,
|
||||
CredentialsAPI,
|
||||
CredentialTypesAPI,
|
||||
GroupsAPI,
|
||||
HostsAPI,
|
||||
InstanceGroupsAPI,
|
||||
InventoriesAPI,
|
||||
|
||||
10
awx/ui_next/src/api/models/Groups.js
Normal file
10
awx/ui_next/src/api/models/Groups.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Groups extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/groups/';
|
||||
}
|
||||
}
|
||||
|
||||
export default Groups;
|
||||
@ -7,6 +7,10 @@ class Inventories extends InstanceGroupsMixin(Base) {
|
||||
this.baseUrl = '/api/v2/inventories/';
|
||||
|
||||
this.readAccessList = this.readAccessList.bind(this);
|
||||
this.readHosts = this.readHosts.bind(this);
|
||||
this.readGroups = this.readGroups.bind(this);
|
||||
this.readGroupsOptions = this.readGroupsOptions.bind(this);
|
||||
this.promoteGroup = this.promoteGroup.bind(this);
|
||||
}
|
||||
|
||||
readAccessList(id, params) {
|
||||
@ -18,6 +22,21 @@ class Inventories extends InstanceGroupsMixin(Base) {
|
||||
readHosts(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/hosts/`, { params });
|
||||
}
|
||||
|
||||
readGroups(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/groups/`, { params });
|
||||
}
|
||||
|
||||
readGroupsOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/groups/`);
|
||||
}
|
||||
|
||||
promoteGroup(inventoryId, groupId) {
|
||||
return this.http.post(`${this.baseUrl}${inventoryId}/groups/`, {
|
||||
id: groupId,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Inventories;
|
||||
|
||||
@ -110,6 +110,7 @@
|
||||
--pf-c-modal-box__footer--PaddingRight: 20px;
|
||||
--pf-c-modal-box__footer--PaddingBottom: 20px;
|
||||
--pf-c-modal-box__footer--PaddingLeft: 20px;
|
||||
--pf-c-modal-box__footer--MarginTop: 24px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import { bool, func, number, oneOfType, string } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Group } from '@types';
|
||||
|
||||
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 VerticalSeparator from '@components/VerticalSeparator';
|
||||
|
||||
function InventoryGroupItem({
|
||||
i18n,
|
||||
group,
|
||||
inventoryId,
|
||||
isSelected,
|
||||
onSelect,
|
||||
}) {
|
||||
const labelId = `check-action-${group.id}`;
|
||||
const detailUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/detail`;
|
||||
const editUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/edit`;
|
||||
|
||||
return (
|
||||
<DataListItem key={group.id} aria-labelledby={labelId}>
|
||||
<DataListItemRow>
|
||||
<DataListCheck
|
||||
aria-labelledby={labelId}
|
||||
id={`select-group-${group.id}`}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
/>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key="divider">
|
||||
<VerticalSeparator />
|
||||
<Link to={`${detailUrl}`} id={labelId}>
|
||||
<b>{group.name}</b>
|
||||
</Link>
|
||||
</DataListCell>,
|
||||
<ActionButtonCell lastcolumn="true" key="action">
|
||||
{group.summary_fields.user_capabilities.edit && (
|
||||
<Tooltip content={i18n._(t`Edit Group`)} position="top">
|
||||
<ListActionButton
|
||||
variant="plain"
|
||||
component={Link}
|
||||
to={editUrl}
|
||||
>
|
||||
<PencilAltIcon />
|
||||
</ListActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ActionButtonCell>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
);
|
||||
}
|
||||
|
||||
InventoryGroupItem.propTypes = {
|
||||
group: Group.isRequired,
|
||||
inventoryId: oneOfType([number, string]).isRequired,
|
||||
isSelected: bool.isRequired,
|
||||
onSelect: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(InventoryGroupItem);
|
||||
@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import InventoryGroupItem from './InventoryGroupItem';
|
||||
|
||||
describe('<InventoryGroupItem />', () => {
|
||||
let wrapper;
|
||||
const mockGroup = {
|
||||
id: 2,
|
||||
type: 'group',
|
||||
name: 'foo',
|
||||
inventory: 1,
|
||||
summary_fields: {
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventoryGroupItem
|
||||
group={mockGroup}
|
||||
inventoryId={1}
|
||||
isSelected={false}
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
test('initially renders successfully', () => {
|
||||
expect(wrapper.find('InventoryGroupItem').length).toBe(1);
|
||||
});
|
||||
|
||||
test('edit button should be shown to users with edit capabilities', () => {
|
||||
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('edit button should be hidden from users without edit capabilities', () => {
|
||||
const copyMockGroup = Object.assign({}, mockGroup);
|
||||
copyMockGroup.summary_fields.user_capabilities.edit = false;
|
||||
|
||||
wrapper = mountWithContexts(
|
||||
<InventoryGroupItem
|
||||
group={copyMockGroup}
|
||||
inventoryId={1}
|
||||
isSelected={false}
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@ -1,10 +1,259 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { TrashAltIcon } from '@patternfly/react-icons';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import { InventoriesAPI, GroupsAPI } from '@api';
|
||||
import { Button, Tooltip } from '@patternfly/react-core';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
import DataListToolbar from '@components/DataListToolbar';
|
||||
import PaginatedDataList, {
|
||||
ToolbarAddButton,
|
||||
} from '@components/PaginatedDataList';
|
||||
import styled from 'styled-components';
|
||||
import InventoryGroupItem from './InventoryGroupItem';
|
||||
import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal';
|
||||
|
||||
class InventoryGroups extends Component {
|
||||
render() {
|
||||
return <CardBody>Coming soon :)</CardBody>;
|
||||
const QS_CONFIG = getQSConfig('host', {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
const DeleteButton = styled(Button)`
|
||||
padding: 5px 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: #d9534f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
color: var(--pf-c-button--m-plain--Color);
|
||||
pointer-events: initial;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
|
||||
function cannotDelete(item) {
|
||||
return !item.summary_fields.user_capabilities.delete;
|
||||
}
|
||||
|
||||
export default InventoryGroups;
|
||||
const useModal = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
function toggleModal() {
|
||||
setIsModalOpen(!isModalOpen);
|
||||
}
|
||||
|
||||
return {
|
||||
isModalOpen,
|
||||
toggleModal,
|
||||
};
|
||||
};
|
||||
|
||||
function InventoryGroups({ i18n, location, match }) {
|
||||
const [actions, setActions] = useState(null);
|
||||
const [contentError, setContentError] = useState(null);
|
||||
const [deletionError, setDeletionError] = useState(null);
|
||||
const [groupCount, setGroupCount] = useState(0);
|
||||
const [groups, setGroups] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const { isModalOpen, toggleModal } = useModal();
|
||||
|
||||
const inventoryId = match.params.id;
|
||||
|
||||
const fetchGroups = (id, queryString) => {
|
||||
const params = parseQueryString(QS_CONFIG, queryString);
|
||||
return InventoriesAPI.readGroups(id, params);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [
|
||||
{
|
||||
data: { count, results },
|
||||
},
|
||||
{
|
||||
data: { actions: optionActions },
|
||||
},
|
||||
] = await Promise.all([
|
||||
fetchGroups(inventoryId, location.search),
|
||||
InventoriesAPI.readGroupsOptions(inventoryId),
|
||||
]);
|
||||
|
||||
setGroups(results);
|
||||
setGroupCount(count);
|
||||
setActions(optionActions);
|
||||
} catch (error) {
|
||||
setContentError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, [inventoryId, location]);
|
||||
|
||||
const handleSelectAll = isSelected => {
|
||||
setSelected(isSelected ? [...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 renderTooltip = () => {
|
||||
const itemsUnableToDelete = selected
|
||||
.filter(cannotDelete)
|
||||
.map(item => item.name)
|
||||
.join(', ');
|
||||
|
||||
if (selected.some(cannotDelete)) {
|
||||
return (
|
||||
<div>
|
||||
{i18n._(
|
||||
t`You do not have permission to delete the following Groups: ${itemsUnableToDelete}`
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (selected.length) {
|
||||
return i18n._(t`Delete`);
|
||||
}
|
||||
return i18n._(t`Select a row to delete`);
|
||||
};
|
||||
|
||||
const promoteGroups = list => {
|
||||
const promotePromises = Object.keys(list)
|
||||
.filter(groupId => list[groupId] === 'promote')
|
||||
.map(groupId => InventoriesAPI.promoteGroup(inventoryId, +groupId));
|
||||
|
||||
return Promise.all(promotePromises);
|
||||
};
|
||||
|
||||
const deleteGroups = list => {
|
||||
const deletePromises = Object.keys(list)
|
||||
.filter(groupId => list[groupId] === 'delete')
|
||||
.map(groupId => GroupsAPI.destroy(+groupId));
|
||||
|
||||
return Promise.all(deletePromises);
|
||||
};
|
||||
|
||||
const handleDelete = async list => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await Promise.all([promoteGroups(list), deleteGroups(list)]);
|
||||
} catch (error) {
|
||||
setDeletionError(error);
|
||||
} finally {
|
||||
toggleModal();
|
||||
setSelected([]);
|
||||
|
||||
try {
|
||||
const {
|
||||
data: { count, results },
|
||||
} = await fetchGroups(inventoryId, location.search);
|
||||
|
||||
setGroups(results);
|
||||
setGroupCount(count);
|
||||
} catch (error) {
|
||||
setContentError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const canAdd =
|
||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||
const isAllSelected =
|
||||
selected.length > 0 && selected.length === groups.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading}
|
||||
items={groups}
|
||||
itemCount={groupCount}
|
||||
qsConfig={QS_CONFIG}
|
||||
renderItem={item => (
|
||||
<InventoryGroupItem
|
||||
key={item.id}
|
||||
group={item}
|
||||
inventoryId={inventoryId}
|
||||
isSelected={selected.some(row => row.id === item.id)}
|
||||
onSelect={() => handleSelect(item)}
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => (
|
||||
<DataListToolbar
|
||||
{...props}
|
||||
showSelectAll
|
||||
isAllSelected={isAllSelected}
|
||||
onSelectAll={handleSelectAll}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={[
|
||||
<Tooltip content={renderTooltip()} position="top" key="delete">
|
||||
<div>
|
||||
<DeleteButton
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`Delete`)}
|
||||
onClick={toggleModal}
|
||||
isDisabled={
|
||||
selected.length === 0 || selected.some(cannotDelete)
|
||||
}
|
||||
>
|
||||
<TrashAltIcon />
|
||||
</DeleteButton>
|
||||
</div>
|
||||
</Tooltip>,
|
||||
canAdd && (
|
||||
<ToolbarAddButton
|
||||
key="add"
|
||||
linkTo={`/inventories/inventory/${inventoryId}/groups/add`}
|
||||
/>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
emptyStateControls={
|
||||
canAdd && (
|
||||
<ToolbarAddButton
|
||||
key="add"
|
||||
linkTo={`/inventories/inventory/${inventoryId}/groups/add`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{deletionError && (
|
||||
<AlertModal
|
||||
isOpen={deletionError}
|
||||
variant="danger"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={() => setDeletionError(null)}
|
||||
>
|
||||
{i18n._(t`Failed to delete one or more groups.`)}
|
||||
<ErrorDetail error={deletionError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
<InventoryGroupsDeleteModal
|
||||
groups={selected}
|
||||
isModalOpen={isModalOpen}
|
||||
onClose={toggleModal}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(withRouter(InventoryGroups));
|
||||
|
||||
@ -0,0 +1,207 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { MemoryRouter, Route } from 'react-router-dom';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import { InventoriesAPI, GroupsAPI } from '@api';
|
||||
import InventoryGroups from './InventoryGroups';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
const mockGroups = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'group',
|
||||
name: 'foo',
|
||||
inventory: 1,
|
||||
url: '/api/v2/groups/1',
|
||||
summary_fields: {
|
||||
user_capabilities: {
|
||||
delete: true,
|
||||
edit: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'group',
|
||||
name: 'bar',
|
||||
inventory: 1,
|
||||
url: '/api/v2/groups/2',
|
||||
summary_fields: {
|
||||
user_capabilities: {
|
||||
delete: true,
|
||||
edit: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'group',
|
||||
name: 'baz',
|
||||
inventory: 1,
|
||||
url: '/api/v2/groups/3',
|
||||
summary_fields: {
|
||||
user_capabilities: {
|
||||
delete: false,
|
||||
edit: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('<InventoryGroups />', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
InventoriesAPI.readGroups.mockResolvedValue({
|
||||
data: {
|
||||
count: mockGroups.length,
|
||||
results: mockGroups,
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readGroupsOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {},
|
||||
POST: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<MemoryRouter initialEntries={['/inventories/inventory/3/groups']}>
|
||||
<Route
|
||||
path="/inventories/inventory/:id/groups"
|
||||
component={() => <InventoryGroups />}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
|
||||
test('initially renders successfully', () => {
|
||||
expect(wrapper.find('InventoryGroups').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should fetch groups from api and render them in the list', async () => {
|
||||
expect(InventoriesAPI.readGroups).toHaveBeenCalled();
|
||||
expect(wrapper.find('InventoryGroupItem').length).toBe(3);
|
||||
});
|
||||
|
||||
test('should check and uncheck the row item', async () => {
|
||||
expect(
|
||||
wrapper.find('PFDataListCheck[id="select-group-1"]').props().checked
|
||||
).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')(
|
||||
true
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('PFDataListCheck[id="select-group-1"]').props().checked
|
||||
).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')(
|
||||
false
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('PFDataListCheck[id="select-group-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 show content error when api throws error on initial render', async () => {
|
||||
InventoriesAPI.readGroupsOptions.mockImplementation(() =>
|
||||
Promise.reject(new Error())
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryGroups />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
||||
|
||||
test('should show content error if groups are not successfully fetched from api', async () => {
|
||||
InventoriesAPI.readGroups.mockImplementation(() =>
|
||||
Promise.reject(new Error())
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')();
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('Toolbar Button[aria-label="Delete"]').invoke('onClick')();
|
||||
});
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'InventoryGroupsDeleteModal',
|
||||
el => el.props().isModalOpen === true
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('ModalBoxFooter Button[aria-label="Delete"]')
|
||||
.invoke('onClick')();
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
||||
|
||||
test('should show error modal when group is not successfully deleted from api', async () => {
|
||||
GroupsAPI.destroy.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'delete',
|
||||
url: '/api/v2/groups/1',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
},
|
||||
})
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')();
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('Toolbar Button[aria-label="Delete"]').invoke('onClick')();
|
||||
});
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'InventoryGroupsDeleteModal',
|
||||
el => el.props().isModalOpen === true
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('ModalBoxFooter Button[aria-label="Delete"]')
|
||||
.invoke('onClick')();
|
||||
});
|
||||
await waitForElement(wrapper, { title: 'Error!', variant: 'danger' });
|
||||
await act(async () => {
|
||||
wrapper.find('ModalBoxCloseButton').invoke('onClose')();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -20,7 +20,7 @@ const mockHost = {
|
||||
},
|
||||
};
|
||||
|
||||
describe.only('<InventoryHostItem />', () => {
|
||||
describe('<InventoryHostItem />', () => {
|
||||
beforeEach(() => {
|
||||
toggleHost = jest.fn();
|
||||
});
|
||||
|
||||
@ -0,0 +1,144 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Button, Radio } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const ListItem = styled.div`
|
||||
padding: 24px 1px;
|
||||
|
||||
dl {
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
}
|
||||
dt {
|
||||
color: var(--pf-global--danger-color--100);
|
||||
margin-right: 10px;
|
||||
}
|
||||
.pf-c-radio {
|
||||
margin-top: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContentWrapper = styled.div`
|
||||
${ListItem} + ${ListItem} {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
border-top-color: #d7d7d7;
|
||||
}
|
||||
${ListItem}:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const InventoryGroupsDeleteModal = ({
|
||||
onClose,
|
||||
onDelete,
|
||||
isModalOpen,
|
||||
groups,
|
||||
i18n,
|
||||
}) => {
|
||||
const [deleteList, setDeleteList] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const groupIds = groups.reduce((obj, group) => {
|
||||
if (group.total_groups > 0 || group.total_hosts > 0) {
|
||||
return { ...obj, [group.id]: null };
|
||||
}
|
||||
return { ...obj, [group.id]: 'delete' };
|
||||
}, {});
|
||||
|
||||
setDeleteList(groupIds);
|
||||
}, [groups]);
|
||||
|
||||
const handleChange = (groupId, radioOption) => {
|
||||
setDeleteList({ ...deleteList, [groupId]: radioOption });
|
||||
};
|
||||
|
||||
const content = groups
|
||||
.map(group => {
|
||||
if (group.total_groups > 0 || group.total_hosts > 0) {
|
||||
return (
|
||||
<ListItem key={group.id}>
|
||||
<dl>
|
||||
<dt>{group.name}</dt>
|
||||
<dd>
|
||||
{i18n._(
|
||||
t`(${group.total_groups} Groups and ${group.total_hosts} Hosts)`
|
||||
)}
|
||||
</dd>
|
||||
</dl>
|
||||
<Radio
|
||||
key="radio-delete"
|
||||
label={i18n._(t`Delete All Groups and Hosts`)}
|
||||
id={`radio-delete-${group.id}`}
|
||||
name={`radio-${group.id}`}
|
||||
onChange={() => handleChange(group.id, 'delete')}
|
||||
/>
|
||||
<Radio
|
||||
key="radio-promote"
|
||||
label={i18n._(t`Promote Child Groups and Hosts`)}
|
||||
id={`radio-promote-${group.id}`}
|
||||
name={`radio-${group.id}`}
|
||||
onChange={() => handleChange(group.id, 'promote')}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ListItem key={group.id}>
|
||||
<dl>
|
||||
<dt>{group.name}</dt>
|
||||
<dd>{i18n._(t`(No Child Groups or Hosts)`)}</dd>
|
||||
</dl>
|
||||
</ListItem>
|
||||
);
|
||||
})
|
||||
.reduce((array, el) => {
|
||||
return array.concat(el);
|
||||
}, []);
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<AlertModal
|
||||
isOpen={isModalOpen}
|
||||
variant="danger"
|
||||
title={
|
||||
groups.length > 1 ? i18n._(t`Delete Groups`) : i18n._(t`Delete Group`)
|
||||
}
|
||||
onClose={onClose}
|
||||
actions={[
|
||||
<Button
|
||||
aria-label={i18n._(t`Delete`)}
|
||||
onClick={() => onDelete(deleteList)}
|
||||
variant="danger"
|
||||
key="delete"
|
||||
isDisabled={Object.keys(deleteList).some(
|
||||
group => deleteList[group] === null
|
||||
)}
|
||||
>
|
||||
{i18n._(t`Delete`)}
|
||||
</Button>,
|
||||
<Button
|
||||
aria-label={i18n._(t`Close`)}
|
||||
onClick={onClose}
|
||||
variant="secondary"
|
||||
key="cancel"
|
||||
>
|
||||
{i18n._(t`Cancel`)}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{i18n._(
|
||||
t`Are you sure you want to delete the ${
|
||||
groups.length > 1 ? i18n._(t`groups`) : i18n._(t`group`)
|
||||
} below?`
|
||||
)}
|
||||
<ContentWrapper>{content}</ContentWrapper>
|
||||
</AlertModal>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default withI18n()(InventoryGroupsDeleteModal);
|
||||
@ -229,3 +229,23 @@ export const User = shape({
|
||||
ldap_dn: string,
|
||||
last_login: string,
|
||||
});
|
||||
|
||||
export const Group = shape({
|
||||
id: number.isRequired,
|
||||
type: oneOf(['group']),
|
||||
url: string,
|
||||
related: shape({}),
|
||||
summary_fields: shape({}),
|
||||
created: string,
|
||||
modified: string,
|
||||
name: string.isRequired,
|
||||
description: string,
|
||||
inventory: number,
|
||||
variables: string,
|
||||
has_active_failures: bool,
|
||||
total_hosts: number,
|
||||
hosts_with_active_failures: number,
|
||||
total_groups: number,
|
||||
groups_with_active_failures: number,
|
||||
has_inventory_sources: bool,
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user