mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 11:00:03 -03:30
add inventory host groups list and host groups lists
This commit is contained in:
parent
a682565758
commit
ab36129395
@ -4,11 +4,23 @@ class Hosts extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/hosts/';
|
||||
|
||||
this.readFacts = this.readFacts.bind(this);
|
||||
this.readGroups = this.readGroups.bind(this);
|
||||
this.readGroupsOptions = this.readGroupsOptions.bind(this);
|
||||
}
|
||||
|
||||
readFacts(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/ansible_facts/`);
|
||||
}
|
||||
|
||||
readGroups(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/groups/`, { params });
|
||||
}
|
||||
|
||||
readGroupsOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/groups/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default Hosts;
|
||||
|
||||
69
awx/ui_next/src/screens/Host/HostGroups/HostGroupItem.jsx
Normal file
69
awx/ui_next/src/screens/Host/HostGroups/HostGroupItem.jsx
Normal file
@ -0,0 +1,69 @@
|
||||
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 {
|
||||
Button,
|
||||
DataListAction,
|
||||
DataListCheck,
|
||||
DataListItem,
|
||||
DataListItemCells,
|
||||
DataListItemRow,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import DataListCell from '@components/DataListCell';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import { PencilAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
function HostGroupItem({ i18n, group, inventoryId, isSelected, onSelect }) {
|
||||
const labelId = `check-action-${group.id}`;
|
||||
const detailUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/details`;
|
||||
const editUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/edit`;
|
||||
|
||||
return (
|
||||
<DataListItem key={group.id} aria-labelledby={labelId} id={`${group.id}`}>
|
||||
<DataListItemRow>
|
||||
<DataListCheck
|
||||
aria-labelledby={labelId}
|
||||
id={`select-group-${group.id}`}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
/>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key="name">
|
||||
<Link to={`${detailUrl}`} id={labelId}>
|
||||
<b>{group.name}</b>
|
||||
</Link>
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
<DataListAction
|
||||
aria-label="actions"
|
||||
aria-labelledby={labelId}
|
||||
id={labelId}
|
||||
>
|
||||
{group.summary_fields.user_capabilities.edit && (
|
||||
<Tooltip content={i18n._(t`Edit Group`)} position="top">
|
||||
<Button variant="plain" component={Link} to={editUrl}>
|
||||
<PencilAltIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</DataListAction>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
);
|
||||
}
|
||||
|
||||
HostGroupItem.propTypes = {
|
||||
group: Group.isRequired,
|
||||
inventoryId: oneOfType([number, string]).isRequired,
|
||||
isSelected: bool.isRequired,
|
||||
onSelect: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(HostGroupItem);
|
||||
@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import HostGroupItem from './HostGroupItem';
|
||||
|
||||
describe('<HostGroupItem />', () => {
|
||||
let wrapper;
|
||||
const mockGroup = {
|
||||
id: 2,
|
||||
type: 'group',
|
||||
name: 'foo',
|
||||
inventory: 1,
|
||||
summary_fields: {
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<HostGroupItem
|
||||
group={mockGroup}
|
||||
inventoryId={1}
|
||||
isSelected={false}
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
test('initially renders successfully', () => {
|
||||
expect(wrapper.find('HostGroupItem').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(
|
||||
<HostGroupItem
|
||||
group={copyMockGroup}
|
||||
inventoryId={1}
|
||||
isSelected={false}
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@ -1,10 +1,23 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@components/Card';
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
|
||||
class HostGroups extends Component {
|
||||
render() {
|
||||
return <CardBody>Coming soon :)</CardBody>;
|
||||
}
|
||||
import { Switch, Route, withRouter } from 'react-router-dom';
|
||||
|
||||
import HostGroupsList from './HostGroupsList';
|
||||
|
||||
function HostGroups({ location, match }) {
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
key="list"
|
||||
path="/hosts/:id/groups"
|
||||
render={() => {
|
||||
return <HostGroupsList location={location} match={match} />;
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
export default HostGroups;
|
||||
export { HostGroups as _HostGroups };
|
||||
export default withI18n()(withRouter(HostGroups));
|
||||
|
||||
31
awx/ui_next/src/screens/Host/HostGroups/HostGroups.test.jsx
Normal file
31
awx/ui_next/src/screens/Host/HostGroups/HostGroups.test.jsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import HostGroups from './HostGroups';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
describe('<HostGroups />', () => {
|
||||
test('initially renders successfully', async () => {
|
||||
let wrapper;
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/hosts/1/groups'],
|
||||
});
|
||||
const host = { id: 1, name: 'Foo' };
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<HostGroups setBreadcrumb={() => {}} host={host} />,
|
||||
|
||||
{
|
||||
context: {
|
||||
router: { history, route: { location: history.location } },
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
expect(wrapper.length).toBe(1);
|
||||
expect(wrapper.find('HostGroupsList').length).toBe(1);
|
||||
});
|
||||
});
|
||||
119
awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx
Normal file
119
awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx
Normal file
@ -0,0 +1,119 @@
|
||||
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 DataListToolbar from '@components/DataListToolbar';
|
||||
import PaginatedDataList from '@components/PaginatedDataList';
|
||||
import HostGroupItem from './HostGroupItem';
|
||||
|
||||
const QS_CONFIG = getQSConfig('group', {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
function HostGroupsList({ i18n, location, match }) {
|
||||
const [selected, setSelected] = useState([]);
|
||||
|
||||
const hostId = match.params.id;
|
||||
|
||||
const {
|
||||
result: { groups, itemCount },
|
||||
error: contentError,
|
||||
isLoading,
|
||||
request: fetchGroups,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
|
||||
const {
|
||||
data: { count, results },
|
||||
} = await HostsAPI.readGroups(hostId, params);
|
||||
|
||||
return {
|
||||
itemCount: count,
|
||||
groups: results,
|
||||
};
|
||||
}, [hostId, location]), // eslint-disable-line react-hooks/exhaustive-deps
|
||||
{
|
||||
groups: [],
|
||||
itemCount: 0,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchGroups();
|
||||
}, [fetchGroups]);
|
||||
|
||||
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 isAllSelected =
|
||||
selected.length > 0 && selected.length === groups.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading}
|
||||
items={groups}
|
||||
itemCount={itemCount}
|
||||
qsConfig={QS_CONFIG}
|
||||
onRowClick={handleSelect}
|
||||
toolbarSearchColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created By (Username)`),
|
||||
key: 'created_by__username',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified By (Username)`),
|
||||
key: 'modified_by__username',
|
||||
},
|
||||
]}
|
||||
toolbarSortColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
},
|
||||
]}
|
||||
renderItem={item => (
|
||||
<HostGroupItem
|
||||
key={item.id}
|
||||
group={item}
|
||||
hostId={hostId}
|
||||
inventoryId={item.summary_fields.inventory.id}
|
||||
isSelected={selected.some(row => row.id === item.id)}
|
||||
onSelect={() => handleSelect(item)}
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => (
|
||||
<DataListToolbar
|
||||
{...props}
|
||||
showSelectAll
|
||||
isAllSelected={isAllSelected}
|
||||
onSelectAll={handleSelectAll}
|
||||
qsConfig={QS_CONFIG}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default withI18n()(withRouter(HostGroupsList));
|
||||
158
awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx
Normal file
158
awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx
Normal file
@ -0,0 +1,158 @@
|
||||
import React from 'react';
|
||||
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 HostGroupsList from './HostGroupsList';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
const mockGroups = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'group',
|
||||
name: 'foo',
|
||||
inventory: 1,
|
||||
url: '/api/v2/groups/1',
|
||||
summary_fields: {
|
||||
inventory: {
|
||||
id: 1,
|
||||
},
|
||||
user_capabilities: {
|
||||
delete: true,
|
||||
edit: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'group',
|
||||
name: 'bar',
|
||||
inventory: 1,
|
||||
url: '/api/v2/groups/2',
|
||||
summary_fields: {
|
||||
inventory: {
|
||||
id: 1,
|
||||
},
|
||||
user_capabilities: {
|
||||
delete: true,
|
||||
edit: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'group',
|
||||
name: 'baz',
|
||||
inventory: 1,
|
||||
url: '/api/v2/groups/3',
|
||||
summary_fields: {
|
||||
inventory: {
|
||||
id: 1,
|
||||
},
|
||||
user_capabilities: {
|
||||
delete: false,
|
||||
edit: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('<HostGroupsList />', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
HostsAPI.readGroups.mockResolvedValue({
|
||||
data: {
|
||||
count: mockGroups.length,
|
||||
results: mockGroups,
|
||||
},
|
||||
});
|
||||
HostsAPI.readGroupsOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {},
|
||||
POST: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/hosts/3/groups'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Route path="/hosts/:id/groups" component={() => <HostGroupsList />} />,
|
||||
{
|
||||
context: {
|
||||
router: { history, route: { location: history.location } },
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
|
||||
test('initially renders successfully', () => {
|
||||
expect(wrapper.find('HostGroupsList').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should fetch groups from api and render them in the list', async () => {
|
||||
expect(HostsAPI.readGroups).toHaveBeenCalled();
|
||||
expect(wrapper.find('HostGroupItem').length).toBe(3);
|
||||
});
|
||||
|
||||
test('should check and uncheck the row item', async () => {
|
||||
expect(
|
||||
wrapper.find('DataListCheck[id="select-group-1"]').props().checked
|
||||
).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('DataListCheck[id="select-group-1"]').invoke('onChange')(
|
||||
true
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('DataListCheck[id="select-group-1"]').props().checked
|
||||
).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('DataListCheck[id="select-group-1"]').invoke('onChange')(
|
||||
false
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('DataListCheck[id="select-group-1"]').props().checked
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('should check all row items when select all is checked', async () => {
|
||||
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);
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('Checkbox#select-all').invoke('onChange')(false);
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.find('DataListCheck').forEach(el => {
|
||||
expect(el.props().checked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
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 />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
||||
});
|
||||
@ -23,6 +23,7 @@ import JobList from '@components/JobList';
|
||||
import InventoryHostDetail from '../InventoryHostDetail';
|
||||
import InventoryHostEdit from '../InventoryHostEdit';
|
||||
import InventoryHostFacts from '../InventoryHostFacts';
|
||||
import InventoryHostGroups from '../InventoryHostGroups';
|
||||
|
||||
function InventoryHost({ i18n, setBreadcrumb, inventory }) {
|
||||
const location = useLocation();
|
||||
@ -147,6 +148,12 @@ function InventoryHost({ i18n, setBreadcrumb, inventory }) {
|
||||
>
|
||||
<InventoryHostFacts host={host} />
|
||||
</Route>
|
||||
<Route
|
||||
key="groups"
|
||||
path="/inventories/inventory/:id/hosts/:hostId/groups"
|
||||
>
|
||||
<InventoryHostGroups />
|
||||
</Route>
|
||||
<Route
|
||||
key="completed-jobs"
|
||||
path="/inventories/inventory/:id/hosts/:hostId/completed_jobs"
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
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 {
|
||||
Button,
|
||||
DataListAction,
|
||||
DataListCheck,
|
||||
DataListItem,
|
||||
DataListItemCells,
|
||||
DataListItemRow,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import DataListCell from '@components/DataListCell';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import { PencilAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
function InventoryHostGroupItem({
|
||||
i18n,
|
||||
group,
|
||||
inventoryId,
|
||||
isSelected,
|
||||
onSelect,
|
||||
}) {
|
||||
const labelId = `check-action-${group.id}`;
|
||||
const detailUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/details`;
|
||||
const editUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/edit`;
|
||||
|
||||
return (
|
||||
<DataListItem key={group.id} aria-labelledby={labelId} id={`${group.id}`}>
|
||||
<DataListItemRow>
|
||||
<DataListCheck
|
||||
aria-labelledby={labelId}
|
||||
id={`select-group-${group.id}`}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
/>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key="name">
|
||||
<Link to={`${detailUrl}`} id={labelId}>
|
||||
<b>{group.name}</b>
|
||||
</Link>
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
<DataListAction
|
||||
aria-label="actions"
|
||||
aria-labelledby={labelId}
|
||||
id={labelId}
|
||||
>
|
||||
{group.summary_fields.user_capabilities.edit && (
|
||||
<Tooltip content={i18n._(t`Edit Group`)} position="top">
|
||||
<Button variant="plain" component={Link} to={editUrl}>
|
||||
<PencilAltIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</DataListAction>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
);
|
||||
}
|
||||
|
||||
InventoryHostGroupItem.propTypes = {
|
||||
group: Group.isRequired,
|
||||
inventoryId: oneOfType([number, string]).isRequired,
|
||||
isSelected: bool.isRequired,
|
||||
onSelect: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(InventoryHostGroupItem);
|
||||
@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import InventoryHostGroupItem from './InventoryHostGroupItem';
|
||||
|
||||
describe('<InventoryHostGroupItem />', () => {
|
||||
let wrapper;
|
||||
const mockGroup = {
|
||||
id: 2,
|
||||
type: 'group',
|
||||
name: 'foo',
|
||||
inventory: 1,
|
||||
summary_fields: {
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventoryHostGroupItem
|
||||
group={mockGroup}
|
||||
inventoryId={1}
|
||||
isSelected={false}
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
test('initially renders successfully', () => {
|
||||
expect(wrapper.find('InventoryHostGroupItem').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(
|
||||
<InventoryHostGroupItem
|
||||
group={copyMockGroup}
|
||||
inventoryId={1}
|
||||
isSelected={false}
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
|
||||
import { Switch, Route, withRouter } from 'react-router-dom';
|
||||
|
||||
import InventoryHostGroupsList from './InventoryHostGroupsList';
|
||||
|
||||
function InventoryHostGroups({ location, match }) {
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
key="list"
|
||||
path="/inventories/inventory/:id/hosts/:hostId/groups"
|
||||
render={() => {
|
||||
return <InventoryHostGroupsList location={location} match={match} />;
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
export { InventoryHostGroups as _InventoryHostGroups };
|
||||
export default withI18n()(withRouter(InventoryHostGroups));
|
||||
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import InventoryHostGroups from './InventoryHostGroups';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
describe('<InventoryHostGroups />', () => {
|
||||
test('initially renders successfully', async () => {
|
||||
let wrapper;
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/inventory/1/hosts/1/groups'],
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryHostGroups />, {
|
||||
context: {
|
||||
router: { history, route: { location: history.location } },
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(wrapper.length).toBe(1);
|
||||
expect(wrapper.find('InventoryHostGroupsList').length).toBe(1);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,118 @@
|
||||
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 DataListToolbar from '@components/DataListToolbar';
|
||||
import PaginatedDataList from '@components/PaginatedDataList';
|
||||
import InventoryHostGroupItem from './InventoryHostGroupItem';
|
||||
|
||||
const QS_CONFIG = getQSConfig('group', {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
function InventoryHostGroupsList({ i18n, location, match }) {
|
||||
const [selected, setSelected] = useState([]);
|
||||
|
||||
const { hostId } = match.params;
|
||||
|
||||
const {
|
||||
result: { groups, itemCount },
|
||||
error: contentError,
|
||||
isLoading,
|
||||
request: fetchGroups,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
|
||||
const {
|
||||
data: { count, results },
|
||||
} = await HostsAPI.readGroups(hostId, params);
|
||||
|
||||
return {
|
||||
itemCount: count,
|
||||
groups: results,
|
||||
};
|
||||
}, [hostId, location]), // eslint-disable-line react-hooks/exhaustive-deps
|
||||
{
|
||||
groups: [],
|
||||
itemCount: 0,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchGroups();
|
||||
}, [fetchGroups]);
|
||||
|
||||
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 isAllSelected =
|
||||
selected.length > 0 && selected.length === groups.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading}
|
||||
items={groups}
|
||||
itemCount={itemCount}
|
||||
qsConfig={QS_CONFIG}
|
||||
onRowClick={handleSelect}
|
||||
toolbarSearchColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created By (Username)`),
|
||||
key: 'created_by__username',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified By (Username)`),
|
||||
key: 'modified_by__username',
|
||||
},
|
||||
]}
|
||||
toolbarSortColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
},
|
||||
]}
|
||||
renderItem={item => (
|
||||
<InventoryHostGroupItem
|
||||
key={item.id}
|
||||
group={item}
|
||||
inventoryId={item.summary_fields.inventory.id}
|
||||
isSelected={selected.some(row => row.id === item.id)}
|
||||
onSelect={() => handleSelect(item)}
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => (
|
||||
<DataListToolbar
|
||||
{...props}
|
||||
showSelectAll
|
||||
isAllSelected={isAllSelected}
|
||||
onSelectAll={handleSelectAll}
|
||||
qsConfig={QS_CONFIG}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default withI18n()(withRouter(InventoryHostGroupsList));
|
||||
@ -0,0 +1,161 @@
|
||||
import React from 'react';
|
||||
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 InventoryHostGroupsList from './InventoryHostGroupsList';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
const mockGroups = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'group',
|
||||
name: 'foo',
|
||||
inventory: 1,
|
||||
url: '/api/v2/groups/1',
|
||||
summary_fields: {
|
||||
inventory: {
|
||||
id: 1,
|
||||
},
|
||||
user_capabilities: {
|
||||
delete: true,
|
||||
edit: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'group',
|
||||
name: 'bar',
|
||||
inventory: 1,
|
||||
url: '/api/v2/groups/2',
|
||||
summary_fields: {
|
||||
inventory: {
|
||||
id: 1,
|
||||
},
|
||||
user_capabilities: {
|
||||
delete: true,
|
||||
edit: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'group',
|
||||
name: 'baz',
|
||||
inventory: 1,
|
||||
url: '/api/v2/groups/3',
|
||||
summary_fields: {
|
||||
inventory: {
|
||||
id: 1,
|
||||
},
|
||||
user_capabilities: {
|
||||
delete: false,
|
||||
edit: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('<InventoryHostGroupsList />', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
HostsAPI.readGroups.mockResolvedValue({
|
||||
data: {
|
||||
count: mockGroups.length,
|
||||
results: mockGroups,
|
||||
},
|
||||
});
|
||||
HostsAPI.readGroupsOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {},
|
||||
POST: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/inventory/1/hosts/3/groups'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Route
|
||||
path="/inventories/inventory/:id/hosts/:hostId/groups"
|
||||
component={() => <InventoryHostGroupsList />}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
router: { history, route: { location: history.location } },
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
|
||||
test('initially renders successfully', () => {
|
||||
expect(wrapper.find('InventoryHostGroupsList').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should fetch groups from api and render them in the list', async () => {
|
||||
expect(HostsAPI.readGroups).toHaveBeenCalled();
|
||||
expect(wrapper.find('InventoryHostGroupItem').length).toBe(3);
|
||||
});
|
||||
|
||||
test('should check and uncheck the row item', async () => {
|
||||
expect(
|
||||
wrapper.find('DataListCheck[id="select-group-1"]').props().checked
|
||||
).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('DataListCheck[id="select-group-1"]').invoke('onChange')(
|
||||
true
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('DataListCheck[id="select-group-1"]').props().checked
|
||||
).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('DataListCheck[id="select-group-1"]').invoke('onChange')(
|
||||
false
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('DataListCheck[id="select-group-1"]').props().checked
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('should check all row items when select all is checked', async () => {
|
||||
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);
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('Checkbox#select-all').invoke('onChange')(false);
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.find('DataListCheck').forEach(el => {
|
||||
expect(el.props().checked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
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(<InventoryHostGroupsList />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './InventoryHostGroups';
|
||||
Loading…
x
Reference in New Issue
Block a user