diff --git a/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx b/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx
index 131787ae95..2ec78aef4a 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx
@@ -15,7 +15,7 @@ import { getAddedAndRemoved } from '../../../util/lists';
function InventoryEdit({ history, i18n, inventory }) {
const [error, setError] = useState(null);
const [associatedInstanceGroups, setInstanceGroups] = useState(null);
- const [isLoading, setIsLoading] = useState(true);
+ const [contentLoading, setContentLoading] = useState(true);
const [credentialTypeId, setCredentialTypeId] = useState(null);
useEffect(() => {
@@ -39,11 +39,11 @@ function InventoryEdit({ history, i18n, inventory }) {
} catch (err) {
setError(err);
} finally {
- setIsLoading(false);
+ setContentLoading(false);
}
};
loadData();
- }, [inventory.id, isLoading, inventory, credentialTypeId]);
+ }, [inventory.id, contentLoading, inventory, credentialTypeId]);
const handleCancel = () => {
history.push('/inventories');
@@ -85,7 +85,7 @@ function InventoryEdit({ history, i18n, inventory }) {
history.push(`${url}`);
}
};
- if (isLoading) {
+ if (contentLoading) {
return ;
}
if (error) {
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx
index db03a1ecf2..1c510f4fe6 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx
@@ -14,8 +14,8 @@ import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail';
function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) {
const [inventoryGroup, setInventoryGroup] = useState(null);
- const [hasContentLoading, setHasContentLoading] = useState(true);
- const [contentError, setHasContentError] = useState(null);
+ const [contentLoading, setContentLoading] = useState(true);
+ const [contentError, setContentError] = useState(null);
useEffect(() => {
const loadData = async () => {
@@ -24,9 +24,9 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) {
setInventoryGroup(data);
setBreadcrumb(inventory, data);
} catch (err) {
- setHasContentError(err);
+ setContentError(err);
} finally {
- setHasContentLoading(false);
+ setContentLoading(false);
}
};
@@ -64,12 +64,32 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) {
id: 2,
},
];
+
+ // In cases where a user manipulates the url such that they try to navigate to a Inventory Group
+ // that is not associated with the Inventory Id in the Url this Content Error is thrown.
+ // Inventory Groups have a 1: 1 relationship to Inventories thus their Ids must corrolate.
+
+ if (contentLoading) {
+ return ;
+ }
+
+ if (
+ inventoryGroup.summary_fields.inventory.id !== parseInt(match.params.id, 10)
+ ) {
+ return (
+
+ {inventoryGroup && (
+
+ {i18n._(t`View Inventory Groups`)}
+
+ )}
+
+ );
+ }
+
if (contentError) {
return ;
}
- if (hasContentLoading) {
- return ;
- }
let cardHeader = null;
if (
@@ -80,12 +100,11 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) {
);
}
-
return (
<>
{cardHeader}
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx
index 4c1b50c95b..6273de12d8 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx
@@ -1,5 +1,6 @@
import React from 'react';
import { GroupsAPI } from '@api';
+import { Route } from 'react-router-dom';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
@@ -14,6 +15,7 @@ GroupsAPI.readDetail.mockResolvedValue({
description: 'Bar',
variables: 'bizz: buzz',
summary_fields: {
+ inventory: { id: 1 },
created_by: { id: 1, name: 'Athena' },
modified_by: { id: 1, name: 'Apollo' },
},
@@ -29,7 +31,12 @@ describe('', () => {
});
await act(async () => {
wrapper = mountWithContexts(
- {}} />,
+ (
+ {}} inventory={inventory} />
+ )}
+ />,
{
context: {
router: {
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupAdd/InventoryGroupAdd.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupAdd/InventoryGroupAdd.jsx
index 720e9d4b3c..eff8a2fe10 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroupAdd/InventoryGroupAdd.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroupAdd/InventoryGroupAdd.jsx
@@ -4,7 +4,7 @@ import { withRouter } from 'react-router-dom';
import { GroupsAPI } from '@api';
import { Card } from '@patternfly/react-core';
-import InventoryGroupForm from '../InventoryGroupForm/InventoryGroupForm';
+import InventoryGroupForm from '../shared/InventoryGroupForm';
function InventoryGroupsAdd({ history, inventory, setBreadcrumb }) {
const [error, setError] = useState(null);
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx
index 28b662e2ee..8cf97cffc0 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx
@@ -33,6 +33,9 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
summary_fields: { created_by, modified_by },
created,
modified,
+ name,
+ description,
+ variables,
} = inventoryGroup;
const [error, setError] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@@ -78,16 +81,13 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
return (
-
-
+
+
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/InventoryGroupEdit.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/InventoryGroupEdit.jsx
index 6ff0e58c58..230314ce7c 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/InventoryGroupEdit.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/InventoryGroupEdit.jsx
@@ -3,7 +3,7 @@ import { withI18n } from '@lingui/react';
import { withRouter } from 'react-router-dom';
import { GroupsAPI } from '@api';
-import InventoryGroupForm from '../InventoryGroupForm/InventoryGroupForm';
+import InventoryGroupForm from '../shared/InventoryGroupForm';
function InventoryGroupEdit({ history, inventoryGroup, inventory, match }) {
const [error, setError] = useState(null);
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupForm/index.js b/awx/ui_next/src/screens/Inventory/InventoryGroupForm/index.js
deleted file mode 100644
index 090b2c2f8a..0000000000
--- a/awx/ui_next/src/screens/Inventory/InventoryGroupForm/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './InventoryGroupForm';
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 8c60d8bfbd..d5a3665247 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx
@@ -1,81 +1,25 @@
import React from 'react';
-import { act } from 'react-dom/test-utils';
import { Route } from 'react-router-dom';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
-import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
-import { InventoriesAPI, GroupsAPI } from '@api';
-import InventoryGroupsList from './InventoryGroupsList';
+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: {},
- },
- },
- });
+describe('', () => {
+ 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(
}
+ component={() => (
+ {}} inventory={inventory} />
+ )}
/>,
{
context: {
@@ -84,134 +28,30 @@ describe('', () => {
}
);
});
- await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
- });
-
- test('initially renders successfully', () => {
+ expect(wrapper.length).toBe(1);
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);
-
+ 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.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/InventoryGroupForm/InventoryGroupForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx
similarity index 100%
rename from awx/ui_next/src/screens/Inventory/InventoryGroupForm/InventoryGroupForm.jsx
rename to awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupForm/InventoryGroupForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.test.jsx
similarity index 100%
rename from awx/ui_next/src/screens/Inventory/InventoryGroupForm/InventoryGroupForm.test.jsx
rename to awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.test.jsx