mirror of
https://github.com/ansible/awx.git
synced 2026-03-24 04:15:02 -02:30
Merge pull request #5600 from AlexSCorey/5266-InventoryHostDetails
Adds Toggle, Variables, User Link and Delete to Inventory Host/Host Details Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -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={() => onRowClick(item)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
|
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -63,15 +62,7 @@ function RoutedTabs(props) {
|
|||||||
eventKey={tab.id}
|
eventKey={tab.id}
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
link={tab.link}
|
link={tab.link}
|
||||||
title={
|
title={tab.name}
|
||||||
tab.isNestedTabs ? (
|
|
||||||
<>
|
|
||||||
<CaretLeftIcon /> {tab.name}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
tab.name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -87,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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ 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 HostEdit from './HostEdit';
|
import HostEdit from './HostEdit';
|
||||||
import HostGroups from './HostGroups';
|
import HostGroups from './HostGroups';
|
||||||
import HostCompletedJobs from './HostCompletedJobs';
|
import HostCompletedJobs from './HostCompletedJobs';
|
||||||
@@ -46,14 +50,20 @@ class Host extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadHost() {
|
async loadHost() {
|
||||||
const { match, setBreadcrumb } = this.props;
|
const { match, setBreadcrumb, history, inventory } = this.props;
|
||||||
const id = parseInt(match.params.id, 10);
|
|
||||||
|
|
||||||
this.setState({ contentError: null, hasContentLoading: true });
|
this.setState({ contentError: null, hasContentLoading: true });
|
||||||
try {
|
try {
|
||||||
const { data } = await HostsAPI.readDetail(id);
|
const { data } = await HostsAPI.readDetail(
|
||||||
setBreadcrumb(data);
|
match.params.hostId || match.params.id
|
||||||
|
);
|
||||||
this.setState({ host: data });
|
this.setState({ host: data });
|
||||||
|
|
||||||
|
if (history.location.pathname.startsWith('/hosts')) {
|
||||||
|
setBreadcrumb(data);
|
||||||
|
} else {
|
||||||
|
setBreadcrumb(inventory, data);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ contentError: err });
|
this.setState({ contentError: err });
|
||||||
} finally {
|
} finally {
|
||||||
@@ -63,20 +73,41 @@ class Host extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { location, match, history, i18n } = this.props;
|
const { location, match, history, i18n } = this.props;
|
||||||
|
const { host, hasContentLoading, isInitialized, contentError } = this.state;
|
||||||
const { host, contentError, hasContentLoading, isInitialized } = this.state;
|
|
||||||
|
|
||||||
const tabsArray = [
|
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`Details`),
|
||||||
{ name: i18n._(t`Groups`), link: `${match.url}/groups`, id: 2 },
|
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`),
|
name: i18n._(t`Completed Jobs`),
|
||||||
link: `${match.url}/completed_jobs`,
|
link: `${match.url}/completed_jobs`,
|
||||||
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
|
||||||
@@ -101,78 +132,98 @@ class Host extends Component {
|
|||||||
cardHeader = null;
|
cardHeader = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasContentLoading && contentError) {
|
if (hasContentLoading) {
|
||||||
return (
|
return <ContentLoading />;
|
||||||
<PageSection>
|
|
||||||
<Card className="awx-c-card">
|
|
||||||
<ContentError error={contentError}>
|
|
||||||
{contentError.response.status === 404 && (
|
|
||||||
<span>
|
|
||||||
{i18n._(`Host not found.`)}{' '}
|
|
||||||
<Link to="/hosts">{i18n._(`View all Hosts.`)}</Link>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</ContentError>
|
|
||||||
</Card>
|
|
||||||
</PageSection>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (!hasContentLoading && contentError) {
|
||||||
<PageSection>
|
return (
|
||||||
<Card className="awx-c-card">
|
<Card className="awx-c-card">
|
||||||
{cardHeader}
|
<ContentError error={contentError}>
|
||||||
<Switch>
|
{contentError.response.status === 404 && (
|
||||||
<Redirect from="/hosts/:id" to="/hosts/:id/details" exact />
|
<span>
|
||||||
{host && (
|
{i18n._(`Host not found.`)}{' '}
|
||||||
<Route
|
<Link to="/hosts">{i18n._(`View all Hosts.`)}</Link>
|
||||||
path="/hosts/:id/edit"
|
</span>
|
||||||
render={() => <HostEdit match={match} host={host} />}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{host && (
|
</ContentError>
|
||||||
<Route
|
|
||||||
path="/hosts/:id/details"
|
|
||||||
render={() => <HostDetail host={host} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{host && (
|
|
||||||
<Route
|
|
||||||
path="/hosts/:id/facts"
|
|
||||||
render={() => <HostFacts host={host} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{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>
|
</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 (
|
||||||
|
<Card className="awx-c-card">
|
||||||
|
{cardHeader}
|
||||||
|
<Switch>
|
||||||
|
{redirect}
|
||||||
|
{host && (
|
||||||
|
<Route
|
||||||
|
path={[
|
||||||
|
'/hosts/:id/details',
|
||||||
|
'/inventories/inventory/:id/hosts/:hostId/details',
|
||||||
|
]}
|
||||||
|
render={() => (
|
||||||
|
<HostDetail
|
||||||
|
host={host}
|
||||||
|
onUpdateHost={newHost => this.setState({ host: newHost })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
))
|
||||||
|
{host && (
|
||||||
|
<Route
|
||||||
|
path={[
|
||||||
|
'/hosts/:id/edit',
|
||||||
|
'/inventories/inventory/:id/hosts/:hostId/edit',
|
||||||
|
]}
|
||||||
|
render={() => <HostEdit match={match} host={host} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{host && (
|
||||||
|
<Route
|
||||||
|
path="/hosts/:id/facts"
|
||||||
|
render={() => <HostFacts host={host} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,167 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useHistory, useParams, useLocation } 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 DeleteButton from '@components/DeleteButton';
|
||||||
|
import Switch from '@components/Switch';
|
||||||
|
import { HostsAPI } from '@api';
|
||||||
|
|
||||||
function HostDetail({ host, i18n }) {
|
function HostDetail({ host, i18n, onUpdateHost }) {
|
||||||
const { created, description, id, modified, name, summary_fields } = host;
|
const {
|
||||||
|
created,
|
||||||
|
description,
|
||||||
|
id,
|
||||||
|
modified,
|
||||||
|
name,
|
||||||
|
enabled,
|
||||||
|
summary_fields: {
|
||||||
|
inventory,
|
||||||
|
recent_jobs,
|
||||||
|
kind,
|
||||||
|
created_by,
|
||||||
|
modified_by,
|
||||||
|
user_capabilities,
|
||||||
|
},
|
||||||
|
} = host;
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const { id: inventoryId, hostId: inventoryHostId } = useParams();
|
||||||
|
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(id, {
|
||||||
|
enabled: !enabled,
|
||||||
|
});
|
||||||
|
onUpdateHost(data);
|
||||||
|
} catch (err) {
|
||||||
|
setToggleError(err);
|
||||||
|
} finally {
|
||||||
|
setToggleLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHostDelete = async () => {
|
||||||
|
setIsloading(true);
|
||||||
|
try {
|
||||||
|
await HostsAPI.destroy(id);
|
||||||
|
setIsloading(false);
|
||||||
|
history.push(`/inventories/inventory/${inventoryId}/hosts`);
|
||||||
|
} catch (err) {
|
||||||
|
setDeletionError(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (toggleError && !toggleLoading) {
|
||||||
|
return (
|
||||||
|
<AlertModal
|
||||||
|
variant="danger"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
isOpen={toggleError && !toggleLoading}
|
||||||
|
onClose={() => setToggleError(false)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to toggle host.`)}
|
||||||
|
<ErrorDetail error={toggleError} />
|
||||||
|
</AlertModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!isLoading && deletionError) {
|
||||||
|
return (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={deletionError}
|
||||||
|
variant="danger"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={() => setDeletionError(false)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to delete ${name}.`)}
|
||||||
|
<ErrorDetail error={deletionError} />
|
||||||
|
</AlertModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
|
<Switch
|
||||||
|
css="padding-bottom: 40px"
|
||||||
|
id={`host-${id}-toggle`}
|
||||||
|
label={i18n._(t`On`)}
|
||||||
|
labelOff={i18n._(t`Off`)}
|
||||||
|
isChecked={enabled}
|
||||||
|
isDisabled={!user_capabilities.edit}
|
||||||
|
onChange={() => handleHostToggle()}
|
||||||
|
aria-label={i18n._(t`Toggle Host`)}
|
||||||
|
/>
|
||||||
<DetailList gutter="sm">
|
<DetailList gutter="sm">
|
||||||
<Detail label={i18n._(t`Name`)} value={name} />
|
<Detail label={i18n._(t`Name`)} value={name} />
|
||||||
|
<Detail
|
||||||
|
css="display: flex; flex: 1;"
|
||||||
|
value={<Sparkline jobs={recent_jobs} />}
|
||||||
|
label={i18n._(t`Activity`)}
|
||||||
|
/>
|
||||||
<Detail label={i18n._(t`Description`)} value={description} />
|
<Detail label={i18n._(t`Description`)} value={description} />
|
||||||
{summary_fields.inventory && (
|
{inventory && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Inventory`)}
|
label={i18n._(t`Inventory`)}
|
||||||
value={
|
value={
|
||||||
<Link
|
<Link
|
||||||
to={`/inventories/${
|
to={`/inventories/${
|
||||||
summary_fields.inventory.kind === 'smart'
|
kind === 'smart' ? 'smart_inventory' : 'inventory'
|
||||||
? 'smart_inventory'
|
}/${inventoryId}/details`}
|
||||||
: 'inventory'
|
|
||||||
}/${summary_fields.inventory.id}/details`}
|
|
||||||
>
|
>
|
||||||
{summary_fields.inventory.name}
|
{inventory.name}
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<UserDateDetail
|
<UserDateDetail
|
||||||
label={i18n._(t`Created`)}
|
|
||||||
date={created}
|
date={created}
|
||||||
user={summary_fields.created_by}
|
label={i18n._(t`Created`)}
|
||||||
|
user={created_by}
|
||||||
/>
|
/>
|
||||||
<UserDateDetail
|
<UserDateDetail
|
||||||
label={i18n._(t`Last Modified`)}
|
label={i18n._(t`Last Modified`)}
|
||||||
|
user={modified_by}
|
||||||
date={modified}
|
date={modified}
|
||||||
user={summary_fields.modified_by}
|
|
||||||
/>
|
/>
|
||||||
<VariablesDetail
|
<VariablesDetail
|
||||||
label={i18n._(t`Variables`)}
|
|
||||||
value={host.variables}
|
value={host.variables}
|
||||||
rows={6}
|
rows={4}
|
||||||
|
label={i18n._(t`Variables`)}
|
||||||
/>
|
/>
|
||||||
</DetailList>
|
</DetailList>
|
||||||
<CardActionsRow>
|
<CardActionsRow>
|
||||||
{summary_fields.user_capabilities &&
|
{user_capabilities && user_capabilities.edit && (
|
||||||
summary_fields.user_capabilities.edit && (
|
<Button
|
||||||
<Button
|
aria-label={i18n._(t`edit`)}
|
||||||
aria-label={i18n._(t`edit`)}
|
component={Link}
|
||||||
component={Link}
|
to={
|
||||||
to={`/hosts/${id}/edit`}
|
pathname.startsWith('/inventories')
|
||||||
>
|
? `/inventories/inventory/${inventoryId}/hosts/${inventoryHostId}/edit`
|
||||||
{i18n._(t`Edit`)}
|
: `/hosts/${id}/edit`
|
||||||
</Button>
|
}
|
||||||
)}
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{user_capabilities && user_capabilities.delete && (
|
||||||
|
<DeleteButton
|
||||||
|
onConfirm={() => handleHostDelete()}
|
||||||
|
modalTitle={i18n._(t`Delete Host`)}
|
||||||
|
name={host.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</CardActionsRow>
|
</CardActionsRow>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,8 +50,7 @@ describe('<HostDetail />', () => {
|
|||||||
|
|
||||||
test('should show edit button for users with edit permission', async () => {
|
test('should show edit button for users with edit permission', async () => {
|
||||||
const wrapper = mountWithContexts(<HostDetail host={mockHost} />);
|
const wrapper = mountWithContexts(<HostDetail host={mockHost} />);
|
||||||
// VariablesDetail has two buttons
|
const editButton = wrapper.find('Button[aria-label="edit"]');
|
||||||
const editButton = wrapper.find('Button').at(2);
|
|
||||||
expect(editButton.text()).toEqual('Edit');
|
expect(editButton.text()).toEqual('Edit');
|
||||||
expect(editButton.prop('to')).toBe('/hosts/1/edit');
|
expect(editButton.prop('to')).toBe('/hosts/1/edit');
|
||||||
});
|
});
|
||||||
@@ -61,7 +60,6 @@ describe('<HostDetail />', () => {
|
|||||||
readOnlyHost.summary_fields.user_capabilities.edit = false;
|
readOnlyHost.summary_fields.user_capabilities.edit = false;
|
||||||
const wrapper = mountWithContexts(<HostDetail host={readOnlyHost} />);
|
const wrapper = mountWithContexts(<HostDetail host={readOnlyHost} />);
|
||||||
await waitForElement(wrapper, 'HostDetail');
|
await waitForElement(wrapper, 'HostDetail');
|
||||||
// VariablesDetail has two buttons
|
expect(wrapper.find('Button[aria-label="edit"]').length).toBe(0);
|
||||||
expect(wrapper.find('Button').length).toBe(2);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { Component, Fragment } from 'react';
|
|||||||
import { withRouter } from 'react-router-dom';
|
import { 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 { Card, PageSection } from '@patternfly/react-core';
|
import { Card } from '@patternfly/react-core';
|
||||||
|
|
||||||
import { HostsAPI } from '@api';
|
import { HostsAPI } from '@api';
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
@@ -180,76 +180,74 @@ class HostsList extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<PageSection>
|
<Card>
|
||||||
<Card>
|
<PaginatedDataList
|
||||||
<PaginatedDataList
|
contentError={contentError}
|
||||||
contentError={contentError}
|
hasContentLoading={hasContentLoading}
|
||||||
hasContentLoading={hasContentLoading}
|
items={hosts}
|
||||||
items={hosts}
|
itemCount={itemCount}
|
||||||
itemCount={itemCount}
|
pluralizedItemName={i18n._(t`Hosts`)}
|
||||||
pluralizedItemName={i18n._(t`Hosts`)}
|
qsConfig={QS_CONFIG}
|
||||||
qsConfig={QS_CONFIG}
|
onRowClick={this.handleSelect}
|
||||||
onRowClick={this.handleSelect}
|
toolbarSearchColumns={[
|
||||||
toolbarSearchColumns={[
|
{
|
||||||
{
|
name: i18n._(t`Name`),
|
||||||
name: i18n._(t`Name`),
|
key: 'name',
|
||||||
key: 'name',
|
isDefault: true,
|
||||||
isDefault: true,
|
},
|
||||||
},
|
{
|
||||||
{
|
name: i18n._(t`Created By (Username)`),
|
||||||
name: i18n._(t`Created By (Username)`),
|
key: 'created_by__username',
|
||||||
key: 'created_by__username',
|
},
|
||||||
},
|
{
|
||||||
{
|
name: i18n._(t`Modified By (Username)`),
|
||||||
name: i18n._(t`Modified By (Username)`),
|
key: 'modified_by__username',
|
||||||
key: 'modified_by__username',
|
},
|
||||||
},
|
]}
|
||||||
]}
|
toolbarSortColumns={[
|
||||||
toolbarSortColumns={[
|
{
|
||||||
{
|
name: i18n._(t`Name`),
|
||||||
name: i18n._(t`Name`),
|
key: 'name',
|
||||||
key: 'name',
|
},
|
||||||
},
|
]}
|
||||||
]}
|
renderToolbar={props => (
|
||||||
renderToolbar={props => (
|
<DataListToolbar
|
||||||
<DataListToolbar
|
{...props}
|
||||||
{...props}
|
showSelectAll
|
||||||
showSelectAll
|
isAllSelected={isAllSelected}
|
||||||
isAllSelected={isAllSelected}
|
onSelectAll={this.handleSelectAll}
|
||||||
onSelectAll={this.handleSelectAll}
|
qsConfig={QS_CONFIG}
|
||||||
qsConfig={QS_CONFIG}
|
additionalControls={[
|
||||||
additionalControls={[
|
<ToolbarDeleteButton
|
||||||
<ToolbarDeleteButton
|
key="delete"
|
||||||
key="delete"
|
onDelete={this.handleHostDelete}
|
||||||
onDelete={this.handleHostDelete}
|
itemsToDelete={selected}
|
||||||
itemsToDelete={selected}
|
pluralizedItemName={i18n._(t`Hosts`)}
|
||||||
pluralizedItemName={i18n._(t`Hosts`)}
|
/>,
|
||||||
/>,
|
canAdd ? (
|
||||||
canAdd ? (
|
<ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
|
||||||
<ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
|
) : null,
|
||||||
) : null,
|
]}
|
||||||
]}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
renderItem={o => (
|
||||||
renderItem={o => (
|
<HostListItem
|
||||||
<HostListItem
|
key={o.id}
|
||||||
key={o.id}
|
host={o}
|
||||||
host={o}
|
detailUrl={`${match.url}/${o.id}/details`}
|
||||||
detailUrl={`${match.url}/${o.id}`}
|
isSelected={selected.some(row => row.id === o.id)}
|
||||||
isSelected={selected.some(row => row.id === o.id)}
|
onSelect={() => this.handleSelect(o)}
|
||||||
onSelect={() => this.handleSelect(o)}
|
onToggleHost={this.handleHostToggle}
|
||||||
toggleHost={this.handleHostToggle}
|
toggleLoading={toggleLoading === o.id}
|
||||||
toggleLoading={toggleLoading === o.id}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
emptyStateControls={
|
||||||
emptyStateControls={
|
canAdd ? (
|
||||||
canAdd ? (
|
<ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
|
||||||
<ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
|
) : null
|
||||||
) : null
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</Card>
|
||||||
</Card>
|
|
||||||
</PageSection>
|
|
||||||
{toggleError && !toggleLoading && (
|
{toggleError && !toggleLoading && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
variant="danger"
|
variant="danger"
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class HostListItem extends React.Component {
|
|||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
detailUrl,
|
detailUrl,
|
||||||
toggleHost,
|
onToggleHost,
|
||||||
toggleLoading,
|
toggleLoading,
|
||||||
i18n,
|
i18n,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -93,7 +93,7 @@ class HostListItem extends React.Component {
|
|||||||
toggleLoading ||
|
toggleLoading ||
|
||||||
!host.summary_fields.user_capabilities.edit
|
!host.summary_fields.user_capabilities.edit
|
||||||
}
|
}
|
||||||
onChange={() => toggleHost(host)}
|
onChange={() => onToggleHost(host)}
|
||||||
aria-label={i18n._(t`Toggle host`)}
|
aria-label={i18n._(t`Toggle host`)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
|||||||
|
|
||||||
import HostsListItem from './HostListItem';
|
import HostsListItem from './HostListItem';
|
||||||
|
|
||||||
let toggleHost;
|
let onToggleHost;
|
||||||
|
|
||||||
const mockHost = {
|
const mockHost = {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -24,7 +24,7 @@ const mockHost = {
|
|||||||
|
|
||||||
describe('<HostsListItem />', () => {
|
describe('<HostsListItem />', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
toggleHost = jest.fn();
|
onToggleHost = jest.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -38,7 +38,7 @@ describe('<HostsListItem />', () => {
|
|||||||
detailUrl="/host/1"
|
detailUrl="/host/1"
|
||||||
onSelect={() => {}}
|
onSelect={() => {}}
|
||||||
host={mockHost}
|
host={mockHost}
|
||||||
toggleHost={toggleHost}
|
onToggleHost={onToggleHost}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
|
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
|
||||||
@@ -52,7 +52,7 @@ describe('<HostsListItem />', () => {
|
|||||||
detailUrl="/host/1"
|
detailUrl="/host/1"
|
||||||
onSelect={() => {}}
|
onSelect={() => {}}
|
||||||
host={copyMockHost}
|
host={copyMockHost}
|
||||||
toggleHost={toggleHost}
|
onToggleHost={onToggleHost}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
|
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
|
||||||
@@ -64,7 +64,7 @@ describe('<HostsListItem />', () => {
|
|||||||
detailUrl="/host/1"
|
detailUrl="/host/1"
|
||||||
onSelect={() => {}}
|
onSelect={() => {}}
|
||||||
host={mockHost}
|
host={mockHost}
|
||||||
toggleHost={toggleHost}
|
onToggleHost={onToggleHost}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
wrapper
|
wrapper
|
||||||
@@ -72,7 +72,7 @@ describe('<HostsListItem />', () => {
|
|||||||
.first()
|
.first()
|
||||||
.find('input')
|
.find('input')
|
||||||
.simulate('change');
|
.simulate('change');
|
||||||
expect(toggleHost).toHaveBeenCalledWith(mockHost);
|
expect(onToggleHost).toHaveBeenCalledWith(mockHost);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handles toggle click when host is disabled', () => {
|
test('handles toggle click when host is disabled', () => {
|
||||||
@@ -82,7 +82,7 @@ describe('<HostsListItem />', () => {
|
|||||||
detailUrl="/host/1"
|
detailUrl="/host/1"
|
||||||
onSelect={() => {}}
|
onSelect={() => {}}
|
||||||
host={mockHost}
|
host={mockHost}
|
||||||
toggleHost={toggleHost}
|
onToggleHost={onToggleHost}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
wrapper
|
wrapper
|
||||||
@@ -90,6 +90,6 @@ describe('<HostsListItem />', () => {
|
|||||||
.first()
|
.first()
|
||||||
.find('input')
|
.find('input')
|
||||||
.simulate('change');
|
.simulate('change');
|
||||||
expect(toggleHost).toHaveBeenCalledWith(mockHost);
|
expect(onToggleHost).toHaveBeenCalledWith(mockHost);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,31 +47,31 @@ class Hosts extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { match, history, location } = 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 path={`${match.path}/add`} render={() => <HostAdd />} />
|
<Switch>
|
||||||
<Route
|
<Route path={`${match.path}/add`} render={() => <HostAdd />} />
|
||||||
path={`${match.path}/:id`}
|
<Route
|
||||||
render={() => (
|
path={`${match.path}/:id`}
|
||||||
<Config>
|
render={() => (
|
||||||
{({ me }) => (
|
<Config>
|
||||||
<Host
|
{({ me }) => (
|
||||||
history={history}
|
<Host
|
||||||
location={location}
|
setBreadcrumb={this.setBreadcrumbConfig}
|
||||||
setBreadcrumb={this.setBreadcrumbConfig}
|
me={me || {}}
|
||||||
me={me || {}}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</Config>
|
||||||
</Config>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
<Route path={`${match.path}`} render={() => <HostList />} />
|
||||||
<Route path={`${match.path}`} render={() => <HostList />} />
|
</Switch>
|
||||||
</Switch>
|
</PageSection>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class Inventories extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setBreadCrumbConfig = (inventory, group) => {
|
setBreadCrumbConfig = (inventory, nestedResource) => {
|
||||||
const { i18n } = this.props;
|
const { i18n } = this.props;
|
||||||
if (!inventory) {
|
if (!inventory) {
|
||||||
return;
|
return;
|
||||||
@@ -39,33 +39,49 @@ 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}/hosts`]: i18n._(t`Hosts`),
|
||||||
|
|
||||||
|
[`/inventories/${inventoryKind}/${inventory.id}/sources`]: i18n._(
|
||||||
|
t`Sources`
|
||||||
|
),
|
||||||
|
|
||||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts/add`]: i18n._(
|
[`/inventories/${inventoryKind}/${inventory.id}/hosts/add`]: i18n._(
|
||||||
t`Create New Host`
|
t`Create New Host`
|
||||||
),
|
),
|
||||||
[`/inventories/inventory/${inventory.id}/sources`]: i18n._(t`Sources`),
|
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
|
||||||
[`/inventories/inventory/${inventory.id}/groups`]: i18n._(t`Groups`),
|
nestedResource.id}/edit`]: i18n._(t`Edit Details`),
|
||||||
[`/inventories/inventory/${inventory.id}/groups/add`]: i18n._(
|
[`/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}/groups/add`]: i18n._(
|
||||||
t`Create New Group`
|
t`Create New Group`
|
||||||
),
|
),
|
||||||
[`/inventories/inventory/${inventory.id}/groups/${group &&
|
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
|
||||||
group.id}`]: `${group && group.name}`,
|
nestedResource.id}/edit`]: i18n._(t`Edit Details`),
|
||||||
[`/inventories/inventory/${inventory.id}/groups/${group &&
|
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
|
||||||
group.id}/details`]: i18n._(t`Group Details`),
|
nestedResource.id}/details`]: i18n._(t`Group Details`),
|
||||||
[`/inventories/inventory/${inventory.id}/groups/${group &&
|
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
|
||||||
group.id}/edit`]: i18n._(t`Edit Details`),
|
nestedResource.id}`]: `${nestedResource && nestedResource.name}`,
|
||||||
};
|
};
|
||||||
this.setState({ breadcrumbConfig });
|
this.setState({ breadcrumbConfig });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,14 +8,15 @@ import CardCloseButton from '@components/CardCloseButton';
|
|||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import RoutedTabs from '@components/RoutedTabs';
|
import RoutedTabs from '@components/RoutedTabs';
|
||||||
import { ResourceAccessList } from '@components/ResourceAccessList';
|
import { ResourceAccessList } from '@components/ResourceAccessList';
|
||||||
|
import ContentLoading from '@components/ContentLoading';
|
||||||
import InventoryDetail from './InventoryDetail';
|
import InventoryDetail from './InventoryDetail';
|
||||||
import InventoryHosts from './InventoryHosts';
|
|
||||||
import InventoryHostAdd from './InventoryHostAdd';
|
|
||||||
import InventoryGroups from './InventoryGroups';
|
import InventoryGroups from './InventoryGroups';
|
||||||
import InventoryCompletedJobs from './InventoryCompletedJobs';
|
import InventoryCompletedJobs from './InventoryCompletedJobs';
|
||||||
import InventorySources from './InventorySources';
|
import InventorySources from './InventorySources';
|
||||||
import { InventoriesAPI } from '@api';
|
import { InventoriesAPI } from '@api';
|
||||||
import InventoryEdit from './InventoryEdit';
|
import InventoryEdit from './InventoryEdit';
|
||||||
|
import InventoryHosts from './InventoryHosts/InventoryHosts';
|
||||||
|
|
||||||
function Inventory({ history, i18n, location, match, setBreadcrumb }) {
|
function Inventory({ history, i18n, location, match, setBreadcrumb }) {
|
||||||
const [contentError, setContentError] = useState(null);
|
const [contentError, setContentError] = useState(null);
|
||||||
@@ -61,10 +62,14 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) {
|
|||||||
if (
|
if (
|
||||||
location.pathname.endsWith('edit') ||
|
location.pathname.endsWith('edit') ||
|
||||||
location.pathname.endsWith('add') ||
|
location.pathname.endsWith('add') ||
|
||||||
location.pathname.includes('groups/')
|
location.pathname.includes('groups/') ||
|
||||||
|
location.pathname.includes('hosts/')
|
||||||
) {
|
) {
|
||||||
cardHeader = null;
|
cardHeader = null;
|
||||||
}
|
}
|
||||||
|
if (hasContentLoading) {
|
||||||
|
return <ContentLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasContentLoading && contentError) {
|
if (!hasContentLoading && contentError) {
|
||||||
return (
|
return (
|
||||||
@@ -111,9 +116,16 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) {
|
|||||||
render={() => <InventoryEdit inventory={inventory} />}
|
render={() => <InventoryEdit inventory={inventory} />}
|
||||||
/>,
|
/>,
|
||||||
<Route
|
<Route
|
||||||
key="host-add"
|
key="hosts"
|
||||||
path="/inventories/inventory/:id/hosts/add"
|
path="/inventories/inventory/:id/hosts"
|
||||||
render={() => <InventoryHostAdd />}
|
render={() => (
|
||||||
|
<InventoryHosts
|
||||||
|
match={match}
|
||||||
|
setBreadcrumb={setBreadcrumb}
|
||||||
|
i18n={i18n}
|
||||||
|
inventory={inventory}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>,
|
/>,
|
||||||
<Route
|
<Route
|
||||||
key="access"
|
key="access"
|
||||||
@@ -138,11 +150,6 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>,
|
/>,
|
||||||
<Route
|
|
||||||
key="hosts"
|
|
||||||
path="/inventories/inventory/:id/hosts"
|
|
||||||
render={() => <InventoryHosts />}
|
|
||||||
/>,
|
|
||||||
<Route
|
<Route
|
||||||
key="sources"
|
key="sources"
|
||||||
path="/inventories/inventory/:id/sources"
|
path="/inventories/inventory/:id/sources"
|
||||||
|
|||||||
@@ -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,10 +41,14 @@ 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,
|
||||||
isNestedTabs: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Details`),
|
name: i18n._(t`Details`),
|
||||||
@@ -65,9 +70,10 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// In cases where a user manipulates the url such that they try to navigate to a Inventory Group
|
// In cases where a user manipulates the url such that they try to navigate to a
|
||||||
// that is not associated with the Inventory Id in the Url this Content Error is thrown.
|
// Inventory Group that is not associated with the Inventory Id in the Url this
|
||||||
// Inventory Groups have a 1: 1 relationship to Inventories thus their Ids must corrolate.
|
// Content Error is thrown. Inventory Groups have a 1:1 relationship to Inventories
|
||||||
|
// thus their Ids must corrolate.
|
||||||
|
|
||||||
if (contentLoading) {
|
if (contentLoading) {
|
||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
|
|||||||
@@ -62,10 +62,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);
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<PaginatedDataList
|
||||||
|
contentError={contentError}
|
||||||
|
hasContentLoading={isLoading}
|
||||||
|
items={hosts}
|
||||||
|
itemCount={hostCount}
|
||||||
|
pluralizedItemName={i18n._(t`Hosts`)}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
toolbarColumns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Name`),
|
||||||
|
key: 'name',
|
||||||
|
isSortable: true,
|
||||||
|
isSearchable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Modified`),
|
||||||
|
key: 'modified',
|
||||||
|
isSortable: true,
|
||||||
|
isNumeric: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Created`),
|
||||||
|
key: 'created',
|
||||||
|
isSortable: true,
|
||||||
|
isNumeric: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
renderToolbar={props => (
|
||||||
|
<DataListToolbar
|
||||||
|
{...props}
|
||||||
|
showSelectAll
|
||||||
|
isAllSelected={isAllSelected}
|
||||||
|
onSelectAll={handleSelectAll}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
additionalControls={[
|
||||||
|
<ToolbarDeleteButton
|
||||||
|
key="delete"
|
||||||
|
onDelete={handleDelete}
|
||||||
|
itemsToDelete={selected}
|
||||||
|
pluralizedItemName={i18n._(t`Hosts`)}
|
||||||
|
/>,
|
||||||
|
canAdd && (
|
||||||
|
<ToolbarAddButton
|
||||||
|
key="add"
|
||||||
|
linkTo={`/inventories/inventory/${match.params.id}/hosts/add`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderItem={o => (
|
||||||
|
<InventoryHostItem
|
||||||
|
key={o.id}
|
||||||
|
host={o}
|
||||||
|
detailUrl={`/inventories/inventory/${match.params.id}/hosts/${o.id}/details`}
|
||||||
|
editUrl={`/inventories/inventory/${match.params.id}/hosts/${o.id}/edit`}
|
||||||
|
isSelected={selected.some(row => row.id === o.id)}
|
||||||
|
onSelect={() => handleSelect(o)}
|
||||||
|
toggleHost={handleToggle}
|
||||||
|
toggleLoading={toggleLoading === o.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
emptyStateControls={
|
||||||
|
canAdd && (
|
||||||
|
<ToolbarAddButton
|
||||||
|
key="add"
|
||||||
|
linkTo={`/inventories/inventory/${match.params.id}/add`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{toggleError && !toggleLoading && (
|
||||||
|
<AlertModal
|
||||||
|
variant="danger"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
isOpen={toggleError && !toggleLoading}
|
||||||
|
onClose={() => setToggleError(false)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to toggle host.`)}
|
||||||
|
<ErrorDetail error={toggleError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{deletionError && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={deletionError}
|
||||||
|
variant="danger"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={() => setDeletionError(null)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to delete one or more hosts.`)}
|
||||||
|
<ErrorDetail error={deletionError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(withRouter(InventoryHostList));
|
||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { InventoriesAPI, HostsAPI } from '@api';
|
import { InventoriesAPI, HostsAPI } from '@api';
|
||||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||||
import InventoryHosts from './InventoryHosts';
|
import InventoryHostList from './InventoryHostList';
|
||||||
import mockInventory from '../shared/data.inventory.json';
|
import mockInventory from '../shared/data.inventory.json';
|
||||||
|
|
||||||
jest.mock('@api');
|
jest.mock('@api');
|
||||||
@@ -62,7 +62,7 @@ const mockHosts = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('<InventoryHosts />', () => {
|
describe('<InventoryHostList />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -81,7 +81,7 @@ describe('<InventoryHosts />', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<InventoryHosts />);
|
wrapper = mountWithContexts(<InventoryHostList />);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
});
|
});
|
||||||
@@ -91,7 +91,7 @@ describe('<InventoryHosts />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders successfully', () => {
|
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 () => {
|
test('should fetch hosts from api and render them in the list', async () => {
|
||||||
@@ -261,7 +261,9 @@ describe('<InventoryHosts />', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<InventoryHosts inventory={mockInventory} />);
|
wrapper = mountWithContexts(
|
||||||
|
<InventoryHostList inventory={mockInventory} />
|
||||||
|
);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
expect(wrapper.find('ToolbarAddButton').length).toBe(0);
|
expect(wrapper.find('ToolbarAddButton').length).toBe(0);
|
||||||
@@ -272,7 +274,9 @@ describe('<InventoryHosts />', () => {
|
|||||||
Promise.reject(new Error())
|
Promise.reject(new Error())
|
||||||
);
|
);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<InventoryHosts inventory={mockInventory} />);
|
wrapper = mountWithContexts(
|
||||||
|
<InventoryHostList inventory={mockInventory} />
|
||||||
|
);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
});
|
});
|
||||||
@@ -1,228 +1,46 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { Switch, Route, 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 Host from '../../Host/Host';
|
||||||
import DataListToolbar from '@components/DataListToolbar';
|
import InventoryHostList from './InventoryHostList';
|
||||||
import ErrorDetail from '@components/ErrorDetail';
|
import InventoryHostAdd from '../InventoryHostAdd';
|
||||||
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;
|
|
||||||
|
|
||||||
|
function InventoryHosts({ match, setBreadcrumb, i18n, inventory }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<Switch>
|
||||||
<PaginatedDataList
|
<Route
|
||||||
contentError={contentError}
|
key="host-add"
|
||||||
hasContentLoading={isLoading}
|
path="/inventories/inventory/:id/hosts/add"
|
||||||
items={hosts}
|
render={() => <InventoryHostAdd match={match} />}
|
||||||
itemCount={hostCount}
|
|
||||||
pluralizedItemName={i18n._(t`Hosts`)}
|
|
||||||
qsConfig={QS_CONFIG}
|
|
||||||
onRowClick={handleSelect}
|
|
||||||
toolbarSearchColumns={[
|
|
||||||
{
|
|
||||||
name: i18n._(t`Name`),
|
|
||||||
key: 'name',
|
|
||||||
isDefault: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n._(t`Created By (Username)`),
|
|
||||||
key: 'created_by__username',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n._(t`Modified By (Username)`),
|
|
||||||
key: 'modified_by__username',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
toolbarSortColumns={[
|
|
||||||
{
|
|
||||||
name: i18n._(t`Name`),
|
|
||||||
key: 'name',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
renderToolbar={props => (
|
|
||||||
<DataListToolbar
|
|
||||||
{...props}
|
|
||||||
showSelectAll
|
|
||||||
isAllSelected={isAllSelected}
|
|
||||||
onSelectAll={handleSelectAll}
|
|
||||||
qsConfig={QS_CONFIG}
|
|
||||||
additionalControls={[
|
|
||||||
<ToolbarDeleteButton
|
|
||||||
key="delete"
|
|
||||||
onDelete={handleDelete}
|
|
||||||
itemsToDelete={selected}
|
|
||||||
pluralizedItemName={i18n._(t`Hosts`)}
|
|
||||||
/>,
|
|
||||||
canAdd && (
|
|
||||||
<ToolbarAddButton
|
|
||||||
key="add"
|
|
||||||
linkTo={`/inventories/inventory/${match.params.id}/hosts/add`}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
renderItem={o => (
|
|
||||||
<InventoryHostItem
|
|
||||||
key={o.id}
|
|
||||||
host={o}
|
|
||||||
detailUrl={`/inventories/inventory/${match.params.id}/hosts/${o.id}/details`}
|
|
||||||
editUrl={`/inventories/inventory/${match.params.id}/hosts/${o.id}/edit`}
|
|
||||||
isSelected={selected.some(row => row.id === o.id)}
|
|
||||||
onSelect={() => handleSelect(o)}
|
|
||||||
toggleHost={handleToggle}
|
|
||||||
toggleLoading={toggleLoading === o.id}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
emptyStateControls={
|
|
||||||
canAdd && (
|
|
||||||
<ToolbarAddButton
|
|
||||||
key="add"
|
|
||||||
linkTo={`/inventories/inventory/${match.params.id}/add`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
,
|
||||||
{toggleError && !toggleLoading && (
|
<Route
|
||||||
<AlertModal
|
key="host"
|
||||||
variant="danger"
|
path="/inventories/inventory/:id/hosts/:hostId"
|
||||||
title={i18n._(t`Error!`)}
|
render={() => (
|
||||||
isOpen={toggleError && !toggleLoading}
|
<Host
|
||||||
onClose={() => setToggleError(false)}
|
setBreadcrumb={setBreadcrumb}
|
||||||
>
|
match={match}
|
||||||
{i18n._(t`Failed to toggle host.`)}
|
i18n={i18n}
|
||||||
<ErrorDetail error={toggleError} />
|
inventory={inventory}
|
||||||
</AlertModal>
|
/>
|
||||||
)}
|
)}
|
||||||
|
/>
|
||||||
{deletionError && (
|
,
|
||||||
<AlertModal
|
<Route
|
||||||
isOpen={deletionError}
|
key="host-list"
|
||||||
variant="danger"
|
path="/inventories/inventory/:id/hosts/"
|
||||||
title={i18n._(t`Error!`)}
|
render={() => (
|
||||||
onClose={() => setDeletionError(null)}
|
<InventoryHostList
|
||||||
>
|
match={match}
|
||||||
{i18n._(t`Failed to delete one or more hosts.`)}
|
setBreadcrumb={setBreadcrumb}
|
||||||
<ErrorDetail error={deletionError} />
|
i18n={i18n}
|
||||||
</AlertModal>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
/>
|
||||||
|
,
|
||||||
|
</Switch>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n()(withRouter(InventoryHosts));
|
export default withRouter(InventoryHosts);
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default } from './InventoryHosts';
|
export { default } from './InventoryHostList';
|
||||||
|
|||||||
Reference in New Issue
Block a user