Improves NestedTabs, Refactors PR, Adds Delete/DeleteError Functionality to HostDetail

This commit is contained in:
Alex Corey
2020-01-08 09:09:11 -05:00
parent 1db88fe4f6
commit 919475a4c7
11 changed files with 241 additions and 324 deletions

View File

@@ -116,6 +116,7 @@ class SelectResourceStep extends React.Component {
name={item[displayKey]} name={item[displayKey]}
label={item[displayKey]} label={item[displayKey]}
onSelect={() => onRowClick(item)} onSelect={() => onRowClick(item)}
onDeselect={() => {}}
/> />
)} )}
renderToolbar={props => <DataListToolbar {...props} fillWidth />} renderToolbar={props => <DataListToolbar {...props} fillWidth />}

View File

@@ -1,9 +1,8 @@
import React from 'react'; 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 { Tab, Tabs as PFTabs } from '@patternfly/react-core';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import styled from 'styled-components'; import styled from 'styled-components';
import { CaretLeftIcon } from '@patternfly/react-icons';
const Tabs = styled(PFTabs)` const Tabs = styled(PFTabs)`
--pf-c-tabs__button--PaddingLeft: 20px; --pf-c-tabs__button--PaddingLeft: 20px;
@@ -57,25 +56,15 @@ function RoutedTabs(props) {
return ( return (
<Tabs activeKey={getActiveTabId()} onSelect={handleTabSelect}> <Tabs activeKey={getActiveTabId()} onSelect={handleTabSelect}>
{tabsArray {tabsArray.map(tab => (
.filter(tab => tab.isNestedTab || !tab.name.startsWith('Return')) <Tab
.map(tab => ( aria-label={`${tab.name}`}
<Tab eventKey={tab.id}
aria-label={`${tab.name}`} key={tab.id}
eventKey={tab.id} link={tab.link}
key={tab.id} title={tab.name}
link={tab.link} />
title={ ))}
tab.isNestedTab ? (
<>
<CaretLeftIcon /> {tab.name}
</>
) : (
tab.name
)
}
/>
))}
</Tabs> </Tabs>
); );
} }
@@ -89,7 +78,7 @@ RoutedTabs.propTypes = {
shape({ shape({
id: number.isRequired, id: number.isRequired,
link: string.isRequired, link: string.isRequired,
name: string.isRequired, name: oneOfType([string.isRequired, node.isRequired]),
}) })
).isRequired, ).isRequired,
}; };

View File

@@ -2,15 +2,16 @@ import React, { Component } from 'react';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom'; 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 { TabbedCardHeader } from '@components/Card';
import CardCloseButton from '@components/CardCloseButton'; import CardCloseButton from '@components/CardCloseButton';
import RoutedTabs from '@components/RoutedTabs'; import RoutedTabs from '@components/RoutedTabs';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
import HostFacts from './HostFacts'; import HostFacts from './HostFacts';
import HostDetail from './HostDetail'; import HostDetail from './HostDetail';
import AlertModal from '@components/AlertModal';
import ErrorDetail from '@components/ErrorDetail';
import HostEdit from './HostEdit'; import HostEdit from './HostEdit';
import HostGroups from './HostGroups'; import HostGroups from './HostGroups';
@@ -26,16 +27,8 @@ class Host extends Component {
hasContentLoading: true, hasContentLoading: true,
contentError: null, contentError: null,
isInitialized: false, isInitialized: false,
toggleLoading: false,
toggleError: null,
deletionError: false,
isDeleteModalOpen: false,
}; };
this.loadHost = this.loadHost.bind(this); 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() { 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() { async loadHost() {
const { match, setBreadcrumb, history, inventory } = this.props; const { match, setBreadcrumb, history, inventory } = this.props;
@@ -102,8 +61,9 @@ class Host extends Component {
if (history.location.pathname.startsWith('/hosts')) { if (history.location.pathname.startsWith('/hosts')) {
setBreadcrumb(data); setBreadcrumb(data);
} else {
setBreadcrumb(inventory, data);
} }
setBreadcrumb(inventory, data);
} catch (err) { } catch (err) {
this.setState({ contentError: err }); this.setState({ contentError: err });
} finally { } finally {
@@ -111,29 +71,10 @@ class Host extends Component {
} }
} }
handleToggleError() {
this.setState({ toggleError: false });
}
render() { render() {
const { location, match, history, i18n } = this.props; const { location, match, history, i18n } = this.props;
const { const { host, hasContentLoading, isInitialized, contentError } = this.state;
deletionError,
host,
isDeleteModalOpen,
toggleError,
hasContentLoading,
toggleLoading,
isInitialized,
contentError,
} = this.state;
const tabsArray = [ 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`), name: i18n._(t`Details`),
link: `${match.url}/details`, link: `${match.url}/details`,
@@ -155,7 +96,18 @@ class Host extends Component {
id: 3, id: 3,
}, },
]; ];
if (!history.location.pathname.startsWith('/hosts')) {
tabsArray.unshift({
name: (
<>
<CaretLeftIcon />
{i18n._(t`Back to Hosts`)}
</>
),
link: `/inventories/inventory/${match.params.id}/hosts`,
id: 99,
});
}
let cardHeader = ( let cardHeader = (
<TabbedCardHeader> <TabbedCardHeader>
<RoutedTabs <RoutedTabs
@@ -180,115 +132,98 @@ class Host extends Component {
cardHeader = null; cardHeader = null;
} }
if (hasContentLoading) {
return <ContentLoading />;
}
if (!hasContentLoading && contentError) { if (!hasContentLoading && contentError) {
return ( return (
<PageSection> <Card className="awx-c-card">
<Card className="awx-c-card"> <ContentError error={contentError}>
<ContentError error={contentError}> {contentError.response.status === 404 && (
{contentError.response.status === 404 && ( <span>
<span> {i18n._(`Host not found.`)}{' '}
{i18n._(`Host not found.`)}{' '} <Link to="/hosts">{i18n._(`View all Hosts.`)}</Link>
<Link to="/hosts">{i18n._(`View all Hosts.`)}</Link> </span>
</span> )}
)} </ContentError>
</ContentError> </Card>
</Card>
</PageSection>
); );
} }
const redirect = location.pathname.startsWith('/hosts') ? (
<Redirect from="/hosts/:id" to="/hosts/:id/details" exact />
) : (
<Redirect
from="/inventories/inventory/:id/hosts/:hostId"
to="/inventories/inventory/:id/hosts/:hostId/details"
exact
/>
);
return ( return (
<> <Card className="awx-c-card">
<PageSection {cardHeader}
css={` <Switch>
${location.pathname.startsWith('/inventories') {redirect}
? 'padding: 0' {host && (
: 'null'} <Route
`} path={[
> '/hosts/:id/details',
<Card className="awx-c-card"> '/inventories/inventory/:id/hosts/:hostId/details',
{cardHeader} ]}
<Switch> render={() => (
<Redirect from="/hosts/:id" to="/hosts/:id/details" exact /> <HostDetail
{host && ( host={host}
<Route onUpdateHost={newHost => this.setState({ host: newHost })}
path={[
'/hosts/:id/edit',
'/inventories/inventory/:id/hosts/:hostId/edit',
]}
render={() => <HostEdit match={match} host={host} />}
/> />
)} )}
{host && ( />
<Route )}
path={[ ))
'/hosts/:id/details', {host && (
'/inventories/inventory/:id/hosts/:hostId/details', <Route
]} path={[
render={() => ( '/hosts/:id/edit',
<HostDetail '/inventories/inventory/:id/hosts/:hostId/edit',
match={match} ]}
host={host} render={() => <HostEdit match={match} host={host} />}
history={history} />
onToggleDeleteModal={this.toggleDeleteModal} )}
isDeleteModalOpen={isDeleteModalOpen} {host && (
onHandleHostToggle={this.handleHostToggle} <Route
toggleError={toggleError} path="/hosts/:id/facts"
toggleLoading={toggleLoading} render={() => <HostFacts host={host} />}
onToggleError={this.handleToggleError} />
onHostDelete={this.handleHostDelete} )}
/> {host && (
<Route
path="/hosts/:id/groups"
render={() => <HostGroups host={host} />}
/>
)}
{host && (
<Route
path="/hosts/:id/completed_jobs"
render={() => <HostCompletedJobs host={host} />}
/>
)}
<Route
key="not-found"
path="*"
render={() =>
!hasContentLoading && (
<ContentError isNotFound>
{match.params.id && (
<Link to={`/hosts/${match.params.id}/details`}>
{i18n._(`View Host Details`)}
</Link>
)} )}
/> </ContentError>
)} )
{host && ( }
<Route />
path="/hosts/:id/facts" ,
render={() => <HostFacts host={host} />} </Switch>
/> </Card>
)}
{host && (
<Route
path="/hosts/:id/groups"
render={() => <HostGroups host={host} />}
/>
)}
{host && (
<Route
path="/hosts/:id/completed_jobs"
render={() => <HostCompletedJobs host={host} />}
/>
)}
<Route
key="not-found"
path="*"
render={() =>
!hasContentLoading && (
<ContentError isNotFound>
{match.params.id && (
<Link to={`/hosts/${match.params.id}/details`}>
{i18n._(`View Host Details`)}
</Link>
)}
</ContentError>
)
}
/>
,
</Switch>
</Card>
</PageSection>
{deletionError && (
<AlertModal
isOpen={deletionError}
variant="danger"
title={i18n._(t`Error!`)}
onClose={() => this.setState({ deletionError: false })}
>
{i18n._(t`Failed to delete ${host.name}.`)}
<ErrorDetail error={deletionError} />
</AlertModal>
)}
</>
); );
} }
} }

View File

@@ -1,14 +1,18 @@
import React from 'react'; import React, { useState } from 'react';
import { Link } from 'react-router-dom'; import { Link, withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Host } from '@types'; import { Host } from '@types';
import { Button } from '@patternfly/react-core'; import { Button } from '@patternfly/react-core';
import { CardBody, CardActionsRow } from '@components/Card'; import { CardBody, CardActionsRow } from '@components/Card';
import AlertModal from '@components/AlertModal';
import ErrorDetail from '@components/ErrorDetail';
import { DetailList, Detail, UserDateDetail } from '@components/DetailList'; import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
import { VariablesDetail } from '@components/CodeMirrorInput'; import { VariablesDetail } from '@components/CodeMirrorInput';
import { Sparkline } from '@components/Sparkline'; import { Sparkline } from '@components/Sparkline';
import DeleteButton from '@components/DeleteButton';
import Switch from '@components/Switch'; import Switch from '@components/Switch';
import { HostsAPI } from '@api';
function HostDetail({ host, i18n }) { function HostDetail({ host, i18n }) {
const ActionButtonWrapper = styled.div` const ActionButtonWrapper = styled.div`
@@ -20,92 +24,62 @@ const ActionButtonWrapper = styled.div`
} }
`; `;
function HostDetail({ function HostDetail({ host, history, match, i18n, onUpdateHost }) {
host,
history,
isDeleteModalOpen,
match,
i18n,
toggleError,
toggleLoading,
onHostDelete,
onToggleDeleteModal,
onToggleError,
onHandleHostToggle,
}) {
const { created, description, id, modified, name, summary_fields } = host; const { created, description, id, modified, name, summary_fields } = host;
let createdBy = '';
if (created) {
if (summary_fields.created_by && summary_fields.created_by.username) {
createdBy = (
<span>
{i18n._(t`${formatDateString(created)} by `)}{' '}
<Link to={`/users/${summary_fields.created_by.id}`}>
{summary_fields.created_by.username}
</Link>
</span>
);
} else {
createdBy = formatDateString(created);
}
}
let modifiedBy = ''; const [isLoading, setIsloading] = useState(false);
if (modified) { const [deletionError, setDeletionError] = useState(false);
if (summary_fields.modified_by && summary_fields.modified_by.username) { const [toggleLoading, setToggleLoading] = useState(false);
modifiedBy = ( const [toggleError, setToggleError] = useState(false);
<span>
{i18n._(t`${formatDateString(modified)} by`)}{' '} const handleHostToggle = async () => {
<Link to={`/users/${summary_fields.modified_by.id}`}> setToggleLoading(true);
{summary_fields.modified_by.username} try {
</Link> const { data } = await HostsAPI.update(host.id, {
</span> enabled: !host.enabled,
); });
} else { onUpdateHost(data);
modifiedBy = formatDateString(modified); } 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) { if (toggleError && !toggleLoading) {
return ( return (
<AlertModal <AlertModal
variant="danger" variant="danger"
title={i18n._(t`Error!`)} title={i18n._(t`Error!`)}
isOpen={toggleError && !toggleLoading} isOpen={toggleError && !toggleLoading}
onClose={onToggleError} onClose={() => setToggleError(false)}
> >
{i18n._(t`Failed to toggle host.`)} {i18n._(t`Failed to toggle host.`)}
<ErrorDetail error={toggleError} /> <ErrorDetail error={toggleError} />
</AlertModal> </AlertModal>
); );
} }
if (isDeleteModalOpen) { if (!isLoading && deletionError) {
return ( return (
<AlertModal <AlertModal
isOpen={isDeleteModalOpen} isOpen={deletionError}
title={i18n._(t`Delete Host`)}
variant="danger" variant="danger"
onClose={() => onToggleDeleteModal()} title={i18n._(t`Error!`)}
onClose={() => setDeletionError(false)}
> >
{i18n._(t`Are you sure you want to delete:`)} {i18n._(t`Failed to delete ${host.name}.`)}
<br /> <ErrorDetail error={deletionError} />
<strong>{host.name}</strong>
<ActionButtonWrapper>
<Button
variant="secondary"
aria-label={i18n._(t`Close`)}
onClick={() => onToggleDeleteModal()}
>
{i18n._(t`Cancel`)}
</Button>
<Button
variant="danger"
aria-label={i18n._(t`Delete`)}
onClick={() => onHostDelete()}
>
{i18n._(t`Delete`)}
</Button>
</ActionButtonWrapper>
</AlertModal> </AlertModal>
); );
} }
@@ -118,7 +92,7 @@ function HostDetail({
labelOff={i18n._(t`Off`)} labelOff={i18n._(t`Off`)}
isChecked={host.enabled} isChecked={host.enabled}
isDisabled={!host.summary_fields.user_capabilities.edit} isDisabled={!host.summary_fields.user_capabilities.edit}
onChange={onHandleHostToggle} onChange={() => handleHostToggle()}
aria-label={i18n._(t`Toggle Host`)} aria-label={i18n._(t`Toggle Host`)}
/> />
<DetailList gutter="sm"> <DetailList gutter="sm">
@@ -145,8 +119,16 @@ function HostDetail({
} }
/> />
)} )}
<Detail label={i18n._(t`Created`)} value={createdBy} /> <UserDateDetail
<Detail label={i18n._(t`Last Modified`)} value={modifiedBy} /> date={created}
label={i18n._(t`Created`)}
user={summary_fields.created_by}
/>
<UserDateDetail
label={i18n._(t`Last Modified`)}
user={summary_fields.modified_by}
date={modified}
/>
<VariablesDetail <VariablesDetail
value={host.variables} value={host.variables}
rows={4} rows={4}
@@ -169,6 +151,14 @@ function HostDetail({
</Button> </Button>
)} )}
</CardActionsRow> </CardActionsRow>
{summary_fields.user_capabilities &&
summary_fields.user_capabilities.delete && (
<DeleteButton
onConfirm={() => handleHostDelete()}
modalTitle={i18n._(t`Delete Host`)}
name={host.name}
/>
)}
</CardBody> </CardBody>
); );
} }

View File

@@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react';
import { Route, withRouter, Switch } from 'react-router-dom'; import { Route, withRouter, Switch } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { PageSection } from '@patternfly/react-core';
import { Config } from '@contexts/Config'; import { Config } from '@contexts/Config';
import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs'; import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs';
@@ -46,35 +47,31 @@ class Hosts extends Component {
}; };
render() { render() {
const { match, history, location, inventory } = this.props; const { match } = this.props;
const { breadcrumbConfig } = this.state; const { breadcrumbConfig } = this.state;
return ( return (
<Fragment> <Fragment>
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} /> <Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
<Switch> <PageSection>
<Route <Switch>
path={`${match.path}/add`} <Route path={`${match.path}/add`} render={() => <HostAdd />} />
render={() => <HostAdd history={history} />} <Route
/> path={`${match.path}/:id`}
<Route render={() => (
path={`${match.path}/:id`} <Config>
render={() => ( {({ me }) => (
<Config> <Host
{({ me }) => ( setBreadcrumb={this.setBreadcrumbConfig}
<Host me={me || {}}
history={history} />
location={location} )}
setBreadcrumb={this.setBreadcrumbConfig} </Config>
me={me || {}} )}
inventory={inventory} />
/> <Route path={`${match.path}`} render={() => <HostList />} />
)} </Switch>
</Config> </PageSection>
)}
/>
<Route path={`${match.path}`} render={() => <HostList />} />
</Switch>
</Fragment> </Fragment>
); );
} }

View File

@@ -39,51 +39,46 @@ class Inventories extends Component {
'/inventories/inventory/add': i18n._(t`Create New Inventory`), '/inventories/inventory/add': i18n._(t`Create New Inventory`),
'/inventories/smart_inventory/add': i18n._(t`Create New Smart Inventory`), '/inventories/smart_inventory/add': i18n._(t`Create New Smart Inventory`),
[`/inventories/${inventoryKind}/${inventory.id}`]: `${inventory.name}`, [`/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._( [`/inventories/${inventoryKind}/${inventory.id}/access`]: i18n._(
t`Access` t`Access`
), ),
[`/inventories/${inventoryKind}/${inventory.id}/completed_jobs`]: i18n._( [`/inventories/${inventoryKind}/${inventory.id}/completed_jobs`]: i18n._(
t`Completed Jobs` 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 && [`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
nestedResource.id}`]: i18n._( nestedResource.id}`]: i18n._(
t`${nestedResource && nestedResource.name}` 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._( [`/inventories/${inventoryKind}/${inventory.id}/groups/add`]: i18n._(
t`Create New Group` t`Create New Group`
), ),
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource && [`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
nestedResource.id}`]: `${nestedResource && nestedResource.name}`, nestedResource.id}/edit`]: i18n._(t`Edit Details`),
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource && [`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
nestedResource.id}/details`]: i18n._(t`Group Details`), nestedResource.id}/details`]: i18n._(t`Group Details`),
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource && [`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
nestedResource.id}/edit`]: i18n._(t`Edit Details`), nestedResource.id}`]: `${nestedResource && nestedResource.name}`,
[`/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 }); this.setState({ breadcrumbConfig });
}; };

View File

@@ -63,7 +63,7 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) {
location.pathname.endsWith('edit') || location.pathname.endsWith('edit') ||
location.pathname.endsWith('add') || location.pathname.endsWith('add') ||
location.pathname.includes('groups/') || location.pathname.includes('groups/') ||
history.location.pathname.includes(`/hosts/`) location.pathname.includes('hosts/')
) { ) {
cardHeader = null; cardHeader = null;
} }

View File

@@ -1,8 +1,9 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { Switch, Route, withRouter, Link, Redirect } from 'react-router-dom'; import { Switch, Route, withRouter, Link, Redirect } from 'react-router-dom';
import { CaretLeftIcon } from '@patternfly/react-icons';
import { GroupsAPI } from '@api'; import { GroupsAPI } from '@api';
import CardCloseButton from '@components/CardCloseButton'; import CardCloseButton from '@components/CardCloseButton';
import RoutedTabs from '@components/RoutedTabs'; import RoutedTabs from '@components/RoutedTabs';
@@ -40,7 +41,12 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) {
const tabsArray = [ const tabsArray = [
{ {
name: i18n._(t`Return to Groups`), name: (
<>
<CaretLeftIcon />
{i18n._(t`Back to Groups`)}
</>
),
link: `/inventories/inventory/${inventory.id}/groups`, link: `/inventories/inventory/${inventory.id}/groups`,
id: 99, id: 99,
isNestedTab: true, isNestedTab: true,

View File

@@ -14,6 +14,8 @@ GroupsAPI.readDetail.mockResolvedValue({
name: 'Foo', name: 'Foo',
description: 'Bar', description: 'Bar',
variables: 'bizz: buzz', variables: 'bizz: buzz',
created: '1/12/2019',
modified: '1/13/2019',
summary_fields: { summary_fields: {
inventory: { id: 1 }, inventory: { id: 1 },
created_by: { id: 1, username: 'Athena' }, created_by: { id: 1, username: 'Athena' },
@@ -62,10 +64,10 @@ describe('<InventoryGroup />', () => {
test('renders successfully', async () => { test('renders successfully', async () => {
expect(wrapper.length).toBe(1); expect(wrapper.length).toBe(1);
}); });
test('expect all tabs to exist, including Return to Groups', async () => { test('expect all tabs to exist, including Back to Groups', async () => {
expect(wrapper.find('button[aria-label="Return to Groups"]').length).toBe( expect(
1 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="Details"]').length).toBe(1);
expect(wrapper.find('button[aria-label="Related Groups"]').length).toBe(1); expect(wrapper.find('button[aria-label="Related Groups"]').length).toBe(1);
expect(wrapper.find('button[aria-label="Hosts"]').length).toBe(1); expect(wrapper.find('button[aria-label="Hosts"]').length).toBe(1);

View File

@@ -3,7 +3,7 @@ import { Switch, Route, withRouter } from 'react-router-dom';
import Host from '../../Host/Host'; import Host from '../../Host/Host';
import InventoryHostList from './InventoryHostList'; import InventoryHostList from './InventoryHostList';
import HostAdd from '../InventoryHostAdd'; import InventoryHostAdd from '../InventoryHostAdd';
function InventoryHosts({ match, setBreadcrumb, i18n, inventory }) { function InventoryHosts({ match, setBreadcrumb, i18n, inventory }) {
return ( return (
@@ -11,11 +11,11 @@ function InventoryHosts({ match, setBreadcrumb, i18n, inventory }) {
<Route <Route
key="host-add" key="host-add"
path="/inventories/inventory/:id/hosts/add" path="/inventories/inventory/:id/hosts/add"
render={() => <HostAdd match={match} />} render={() => <InventoryHostAdd match={match} />}
/> />
, ,
<Route <Route
key="details and edit" key="host"
path="/inventories/inventory/:id/hosts/:hostId" path="/inventories/inventory/:id/hosts/:hostId"
render={() => ( render={() => (
<Host <Host

View File

@@ -17,6 +17,8 @@ describe('<JobTemplateDetail />', () => {
playbook: '', playbook: '',
id: 1, id: 1,
verbosity: 1, verbosity: 1,
created: '1/12/2019',
modified: '1/13/2019',
summary_fields: { summary_fields: {
user_capabilities: { edit: true }, user_capabilities: { edit: true },
created_by: { id: 1, username: 'Joe' }, created_by: { id: 1, username: 'Joe' },