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' },