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 (