mirror of
https://github.com/ansible/awx.git
synced 2024-11-01 08:21:15 +03:00
Adds Toggle, Variables, user Link and Delete to Inventory Host and Host Details
If the user comes to Host details through Inventory Host they will get a Return To Host tab in addition to the others. This PR allows Inventory Host to share many of the same components with Host but does add some complexity to the routing files in Host.jsx
This commit is contained in:
parent
200be3297a
commit
1db88fe4f6
@ -57,23 +57,25 @@ function RoutedTabs(props) {
|
||||
|
||||
return (
|
||||
<Tabs activeKey={getActiveTabId()} onSelect={handleTabSelect}>
|
||||
{tabsArray.map(tab => (
|
||||
<Tab
|
||||
aria-label={`${tab.name}`}
|
||||
eventKey={tab.id}
|
||||
key={tab.id}
|
||||
link={tab.link}
|
||||
title={
|
||||
tab.isNestedTabs ? (
|
||||
<>
|
||||
<CaretLeftIcon /> {tab.name}
|
||||
</>
|
||||
) : (
|
||||
tab.name
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{tabsArray
|
||||
.filter(tab => tab.isNestedTab || !tab.name.startsWith('Return'))
|
||||
.map(tab => (
|
||||
<Tab
|
||||
aria-label={`${tab.name}`}
|
||||
eventKey={tab.id}
|
||||
key={tab.id}
|
||||
link={tab.link}
|
||||
title={
|
||||
tab.isNestedTab ? (
|
||||
<>
|
||||
<CaretLeftIcon /> {tab.name}
|
||||
</>
|
||||
) : (
|
||||
tab.name
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
@ -9,6 +9,9 @@ import RoutedTabs from '@components/RoutedTabs';
|
||||
import ContentError from '@components/ContentError';
|
||||
import HostFacts from './HostFacts';
|
||||
import HostDetail from './HostDetail';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
|
||||
import HostEdit from './HostEdit';
|
||||
import HostGroups from './HostGroups';
|
||||
import HostCompletedJobs from './HostCompletedJobs';
|
||||
@ -23,8 +26,16 @@ class Host extends Component {
|
||||
hasContentLoading: true,
|
||||
contentError: null,
|
||||
isInitialized: false,
|
||||
toggleLoading: false,
|
||||
toggleError: null,
|
||||
deletionError: false,
|
||||
isDeleteModalOpen: false,
|
||||
};
|
||||
this.loadHost = this.loadHost.bind(this);
|
||||
this.handleHostToggle = this.handleHostToggle.bind(this);
|
||||
this.handleToggleError = this.handleToggleError.bind(this);
|
||||
this.handleHostDelete = this.handleHostDelete.bind(this);
|
||||
this.toggleDeleteModal = this.toggleDeleteModal.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
@ -45,15 +56,54 @@ class Host extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
toggleDeleteModal() {
|
||||
const { isDeleteModalOpen } = this.state;
|
||||
this.setState({ isDeleteModalOpen: !isDeleteModalOpen });
|
||||
}
|
||||
|
||||
async handleHostToggle() {
|
||||
const { host } = this.state;
|
||||
this.setState({ toggleLoading: true });
|
||||
try {
|
||||
const { data } = await HostsAPI.update(host.id, {
|
||||
enabled: !host.enabled,
|
||||
});
|
||||
this.setState({ host: data });
|
||||
} catch (err) {
|
||||
this.setState({ toggleError: err });
|
||||
} finally {
|
||||
this.setState({ toggleLoading: null });
|
||||
}
|
||||
}
|
||||
|
||||
async handleHostDelete() {
|
||||
const { host } = this.state;
|
||||
const { match, history } = this.props;
|
||||
|
||||
this.setState({ hasContentLoading: true });
|
||||
try {
|
||||
await HostsAPI.destroy(host.id);
|
||||
this.setState({ hasContentLoading: false });
|
||||
history.push(`/inventories/inventory/${match.params.id}/hosts`);
|
||||
} catch (err) {
|
||||
this.setState({ deletionError: err });
|
||||
}
|
||||
}
|
||||
|
||||
async loadHost() {
|
||||
const { match, setBreadcrumb } = this.props;
|
||||
const id = parseInt(match.params.id, 10);
|
||||
const { match, setBreadcrumb, history, inventory } = this.props;
|
||||
|
||||
this.setState({ contentError: null, hasContentLoading: true });
|
||||
try {
|
||||
const { data } = await HostsAPI.readDetail(id);
|
||||
setBreadcrumb(data);
|
||||
const { data } = await HostsAPI.readDetail(
|
||||
match.params.hostId || match.params.id
|
||||
);
|
||||
this.setState({ host: data });
|
||||
|
||||
if (history.location.pathname.startsWith('/hosts')) {
|
||||
setBreadcrumb(data);
|
||||
}
|
||||
setBreadcrumb(inventory, data);
|
||||
} catch (err) {
|
||||
this.setState({ contentError: err });
|
||||
} finally {
|
||||
@ -61,15 +111,44 @@ class Host extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleError() {
|
||||
this.setState({ toggleError: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { location, match, history, i18n } = this.props;
|
||||
|
||||
const { host, contentError, hasContentLoading, isInitialized } = this.state;
|
||||
|
||||
const {
|
||||
deletionError,
|
||||
host,
|
||||
isDeleteModalOpen,
|
||||
toggleError,
|
||||
hasContentLoading,
|
||||
toggleLoading,
|
||||
isInitialized,
|
||||
contentError,
|
||||
} = this.state;
|
||||
const tabsArray = [
|
||||
{ name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 },
|
||||
{ name: i18n._(t`Facts`), link: `${match.url}/facts`, id: 1 },
|
||||
{ name: i18n._(t`Groups`), link: `${match.url}/groups`, id: 2 },
|
||||
{
|
||||
name: i18n._(t`Return to Hosts`),
|
||||
link: `/inventories/inventory/${match.params.id}/hosts`,
|
||||
id: 99,
|
||||
isNestedTab: !history.location.pathname.startsWith('/hosts'),
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Details`),
|
||||
link: `${match.url}/details`,
|
||||
id: 0,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Facts`),
|
||||
link: `${match.url}/facts`,
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Groups`),
|
||||
link: `${match.url}/groups`,
|
||||
id: 2,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Completed Jobs`),
|
||||
link: `${match.url}/completed_jobs`,
|
||||
@ -117,62 +196,99 @@ class Host extends Component {
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageSection>
|
||||
<Card className="awx-c-card">
|
||||
{cardHeader}
|
||||
<Switch>
|
||||
<Redirect from="/hosts/:id" to="/hosts/:id/details" exact />
|
||||
{host && (
|
||||
<>
|
||||
<PageSection
|
||||
css={`
|
||||
${location.pathname.startsWith('/inventories')
|
||||
? 'padding: 0'
|
||||
: 'null'}
|
||||
`}
|
||||
>
|
||||
<Card className="awx-c-card">
|
||||
{cardHeader}
|
||||
<Switch>
|
||||
<Redirect from="/hosts/:id" to="/hosts/:id/details" exact />
|
||||
{host && (
|
||||
<Route
|
||||
path={[
|
||||
'/hosts/:id/edit',
|
||||
'/inventories/inventory/:id/hosts/:hostId/edit',
|
||||
]}
|
||||
render={() => <HostEdit match={match} host={host} />}
|
||||
/>
|
||||
)}
|
||||
{host && (
|
||||
<Route
|
||||
path={[
|
||||
'/hosts/:id/details',
|
||||
'/inventories/inventory/:id/hosts/:hostId/details',
|
||||
]}
|
||||
render={() => (
|
||||
<HostDetail
|
||||
match={match}
|
||||
host={host}
|
||||
history={history}
|
||||
onToggleDeleteModal={this.toggleDeleteModal}
|
||||
isDeleteModalOpen={isDeleteModalOpen}
|
||||
onHandleHostToggle={this.handleHostToggle}
|
||||
toggleError={toggleError}
|
||||
toggleLoading={toggleLoading}
|
||||
onToggleError={this.handleToggleError}
|
||||
onHostDelete={this.handleHostDelete}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{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
|
||||
path="/hosts/:id/edit"
|
||||
render={() => <HostEdit match={match} host={host} />}
|
||||
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/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>
|
||||
</PageSection>
|
||||
,
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -7,14 +7,127 @@ import { Button } from '@patternfly/react-core';
|
||||
import { CardBody, CardActionsRow } from '@components/Card';
|
||||
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
|
||||
import { VariablesDetail } from '@components/CodeMirrorInput';
|
||||
import { Sparkline } from '@components/Sparkline';
|
||||
import Switch from '@components/Switch';
|
||||
|
||||
function HostDetail({ host, i18n }) {
|
||||
const { created, description, id, modified, name, summary_fields } = host;
|
||||
const ActionButtonWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
& > :not(:first-child) {
|
||||
margin-left: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
function HostDetail({
|
||||
host,
|
||||
history,
|
||||
isDeleteModalOpen,
|
||||
match,
|
||||
i18n,
|
||||
toggleError,
|
||||
toggleLoading,
|
||||
onHostDelete,
|
||||
onToggleDeleteModal,
|
||||
onToggleError,
|
||||
onHandleHostToggle,
|
||||
}) {
|
||||
const { created, description, id, modified, name, summary_fields } = host;
|
||||
let createdBy = '';
|
||||
if (created) {
|
||||
if (summary_fields.created_by && summary_fields.created_by.username) {
|
||||
createdBy = (
|
||||
<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 = '';
|
||||
if (modified) {
|
||||
if (summary_fields.modified_by && summary_fields.modified_by.username) {
|
||||
modifiedBy = (
|
||||
<span>
|
||||
{i18n._(t`${formatDateString(modified)} by`)}{' '}
|
||||
<Link to={`/users/${summary_fields.modified_by.id}`}>
|
||||
{summary_fields.modified_by.username}
|
||||
</Link>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
modifiedBy = formatDateString(modified);
|
||||
}
|
||||
}
|
||||
if (toggleError && !toggleLoading) {
|
||||
return (
|
||||
<AlertModal
|
||||
variant="danger"
|
||||
title={i18n._(t`Error!`)}
|
||||
isOpen={toggleError && !toggleLoading}
|
||||
onClose={onToggleError}
|
||||
>
|
||||
{i18n._(t`Failed to toggle host.`)}
|
||||
<ErrorDetail error={toggleError} />
|
||||
</AlertModal>
|
||||
);
|
||||
}
|
||||
if (isDeleteModalOpen) {
|
||||
return (
|
||||
<AlertModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
title={i18n._(t`Delete Host`)}
|
||||
variant="danger"
|
||||
onClose={() => onToggleDeleteModal()}
|
||||
>
|
||||
{i18n._(t`Are you sure you want to delete:`)}
|
||||
<br />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<CardBody>
|
||||
<Switch
|
||||
css="padding-bottom: 40px; padding-top: 20px"
|
||||
id={`host-${host.id}-toggle`}
|
||||
label={i18n._(t`On`)}
|
||||
labelOff={i18n._(t`Off`)}
|
||||
isChecked={host.enabled}
|
||||
isDisabled={!host.summary_fields.user_capabilities.edit}
|
||||
onChange={onHandleHostToggle}
|
||||
aria-label={i18n._(t`Toggle Host`)}
|
||||
/>
|
||||
<DetailList gutter="sm">
|
||||
<Detail label={i18n._(t`Name`)} value={name} />
|
||||
<Detail
|
||||
css="display: flex; flex: 1;"
|
||||
value={<Sparkline jobs={host.summary_fields.recent_jobs} />}
|
||||
label={i18n._(t`Activity`)}
|
||||
/>
|
||||
<Detail label={i18n._(t`Description`)} value={description} />
|
||||
{summary_fields.inventory && (
|
||||
<Detail
|
||||
@ -32,20 +145,12 @@ function HostDetail({ host, i18n }) {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Created`)}
|
||||
date={created}
|
||||
user={summary_fields.created_by}
|
||||
/>
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Last Modified`)}
|
||||
date={modified}
|
||||
user={summary_fields.modified_by}
|
||||
/>
|
||||
<Detail label={i18n._(t`Created`)} value={createdBy} />
|
||||
<Detail label={i18n._(t`Last Modified`)} value={modifiedBy} />
|
||||
<VariablesDetail
|
||||
label={i18n._(t`Variables`)}
|
||||
value={host.variables}
|
||||
rows={6}
|
||||
rows={4}
|
||||
label={i18n._(t`Variables`)}
|
||||
/>
|
||||
</DetailList>
|
||||
<CardActionsRow>
|
||||
@ -54,7 +159,11 @@ function HostDetail({ host, i18n }) {
|
||||
<Button
|
||||
aria-label={i18n._(t`edit`)}
|
||||
component={Link}
|
||||
to={`/hosts/${id}/edit`}
|
||||
to={
|
||||
history.location.pathname.startsWith('/inventories')
|
||||
? `/inventories/inventory/${match.params.id}/hosts/${match.params.hostId}/edit`
|
||||
: `/hosts/${id}/edit`
|
||||
}
|
||||
>
|
||||
{i18n._(t`Edit`)}
|
||||
</Button>
|
||||
|
@ -50,8 +50,7 @@ describe('<HostDetail />', () => {
|
||||
|
||||
test('should show edit button for users with edit permission', async () => {
|
||||
const wrapper = mountWithContexts(<HostDetail host={mockHost} />);
|
||||
// VariablesDetail has two buttons
|
||||
const editButton = wrapper.find('Button').at(2);
|
||||
const editButton = wrapper.find('Button[aria-label="edit"]');
|
||||
expect(editButton.text()).toEqual('Edit');
|
||||
expect(editButton.prop('to')).toBe('/hosts/1/edit');
|
||||
});
|
||||
@ -61,7 +60,6 @@ describe('<HostDetail />', () => {
|
||||
readOnlyHost.summary_fields.user_capabilities.edit = false;
|
||||
const wrapper = mountWithContexts(<HostDetail host={readOnlyHost} />);
|
||||
await waitForElement(wrapper, 'HostDetail');
|
||||
// VariablesDetail has two buttons
|
||||
expect(wrapper.find('Button').length).toBe(2);
|
||||
expect(wrapper.find('Button[aria-label="edit"]').length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
@ -238,7 +238,7 @@ class HostsList extends Component {
|
||||
detailUrl={`${match.url}/${o.id}`}
|
||||
isSelected={selected.some(row => row.id === o.id)}
|
||||
onSelect={() => this.handleSelect(o)}
|
||||
toggleHost={this.handleHostToggle}
|
||||
onToggleHost={this.handleHostToggle}
|
||||
toggleLoading={toggleLoading === o.id}
|
||||
/>
|
||||
)}
|
||||
|
@ -34,7 +34,7 @@ class HostListItem extends React.Component {
|
||||
isSelected,
|
||||
onSelect,
|
||||
detailUrl,
|
||||
toggleHost,
|
||||
onToggleHost,
|
||||
toggleLoading,
|
||||
i18n,
|
||||
} = this.props;
|
||||
@ -93,7 +93,7 @@ class HostListItem extends React.Component {
|
||||
toggleLoading ||
|
||||
!host.summary_fields.user_capabilities.edit
|
||||
}
|
||||
onChange={() => toggleHost(host)}
|
||||
onChange={() => onToggleHost(host)}
|
||||
aria-label={i18n._(t`Toggle host`)}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
@ -4,7 +4,7 @@ import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
|
||||
import HostsListItem from './HostListItem';
|
||||
|
||||
let toggleHost;
|
||||
let onToggleHost;
|
||||
|
||||
const mockHost = {
|
||||
id: 1,
|
||||
@ -24,7 +24,7 @@ const mockHost = {
|
||||
|
||||
describe('<HostsListItem />', () => {
|
||||
beforeEach(() => {
|
||||
toggleHost = jest.fn();
|
||||
onToggleHost = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -38,7 +38,7 @@ describe('<HostsListItem />', () => {
|
||||
detailUrl="/host/1"
|
||||
onSelect={() => {}}
|
||||
host={mockHost}
|
||||
toggleHost={toggleHost}
|
||||
onToggleHost={onToggleHost}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
|
||||
@ -52,7 +52,7 @@ describe('<HostsListItem />', () => {
|
||||
detailUrl="/host/1"
|
||||
onSelect={() => {}}
|
||||
host={copyMockHost}
|
||||
toggleHost={toggleHost}
|
||||
onToggleHost={onToggleHost}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
|
||||
@ -64,7 +64,7 @@ describe('<HostsListItem />', () => {
|
||||
detailUrl="/host/1"
|
||||
onSelect={() => {}}
|
||||
host={mockHost}
|
||||
toggleHost={toggleHost}
|
||||
onToggleHost={onToggleHost}
|
||||
/>
|
||||
);
|
||||
wrapper
|
||||
@ -72,7 +72,7 @@ describe('<HostsListItem />', () => {
|
||||
.first()
|
||||
.find('input')
|
||||
.simulate('change');
|
||||
expect(toggleHost).toHaveBeenCalledWith(mockHost);
|
||||
expect(onToggleHost).toHaveBeenCalledWith(mockHost);
|
||||
});
|
||||
|
||||
test('handles toggle click when host is disabled', () => {
|
||||
@ -82,7 +82,7 @@ describe('<HostsListItem />', () => {
|
||||
detailUrl="/host/1"
|
||||
onSelect={() => {}}
|
||||
host={mockHost}
|
||||
toggleHost={toggleHost}
|
||||
onToggleHost={onToggleHost}
|
||||
/>
|
||||
);
|
||||
wrapper
|
||||
@ -90,6 +90,6 @@ describe('<HostsListItem />', () => {
|
||||
.first()
|
||||
.find('input')
|
||||
.simulate('change');
|
||||
expect(toggleHost).toHaveBeenCalledWith(mockHost);
|
||||
expect(onToggleHost).toHaveBeenCalledWith(mockHost);
|
||||
});
|
||||
});
|
||||
|
@ -46,14 +46,17 @@ class Hosts extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { match, history, location } = this.props;
|
||||
const { match, history, location, inventory } = this.props;
|
||||
const { breadcrumbConfig } = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
||||
<Switch>
|
||||
<Route path={`${match.path}/add`} render={() => <HostAdd />} />
|
||||
<Route
|
||||
path={`${match.path}/add`}
|
||||
render={() => <HostAdd history={history} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${match.path}/:id`}
|
||||
render={() => (
|
||||
@ -64,6 +67,7 @@ class Hosts extends Component {
|
||||
location={location}
|
||||
setBreadcrumb={this.setBreadcrumbConfig}
|
||||
me={me || {}}
|
||||
inventory={inventory}
|
||||
/>
|
||||
)}
|
||||
</Config>
|
||||
|
@ -27,7 +27,7 @@ class Inventories extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
setBreadCrumbConfig = (inventory, group) => {
|
||||
setBreadCrumbConfig = (inventory, nestedResource) => {
|
||||
const { i18n } = this.props;
|
||||
if (!inventory) {
|
||||
return;
|
||||
@ -51,21 +51,39 @@ class Inventories extends Component {
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/completed_jobs`]: i18n._(
|
||||
t`Completed Jobs`
|
||||
),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts`]: i18n._(t`Hosts`),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
|
||||
nestedResource.id}`]: i18n._(
|
||||
t`${nestedResource && nestedResource.name}`
|
||||
),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
|
||||
nestedResource.id}/details`]: i18n._(t`Details`),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
|
||||
nestedResource.id}/edit`]: i18n._(t`Edit Details`),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts/add`]: i18n._(
|
||||
t`Create New Host`
|
||||
),
|
||||
[`/inventories/inventory/${inventory.id}/sources`]: i18n._(t`Sources`),
|
||||
[`/inventories/inventory/${inventory.id}/groups`]: i18n._(t`Groups`),
|
||||
[`/inventories/inventory/${inventory.id}/groups/add`]: i18n._(
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/sources`]: i18n._(
|
||||
t`Sources`
|
||||
),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/groups`]: i18n._(
|
||||
t`Groups`
|
||||
),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/groups/add`]: i18n._(
|
||||
t`Create New Group`
|
||||
),
|
||||
[`/inventories/inventory/${inventory.id}/groups/${group &&
|
||||
group.id}`]: `${group && group.name}`,
|
||||
[`/inventories/inventory/${inventory.id}/groups/${group &&
|
||||
group.id}/details`]: i18n._(t`Group Details`),
|
||||
[`/inventories/inventory/${inventory.id}/groups/${group &&
|
||||
group.id}/edit`]: i18n._(t`Edit Details`),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
|
||||
nestedResource.id}`]: `${nestedResource && nestedResource.name}`,
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
|
||||
nestedResource.id}/details`]: i18n._(t`Group Details`),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
|
||||
nestedResource.id}/edit`]: i18n._(t`Edit Details`),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts`]: i18n._(t`Hosts`),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/sources`]: i18n._(
|
||||
t`Sources`
|
||||
),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/groups`]: i18n._(
|
||||
t`Groups`
|
||||
),
|
||||
};
|
||||
this.setState({ breadcrumbConfig });
|
||||
};
|
||||
|
@ -8,14 +8,15 @@ import CardCloseButton from '@components/CardCloseButton';
|
||||
import ContentError from '@components/ContentError';
|
||||
import RoutedTabs from '@components/RoutedTabs';
|
||||
import { ResourceAccessList } from '@components/ResourceAccessList';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import InventoryDetail from './InventoryDetail';
|
||||
import InventoryHosts from './InventoryHosts';
|
||||
import InventoryHostAdd from './InventoryHostAdd';
|
||||
|
||||
import InventoryGroups from './InventoryGroups';
|
||||
import InventoryCompletedJobs from './InventoryCompletedJobs';
|
||||
import InventorySources from './InventorySources';
|
||||
import { InventoriesAPI } from '@api';
|
||||
import InventoryEdit from './InventoryEdit';
|
||||
import InventoryHosts from './InventoryHosts/InventoryHosts';
|
||||
|
||||
function Inventory({ history, i18n, location, match, setBreadcrumb }) {
|
||||
const [contentError, setContentError] = useState(null);
|
||||
@ -61,10 +62,14 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) {
|
||||
if (
|
||||
location.pathname.endsWith('edit') ||
|
||||
location.pathname.endsWith('add') ||
|
||||
location.pathname.includes('groups/')
|
||||
location.pathname.includes('groups/') ||
|
||||
history.location.pathname.includes(`/hosts/`)
|
||||
) {
|
||||
cardHeader = null;
|
||||
}
|
||||
if (hasContentLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
if (!hasContentLoading && contentError) {
|
||||
return (
|
||||
@ -111,9 +116,16 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) {
|
||||
render={() => <InventoryEdit inventory={inventory} />}
|
||||
/>,
|
||||
<Route
|
||||
key="host-add"
|
||||
path="/inventories/inventory/:id/hosts/add"
|
||||
render={() => <InventoryHostAdd />}
|
||||
key="hosts"
|
||||
path="/inventories/inventory/:id/hosts"
|
||||
render={() => (
|
||||
<InventoryHosts
|
||||
match={match}
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
i18n={i18n}
|
||||
inventory={inventory}
|
||||
/>
|
||||
)}
|
||||
/>,
|
||||
<Route
|
||||
key="access"
|
||||
@ -138,11 +150,6 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) {
|
||||
/>
|
||||
)}
|
||||
/>,
|
||||
<Route
|
||||
key="hosts"
|
||||
path="/inventories/inventory/:id/hosts"
|
||||
render={() => <InventoryHosts />}
|
||||
/>,
|
||||
<Route
|
||||
key="sources"
|
||||
path="/inventories/inventory/:id/sources"
|
||||
|
@ -43,7 +43,7 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) {
|
||||
name: i18n._(t`Return to Groups`),
|
||||
link: `/inventories/inventory/${inventory.id}/groups`,
|
||||
id: 99,
|
||||
isNestedTabs: true,
|
||||
isNestedTab: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Details`),
|
||||
@ -65,9 +65,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
|
||||
// that is not associated with the Inventory Id in the Url this Content Error is thrown.
|
||||
// Inventory Groups have a 1: 1 relationship to Inventories thus their Ids must corrolate.
|
||||
// In cases where a user manipulates the url such that they try to navigate to a
|
||||
// Inventory Group that is not associated with the Inventory Id in the Url this
|
||||
// Content Error is thrown. Inventory Groups have a 1:1 relationship to Inventories
|
||||
// thus their Ids must corrolate.
|
||||
|
||||
if (contentLoading) {
|
||||
return <ContentLoading />;
|
||||
|
@ -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 { InventoriesAPI, HostsAPI } from '@api';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import InventoryHosts from './InventoryHosts';
|
||||
import InventoryHostList from './InventoryHostList';
|
||||
import mockInventory from '../shared/data.inventory.json';
|
||||
|
||||
jest.mock('@api');
|
||||
@ -62,7 +62,7 @@ const mockHosts = [
|
||||
},
|
||||
];
|
||||
|
||||
describe('<InventoryHosts />', () => {
|
||||
describe('<InventoryHostList />', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -81,7 +81,7 @@ describe('<InventoryHosts />', () => {
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryHosts />);
|
||||
wrapper = mountWithContexts(<InventoryHostList />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
@ -91,7 +91,7 @@ describe('<InventoryHosts />', () => {
|
||||
});
|
||||
|
||||
test('initially renders successfully', () => {
|
||||
expect(wrapper.find('InventoryHosts').length).toBe(1);
|
||||
expect(wrapper.find('InventoryHostList').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should fetch hosts from api and render them in the list', async () => {
|
||||
@ -261,7 +261,9 @@ describe('<InventoryHosts />', () => {
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryHosts inventory={mockInventory} />);
|
||||
wrapper = mountWithContexts(
|
||||
<InventoryHostList inventory={mockInventory} />
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(wrapper.find('ToolbarAddButton').length).toBe(0);
|
||||
@ -272,7 +274,9 @@ describe('<InventoryHosts />', () => {
|
||||
Promise.reject(new Error())
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryHosts inventory={mockInventory} />);
|
||||
wrapper = mountWithContexts(
|
||||
<InventoryHostList inventory={mockInventory} />
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
@ -1,228 +1,46 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import { InventoriesAPI, HostsAPI } from '@api';
|
||||
import React from 'react';
|
||||
import { Switch, Route, withRouter } from 'react-router-dom';
|
||||
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import DataListToolbar from '@components/DataListToolbar';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
import PaginatedDataList, {
|
||||
ToolbarAddButton,
|
||||
ToolbarDeleteButton,
|
||||
} from '@components/PaginatedDataList';
|
||||
import InventoryHostItem from './InventoryHostItem';
|
||||
|
||||
const QS_CONFIG = getQSConfig('host', {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
function InventoryHosts({ i18n, location, match }) {
|
||||
const [actions, setActions] = useState(null);
|
||||
const [contentError, setContentError] = useState(null);
|
||||
const [deletionError, setDeletionError] = useState(null);
|
||||
const [hostCount, setHostCount] = useState(0);
|
||||
const [hosts, setHosts] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [toggleError, setToggleError] = useState(null);
|
||||
const [toggleLoading, setToggleLoading] = useState(null);
|
||||
|
||||
const fetchHosts = (id, queryString) => {
|
||||
const params = parseQueryString(QS_CONFIG, queryString);
|
||||
return InventoriesAPI.readHosts(id, params);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [
|
||||
{
|
||||
data: { count, results },
|
||||
},
|
||||
{
|
||||
data: { actions: optionActions },
|
||||
},
|
||||
] = await Promise.all([
|
||||
fetchHosts(match.params.id, location.search),
|
||||
InventoriesAPI.readOptions(),
|
||||
]);
|
||||
|
||||
setHosts(results);
|
||||
setHostCount(count);
|
||||
setActions(optionActions);
|
||||
} catch (error) {
|
||||
setContentError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, [match.params.id, location]);
|
||||
|
||||
const handleSelectAll = isSelected => {
|
||||
setSelected(isSelected ? [...hosts] : []);
|
||||
};
|
||||
|
||||
const handleSelect = row => {
|
||||
if (selected.some(s => s.id === row.id)) {
|
||||
setSelected(selected.filter(s => s.id !== row.id));
|
||||
} else {
|
||||
setSelected(selected.concat(row));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await Promise.all(selected.map(host => HostsAPI.destroy(host.id)));
|
||||
} catch (error) {
|
||||
setDeletionError(error);
|
||||
} finally {
|
||||
setSelected([]);
|
||||
try {
|
||||
const {
|
||||
data: { count, results },
|
||||
} = await fetchHosts(match.params.id, location.search);
|
||||
|
||||
setHosts(results);
|
||||
setHostCount(count);
|
||||
} catch (error) {
|
||||
setContentError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle = async hostToToggle => {
|
||||
setToggleLoading(hostToToggle.id);
|
||||
|
||||
try {
|
||||
const { data: updatedHost } = await HostsAPI.update(hostToToggle.id, {
|
||||
enabled: !hostToToggle.enabled,
|
||||
});
|
||||
|
||||
setHosts(
|
||||
hosts.map(host => (host.id === updatedHost.id ? updatedHost : host))
|
||||
);
|
||||
} catch (error) {
|
||||
setToggleError(error);
|
||||
} finally {
|
||||
setToggleLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const canAdd =
|
||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||
const isAllSelected = selected.length > 0 && selected.length === hosts.length;
|
||||
import Host from '../../Host/Host';
|
||||
import InventoryHostList from './InventoryHostList';
|
||||
import HostAdd from '../InventoryHostAdd';
|
||||
|
||||
function InventoryHosts({ match, setBreadcrumb, i18n, inventory }) {
|
||||
return (
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading}
|
||||
items={hosts}
|
||||
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`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<Switch>
|
||||
<Route
|
||||
key="host-add"
|
||||
path="/inventories/inventory/:id/hosts/add"
|
||||
render={() => <HostAdd match={match} />}
|
||||
/>
|
||||
|
||||
{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>
|
||||
)}
|
||||
</>
|
||||
,
|
||||
<Route
|
||||
key="details and edit"
|
||||
path="/inventories/inventory/:id/hosts/:hostId"
|
||||
render={() => (
|
||||
<Host
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
match={match}
|
||||
i18n={i18n}
|
||||
inventory={inventory}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
,
|
||||
<Route
|
||||
key="host-list"
|
||||
path="/inventories/inventory/:id/hosts/"
|
||||
render={() => (
|
||||
<InventoryHostList
|
||||
match={match}
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
i18n={i18n}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
,
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(withRouter(InventoryHosts));
|
||||
export default withRouter(InventoryHosts);
|
||||
|
@ -1 +1 @@
|
||||
export { default } from './InventoryHosts';
|
||||
export { default } from './InventoryHostList';
|
||||
|
Loading…
Reference in New Issue
Block a user