{}}
+ inventory={{ id: 1 }}
+ inventoryGroup={{ id: 2 }}
+ />
+ )}
+ />,
+ {
+ context: {
+ router: {
+ history,
+ route: {
+ match: {
+ params: { groupId: 13 },
+ },
+ location: history.location,
+ },
+ },
+ },
+ }
+ );
+ });
+ });
+ afterEach(() => {
+ wrapper.unmount();
+ });
+ test('InventoryGroupEdit renders successfully', () => {
+ expect(wrapper.length).toBe(1);
+ });
+ test('cancel should navigate user to Inventory Groups List', async () => {
+ wrapper.find('button[aria-label="Cancel"]').simulate('click');
+ expect(history.location.pathname).toEqual(
+ '/inventories/inventory/1/groups/2'
+ );
+ });
+ test('handleSubmit should call api', async () => {
+ wrapper.find('InventoryGroupForm').prop('handleSubmit')({
+ name: 'Bar',
+ description: 'Ansible',
+ variables: 'ying: yang',
+ });
+ expect(GroupsAPI.update).toBeCalled();
+ });
+});
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/index.js b/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/index.js
new file mode 100644
index 0000000000..75519c821b
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/index.js
@@ -0,0 +1 @@
+export { default } from './InventoryGroupEdit';
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupItem.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupItem.jsx
index c8f6b78621..91f0b0c4f9 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupItem.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupItem.jsx
@@ -27,7 +27,7 @@ function InventoryGroupItem({
onSelect,
}) {
const labelId = `check-action-${group.id}`;
- const detailUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/detail`;
+ const detailUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/details`;
const editUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/edit`;
return (
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx
index 49f55cd4d8..2917f3f96d 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx
@@ -1,250 +1,45 @@
-import React, { useState, useEffect } from 'react';
-import { TrashAltIcon } from '@patternfly/react-icons';
-import { withRouter } from 'react-router-dom';
+import React from 'react';
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';
-const QS_CONFIG = getQSConfig('host', {
- page: 1,
- page_size: 20,
- order_by: 'name',
-});
+import { Switch, Route, withRouter } from 'react-router-dom';
-const DeleteButton = styled(Button)`
- padding: 5px 8px;
+import InventoryGroupAdd from '../InventoryGroupAdd/InventoryGroupAdd';
- &: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;
-}
-
-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 (
-
- {i18n._(
- t`You do not have permission to delete the following Groups: ${itemsUnableToDelete}`
- )}
-
- );
- }
- if (selected.length) {
- return i18n._(t`Delete`);
- }
- return i18n._(t`Select a row to delete`);
- };
-
- const handleDelete = async option => {
- setIsLoading(true);
-
- try {
- /* eslint-disable no-await-in-loop, no-restricted-syntax */
- /* Delete groups sequentially to avoid api integrity errors */
- for (const group of selected) {
- if (option === 'delete') {
- await GroupsAPI.destroy(+group.id);
- } else if (option === 'promote') {
- await InventoriesAPI.promoteGroup(inventoryId, +group.id);
- }
- }
- /* eslint-enable no-await-in-loop, no-restricted-syntax */
- } catch (error) {
- setDeletionError(error);
- }
-
- toggleModal();
-
- try {
- const {
- data: { count, results },
- } = await fetchGroups(inventoryId, location.search);
- setGroups(results);
- setGroupCount(count);
- } catch (error) {
- setContentError(error);
- }
-
- setIsLoading(false);
- };
-
- const canAdd =
- actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
- const isAllSelected =
- selected.length > 0 && selected.length === groups.length;
+import InventoryGroup from '../InventoryGroup/InventoryGroup';
+import InventoryGroupsList from './InventoryGroupsList';
+function InventoryGroups({ setBreadcrumb, inventory, location, match }) {
return (
- <>
- (
- row.id === item.id)}
- onSelect={() => handleSelect(item)}
- />
- )}
- renderToolbar={props => (
-
-
-
-
-
-
- ,
- canAdd && (
-
- ),
- ]}
- />
- )}
- emptyStateControls={
- canAdd && (
-
+ {
+ return (
+
- )
- }
+ );
+ }}
/>
- {deletionError && (
- setDeletionError(null)}
- >
- {i18n._(t`Failed to delete one or more groups.`)}
-
-
- )}
- (
+
+ )}
/>
- >
+ {
+ return ;
+ }}
+ />
+
);
}
+export { InventoryGroups as _InventoryGroups };
export default withI18n()(withRouter(InventoryGroups));
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx
index 2b5a7340c0..935ef7bb04 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx
@@ -1,82 +1,23 @@
import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { act } from 'react-dom/test-utils';
-import { Route } from 'react-router-dom';
import { createMemoryHistory } from 'history';
-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('', () => {
- let wrapper;
-
- beforeEach(async () => {
- InventoriesAPI.readGroups.mockResolvedValue({
- data: {
- count: mockGroups.length,
- results: mockGroups,
- },
- });
- InventoriesAPI.readGroupsOptions.mockResolvedValue({
- data: {
- actions: {
- GET: {},
- POST: {},
- },
- },
- });
+ test('initially renders successfully', async () => {
+ let wrapper;
const history = createMemoryHistory({
- initialEntries: ['/inventories/inventory/3/groups'],
+ initialEntries: ['/inventories/inventory/1/groups'],
});
+ const inventory = { id: 1, name: 'Foo' };
+
await act(async () => {
wrapper = mountWithContexts(
- }
- />,
+ {}} inventory={inventory} />,
+
{
context: {
router: { history, route: { location: history.location } },
@@ -84,134 +25,25 @@ describe('', () => {
}
);
});
- await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ expect(wrapper.length).toBe(1);
+ expect(wrapper.find('InventoryGroupsList').length).toBe(1);
});
-
- 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);
-
+ test('test that InventoryGroupsAdd renders', async () => {
+ const history = createMemoryHistory({
+ initialEntries: ['/inventories/inventory/1/groups/add'],
+ });
+ const inventory = { id: 1, name: 'Foo' };
+ let wrapper;
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();
- });
- 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',
+ wrapper = mountWithContexts(
+ {}} inventory={inventory} />,
+ {
+ context: {
+ router: { history, route: { location: history.location } },
},
- 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('Radio[id="radio-delete"]').invoke('onChange')();
- });
- wrapper.update();
- 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')();
+ }
+ );
});
+ expect(wrapper.find('InventoryGroupsAdd').length).toBe(1);
});
});
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx
new file mode 100644
index 0000000000..1840c3815c
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx
@@ -0,0 +1,248 @@
+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';
+
+const QS_CONFIG = getQSConfig('group', {
+ 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;
+}
+
+const useModal = () => {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ function toggleModal() {
+ setIsModalOpen(!isModalOpen);
+ }
+
+ return {
+ isModalOpen,
+ toggleModal,
+ };
+};
+
+function InventoryGroupsList({ 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 (
+
+ {i18n._(
+ t`You do not have permission to delete the following Groups: ${itemsUnableToDelete}`
+ )}
+
+ );
+ }
+ if (selected.length) {
+ return i18n._(t`Delete`);
+ }
+ return i18n._(t`Select a row to delete`);
+ };
+
+ const handleDelete = async option => {
+ setIsLoading(true);
+
+ try {
+ /* eslint-disable no-await-in-loop, no-restricted-syntax */
+ /* Delete groups sequentially to avoid api integrity errors */
+ for (const group of selected) {
+ if (option === 'delete') {
+ await GroupsAPI.destroy(+group.id);
+ } else if (option === 'promote') {
+ await InventoriesAPI.promoteGroup(inventoryId, +group.id);
+ }
+ }
+ /* eslint-enable no-await-in-loop, no-restricted-syntax */
+ } catch (error) {
+ setDeletionError(error);
+ }
+
+ toggleModal();
+
+ try {
+ const {
+ data: { count, results },
+ } = await fetchGroups(inventoryId, location.search);
+ setGroups(results);
+ setGroupCount(count);
+ } catch (error) {
+ setContentError(error);
+ }
+
+ setIsLoading(false);
+ };
+
+ const canAdd =
+ actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
+ const isAllSelected =
+ selected.length > 0 && selected.length === groups.length;
+
+ return (
+ <>
+ (
+ row.id === item.id)}
+ onSelect={() => handleSelect(item)}
+ />
+ )}
+ renderToolbar={props => (
+
+
+
+
+
+
+ ,
+ canAdd && (
+
+ ),
+ ]}
+ />
+ )}
+ emptyStateControls={
+ canAdd && (
+
+ )
+ }
+ />
+ {deletionError && (
+ setDeletionError(null)}
+ >
+ {i18n._(t`Failed to delete one or more groups.`)}
+
+
+ )}
+
+ >
+ );
+}
+export default withI18n()(withRouter(InventoryGroupsList));
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.jsx
new file mode 100644
index 0000000000..8c60d8bfbd
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.jsx
@@ -0,0 +1,217 @@
+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 { InventoriesAPI, GroupsAPI } from '@api';
+import InventoryGroupsList from './InventoryGroupsList';
+
+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('', () => {
+ let wrapper;
+
+ beforeEach(async () => {
+ InventoriesAPI.readGroups.mockResolvedValue({
+ data: {
+ count: mockGroups.length,
+ results: mockGroups,
+ },
+ });
+ InventoriesAPI.readGroupsOptions.mockResolvedValue({
+ data: {
+ actions: {
+ GET: {},
+ POST: {},
+ },
+ },
+ });
+ const history = createMemoryHistory({
+ initialEntries: ['/inventories/inventory/3/groups'],
+ });
+ await act(async () => {
+ wrapper = mountWithContexts(
+ }
+ />,
+ {
+ context: {
+ router: { history, route: { location: history.location } },
+ },
+ }
+ );
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ });
+
+ test('initially renders successfully', () => {
+ expect(wrapper.find('InventoryGroupsList').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();
+ });
+ 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('Radio[id="radio-delete"]').invoke('onChange')();
+ });
+ wrapper.update();
+ 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')();
+ });
+ });
+});
diff --git a/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx
new file mode 100644
index 0000000000..f6dea49aee
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import { withRouter } from 'react-router-dom';
+import { withI18n } from '@lingui/react';
+import { Formik } from 'formik';
+import { Form, Card, CardBody } from '@patternfly/react-core';
+import { t } from '@lingui/macro';
+
+import FormRow from '@components/FormRow';
+import FormField from '@components/FormField';
+import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
+import { VariablesField } from '@components/CodeMirrorInput';
+import { required } from '@util/validators';
+
+function InventoryGroupForm({
+ i18n,
+ error,
+ group = {},
+ handleSubmit,
+ handleCancel,
+}) {
+ const initialValues = {
+ name: group.name || '',
+ description: group.description || '',
+ variables: group.variables || '---',
+ };
+
+ return (
+
+
+ (
+
+ )}
+ />
+
+
+ );
+}
+
+export default withI18n()(withRouter(InventoryGroupForm));
diff --git a/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.test.jsx
new file mode 100644
index 0000000000..ebf459f76f
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.test.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import InventoryGroupForm from './InventoryGroupForm';
+
+const group = {
+ id: 1,
+ name: 'Foo',
+ description: 'Bar',
+ variables: 'ying: false',
+};
+describe('', () => {
+ let wrapper;
+ beforeEach(() => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ afterEach(() => {
+ wrapper.unmount();
+ });
+ test('initially renders successfully', () => {
+ expect(wrapper.length).toBe(1);
+ });
+ test('should render values for the fields that have them', () => {
+ expect(wrapper.find("FormGroup[label='Name']").length).toBe(1);
+ expect(wrapper.find("FormGroup[label='Description']").length).toBe(1);
+ expect(wrapper.find("VariablesField[label='Variables']").length).toBe(1);
+ });
+});