diff --git a/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx b/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx index 64e60c9f21..e208686ab5 100644 --- a/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx +++ b/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx @@ -116,6 +116,7 @@ class SelectResourceStep extends React.Component { name={item[displayKey]} label={item[displayKey]} onSelect={() => onRowClick(item)} + onDeselect={() => {}} /> )} renderToolbar={props => } diff --git a/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx b/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx index ca1fd60143..1cc90f3995 100644 --- a/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx +++ b/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx @@ -1,9 +1,8 @@ import React from 'react'; -import { shape, string, number, arrayOf } from 'prop-types'; +import { shape, string, number, arrayOf, node, oneOfType } 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; @@ -57,25 +56,15 @@ function RoutedTabs(props) { return ( - {tabsArray - .filter(tab => tab.isNestedTab || !tab.name.startsWith('Return')) - .map(tab => ( - - {tab.name} - - ) : ( - tab.name - ) - } - /> - ))} + {tabsArray.map(tab => ( + + ))} ); } @@ -89,7 +78,7 @@ RoutedTabs.propTypes = { shape({ id: number.isRequired, link: string.isRequired, - name: string.isRequired, + name: oneOfType([string.isRequired, node.isRequired]), }) ).isRequired, }; diff --git a/awx/ui_next/src/screens/Host/Host.jsx b/awx/ui_next/src/screens/Host/Host.jsx index cbd6ff6659..203ec74212 100644 --- a/awx/ui_next/src/screens/Host/Host.jsx +++ b/awx/ui_next/src/screens/Host/Host.jsx @@ -2,15 +2,16 @@ import React, { Component } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom'; -import { Card, PageSection } from '@patternfly/react-core'; +import { Card } from '@patternfly/react-core'; +import { CaretLeftIcon } from '@patternfly/react-icons'; + import { TabbedCardHeader } from '@components/Card'; import CardCloseButton from '@components/CardCloseButton'; import RoutedTabs from '@components/RoutedTabs'; import ContentError from '@components/ContentError'; +import ContentLoading from '@components/ContentLoading'; 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'; @@ -26,16 +27,8 @@ 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() { @@ -56,40 +49,6 @@ 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, history, inventory } = this.props; @@ -102,8 +61,9 @@ class Host extends Component { if (history.location.pathname.startsWith('/hosts')) { setBreadcrumb(data); + } else { + setBreadcrumb(inventory, data); } - setBreadcrumb(inventory, data); } catch (err) { this.setState({ contentError: err }); } finally { @@ -111,29 +71,10 @@ class Host extends Component { } } - handleToggleError() { - this.setState({ toggleError: false }); - } - render() { const { location, match, history, i18n } = this.props; - const { - deletionError, - host, - isDeleteModalOpen, - toggleError, - hasContentLoading, - toggleLoading, - isInitialized, - contentError, - } = this.state; + const { host, hasContentLoading, isInitialized, contentError } = this.state; const tabsArray = [ - { - 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`, @@ -155,7 +96,18 @@ class Host extends Component { id: 3, }, ]; - + if (!history.location.pathname.startsWith('/hosts')) { + tabsArray.unshift({ + name: ( + <> + + {i18n._(t`Back to Hosts`)} + + ), + link: `/inventories/inventory/${match.params.id}/hosts`, + id: 99, + }); + } let cardHeader = ( ; + } + if (!hasContentLoading && contentError) { return ( - - - - {contentError.response.status === 404 && ( - - {i18n._(`Host not found.`)}{' '} - {i18n._(`View all Hosts.`)} - - )} - - - + + + {contentError.response.status === 404 && ( + + {i18n._(`Host not found.`)}{' '} + {i18n._(`View all Hosts.`)} + + )} + + ); } + const redirect = location.pathname.startsWith('/hosts') ? ( + + ) : ( + + ); return ( - <> - - - {cardHeader} - - - {host && ( - } + + {cardHeader} + + {redirect} + {host && ( + ( + this.setState({ host: newHost })} /> )} - {host && ( - ( - + /> + )} + )) + {host && ( + } + /> + )} + {host && ( + } + /> + )} + {host && ( + } + /> + )} + {host && ( + } + /> + )} + + !hasContentLoading && ( + + {match.params.id && ( + + {i18n._(`View Host Details`)} + )} - /> - )} - {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 91f8416176..282edbc471 100644 --- a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx +++ b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx @@ -1,14 +1,18 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; +import React, { useState } from 'react'; +import { Link, withRouter } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Host } from '@types'; import { Button } from '@patternfly/react-core'; import { CardBody, CardActionsRow } from '@components/Card'; +import AlertModal from '@components/AlertModal'; +import ErrorDetail from '@components/ErrorDetail'; import { DetailList, Detail, UserDateDetail } from '@components/DetailList'; import { VariablesDetail } from '@components/CodeMirrorInput'; import { Sparkline } from '@components/Sparkline'; +import DeleteButton from '@components/DeleteButton'; import Switch from '@components/Switch'; +import { HostsAPI } from '@api'; function HostDetail({ host, i18n }) { const ActionButtonWrapper = styled.div` @@ -20,92 +24,62 @@ const ActionButtonWrapper = styled.div` } `; -function HostDetail({ - host, - history, - isDeleteModalOpen, - match, - i18n, - toggleError, - toggleLoading, - onHostDelete, - onToggleDeleteModal, - onToggleError, - onHandleHostToggle, -}) { +function HostDetail({ host, history, match, i18n, onUpdateHost }) { 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); + const [isLoading, setIsloading] = useState(false); + const [deletionError, setDeletionError] = useState(false); + const [toggleLoading, setToggleLoading] = useState(false); + const [toggleError, setToggleError] = useState(false); + + const handleHostToggle = async () => { + setToggleLoading(true); + try { + const { data } = await HostsAPI.update(host.id, { + enabled: !host.enabled, + }); + onUpdateHost(data); + } catch (err) { + setToggleError(err); + } finally { + setToggleLoading(false); } - } + }; + + const handleHostDelete = async () => { + setIsloading(true); + try { + await HostsAPI.destroy(host.id); + setIsloading(false); + history.push(`/inventories/inventory/${match.params.id}/hosts`); + } catch (err) { + setDeletionError(err); + } + }; + if (toggleError && !toggleLoading) { return ( setToggleError(false)} > {i18n._(t`Failed to toggle host.`)} ); } - if (isDeleteModalOpen) { + if (!isLoading && deletionError) { return ( onToggleDeleteModal()} + title={i18n._(t`Error!`)} + onClose={() => setDeletionError(false)} > - {i18n._(t`Are you sure you want to delete:`)} -
- {host.name} - - - - - + {i18n._(t`Failed to delete ${host.name}.`)} +
); } @@ -118,7 +92,7 @@ function HostDetail({ labelOff={i18n._(t`Off`)} isChecked={host.enabled} isDisabled={!host.summary_fields.user_capabilities.edit} - onChange={onHandleHostToggle} + onChange={() => handleHostToggle()} aria-label={i18n._(t`Toggle Host`)} /> @@ -145,8 +119,16 @@ function HostDetail({ } /> )} - - + + )} + {summary_fields.user_capabilities && + summary_fields.user_capabilities.delete && ( + handleHostDelete()} + modalTitle={i18n._(t`Delete Host`)} + name={host.name} + /> + )} ); } diff --git a/awx/ui_next/src/screens/Host/Hosts.jsx b/awx/ui_next/src/screens/Host/Hosts.jsx index 59bb53bd06..1ea3e92905 100644 --- a/awx/ui_next/src/screens/Host/Hosts.jsx +++ b/awx/ui_next/src/screens/Host/Hosts.jsx @@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react'; import { Route, withRouter, Switch } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; +import { PageSection } from '@patternfly/react-core'; import { Config } from '@contexts/Config'; import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs'; @@ -46,35 +47,31 @@ class Hosts extends Component { }; render() { - const { match, history, location, inventory } = this.props; + const { match } = this.props; const { breadcrumbConfig } = this.state; return ( - - } - /> - ( - - {({ me }) => ( - - )} - - )} - /> - } /> - + + + } /> + ( + + {({ me }) => ( + + )} + + )} + /> + } /> + + ); } diff --git a/awx/ui_next/src/screens/Inventory/Inventories.jsx b/awx/ui_next/src/screens/Inventory/Inventories.jsx index 49264336b5..8a17787c30 100644 --- a/awx/ui_next/src/screens/Inventory/Inventories.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventories.jsx @@ -39,51 +39,46 @@ class Inventories extends Component { '/inventories/inventory/add': i18n._(t`Create New Inventory`), '/inventories/smart_inventory/add': i18n._(t`Create New Smart Inventory`), [`/inventories/${inventoryKind}/${inventory.id}`]: `${inventory.name}`, - [`/inventories/${inventoryKind}/${inventory.id}/details`]: i18n._( - t`Details` - ), - [`/inventories/${inventoryKind}/${inventory.id}/edit`]: i18n._( - t`Edit Details` - ), + [`/inventories/${inventoryKind}/${inventory.id}/access`]: i18n._( t`Access` ), [`/inventories/${inventoryKind}/${inventory.id}/completed_jobs`]: i18n._( t`Completed Jobs` ), + [`/inventories/${inventoryKind}/${inventory.id}/details`]: i18n._( + t`Details` + ), + [`/inventories/${inventoryKind}/${inventory.id}/edit`]: i18n._( + t`Edit Details` + ), + [`/inventories/${inventoryKind}/${inventory.id}/groups`]: i18n._( + t`Groups` + ), + [`/inventories/${inventoryKind}/${inventory.id}/hosts`]: i18n._(t`Hosts`), + + [`/inventories/${inventoryKind}/${inventory.id}/sources`]: i18n._( + t`Sources` + ), + + [`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource && + nestedResource.id}/edit`]: i18n._(t`Edit Details`), + [`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource && + nestedResource.id}/details`]: i18n._(t`Host Details`), [`/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/${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/${inventoryKind}/${inventory.id}/groups/${nestedResource && - nestedResource.id}`]: `${nestedResource && nestedResource.name}`, + nestedResource.id}/edit`]: i18n._(t`Edit Details`), [`/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` - ), + nestedResource.id}`]: `${nestedResource && nestedResource.name}`, }; this.setState({ breadcrumbConfig }); }; diff --git a/awx/ui_next/src/screens/Inventory/Inventory.jsx b/awx/ui_next/src/screens/Inventory/Inventory.jsx index c2cb831331..edf8e088fc 100644 --- a/awx/ui_next/src/screens/Inventory/Inventory.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventory.jsx @@ -63,7 +63,7 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) { location.pathname.endsWith('edit') || location.pathname.endsWith('add') || location.pathname.includes('groups/') || - history.location.pathname.includes(`/hosts/`) + location.pathname.includes('hosts/') ) { cardHeader = null; } diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx index ab8ccace46..eed60f4254 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx @@ -1,8 +1,9 @@ import React, { useEffect, useState } from 'react'; import { t } from '@lingui/macro'; import { withI18n } from '@lingui/react'; - import { Switch, Route, withRouter, Link, Redirect } from 'react-router-dom'; +import { CaretLeftIcon } from '@patternfly/react-icons'; + import { GroupsAPI } from '@api'; import CardCloseButton from '@components/CardCloseButton'; import RoutedTabs from '@components/RoutedTabs'; @@ -40,7 +41,12 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { const tabsArray = [ { - name: i18n._(t`Return to Groups`), + name: ( + <> + + {i18n._(t`Back to Groups`)} + + ), link: `/inventories/inventory/${inventory.id}/groups`, id: 99, isNestedTab: true, diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx index 585d18bc51..b7eb395a64 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx @@ -14,6 +14,8 @@ GroupsAPI.readDetail.mockResolvedValue({ name: 'Foo', description: 'Bar', variables: 'bizz: buzz', + created: '1/12/2019', + modified: '1/13/2019', summary_fields: { inventory: { id: 1 }, created_by: { id: 1, username: 'Athena' }, @@ -62,10 +64,10 @@ describe('', () => { test('renders successfully', async () => { expect(wrapper.length).toBe(1); }); - test('expect all tabs to exist, including Return to Groups', async () => { - expect(wrapper.find('button[aria-label="Return to Groups"]').length).toBe( - 1 - ); + test('expect all tabs to exist, including Back to Groups', async () => { + expect( + wrapper.find('button[link="/inventories/inventory/1/groups"]').length + ).toBe(1); expect(wrapper.find('button[aria-label="Details"]').length).toBe(1); expect(wrapper.find('button[aria-label="Related Groups"]').length).toBe(1); expect(wrapper.find('button[aria-label="Hosts"]').length).toBe(1); diff --git a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.jsx b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.jsx index c88362686a..cbb0b4d6d3 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHosts.jsx @@ -3,7 +3,7 @@ import { Switch, Route, withRouter } from 'react-router-dom'; import Host from '../../Host/Host'; import InventoryHostList from './InventoryHostList'; -import HostAdd from '../InventoryHostAdd'; +import InventoryHostAdd from '../InventoryHostAdd'; function InventoryHosts({ match, setBreadcrumb, i18n, inventory }) { return ( @@ -11,11 +11,11 @@ function InventoryHosts({ match, setBreadcrumb, i18n, inventory }) { } + render={() => } /> , ( ', () => { playbook: '', id: 1, verbosity: 1, + created: '1/12/2019', + modified: '1/13/2019', summary_fields: { user_capabilities: { edit: true }, created_by: { id: 1, username: 'Joe' },