From febfb985a40b445cdef3ad22d3540fe550c42735 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Mon, 24 Aug 2020 17:19:22 -0400 Subject: [PATCH] Add smart inventory host detail view * Remove host toggle from smart inv host list --- .../src/screens/Inventory/SmartInventory.jsx | 7 +- .../SmartInventoryHost/SmartInventoryHost.jsx | 90 ++++++++++++++++++ .../SmartInventoryHost.test.jsx | 91 +++++++++++++++++++ .../Inventory/SmartInventoryHost/index.js | 1 + .../SmartInventoryHostDetail.jsx | 75 +++++++++++++++ .../SmartInventoryHostDetail.test.jsx | 34 +++++++ .../SmartInventoryHostDetail/index.js | 1 + .../SmartInventoryHostListItem.jsx | 23 +---- .../SmartInventoryHostListItem.test.jsx | 5 - .../SmartInventoryHosts.jsx | 19 +++- .../SmartInventoryHosts.test.jsx | 30 +++++- 11 files changed, 341 insertions(+), 35 deletions(-) create mode 100644 awx/ui_next/src/screens/Inventory/SmartInventoryHost/SmartInventoryHost.jsx create mode 100644 awx/ui_next/src/screens/Inventory/SmartInventoryHost/SmartInventoryHost.test.jsx create mode 100644 awx/ui_next/src/screens/Inventory/SmartInventoryHost/index.js create mode 100644 awx/ui_next/src/screens/Inventory/SmartInventoryHostDetail/SmartInventoryHostDetail.jsx create mode 100644 awx/ui_next/src/screens/Inventory/SmartInventoryHostDetail/SmartInventoryHostDetail.test.jsx create mode 100644 awx/ui_next/src/screens/Inventory/SmartInventoryHostDetail/index.js 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(); + }); });