diff --git a/awx/ui_next/src/screens/Inventory/SmartInventory.jsx b/awx/ui_next/src/screens/Inventory/SmartInventory.jsx
index 18291a2959..91f0f537e6 100644
--- a/awx/ui_next/src/screens/Inventory/SmartInventory.jsx
+++ b/awx/ui_next/src/screens/Inventory/SmartInventory.jsx
@@ -111,7 +111,7 @@ function SmartInventory({ i18n, setBreadcrumb }) {
let showCardHeader = true;
- if (location.pathname.endsWith('edit')) {
+ if (['edit', 'hosts/'].some(name => location.pathname.includes(name))) {
showCardHeader = false;
}
@@ -145,7 +145,10 @@ function SmartInventory({ i18n, setBreadcrumb }) {
/>
,
-
+
,
{
+ const response = await InventoriesAPI.readHostDetail(
+ inventory.id,
+ params.hostId
+ );
+ return response;
+ }, [inventory.id, params.hostId]),
+ null
+ );
+
+ useEffect(() => {
+ fetchHost();
+ }, [fetchHost]);
+
+ useEffect(() => {
+ if (inventory && host) {
+ setBreadcrumb(inventory, host);
+ }
+ }, [inventory, host, setBreadcrumb]);
+
+ if (error) {
+ return ;
+ }
+
+ const tabsArray = [
+ {
+ name: (
+ <>
+
+ {i18n._(t`Back to Hosts`)}
+ >
+ ),
+ link: `/inventories/smart_inventory/${inventory.id}/hosts`,
+ id: 0,
+ },
+ {
+ name: i18n._(t`Details`),
+ link: `${url}/details`,
+ id: 1,
+ },
+ ];
+
+ return (
+ <>
+
+
+ {isLoading && }
+
+ {!isLoading && host && (
+
+
+
+
+
+
+
+
+ {i18n._(t`View smart inventory host details`)}
+
+
+
+
+ )}
+ >
+ );
+}
+
+export default withI18n()(SmartInventoryHost);
diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHost/SmartInventoryHost.test.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHost/SmartInventoryHost.test.jsx
new file mode 100644
index 0000000000..24e59142eb
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHost/SmartInventoryHost.test.jsx
@@ -0,0 +1,91 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { createMemoryHistory } from 'history';
+import { InventoriesAPI } from '../../../api';
+import {
+ mountWithContexts,
+ waitForElement,
+} from '../../../../testUtils/enzymeHelpers';
+import mockHost from '../shared/data.host.json';
+import SmartInventoryHost from './SmartInventoryHost';
+
+jest.mock('../../../api');
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useRouteMatch: () => ({
+ params: { id: 1234, hostId: 2 },
+ path: '/inventories/smart_inventory/:id/hosts/:hostId',
+ url: '/inventories/smart_inventory/1234/hosts/2',
+ }),
+}));
+
+const mockSmartInventory = {
+ id: 1234,
+ name: 'Mock Smart Inventory',
+};
+
+describe('', () => {
+ let wrapper;
+ let history;
+
+ afterEach(() => {
+ wrapper.unmount();
+ jest.clearAllMocks();
+ });
+
+ test('should render expected tabs', async () => {
+ InventoriesAPI.readHostDetail.mockResolvedValue({
+ data: { ...mockHost },
+ });
+ await act(async () => {
+ wrapper = mountWithContexts(
+ {}}
+ />
+ );
+ });
+
+ const expectedTabs = ['Back to Hosts', 'Details'];
+
+ expect(wrapper.find('RoutedTabs li').length).toBe(2);
+ wrapper.find('RoutedTabs li').forEach((tab, index) => {
+ expect(tab.text()).toEqual(expectedTabs[index]);
+ });
+ });
+
+ test('should show content error when api throws error on initial render', async () => {
+ InventoriesAPI.readHostDetail.mockRejectedValueOnce(new Error());
+ await act(async () => {
+ wrapper = mountWithContexts(
+ {}}
+ />
+ );
+ });
+
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ await waitForElement(wrapper, 'ContentError', el => el.length === 1);
+ expect(wrapper.find('ContentError Title').text()).toEqual(
+ 'Something went wrong...'
+ );
+ });
+
+ test('should show content error when user attempts to navigate to erroneous route', async () => {
+ history = createMemoryHistory({
+ initialEntries: ['/inventories/smart_inventory/1/hosts/1/foobar'],
+ });
+ await act(async () => {
+ wrapper = mountWithContexts(
+ {}}
+ />,
+ { context: { router: { history } } }
+ );
+ });
+ await waitForElement(wrapper, 'ContentError', el => el.length === 1);
+ expect(wrapper.find('ContentError Title').text()).toEqual('Not Found');
+ });
+});
diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHost/index.js b/awx/ui_next/src/screens/Inventory/SmartInventoryHost/index.js
new file mode 100644
index 0000000000..7e634beb10
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHost/index.js
@@ -0,0 +1 @@
+export { default } from './SmartInventoryHost';
diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHostDetail/SmartInventoryHostDetail.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHostDetail/SmartInventoryHostDetail.jsx
new file mode 100644
index 0000000000..ca992575b1
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHostDetail/SmartInventoryHostDetail.jsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import { Host } from '../../../types';
+import { CardBody } from '../../../components/Card';
+import {
+ Detail,
+ DetailList,
+ UserDateDetail,
+} from '../../../components/DetailList';
+import Sparkline from '../../../components/Sparkline';
+import { VariablesDetail } from '../../../components/CodeMirrorInput';
+
+function SmartInventoryHostDetail({ host, i18n }) {
+ const {
+ created,
+ description,
+ enabled,
+ modified,
+ name,
+ variables,
+ summary_fields: { inventory, recent_jobs, created_by, modified_by },
+ } = host;
+
+ const recentPlaybookJobs = recent_jobs?.map(job => ({ ...job, type: 'job' }));
+
+ return (
+
+
+
+ {recentPlaybookJobs?.length > 0 && (
+ }
+ />
+ )}
+
+
+ {inventory?.name}
+
+ }
+ />
+
+
+
+
+
+
+ );
+}
+
+SmartInventoryHostDetail.propTypes = {
+ host: Host.isRequired,
+};
+
+export default withI18n()(SmartInventoryHostDetail);
diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHostDetail/SmartInventoryHostDetail.test.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHostDetail/SmartInventoryHostDetail.test.jsx
new file mode 100644
index 0000000000..4243dd6589
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHostDetail/SmartInventoryHostDetail.test.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
+import SmartInventoryHostDetail from './SmartInventoryHostDetail';
+import mockHost from '../shared/data.host.json';
+
+jest.mock('../../../api');
+
+describe('', () => {
+ let wrapper;
+
+ beforeAll(() => {
+ wrapper = mountWithContexts();
+ });
+
+ afterAll(() => {
+ wrapper.unmount();
+ });
+
+ test('should render Details', () => {
+ function assertDetail(label, value) {
+ expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label);
+ expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value);
+ }
+
+ assertDetail('Name', 'localhost');
+ assertDetail('Description', 'localhost description');
+ assertDetail('Inventory', 'Mikes Inventory');
+ assertDetail('Enabled', 'On');
+ assertDetail('Created', '10/28/2019, 9:26:54 PM');
+ assertDetail('Last modified', '10/29/2019, 8:18:41 PM');
+ expect(wrapper.find('Detail[label="Activity"] Sparkline')).toHaveLength(1);
+ expect(wrapper.find('VariablesDetail')).toHaveLength(1);
+ });
+});
diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHostDetail/index.js b/awx/ui_next/src/screens/Inventory/SmartInventoryHostDetail/index.js
new file mode 100644
index 0000000000..4c166ddc01
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHostDetail/index.js
@@ -0,0 +1 @@
+export { default } from './SmartInventoryHostDetail';
diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.jsx
index 72fb90079b..30a807c459 100644
--- a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.jsx
+++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.jsx
@@ -2,18 +2,16 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { string, bool, func } from 'prop-types';
import { withI18n } from '@lingui/react';
-import { t, Trans } from '@lingui/macro';
+import { t } from '@lingui/macro';
import 'styled-components/macro';
import {
- DataListAction,
DataListCheck,
DataListItem,
DataListItemCells,
DataListItemRow,
} from '@patternfly/react-core';
import DataListCell from '../../../components/DataListCell';
-import HostToggle from '../../../components/HostToggle';
import Sparkline from '../../../components/Sparkline';
import { Host } from '../../../types';
@@ -62,25 +60,6 @@ function SmartInventoryHostListItem({
,
]}
/>
-
-
- Smart inventory hosts are read-only.
-
- Toggle indicates if a host is available and should be included
- in running jobs. For hosts that are part of an external
- inventory, this may be reset by the inventory sync process.
-
- }
- />
-
);
diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.test.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.test.jsx
index a2462a831a..9a33460fcb 100644
--- a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.test.jsx
@@ -44,9 +44,4 @@ describe('', () => {
expect(cells.at(1).find('Sparkline').length).toEqual(1);
expect(cells.at(2).text()).toContain('Inv 1');
});
-
- test('should display disabled host toggle', () => {
- expect(wrapper.find('HostToggle').length).toBe(1);
- expect(wrapper.find('HostToggle Switch').prop('isDisabled')).toEqual(true);
- });
});
diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.jsx
index 0aa24cdc59..41a05952c2 100644
--- a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.jsx
+++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.jsx
@@ -1,13 +1,22 @@
import React from 'react';
-import { Route } from 'react-router-dom';
+import { Route, Switch } from 'react-router-dom';
import SmartInventoryHostList from './SmartInventoryHostList';
+import SmartInventoryHost from '../SmartInventoryHost';
import { Inventory } from '../../../types';
-function SmartInventoryHosts({ inventory }) {
+function SmartInventoryHosts({ inventory, setBreadcrumb }) {
return (
-
-
-
+
+
+
+
+
+
+
+
);
}
diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.test.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.test.jsx
index 8fed1ef7f7..1db767dfe4 100644
--- a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.test.jsx
@@ -1,6 +1,10 @@
import React from 'react';
+import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
-import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
+import {
+ mountWithContexts,
+ waitForElement,
+} from '../../../../testUtils/enzymeHelpers';
import SmartInventoryHosts from './SmartInventoryHosts';
jest.mock('../../../api');
@@ -25,4 +29,28 @@ describe('', () => {
jest.clearAllMocks();
wrapper.unmount();
});
+
+ test('should render smart inventory host details', async () => {
+ let wrapper;
+ const history = createMemoryHistory({
+ initialEntries: ['/inventories/smart_inventory/1/hosts/2'],
+ });
+ const match = {
+ path: '/inventories/smart_inventory/:id/hosts/:hostId',
+ url: '/inventories/smart_inventory/1/hosts/2',
+ isExact: true,
+ };
+ await act(async () => {
+ wrapper = mountWithContexts(
+ {}} />,
+ {
+ context: { router: { history, route: { match } } },
+ }
+ );
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ expect(wrapper.find('SmartInventoryHost').length).toBe(1);
+ jest.clearAllMocks();
+ wrapper.unmount();
+ });
});