diff --git a/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx b/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx index 0e90017b22..964806f05e 100644 --- a/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx +++ b/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx @@ -3,6 +3,7 @@ import { shape, string, number, arrayOf } from 'prop-types'; import { Tab, Tabs as PFTabs } from '@patternfly/react-core'; import { withRouter } from 'react-router-dom'; import styled from 'styled-components'; +import { CaretLeftIcon } from '@patternfly/react-icons'; const Tabs = styled(PFTabs)` --pf-c-tabs__button--PaddingLeft: 20px; @@ -62,7 +63,15 @@ function RoutedTabs(props) { eventKey={tab.id} key={tab.id} link={tab.link} - title={tab.name} + title={ + tab.isNestedTabs ? ( + <> + {tab.name} + + ) : ( + tab.name + ) + } /> ))} diff --git a/awx/ui_next/src/screens/Inventory/Inventories.jsx b/awx/ui_next/src/screens/Inventory/Inventories.jsx index 8e48139bc9..4253078486 100644 --- a/awx/ui_next/src/screens/Inventory/Inventories.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventories.jsx @@ -61,9 +61,11 @@ class Inventories extends Component { t`Create New Group` ), [`/inventories/inventory/${inventory.id}/groups/${group && - group.id}/details`]: i18n._(t`Details`), + group.id}`]: `${group && group.name}`, [`/inventories/inventory/${inventory.id}/groups/${group && - group.id}/edit`]: `${group && group.name}`, + group.id}/details`]: i18n._(t`Group Details`), + [`/inventories/inventory/${inventory.id}/groups/${group && + group.id}/edit`]: i18n._(t`Edit Details`), }; this.setState({ breadcrumbConfig }); }; diff --git a/awx/ui_next/src/screens/Inventory/Inventory.jsx b/awx/ui_next/src/screens/Inventory/Inventory.jsx index da49aa6d8e..f3e78d584b 100644 --- a/awx/ui_next/src/screens/Inventory/Inventory.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventory.jsx @@ -57,7 +57,11 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) { ); - if (location.pathname.endsWith('edit') || location.pathname.endsWith('add')) { + if ( + location.pathname.endsWith('edit') || + location.pathname.endsWith('add') || + location.pathname.includes('groups/') + ) { cardHeader = null; } @@ -127,6 +131,7 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) { diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx index d665c19176..f5fbada3fe 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx @@ -1,18 +1,18 @@ import React, { useEffect, useState } from 'react'; import { t } from '@lingui/macro'; import { withI18n } from '@lingui/react'; +import { CardHeader } from '@patternfly/react-core'; import { Switch, Route, withRouter, Link, Redirect } from 'react-router-dom'; import { GroupsAPI } from '@api'; - +import CardCloseButton from '@components/CardCloseButton'; +import RoutedTabs from '@components/RoutedTabs'; import ContentError from '@components/ContentError'; import ContentLoading from '@components/ContentLoading'; - import InventoryGroupEdit from '../InventoryGroupEdit/InventoryGroupEdit'; - import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail'; -function InventoryGroups({ i18n, match, setBreadcrumb, inventory }) { +function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { const [inventoryGroup, setInventoryGroup] = useState(null); const [hasContentLoading, setContentLoading] = useState(true); const [hasContentError, setHasContentError] = useState(false); @@ -32,64 +32,101 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory }) { loadData(); }, [match.params.groupId, setBreadcrumb, inventory]); - + const tabsArray = [ + { + name: i18n._(t`Return to Groups`), + link: `/inventories/inventory/${inventory.id}/groups`, + id: 99, + isNestedTabs: true, + }, + { + name: i18n._(t`Details`), + link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup && + inventoryGroup.id}/details`, + id: 0, + }, + { + name: i18n._(t`RelatedGroups`), + link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup && + inventoryGroup.id}/nested_groups`, + id: 1, + }, + { + name: i18n._(t`Hosts`), + link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup && + inventoryGroup.id}/nested_hosts`, + id: 2, + }, + ]; if (hasContentError) { return ; } if (hasContentLoading) { return ; } - return ( - - + + - {inventoryGroup && [ - { - return ( - - ); - }} - />, - { - return ( - - ); - }} - />, + + ); + if ( + !history.location.pathname.includes('groups/') || + history.location.pathname.endsWith('edit') + ) { + cardHeader = null; + } + return ( + <> + {cardHeader} + + + {inventoryGroup && [ + { + return ( + + ); + }} + />, + { + return ; + }} + />, + ]} - !hasContentLoading && ( - - {match.params.id && ( - - {i18n._(t`View Inventory Details`)} - - )} - - ) - } - />, - ]} - + render={() => { + return ( + !hasContentLoading && ( + + {inventory && ( + + {i18n._(t`View Inventory Details`)} + + )} + + ) + ); + }} + /> + + ); } diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.test.jsx new file mode 100644 index 0000000000..afec079a6d --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.test.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { GroupsAPI } from '@api'; +import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; + +import InventoryGroup from './InventoryGroup'; + +jest.mock('@api'); +GroupsAPI.readDetail.mockResolvedValue({ + data: { + id: 1, + name: 'Foo', + description: 'Bar', + variables: 'bizz: buzz', + summary_fields: { + created_by: { id: 1, name: 'Athena' }, + modified_by: { id: 1, name: 'Apollo' }, + }, + }, +}); +describe('', () => { + let wrapper; + let history; + const inventory = { id: 1, name: 'Foo' }; + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/inventories/inventory/1/groups/1/details'], + }); + await act(async () => { + wrapper = mountWithContexts( + {}} />, + { + context: { + router: { + history, + route: { + location: history.location, + match: { + params: { id: 1 }, + }, + }, + }, + }, + } + ); + }); + }); + afterEach(() => { + wrapper.unmount(); + }); + test('renders successfully', async () => { + await act(async () => { + waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + expect(wrapper.length).toBe(1); + expect(wrapper.find('button[aria-label="Return to Groups"]').length).toBe( + 1 + ); + }); + test('expect Return to Groups tab to exist', async () => { + await act(async () => { + waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + expect(wrapper.length).toBe(1); + expect(wrapper.find('button[aria-label="Return to Groups"]').length).toBe( + 1 + ); + }); +}); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx index f09510a771..248309b005 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx @@ -2,8 +2,8 @@ import React, { useState, useEffect } from 'react'; import { withI18n } from '@lingui/react'; import { withRouter } from 'react-router-dom'; import { GroupsAPI } from '@api'; +import { Card } from '@patternfly/react-core'; -import ContentError from '@components/ContentError'; import InventoryGroupForm from '../InventoryGroupForm/InventoryGroupForm'; function InventoryGroupsAdd({ history, inventory, setBreadcrumb }) { @@ -21,15 +21,14 @@ function InventoryGroupsAdd({ history, inventory, setBreadcrumb }) { const handleCancel = () => { history.push(`/inventories/inventory/${inventory.id}/groups`); }; - if (error) { - return ; - } return ( - + + + ); } export default withI18n()(withRouter(InventoryGroupsAdd)); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.test.jsx index 9155a5054a..67ca98f99c 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.test.jsx @@ -13,7 +13,7 @@ describe('', () => { let history; beforeEach(async () => { history = createMemoryHistory({ - initialEntries: ['/inventories/1/groups'], + initialEntries: ['/inventories/inventory/1/groups'], }); await act(async () => { wrapper = mountWithContexts( @@ -34,14 +34,16 @@ describe('', () => { afterEach(() => { wrapper.unmount(); }); - test('InventoryGroupEdit renders successfully', () => { + test('InventoryGroupAdd renders successfully', () => { expect(wrapper.length).toBe(1); }); test('cancel should navigate user to Inventory Groups List', async () => { await act(async () => { waitForElement(wrapper, 'isLoading', el => el.length === 0); }); - expect(history.location.pathname).toEqual('/inventories/1/groups'); + expect(history.location.pathname).toEqual( + '/inventories/inventory/1/groups' + ); }); test('handleSubmit should call api', async () => { await act(async () => { diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx index 0835e87c25..7244385471 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx @@ -5,14 +5,21 @@ import { CardBody, Button } from '@patternfly/react-core'; import { withI18n } from '@lingui/react'; import { withRouter, Link } from 'react-router-dom'; import styled from 'styled-components'; -import { VariablesInput } from '@components/CodeMirrorInput'; -import ContentError from '@components/ContentError'; +import { VariablesInput as CodeMirrorInput } from '@components/CodeMirrorInput'; +import ErrorDetail from '@components/ErrorDetail'; import AlertModal from '@components/AlertModal'; import { formatDateString } from '@util/dates'; import { GroupsAPI } from '@api'; import { DetailList, Detail } from '@components/DetailList'; +const VariablesInput = styled(CodeMirrorInput)` + .pf-c-form__label { + font-weight: 600; + font-size: 16px; + } + margin: 20px 0; +`; const ActionButtonWrapper = styled.div` display: flex; justify-content: flex-end; @@ -26,6 +33,7 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const handleDelete = async () => { + setIsDeleteModalOpen(false); try { await GroupsAPI.destroy(inventoryGroup.id); history.push(`/inventories/inventory/${match.params.id}/groups`); @@ -34,7 +42,17 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { } }; if (error) { - return ; + return ( + setError(false)} + > + {i18n._(t`Failed to delete group ${inventoryGroup.name}.`)} + + + ); } if (isDeleteModalOpen) { return ( @@ -77,21 +95,14 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { label={i18n._(t`Description`)} value={inventoryGroup.description} /> - - } - /> + ', () => { let wrapper; + let history; beforeEach(async () => { await act(async () => { + history = createMemoryHistory({ + initialEntries: ['/inventories/inventory/1/groups/1/edit'], + }); wrapper = mountWithContexts( - - ( - - )} - /> - + ( + + )} + />, + { + context: { + router: { history, route: { location: history.location } }, + }, + } ); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); }); @@ -51,20 +57,22 @@ describe('', () => { test('InventoryGroupDetail renders successfully', () => { expect(wrapper.length).toBe(1); }); - test('should open delete modal and then call api to delete the group', () => { - wrapper.find('button[aria-label="Delete"]').simulate('click'); + test('should open delete modal and then call api to delete the group', async () => { + await act(async () => { + wrapper.find('button[aria-label="Delete"]').simulate('click'); + }); + await waitForElement(wrapper, 'Modal', el => el.length === 1); expect(wrapper.find('Modal').length).toBe(1); - wrapper.find('button[aria-label="confirm delete"]').simulate('click'); + await act(async () => { + wrapper.find('button[aria-label="confirm delete"]').simulate('click'); + }); expect(GroupsAPI.destroy).toBeCalledWith(1); }); test('should navigate user to edit form on edit button click', async () => { wrapper.find('button[aria-label="Edit"]').prop('onClick'); - expect( - wrapper - .find('Router') - .at(1) - .prop('history').location.pathname - ).toEqual('/inventories/inventory/1/groups/1/edit'); + expect(history.location.pathname).toEqual( + '/inventories/inventory/1/groups/1/edit' + ); }); test('details shoudld render with the proper values', () => { expect(wrapper.find('Detail[label="Name"]').prop('value')).toBe('Foo'); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx index f9e559c6c3..42d2fcde2a 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx @@ -14,11 +14,15 @@ function InventoryGroupEdit({ history, inventoryGroup, inventory, match }) { } catch (err) { setError(err); } finally { - history.push(`/inventories/inventory/${inventory.id}/groups`); + history.push( + `/inventories/inventory/${inventory.id}/groups/${inventoryGroup.id}/details` + ); } }; const handleCancel = () => { - history.push(`/inventories/inventory/${inventory.id}/groups`); + history.push( + `/inventories/inventory/${inventory.id}/groups/${inventoryGroup.id}/details` + ); }; return (