From e985b98d61fd9a1c82f8927d2cf9e98760a350d8 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Thu, 23 Feb 2023 13:43:54 -0500 Subject: [PATCH] Adds constructed inventory groups and related groups. --- .../src/api/models/ConstructedInventories.js | 1 - .../screens/Inventory/ConstructedInventory.js | 9 +- .../Inventory/ConstructedInventory.test.js | 6 - .../ConstructedInventoryGroups.js | 13 -- .../ConstructedInventoryGroups.test.js | 15 --- .../ConstructedInventoryGroups/index.js | 1 - .../InventoryGroup/InventoryGroup.js | 24 ++-- .../InventoryGroup/InventoryGroup.test.js | 82 ++++++++++-- .../InventoryGroupDetail.js | 57 ++++---- .../InventoryGroupDetail.test.js | 60 +++++++++ .../InventoryGroupHostList.js | 30 +++-- .../InventoryGroupHostList.test.js | 76 ++++++++++- .../InventoryGroupHostListItem.js | 38 +++--- .../InventoryGroupHostListItem.test.js | 98 ++++++++++---- .../InventoryGroupHosts.js | 2 +- .../InventoryGroups/InventoryGroupItem.js | 50 ++++--- .../InventoryGroupItem.test.js | 37 +++++ .../InventoryGroups/InventoryGroups.js | 9 +- .../InventoryGroups/InventoryGroupsList.js | 47 ++++--- .../InventoryGroupsList.test.js | 91 +++++++++++-- .../InventoryRelatedGroupList.js | 27 ++-- .../InventoryRelatedGroupList.test.js | 126 +++++++++++++++--- .../InventoryRelatedGroupListItem.js | 36 ++--- .../InventoryRelatedGroupListItem.test.js | 109 +++++++++++---- .../InventoryRelatedGroups.js | 4 +- 25 files changed, 765 insertions(+), 283 deletions(-) delete mode 100644 awx/ui/src/screens/Inventory/ConstructedInventoryGroups/ConstructedInventoryGroups.js delete mode 100644 awx/ui/src/screens/Inventory/ConstructedInventoryGroups/ConstructedInventoryGroups.test.js delete mode 100644 awx/ui/src/screens/Inventory/ConstructedInventoryGroups/index.js diff --git a/awx/ui/src/api/models/ConstructedInventories.js b/awx/ui/src/api/models/ConstructedInventories.js index b62bffd3f3..d1384e915e 100644 --- a/awx/ui/src/api/models/ConstructedInventories.js +++ b/awx/ui/src/api/models/ConstructedInventories.js @@ -7,5 +7,4 @@ class ConstructedInventories extends InstanceGroupsMixin(Base) { this.baseUrl = 'api/v2/constructed_inventories/'; } } - export default ConstructedInventories; diff --git a/awx/ui/src/screens/Inventory/ConstructedInventory.js b/awx/ui/src/screens/Inventory/ConstructedInventory.js index 242cbab585..086c755adb 100644 --- a/awx/ui/src/screens/Inventory/ConstructedInventory.js +++ b/awx/ui/src/screens/Inventory/ConstructedInventory.js @@ -22,7 +22,7 @@ import { ResourceAccessList } from 'components/ResourceAccessList'; import RoutedTabs from 'components/RoutedTabs'; import ConstructedInventoryDetail from './ConstructedInventoryDetail'; import ConstructedInventoryEdit from './ConstructedInventoryEdit'; -import ConstructedInventoryGroups from './ConstructedInventoryGroups'; +import InventoryGroups from './InventoryGroups'; import AdvancedInventoryHosts from './AdvancedInventoryHosts'; import { getInventoryPath } from './shared/utils'; @@ -164,9 +164,12 @@ function ConstructedInventory({ setBreadcrumb }) { , - + , ({ describe('', () => { let wrapper; - // beforeEach(async () => { - // ConstructedInventoriesAPI.readDetail.mockResolvedValue({ - // data: mockInventory, - // }); - // }); - test('should render expected tabs', async () => { ConstructedInventoriesAPI.readDetail.mockResolvedValue({ data: mockInventory, diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/ConstructedInventoryGroups.js b/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/ConstructedInventoryGroups.js deleted file mode 100644 index 964dfa9062..0000000000 --- a/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/ConstructedInventoryGroups.js +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint i18next/no-literal-string: "off" */ -import React from 'react'; -import { CardBody } from 'components/Card'; - -function ConstructedInventoryGroups() { - return ( - -
Coming Soon!
-
- ); -} - -export default ConstructedInventoryGroups; diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/ConstructedInventoryGroups.test.js b/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/ConstructedInventoryGroups.test.js deleted file mode 100644 index db2720ff44..0000000000 --- a/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/ConstructedInventoryGroups.test.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; -import ConstructedInventoryGroups from './ConstructedInventoryGroups'; - -describe('', () => { - test('initially renders successfully', async () => { - let wrapper; - await act(async () => { - wrapper = mountWithContexts(); - }); - expect(wrapper.length).toBe(1); - expect(wrapper.find('ConstructedInventoryGroups').length).toBe(1); - }); -}); diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/index.js b/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/index.js deleted file mode 100644 index 7f1b4343b2..0000000000 --- a/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './ConstructedInventoryGroups'; diff --git a/awx/ui/src/screens/Inventory/InventoryGroup/InventoryGroup.js b/awx/ui/src/screens/Inventory/InventoryGroup/InventoryGroup.js index 0a8bc79374..6a5765114f 100644 --- a/awx/ui/src/screens/Inventory/InventoryGroup/InventoryGroup.js +++ b/awx/ui/src/screens/Inventory/InventoryGroup/InventoryGroup.js @@ -23,7 +23,7 @@ function InventoryGroup({ setBreadcrumb, inventory }) { const [inventoryGroup, setInventoryGroup] = useState(null); const [contentLoading, setContentLoading] = useState(true); const [contentError, setContentError] = useState(null); - const { id: inventoryId, groupId } = useParams(); + const { id: inventoryId, groupId, inventoryType } = useParams(); const location = useLocation(); useEffect(() => { @@ -50,22 +50,22 @@ function InventoryGroup({ setBreadcrumb, inventory }) { {t`Back to Groups`} ), - link: `/inventories/inventory/${inventory.id}/groups`, + link: `/inventories/${inventoryType}/${inventoryId}/groups`, id: 99, }, { name: t`Details`, - link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup?.id}/details`, + link: `/inventories/${inventoryType}/${inventoryId}/groups/${inventoryGroup?.id}/details`, id: 0, }, { name: t`Related Groups`, - link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup?.id}/nested_groups`, + link: `/inventories/${inventoryType}/${inventoryId}/groups/${inventoryGroup?.id}/nested_groups`, id: 1, }, { name: t`Hosts`, - link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup?.id}/nested_hosts`, + link: `/inventories/${inventoryType}/${inventoryId}/groups/${inventoryGroup?.id}/nested_hosts`, id: 2, }, ]; @@ -105,32 +105,32 @@ function InventoryGroup({ setBreadcrumb, inventory }) { {showCardHeader && } {inventoryGroup && [ , , , , @@ -138,7 +138,7 @@ function InventoryGroup({ setBreadcrumb, inventory }) { {inventory && ( - + {t`View Inventory Details`} )} diff --git a/awx/ui/src/screens/Inventory/InventoryGroup/InventoryGroup.test.js b/awx/ui/src/screens/Inventory/InventoryGroup/InventoryGroup.test.js index ee468bf7d4..03182dba5c 100644 --- a/awx/ui/src/screens/Inventory/InventoryGroup/InventoryGroup.test.js +++ b/awx/ui/src/screens/Inventory/InventoryGroup/InventoryGroup.test.js @@ -11,15 +11,16 @@ import { import InventoryGroup from './InventoryGroup'; jest.mock('../../../api'); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ - id: 1, - groupId: 2, - }), -})); - describe('', () => { + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + groupId: 1, + inventoryType: 'inventory', + }), + })); + let wrapper; let history; const inventory = { id: 1, name: 'Foo' }; @@ -41,11 +42,11 @@ describe('', () => { }, }); history = createMemoryHistory({ - initialEntries: ['/inventories/inventory/1/groups/1/details'], + initialEntries: [`/inventories/inventory/1/groups/1/details`], }); await act(async () => { wrapper = mountWithContexts( - + {}} inventory={inventory} /> , { context: { router: { history } } } @@ -63,7 +64,7 @@ describe('', () => { expect(routedTabs).toHaveLength(1); const tabs = routedTabs.prop('tabsArray'); - expect(tabs[0].link).toEqual('/inventories/inventory/1/groups'); + expect(tabs[0].link).toEqual(`/inventories/inventory/1/groups`); expect(tabs[1].name).toEqual('Details'); expect(tabs[2].name).toEqual('Related Groups'); expect(tabs[3].name).toEqual('Hosts'); @@ -71,7 +72,7 @@ describe('', () => { test('should show content error when user attempts to navigate to erroneous route', async () => { history = createMemoryHistory({ - initialEntries: ['/inventories/inventory/1/groups/1/foobar'], + initialEntries: [`/inventories/inventory/1/groups/1/foobar`], }); await act(async () => { wrapper = mountWithContexts( @@ -92,3 +93,60 @@ describe('', () => { await waitForElement(wrapper, 'ContentError', (el) => el.length === 1); }); }); + +describe('constructed inventory', () => { + let wrapper; + let history; + const inventory = { id: 1, name: 'Foo' }; + + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + groupId: 2, + inventoryType: 'constructed_inventory', + }), + })); + + beforeEach(async () => { + GroupsAPI.readDetail.mockResolvedValue({ + data: { + id: 1, + name: 'Foo', + description: 'Bar', + variables: 'bizz: buzz', + summary_fields: { + inventory: { id: 1 }, + created_by: { id: 1, username: 'Athena' }, + modified_by: { id: 1, username: 'Apollo' }, + }, + created: '2020-04-25T01:23:45.678901Z', + modified: '2020-04-25T01:23:45.678901Z', + }, + }); + history = createMemoryHistory({ + initialEntries: [`/inventories/constructed_inventory/1/groups/1/details`], + }); + + await act(async () => { + wrapper = mountWithContexts( + + {}} inventory={inventory} /> + , + { context: { router: { history } } } + ); + }); + await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); + }); + + test('Constructed Inventory expect all tabs to exist, including Back to Groups', () => { + const routedTabs = wrapper.find('RoutedTabs'); + expect(routedTabs).toHaveLength(1); + + const tabs = routedTabs.prop('tabsArray'); + expect(tabs[0].link).toEqual(`/inventories/constructed_inventory/1/groups`); + expect(tabs[1].name).toEqual('Details'); + expect(tabs[2].name).toEqual('Related Groups'); + expect(tabs[3].name).toEqual('Hosts'); + }); +}); diff --git a/awx/ui/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.js b/awx/ui/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.js index 94fd284076..6200e49416 100644 --- a/awx/ui/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.js +++ b/awx/ui/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.js @@ -1,9 +1,8 @@ import React, { useState } from 'react'; import { t } from '@lingui/macro'; - +import { useHistory, useParams } from 'react-router-dom'; import { Button } from '@patternfly/react-core'; -import { useHistory, useParams } from 'react-router-dom'; import { VariablesDetail } from 'components/CodeEditor'; import { CardBody, CardActionsRow } from 'components/Card'; import ErrorDetail from 'components/ErrorDetail'; @@ -12,6 +11,7 @@ import { DetailList, Detail, UserDateDetail } from 'components/DetailList'; import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal'; function InventoryGroupDetail({ inventoryGroup }) { + const { inventoryType, id, groupId } = useParams(); const { summary_fields: { created_by, modified_by, user_capabilities }, created, @@ -22,7 +22,6 @@ function InventoryGroupDetail({ inventoryGroup }) { } = inventoryGroup; const [error, setError] = useState(false); const history = useHistory(); - const params = useParams(); return ( @@ -47,31 +46,33 @@ function InventoryGroupDetail({ inventoryGroup }) { user={modified_by} /> - - {user_capabilities?.edit && ( - - )} - {user_capabilities?.delete && ( - - history.push(`/inventories/inventory/${params.id}/groups`) - } - /> - )} - + {inventoryType !== 'constructed_inventory' && ( + + {user_capabilities?.edit && ( + + )} + {user_capabilities?.delete && ( + + history.push(`/inventories/inventory/${id}/groups`) + } + /> + )} + + )} {error && ( ', () => { let history; describe('User has full permissions', () => { + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + groupId: 3, + inventoryType: 'inventory', + }), + })); beforeEach(async () => { await act(async () => { history = createMemoryHistory({ @@ -116,6 +124,14 @@ describe('', () => { }); describe('User has read-only permissions', () => { + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + groupId: 3, + inventoryType: 'inventory', + }), + })); test('should hide edit/delete buttons', async () => { const readOnlyGroup = { ...inventoryGroup, @@ -159,4 +175,48 @@ describe('', () => { expect(wrapper.find('button[aria-label="Delete"]').length).toBe(0); }); }); + describe('Cannot edit or delete constructed inventory group', () => { + beforeEach(async () => { + await act(async () => { + history = createMemoryHistory({ + initialEntries: ['/inventories/inventory/1/groups/1/details'], + }); + wrapper = mountWithContexts( + + + , + { + context: { + router: { + history, + route: { + location: history.location, + match: { + params: { + id: 1, + group: 2, + inventoryType: 'constructed_inventory', + }, + }, + }, + }, + }, + } + ); + await waitForElement( + wrapper, + 'ContentLoading', + (el) => el.length === 0 + ); + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + test('should not show edit button', () => { + const editButton = wrapper.find('Button[aria-label="edit"]'); + expect(editButton.length).toBe(0); + expect(wrapper.find('Button[aria-label="delete"]').length).toBe(0); + }); + }); }); diff --git a/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.js b/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.js index 2825715ede..42903b7db6 100644 --- a/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.js +++ b/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.js @@ -34,7 +34,7 @@ const QS_CONFIG = getQSConfig('host', { function InventoryGroupHostList() { const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); - const { id: inventoryId, groupId } = useParams(); + const { id: inventoryId, groupId, inventoryType } = useParams(); const location = useLocation(); const { @@ -145,9 +145,11 @@ function InventoryGroupHostList() { useDismissableError(associateErr); const { error: disassociateError, dismissError: dismissDisassociateError } = useDismissableError(disassociateErr); - + const isNotConstructedInventory = inventoryType !== 'constructed_inventory'; const canAdd = - actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); + actions && + Object.prototype.hasOwnProperty.call(actions, 'POST') && + isNotConstructedInventory; const addFormUrl = `/inventories/inventory/${inventoryId}/groups/${groupId}/nested_hosts/add`; const addExistingHost = t`Add existing host`; const addNewHost = t`Add new host`; @@ -240,17 +242,21 @@ function InventoryGroupHostList() { />, ] : []), - , + />, + ] + : []), ]} /> )} @@ -259,8 +265,8 @@ function InventoryGroupHostList() { key={host.id} rowIndex={index} host={host} - detailUrl={`/inventories/inventory/${inventoryId}/hosts/${host.id}/details`} - editUrl={`/inventories/inventory/${inventoryId}/hosts/${host.id}/edit`} + detailUrl={`/inventories/${inventoryType}/${inventoryId}/hosts/${host.id}/details`} + editUrl={`/inventories/${inventoryType}/${inventoryId}/hosts/${host.id}/edit`} isSelected={selected.some((row) => row.id === host.id)} onSelect={() => handleSelect(host)} /> diff --git a/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.test.js b/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.test.js index 4205a43171..3385e38d71 100644 --- a/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.test.js +++ b/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.test.js @@ -8,19 +8,20 @@ import { } from '../../../../testUtils/enzymeHelpers'; import InventoryGroupHostList from './InventoryGroupHostList'; import mockHosts from '../shared/data.hosts.json'; +import { Route } from 'react-router-dom'; jest.mock('../../../api/models/Groups'); jest.mock('../../../api/models/Inventories'); jest.mock('../../../api/models/CredentialTypes'); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ - id: 1, - groupId: 2, - }), -})); describe('', () => { + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + groupId: 2, + }), + })); let wrapper; beforeEach(async () => { @@ -303,3 +304,64 @@ describe('', () => { expect(wrapper.find('AdHocCommands')).toHaveLength(0); }); }); + +describe(' for constructed inventories', () => { + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + groupId: 2, + inventoryType: 'constructed_inventory', + }), + })); + let wrapper; + + beforeEach(async () => { + GroupsAPI.readAllHosts.mockResolvedValue({ + data: { ...mockHosts }, + }); + InventoriesAPI.readHostsOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + }, + }); + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { + module_name: { + choices: [ + ['command', 'command'], + ['shell', 'shell'], + ], + }, + }, + POST: {}, + }, + }, + }); + const history = createMemoryHistory({ + initialEntries: ['/inventories/constructed_inventory/1/groups/2/hosts'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { context: { router: { history } } } + ); + }); + await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + test('Should not show associate, or disassociate button', async () => { + expect(wrapper.find('AddDropDownButton').length).toBe(0); + expect(wrapper.find('DisassociateButton').length).toBe(0); + }); +}); diff --git a/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.js b/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.js index b5d56925b4..f5e435c024 100644 --- a/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.js +++ b/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.js @@ -1,6 +1,6 @@ import 'styled-components/macro'; import React from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useParams } from 'react-router-dom'; import { string, bool, func, number } from 'prop-types'; import { t } from '@lingui/macro'; import { Button, Tooltip } from '@patternfly/react-core'; @@ -24,7 +24,7 @@ function InventoryGroupHostListItem({ ...job, type: 'job', })); - + const { inventoryType } = useParams(); const labelId = `check-action-${host.id}`; return ( @@ -57,22 +57,24 @@ function InventoryGroupHostListItem({ > - - - - - + {inventoryType !== 'constructed_inventory' && ( + + + + + + )} ); diff --git a/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.test.js b/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.test.js index c26ac566f8..4667c9b02d 100644 --- a/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.test.js +++ b/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.test.js @@ -1,28 +1,35 @@ import React from 'react'; +import { createMemoryHistory } from 'history'; import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; import InventoryGroupHostListItem from './InventoryGroupHostListItem'; import mockHosts from '../shared/data.hosts.json'; +import { Route } from 'react-router-dom'; jest.mock('../../../api'); describe('', () => { let wrapper; const mockHost = mockHosts.results[0]; - + const history = createMemoryHistory({ + initialEntries: ['/inventories/inventory/1/groups/2/hosts'], + }); beforeEach(() => { wrapper = mountWithContexts( - - - {}} - rowIndex={0} - /> - -
+ + + + {}} + rowIndex={0} + /> + +
+
, + { context: { router: { history } } } ); }); @@ -52,19 +59,60 @@ describe('', () => { const copyMockHost = { ...mockHost }; copyMockHost.summary_fields.user_capabilities.edit = false; wrapper = mountWithContexts( - - - {}} - rowIndex={0} - /> - -
+ + + + {}} + rowIndex={0} + /> + +
+
, + { context: { router: { history } } } ); expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy(); }); }); + +describe(' inside constructed inventories', () => { + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + groupId: 2, + inventoryType: 'constructed_inventory', + }), + })); + let wrapper; + const mockHost = mockHosts.results[0]; + const history = createMemoryHistory({ + initialEntries: ['/inventories/constructed_inventory/1/groups/2/hosts'], + }); + beforeEach(() => { + wrapper = mountWithContexts( + + + + {}} + rowIndex={0} + /> + +
+
, + { context: { router: { history } } } + ); + }); + test('Edit button hidden for constructed inventory', () => { + expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy(); + }); +}); diff --git a/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHosts.js b/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHosts.js index d0e4c34d70..696b5bede8 100644 --- a/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHosts.js +++ b/awx/ui/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHosts.js @@ -9,7 +9,7 @@ function InventoryGroupHosts({ inventoryGroup }) { - +
diff --git a/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroupItem.js b/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroupItem.js index 966f4fe2a5..2f8b5b2ab4 100644 --- a/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroupItem.js +++ b/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroupItem.js @@ -1,25 +1,20 @@ import React from 'react'; -import { bool, func, number, oneOfType, string } from 'prop-types'; +import { bool, func } from 'prop-types'; import { t } from '@lingui/macro'; import { Button } from '@patternfly/react-core'; import { Tr, Td } from '@patternfly/react-table'; -import { Link } from 'react-router-dom'; +import { Link, useParams } from 'react-router-dom'; import { PencilAltIcon } from '@patternfly/react-icons'; import { ActionsTd, ActionItem } from 'components/PaginatedTable'; import { Group } from 'types'; -function InventoryGroupItem({ - group, - inventoryId, - isSelected, - onSelect, - rowIndex, -}) { +function InventoryGroupItem({ group, isSelected, onSelect, rowIndex }) { + const { id: inventoryId, inventoryType } = useParams(); const labelId = `check-action-${group.id}`; - const detailUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/details`; - const editUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/edit`; + const detailUrl = `/inventories/${inventoryType}/${inventoryId}/groups/${group.id}/details`; + const editUrl = `/inventories/${inventoryType}/${inventoryId}/groups/${group.id}/edit`; return ( @@ -36,29 +31,30 @@ function InventoryGroupItem({ {group.name} - - - - - + + + + )} ); } InventoryGroupItem.propTypes = { group: Group.isRequired, - inventoryId: oneOfType([number, string]).isRequired, isSelected: bool.isRequired, onSelect: func.isRequired, }; diff --git a/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroupItem.test.js b/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroupItem.test.js index cb4956e44a..49f4fe22b5 100644 --- a/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroupItem.test.js +++ b/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroupItem.test.js @@ -1,4 +1,6 @@ import React from 'react'; +import { Route } from 'react-router-dom'; +import { act } from 'react-dom/test-utils'; import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; import InventoryGroupItem from './InventoryGroupItem'; @@ -57,4 +59,39 @@ describe('', () => { ); expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy(); }); + test('edit button should be hidden from constructed inventory group', async () => { + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ id: 42, inventoryType: 'constructed_inventory' }), + })); + const mockGroup = { + id: 2, + type: 'group', + name: 'foo', + inventory: 1, + summary_fields: { + user_capabilities: { + edit: true, + }, + }, + }; + + await act(async () => { + wrapper = mountWithContexts( + + + + {}} + /> + +
+
+ ); + }); + expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy(); + }); }); diff --git a/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroups.js b/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroups.js index ae19f09660..97eef35dc6 100644 --- a/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroups.js +++ b/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroups.js @@ -16,11 +16,14 @@ function InventoryGroups({ setBreadcrumb, inventory }) { inventory={inventory} />
- + - - + + ); diff --git a/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroupsList.js b/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroupsList.js index 77bd67c404..fa474845c6 100644 --- a/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroupsList.js +++ b/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroupsList.js @@ -29,7 +29,7 @@ function cannotDelete(item) { function InventoryGroupsList() { const location = useLocation(); - const { id: inventoryId } = useParams(); + const { id: inventoryId, inventoryType } = useParams(); const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false); const { @@ -102,9 +102,11 @@ function InventoryGroupsList() { } return t`Select a row to delete`; }; - + const isNotConstructedInventory = inventoryType !== 'constructed_inventory'; const canAdd = - actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); + actions && + Object.prototype.hasOwnProperty.call(actions, 'POST') && + isNotConstructedInventory; return ( {t`Name`} - {t`Actions`} + {isNotConstructedInventory && {t`Actions`}} } renderRow={(item, index) => ( row.id === item.id)} onSelect={() => handleSelect(item)} rowIndex={index} @@ -177,20 +178,28 @@ function InventoryGroupsList() { />, ] : []), - -
- { - fetchData(); - clearSelected(); - }} - /> -
-
, + ...(isNotConstructedInventory + ? [ + +
+ { + fetchData(); + clearSelected(); + }} + /> +
+
, + ] + : []), ]} /> )} diff --git a/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.js b/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.js index 5743fc96c8..d33127899a 100644 --- a/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.js +++ b/awx/ui/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.js @@ -10,12 +10,6 @@ import { import InventoryGroupsList from './InventoryGroupsList'; jest.mock('../../../api'); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ - id: 1, - }), -})); const mockGroups = [ { id: 1, @@ -60,7 +54,14 @@ const mockGroups = [ describe('', () => { let wrapper; - + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + groupId: 2, + inventoryType: 'inventory', + }), + })); beforeEach(async () => { InventoriesAPI.readGroups.mockResolvedValue({ data: { @@ -96,7 +97,7 @@ describe('', () => { }); await act(async () => { wrapper = mountWithContexts( - + , { @@ -316,3 +317,77 @@ describe(' error handling', () => { }); }); }); + +describe('Constructed Inventory group', () => { + let wrapper; + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + groupId: 2, + inventoryType: 'constructed_inventory', + }), + })); + + beforeEach(async () => { + InventoriesAPI.readGroups.mockResolvedValue({ + data: { + count: mockGroups.length, + results: mockGroups, + }, + }); + InventoriesAPI.readGroupsOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + }, + }); + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { + module_name: { + choices: [ + ['command', 'command'], + ['shell', 'shell'], + ], + }, + }, + POST: {}, + }, + }, + }); + const history = createMemoryHistory({ + initialEntries: ['/inventories/constructed_inventory/3/groups'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { + router: { + history, + route: { + location: history.location, + }, + }, + }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + test('should not show add button', () => { + expect(wrapper.find('ToolbarAddButton').length).toBe(0); + expect(wrapper.find('ToolbarDeleteButton').length).toBe(0); + expect(wrapper.find('AdHocCommands').length).toBe(1); + }); +}); diff --git a/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.js b/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.js index 98aa701e00..0b8ec5054d 100644 --- a/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.js +++ b/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.js @@ -33,7 +33,7 @@ function InventoryRelatedGroupList() { const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false); const [associateError, setAssociateError] = useState(null); const [disassociateError, setDisassociateError] = useState(null); - const { id: inventoryId, groupId } = useParams(); + const { id: inventoryId, groupId, inventoryType } = useParams(); const location = useLocation(); const { @@ -69,9 +69,10 @@ function InventoryRelatedGroupList() { searchableKeys: getSearchableKeys(actions.data.actions?.GET), canAdd: actions.data.actions && - Object.prototype.hasOwnProperty.call(actions.data.actions, 'POST'), + Object.prototype.hasOwnProperty.call(actions.data.actions, 'POST') && + inventoryType !== 'constructed_inventory', }; - }, [groupId, location.search, inventoryId]), + }, [groupId, location.search, inventoryType, inventoryId]), { groups: [], itemCount: 0, @@ -164,7 +165,7 @@ function InventoryRelatedGroupList() { ]} /> ); - + const isNotConstructedInventory = inventoryType !== 'constructed_inventory'; return ( <> , ] : []), - , + ...(isNotConstructedInventory + ? [ + , + ] + : []), ]} /> )} headerRow={ {t`Name`} - {t`Actions`} + {isNotConstructedInventory && {t`Actions`}} } renderRow={(group, index) => ( diff --git a/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.test.js b/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.test.js index 0c6045b7dd..a8cba4aeac 100644 --- a/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.test.js +++ b/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.test.js @@ -1,6 +1,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; - +import { createMemoryHistory } from 'history'; +import { Route } from 'react-router-dom'; import { GroupsAPI, InventoriesAPI } from 'api'; import { mountWithContexts, @@ -13,14 +14,6 @@ jest.mock('../../../api/models/Groups'); jest.mock('../../../api/models/Inventories'); jest.mock('../../../api/models/CredentialTypes'); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ - id: 1, - groupId: 2, - }), -})); - const mockGroups = [ { id: 1, @@ -65,6 +58,14 @@ const mockGroups = [ describe('', () => { let wrapper; + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 2, + groupId: 2, + inventoryType: 'inventory', + }), + })); beforeEach(async () => { GroupsAPI.readChildren.mockResolvedValue({ @@ -210,11 +211,22 @@ describe('', () => { GroupsAPI.readPotentialGroups.mockResolvedValue({ data: { count: mockGroups.length, results: mockGroups }, }); - await act(async () => { - wrapper = mountWithContexts(); + const history = createMemoryHistory({ + initialEntries: ['/inventories/inventory/2/groups/2/nested_groups'], }); - await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); - + await act(async () => { + wrapper = mountWithContexts( + + + , + { context: { router: { history } } } + ); + }); + await waitForElement( + wrapper, + 'InventoryRelatedGroupList', + (el) => el.length > 0 + ); act(() => wrapper.find('Button[aria-label="Add"]').prop('onClick')()); wrapper.update(); await act(async () => @@ -222,9 +234,9 @@ describe('', () => { .find('DropdownItem[aria-label="Add existing group"]') .prop('onClick')() ); - expect(GroupsAPI.readPotentialGroups).toBeCalledWith(2, { - not__id: 2, - not__parents: 2, + expect(GroupsAPI.readPotentialGroups).toBeCalledWith('2', { + not__id: '2', + not__parents: '2', order_by: 'name', page: 1, page_size: 5, @@ -261,3 +273,85 @@ describe('', () => { expect(wrapper.find('AdHocCommands')).toHaveLength(0); }); }); + +describe(' for constructed inventories', () => { + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + groupId: 2, + inventoryType: 'constructed_inventory', + }), + })); + let wrapper; + + beforeEach(async () => { + GroupsAPI.readChildren.mockResolvedValue({ + data: { ...mockRelatedGroups }, + }); + InventoriesAPI.readGroupsOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [ + 'parents__search', + 'inventory__search', + 'inventory_sources__search', + 'created_by__search', + 'children__search', + 'modified_by__search', + 'hosts__search', + ], + }, + }); + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { + module_name: { + choices: [ + ['command', 'command'], + ['shell', 'shell'], + ], + }, + }, + POST: {}, + }, + }, + }); + const history = createMemoryHistory({ + initialEntries: [ + '/inventories/constructed_inventory/1/groups/2/nested_groupss', + ], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { context: { router: { history } } } + ); + }); + await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Should not show associate, or disassociate button', async () => { + InventoriesAPI.readHostsOptions.mockResolvedValueOnce({ + data: { + actions: { + GET: {}, + }, + }, + }); + + await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); + expect(wrapper.find('AddDropDownButton').length).toBe(0); + expect(wrapper.find('DisassociateButton').length).toBe(0); + }); +}); diff --git a/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.js b/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.js index 3c2c9c090f..b30c872ba7 100644 --- a/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.js +++ b/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.js @@ -1,6 +1,6 @@ import 'styled-components/macro'; import React from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useParams } from 'react-router-dom'; import { string, bool, func, number } from 'prop-types'; import { t } from '@lingui/macro'; @@ -21,7 +21,7 @@ function InventoryRelatedGroupListItem({ onSelect, }) { const labelId = `check-action-${group.id}`; - + const { inventoryType } = useParams(); return ( {group.name} - - - - - + + + + )} ); } diff --git a/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.test.js b/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.test.js index 4ab8fb17b1..eb3b6d99c2 100644 --- a/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.test.js +++ b/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.test.js @@ -1,28 +1,43 @@ import React from 'react'; +import { createMemoryHistory } from 'history'; import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; import InventoryRelatedGroupListItem from './InventoryRelatedGroupListItem'; import mockRelatedGroups from '../shared/data.relatedGroups.json'; +import { Route } from 'react-router-dom'; jest.mock('../../../api'); +const mockGroup = mockRelatedGroups.results[0]; describe('', () => { let wrapper; - const mockGroup = mockRelatedGroups.results[0]; - + const history = createMemoryHistory({ + initialEntries: ['/inventories/inventory/1/groups/2/nested_groups'], + }); + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + groupId: 2, + inventoryType: 'inventory', + }), + })); beforeEach(() => { wrapper = mountWithContexts( - - - {}} - rowIndex={0} - /> - -
+ + + + {}} + rowIndex={0} + /> + +
+
, + { context: { router: { history } } } ); }); @@ -36,18 +51,60 @@ describe('', () => { test('edit button hidden from users without edit capabilities', () => { wrapper = mountWithContexts( - - - {}} - rowIndex={0} - /> - -
+ + + + {}} + rowIndex={0} + /> + +
+
, + { context: { router: { history } } } + ); + expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy(); + }); +}); + +describe(' for constructed inventories', () => { + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + groupId: 2, + inventoryType: 'constructed_inventory', + }), + })); + + let wrapper; + + test('edit button hidden from users without edit capabilities', () => { + const history = createMemoryHistory({ + initialEntries: [ + '/inventories/constructed_inventory/1/groups/2/nested_groups', + ], + }); + wrapper = mountWithContexts( + + + + {}} + rowIndex={0} + /> + +
+
, + { context: { router: { history } } } ); expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy(); }); diff --git a/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroups.js b/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroups.js index d5904062b3..bca8ffc26a 100644 --- a/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroups.js +++ b/awx/ui/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroups.js @@ -8,13 +8,13 @@ function InventoryRelatedGroups() {