diff --git a/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx b/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx index 964806f05e..ca1fd60143 100644 --- a/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx +++ b/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx @@ -57,23 +57,25 @@ function RoutedTabs(props) { return ( - {tabsArray.map(tab => ( - - {tab.name} - - ) : ( - tab.name - ) - } - /> - ))} + {tabsArray + .filter(tab => tab.isNestedTab || !tab.name.startsWith('Return')) + .map(tab => ( + + {tab.name} + + ) : ( + tab.name + ) + } + /> + ))} ); } diff --git a/awx/ui_next/src/screens/Host/Host.jsx b/awx/ui_next/src/screens/Host/Host.jsx index 7fc96f9ea5..cbd6ff6659 100644 --- a/awx/ui_next/src/screens/Host/Host.jsx +++ b/awx/ui_next/src/screens/Host/Host.jsx @@ -9,6 +9,9 @@ import RoutedTabs from '@components/RoutedTabs'; import ContentError from '@components/ContentError'; import HostFacts from './HostFacts'; import HostDetail from './HostDetail'; +import AlertModal from '@components/AlertModal'; +import ErrorDetail from '@components/ErrorDetail'; + import HostEdit from './HostEdit'; import HostGroups from './HostGroups'; import HostCompletedJobs from './HostCompletedJobs'; @@ -23,8 +26,16 @@ class Host extends Component { hasContentLoading: true, contentError: null, isInitialized: false, + toggleLoading: false, + toggleError: null, + deletionError: false, + isDeleteModalOpen: false, }; this.loadHost = this.loadHost.bind(this); + this.handleHostToggle = this.handleHostToggle.bind(this); + this.handleToggleError = this.handleToggleError.bind(this); + this.handleHostDelete = this.handleHostDelete.bind(this); + this.toggleDeleteModal = this.toggleDeleteModal.bind(this); } async componentDidMount() { @@ -45,15 +56,54 @@ class Host extends Component { } } + toggleDeleteModal() { + const { isDeleteModalOpen } = this.state; + this.setState({ isDeleteModalOpen: !isDeleteModalOpen }); + } + + async handleHostToggle() { + const { host } = this.state; + this.setState({ toggleLoading: true }); + try { + const { data } = await HostsAPI.update(host.id, { + enabled: !host.enabled, + }); + this.setState({ host: data }); + } catch (err) { + this.setState({ toggleError: err }); + } finally { + this.setState({ toggleLoading: null }); + } + } + + async handleHostDelete() { + const { host } = this.state; + const { match, history } = this.props; + + this.setState({ hasContentLoading: true }); + try { + await HostsAPI.destroy(host.id); + this.setState({ hasContentLoading: false }); + history.push(`/inventories/inventory/${match.params.id}/hosts`); + } catch (err) { + this.setState({ deletionError: err }); + } + } + async loadHost() { - const { match, setBreadcrumb } = this.props; - const id = parseInt(match.params.id, 10); + const { match, setBreadcrumb, history, inventory } = this.props; this.setState({ contentError: null, hasContentLoading: true }); try { - const { data } = await HostsAPI.readDetail(id); - setBreadcrumb(data); + const { data } = await HostsAPI.readDetail( + match.params.hostId || match.params.id + ); this.setState({ host: data }); + + if (history.location.pathname.startsWith('/hosts')) { + setBreadcrumb(data); + } + setBreadcrumb(inventory, data); } catch (err) { this.setState({ contentError: err }); } finally { @@ -61,15 +111,44 @@ class Host extends Component { } } + handleToggleError() { + this.setState({ toggleError: false }); + } + render() { const { location, match, history, i18n } = this.props; - - const { host, contentError, hasContentLoading, isInitialized } = this.state; - + const { + deletionError, + host, + isDeleteModalOpen, + toggleError, + hasContentLoading, + toggleLoading, + isInitialized, + contentError, + } = this.state; const tabsArray = [ - { name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 }, - { name: i18n._(t`Facts`), link: `${match.url}/facts`, id: 1 }, - { name: i18n._(t`Groups`), link: `${match.url}/groups`, id: 2 }, + { + name: i18n._(t`Return to Hosts`), + link: `/inventories/inventory/${match.params.id}/hosts`, + id: 99, + isNestedTab: !history.location.pathname.startsWith('/hosts'), + }, + { + name: i18n._(t`Details`), + link: `${match.url}/details`, + id: 0, + }, + { + name: i18n._(t`Facts`), + link: `${match.url}/facts`, + id: 1, + }, + { + name: i18n._(t`Groups`), + link: `${match.url}/groups`, + id: 2, + }, { name: i18n._(t`Completed Jobs`), link: `${match.url}/completed_jobs`, @@ -117,62 +196,99 @@ class Host extends Component { ); } - return ( - - - {cardHeader} - - - {host && ( + <> + + + {cardHeader} + + + {host && ( + } + /> + )} + {host && ( + ( + + )} + /> + )} + {host && ( + } + /> + )} + {host && ( + } + /> + )} + {host && ( + } + /> + )} } + key="not-found" + path="*" + render={() => + !hasContentLoading && ( + + {match.params.id && ( + + {i18n._(`View Host Details`)} + + )} + + ) + } /> - )} - {host && ( - } - /> - )} - {host && ( - } - /> - )} - {host && ( - } - /> - )} - {host && ( - } - /> - )} - - !hasContentLoading && ( - - {match.params.id && ( - - {i18n._(`View Host Details`)} - - )} - - ) - } - /> - , - - - + , + + + + {deletionError && ( + this.setState({ deletionError: false })} + > + {i18n._(t`Failed to delete ${host.name}.`)} + + + )} + ); } } diff --git a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx index db62e11f60..91f8416176 100644 --- a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx +++ b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx @@ -7,14 +7,127 @@ import { Button } from '@patternfly/react-core'; import { CardBody, CardActionsRow } from '@components/Card'; import { DetailList, Detail, UserDateDetail } from '@components/DetailList'; import { VariablesDetail } from '@components/CodeMirrorInput'; +import { Sparkline } from '@components/Sparkline'; +import Switch from '@components/Switch'; function HostDetail({ host, i18n }) { - const { created, description, id, modified, name, summary_fields } = host; +const ActionButtonWrapper = styled.div` + display: flex; + justify-content: flex-end; + margin-top: 20px; + & > :not(:first-child) { + margin-left: 20px; + } +`; +function HostDetail({ + host, + history, + isDeleteModalOpen, + match, + i18n, + toggleError, + toggleLoading, + onHostDelete, + onToggleDeleteModal, + onToggleError, + onHandleHostToggle, +}) { + const { created, description, id, modified, name, summary_fields } = host; + let createdBy = ''; + if (created) { + if (summary_fields.created_by && summary_fields.created_by.username) { + createdBy = ( + + {i18n._(t`${formatDateString(created)} by `)}{' '} + + {summary_fields.created_by.username} + + + ); + } else { + createdBy = formatDateString(created); + } + } + + let modifiedBy = ''; + if (modified) { + if (summary_fields.modified_by && summary_fields.modified_by.username) { + modifiedBy = ( + + {i18n._(t`${formatDateString(modified)} by`)}{' '} + + {summary_fields.modified_by.username} + + + ); + } else { + modifiedBy = formatDateString(modified); + } + } + if (toggleError && !toggleLoading) { + return ( + + {i18n._(t`Failed to toggle host.`)} + + + ); + } + if (isDeleteModalOpen) { + return ( + onToggleDeleteModal()} + > + {i18n._(t`Are you sure you want to delete:`)} +
+ {host.name} + + + + + +
+ ); + } return ( + + } + label={i18n._(t`Activity`)} + /> {summary_fields.inventory && ( )} - - + + @@ -54,7 +159,11 @@ function HostDetail({ host, i18n }) { diff --git a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.test.jsx b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.test.jsx index 749c512171..5562a5cc05 100644 --- a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.test.jsx +++ b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.test.jsx @@ -50,8 +50,7 @@ describe('', () => { test('should show edit button for users with edit permission', async () => { const wrapper = mountWithContexts(); - // VariablesDetail has two buttons - const editButton = wrapper.find('Button').at(2); + const editButton = wrapper.find('Button[aria-label="edit"]'); expect(editButton.text()).toEqual('Edit'); expect(editButton.prop('to')).toBe('/hosts/1/edit'); }); @@ -61,7 +60,6 @@ describe('', () => { readOnlyHost.summary_fields.user_capabilities.edit = false; const wrapper = mountWithContexts(); await waitForElement(wrapper, 'HostDetail'); - // VariablesDetail has two buttons - expect(wrapper.find('Button').length).toBe(2); + expect(wrapper.find('Button[aria-label="edit"]').length).toBe(0); }); }); diff --git a/awx/ui_next/src/screens/Host/HostList/HostList.jsx b/awx/ui_next/src/screens/Host/HostList/HostList.jsx index 4231ef2cc0..978ff2ba59 100644 --- a/awx/ui_next/src/screens/Host/HostList/HostList.jsx +++ b/awx/ui_next/src/screens/Host/HostList/HostList.jsx @@ -238,7 +238,7 @@ class HostsList extends Component { detailUrl={`${match.url}/${o.id}`} isSelected={selected.some(row => row.id === o.id)} onSelect={() => this.handleSelect(o)} - toggleHost={this.handleHostToggle} + onToggleHost={this.handleHostToggle} toggleLoading={toggleLoading === o.id} /> )} diff --git a/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx b/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx index 9123136749..6e74f9da3b 100644 --- a/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx +++ b/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx @@ -34,7 +34,7 @@ class HostListItem extends React.Component { isSelected, onSelect, detailUrl, - toggleHost, + onToggleHost, toggleLoading, i18n, } = this.props; @@ -93,7 +93,7 @@ class HostListItem extends React.Component { toggleLoading || !host.summary_fields.user_capabilities.edit } - onChange={() => toggleHost(host)} + onChange={() => onToggleHost(host)} aria-label={i18n._(t`Toggle host`)} /> diff --git a/awx/ui_next/src/screens/Host/HostList/HostListItem.test.jsx b/awx/ui_next/src/screens/Host/HostList/HostListItem.test.jsx index d9e5987661..285f71fba4 100644 --- a/awx/ui_next/src/screens/Host/HostList/HostListItem.test.jsx +++ b/awx/ui_next/src/screens/Host/HostList/HostListItem.test.jsx @@ -4,7 +4,7 @@ import { mountWithContexts } from '@testUtils/enzymeHelpers'; import HostsListItem from './HostListItem'; -let toggleHost; +let onToggleHost; const mockHost = { id: 1, @@ -24,7 +24,7 @@ const mockHost = { describe('', () => { beforeEach(() => { - toggleHost = jest.fn(); + onToggleHost = jest.fn(); }); afterEach(() => { @@ -38,7 +38,7 @@ describe('', () => { detailUrl="/host/1" onSelect={() => {}} host={mockHost} - toggleHost={toggleHost} + onToggleHost={onToggleHost} /> ); expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy(); @@ -52,7 +52,7 @@ describe('', () => { detailUrl="/host/1" onSelect={() => {}} host={copyMockHost} - toggleHost={toggleHost} + onToggleHost={onToggleHost} /> ); expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy(); @@ -64,7 +64,7 @@ describe('', () => { detailUrl="/host/1" onSelect={() => {}} host={mockHost} - toggleHost={toggleHost} + onToggleHost={onToggleHost} /> ); wrapper @@ -72,7 +72,7 @@ describe('', () => { .first() .find('input') .simulate('change'); - expect(toggleHost).toHaveBeenCalledWith(mockHost); + expect(onToggleHost).toHaveBeenCalledWith(mockHost); }); test('handles toggle click when host is disabled', () => { @@ -82,7 +82,7 @@ describe('', () => { detailUrl="/host/1" onSelect={() => {}} host={mockHost} - toggleHost={toggleHost} + onToggleHost={onToggleHost} /> ); wrapper @@ -90,6 +90,6 @@ describe('', () => { .first() .find('input') .simulate('change'); - expect(toggleHost).toHaveBeenCalledWith(mockHost); + expect(onToggleHost).toHaveBeenCalledWith(mockHost); }); }); diff --git a/awx/ui_next/src/screens/Host/Hosts.jsx b/awx/ui_next/src/screens/Host/Hosts.jsx index 0a17916223..59bb53bd06 100644 --- a/awx/ui_next/src/screens/Host/Hosts.jsx +++ b/awx/ui_next/src/screens/Host/Hosts.jsx @@ -46,14 +46,17 @@ class Hosts extends Component { }; render() { - const { match, history, location } = this.props; + const { match, history, location, inventory } = this.props; const { breadcrumbConfig } = this.state; return ( - } /> + } + /> ( @@ -64,6 +67,7 @@ class Hosts extends Component { location={location} setBreadcrumb={this.setBreadcrumbConfig} me={me || {}} + inventory={inventory} /> )} diff --git a/awx/ui_next/src/screens/Inventory/Inventories.jsx b/awx/ui_next/src/screens/Inventory/Inventories.jsx index 4253078486..49264336b5 100644 --- a/awx/ui_next/src/screens/Inventory/Inventories.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventories.jsx @@ -27,7 +27,7 @@ class Inventories extends Component { }; } - setBreadCrumbConfig = (inventory, group) => { + setBreadCrumbConfig = (inventory, nestedResource) => { const { i18n } = this.props; if (!inventory) { return; @@ -51,21 +51,39 @@ class Inventories extends Component { [`/inventories/${inventoryKind}/${inventory.id}/completed_jobs`]: i18n._( t`Completed Jobs` ), - [`/inventories/${inventoryKind}/${inventory.id}/hosts`]: i18n._(t`Hosts`), + [`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource && + nestedResource.id}`]: i18n._( + t`${nestedResource && nestedResource.name}` + ), + [`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource && + nestedResource.id}/details`]: i18n._(t`Details`), + [`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource && + nestedResource.id}/edit`]: i18n._(t`Edit Details`), [`/inventories/${inventoryKind}/${inventory.id}/hosts/add`]: i18n._( t`Create New Host` ), - [`/inventories/inventory/${inventory.id}/sources`]: i18n._(t`Sources`), - [`/inventories/inventory/${inventory.id}/groups`]: i18n._(t`Groups`), - [`/inventories/inventory/${inventory.id}/groups/add`]: i18n._( + [`/inventories/${inventoryKind}/${inventory.id}/sources`]: i18n._( + t`Sources` + ), + [`/inventories/${inventoryKind}/${inventory.id}/groups`]: i18n._( + t`Groups` + ), + [`/inventories/${inventoryKind}/${inventory.id}/groups/add`]: i18n._( t`Create New Group` ), - [`/inventories/inventory/${inventory.id}/groups/${group && - group.id}`]: `${group && group.name}`, - [`/inventories/inventory/${inventory.id}/groups/${group && - group.id}/details`]: i18n._(t`Group Details`), - [`/inventories/inventory/${inventory.id}/groups/${group && - group.id}/edit`]: i18n._(t`Edit Details`), + [`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource && + nestedResource.id}`]: `${nestedResource && nestedResource.name}`, + [`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource && + nestedResource.id}/details`]: i18n._(t`Group Details`), + [`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource && + nestedResource.id}/edit`]: i18n._(t`Edit Details`), + [`/inventories/${inventoryKind}/${inventory.id}/hosts`]: i18n._(t`Hosts`), + [`/inventories/${inventoryKind}/${inventory.id}/sources`]: i18n._( + t`Sources` + ), + [`/inventories/${inventoryKind}/${inventory.id}/groups`]: i18n._( + t`Groups` + ), }; this.setState({ breadcrumbConfig }); }; diff --git a/awx/ui_next/src/screens/Inventory/Inventory.jsx b/awx/ui_next/src/screens/Inventory/Inventory.jsx index e632610660..c2cb831331 100644 --- a/awx/ui_next/src/screens/Inventory/Inventory.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventory.jsx @@ -8,14 +8,15 @@ import CardCloseButton from '@components/CardCloseButton'; import ContentError from '@components/ContentError'; import RoutedTabs from '@components/RoutedTabs'; import { ResourceAccessList } from '@components/ResourceAccessList'; +import ContentLoading from '@components/ContentLoading'; import InventoryDetail from './InventoryDetail'; -import InventoryHosts from './InventoryHosts'; -import InventoryHostAdd from './InventoryHostAdd'; + import InventoryGroups from './InventoryGroups'; import InventoryCompletedJobs from './InventoryCompletedJobs'; import InventorySources from './InventorySources'; import { InventoriesAPI } from '@api'; import InventoryEdit from './InventoryEdit'; +import InventoryHosts from './InventoryHosts/InventoryHosts'; function Inventory({ history, i18n, location, match, setBreadcrumb }) { const [contentError, setContentError] = useState(null); @@ -61,10 +62,14 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) { if ( location.pathname.endsWith('edit') || location.pathname.endsWith('add') || - location.pathname.includes('groups/') + location.pathname.includes('groups/') || + history.location.pathname.includes(`/hosts/`) ) { cardHeader = null; } + if (hasContentLoading) { + return ; + } if (!hasContentLoading && contentError) { return ( @@ -111,9 +116,16 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) { render={() => } />, } + key="hosts" + path="/inventories/inventory/:id/hosts" + render={() => ( + + )} />, )} />, - } - />, ; diff --git a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.jsx b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.jsx new file mode 100644 index 0000000000..df4d7783fd --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.jsx @@ -0,0 +1,226 @@ +import React, { useEffect, useState } from 'react'; +import { withRouter } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { getQSConfig, parseQueryString } from '@util/qs'; +import { InventoriesAPI, HostsAPI } from '@api'; + +import AlertModal from '@components/AlertModal'; +import DataListToolbar from '@components/DataListToolbar'; +import ErrorDetail from '@components/ErrorDetail'; +import PaginatedDataList, { + ToolbarAddButton, + ToolbarDeleteButton, +} from '@components/PaginatedDataList'; +import InventoryHostItem from './InventoryHostItem'; + +const QS_CONFIG = getQSConfig('host', { + page: 1, + page_size: 20, + order_by: 'name', +}); + +function InventoryHostList({ i18n, location, match }) { + const [actions, setActions] = useState(null); + const [contentError, setContentError] = useState(null); + const [deletionError, setDeletionError] = useState(null); + const [hostCount, setHostCount] = useState(0); + const [hosts, setHosts] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [selected, setSelected] = useState([]); + const [toggleError, setToggleError] = useState(null); + const [toggleLoading, setToggleLoading] = useState(null); + + const fetchHosts = (id, queryString) => { + const params = parseQueryString(QS_CONFIG, queryString); + return InventoriesAPI.readHosts(id, params); + }; + + useEffect(() => { + async function fetchData() { + try { + const [ + { + data: { count, results }, + }, + { + data: { actions: optionActions }, + }, + ] = await Promise.all([ + fetchHosts(match.params.id, location.search), + InventoriesAPI.readOptions(), + ]); + + setHosts(results); + setHostCount(count); + setActions(optionActions); + } catch (error) { + setContentError(error); + } finally { + setIsLoading(false); + } + } + + fetchData(); + }, [match.params.id, location]); + + const handleSelectAll = isSelected => { + setSelected(isSelected ? [...hosts] : []); + }; + + const handleSelect = row => { + if (selected.some(s => s.id === row.id)) { + setSelected(selected.filter(s => s.id !== row.id)); + } else { + setSelected(selected.concat(row)); + } + }; + + const handleDelete = async () => { + setIsLoading(true); + + try { + await Promise.all(selected.map(host => HostsAPI.destroy(host.id))); + } catch (error) { + setDeletionError(error); + } finally { + setSelected([]); + try { + const { + data: { count, results }, + } = await fetchHosts(match.params.id, location.search); + + setHosts(results); + setHostCount(count); + } catch (error) { + setContentError(error); + } finally { + setIsLoading(false); + } + } + }; + + const handleToggle = async hostToToggle => { + setToggleLoading(hostToToggle.id); + + try { + const { data: updatedHost } = await HostsAPI.update(hostToToggle.id, { + enabled: !hostToToggle.enabled, + }); + + setHosts( + hosts.map(host => (host.id === updatedHost.id ? updatedHost : host)) + ); + } catch (error) { + setToggleError(error); + } finally { + setToggleLoading(null); + } + }; + + const canAdd = + actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); + const isAllSelected = selected.length > 0 && selected.length === hosts.length; + + return ( + <> + ( + , + canAdd && ( + + ), + ]} + /> + )} + renderItem={o => ( + row.id === o.id)} + onSelect={() => handleSelect(o)} + toggleHost={handleToggle} + toggleLoading={toggleLoading === o.id} + /> + )} + emptyStateControls={ + canAdd && ( + + ) + } + /> + + {toggleError && !toggleLoading && ( + setToggleError(false)} + > + {i18n._(t`Failed to toggle host.`)} + + + )} + + {deletionError && ( + setDeletionError(null)} + > + {i18n._(t`Failed to delete one or more hosts.`)} + + + )} + + ); +} + +export default withI18n()(withRouter(InventoryHostList)); diff --git a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.test.jsx similarity index 94% rename from awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.test.jsx rename to awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.test.jsx index 715413c81b..d35d4da489 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.test.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { InventoriesAPI, HostsAPI } from '@api'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; -import InventoryHosts from './InventoryHosts'; +import InventoryHostList from './InventoryHostList'; import mockInventory from '../shared/data.inventory.json'; jest.mock('@api'); @@ -62,7 +62,7 @@ const mockHosts = [ }, ]; -describe('', () => { +describe('', () => { let wrapper; beforeEach(async () => { @@ -81,7 +81,7 @@ describe('', () => { }, }); await act(async () => { - wrapper = mountWithContexts(); + wrapper = mountWithContexts(); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); }); @@ -91,7 +91,7 @@ describe('', () => { }); test('initially renders successfully', () => { - expect(wrapper.find('InventoryHosts').length).toBe(1); + expect(wrapper.find('InventoryHostList').length).toBe(1); }); test('should fetch hosts from api and render them in the list', async () => { @@ -261,7 +261,9 @@ describe('', () => { }, }); await act(async () => { - wrapper = mountWithContexts(); + wrapper = mountWithContexts( + + ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); expect(wrapper.find('ToolbarAddButton').length).toBe(0); @@ -272,7 +274,9 @@ describe('', () => { Promise.reject(new Error()) ); await act(async () => { - wrapper = mountWithContexts(); + wrapper = mountWithContexts( + + ); }); await waitForElement(wrapper, 'ContentError', el => el.length === 1); }); diff --git a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.jsx b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.jsx index 9e96793e3f..c88362686a 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.jsx @@ -1,228 +1,46 @@ -import React, { useEffect, useState } from 'react'; -import { withRouter } from 'react-router-dom'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; -import { getQSConfig, parseQueryString } from '@util/qs'; -import { InventoriesAPI, HostsAPI } from '@api'; +import React from 'react'; +import { Switch, Route, withRouter } from 'react-router-dom'; -import AlertModal from '@components/AlertModal'; -import DataListToolbar from '@components/DataListToolbar'; -import ErrorDetail from '@components/ErrorDetail'; -import PaginatedDataList, { - ToolbarAddButton, - ToolbarDeleteButton, -} from '@components/PaginatedDataList'; -import InventoryHostItem from './InventoryHostItem'; - -const QS_CONFIG = getQSConfig('host', { - page: 1, - page_size: 20, - order_by: 'name', -}); - -function InventoryHosts({ i18n, location, match }) { - const [actions, setActions] = useState(null); - const [contentError, setContentError] = useState(null); - const [deletionError, setDeletionError] = useState(null); - const [hostCount, setHostCount] = useState(0); - const [hosts, setHosts] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [selected, setSelected] = useState([]); - const [toggleError, setToggleError] = useState(null); - const [toggleLoading, setToggleLoading] = useState(null); - - const fetchHosts = (id, queryString) => { - const params = parseQueryString(QS_CONFIG, queryString); - return InventoriesAPI.readHosts(id, params); - }; - - useEffect(() => { - async function fetchData() { - try { - const [ - { - data: { count, results }, - }, - { - data: { actions: optionActions }, - }, - ] = await Promise.all([ - fetchHosts(match.params.id, location.search), - InventoriesAPI.readOptions(), - ]); - - setHosts(results); - setHostCount(count); - setActions(optionActions); - } catch (error) { - setContentError(error); - } finally { - setIsLoading(false); - } - } - - fetchData(); - }, [match.params.id, location]); - - const handleSelectAll = isSelected => { - setSelected(isSelected ? [...hosts] : []); - }; - - const handleSelect = row => { - if (selected.some(s => s.id === row.id)) { - setSelected(selected.filter(s => s.id !== row.id)); - } else { - setSelected(selected.concat(row)); - } - }; - - const handleDelete = async () => { - setIsLoading(true); - - try { - await Promise.all(selected.map(host => HostsAPI.destroy(host.id))); - } catch (error) { - setDeletionError(error); - } finally { - setSelected([]); - try { - const { - data: { count, results }, - } = await fetchHosts(match.params.id, location.search); - - setHosts(results); - setHostCount(count); - } catch (error) { - setContentError(error); - } finally { - setIsLoading(false); - } - } - }; - - const handleToggle = async hostToToggle => { - setToggleLoading(hostToToggle.id); - - try { - const { data: updatedHost } = await HostsAPI.update(hostToToggle.id, { - enabled: !hostToToggle.enabled, - }); - - setHosts( - hosts.map(host => (host.id === updatedHost.id ? updatedHost : host)) - ); - } catch (error) { - setToggleError(error); - } finally { - setToggleLoading(null); - } - }; - - const canAdd = - actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); - const isAllSelected = selected.length > 0 && selected.length === hosts.length; +import Host from '../../Host/Host'; +import InventoryHostList from './InventoryHostList'; +import HostAdd from '../InventoryHostAdd'; +function InventoryHosts({ match, setBreadcrumb, i18n, inventory }) { return ( - <> - ( - , - canAdd && ( - - ), - ]} - /> - )} - renderItem={o => ( - row.id === o.id)} - onSelect={() => handleSelect(o)} - toggleHost={handleToggle} - toggleLoading={toggleLoading === o.id} - /> - )} - emptyStateControls={ - canAdd && ( - - ) - } + + } /> - - {toggleError && !toggleLoading && ( - setToggleError(false)} - > - {i18n._(t`Failed to toggle host.`)} - - - )} - - {deletionError && ( - setDeletionError(null)} - > - {i18n._(t`Failed to delete one or more hosts.`)} - - - )} - + , + ( + + )} + /> + , + ( + + )} + /> + , + ); } -export default withI18n()(withRouter(InventoryHosts)); +export default withRouter(InventoryHosts); diff --git a/awx/ui_next/src/screens/Inventory/InventoryHosts/index.js b/awx/ui_next/src/screens/Inventory/InventoryHosts/index.js index 6d33814f29..0cb4fe95bc 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHosts/index.js +++ b/awx/ui_next/src/screens/Inventory/InventoryHosts/index.js @@ -1 +1 @@ -export { default } from './InventoryHosts'; +export { default } from './InventoryHostList';