Add inventory host edit form

This commit is contained in:
Marliana Lara
2020-01-22 11:06:45 -05:00
parent c2fe3fcf13
commit d33bbdd4f6
12 changed files with 345 additions and 524 deletions

View File

@@ -1,7 +1,14 @@
import React, { Component } from 'react'; import React, { useEffect, useState } 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,
Redirect,
Link,
useRouteMatch,
useLocation,
} from 'react-router-dom';
import { Card } from '@patternfly/react-core'; import { Card } from '@patternfly/react-core';
import { CaretLeftIcon } from '@patternfly/react-icons'; import { CaretLeftIcon } from '@patternfly/react-icons';
@@ -12,217 +19,190 @@ import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading'; 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';
import { HostsAPI } from '@api'; import { HostsAPI } from '@api';
class Host extends Component { function Host({ inventory, i18n, setBreadcrumb }) {
constructor(props) { const [host, setHost] = useState(null);
super(props); const [contentError, setContentError] = useState(null);
const [hasContentLoading, setHasContentLoading] = useState(true);
this.state = { const location = useLocation();
host: null, const hostsMatch = useRouteMatch('/hosts/:id');
hasContentLoading: true, const inventoriesMatch = useRouteMatch(
contentError: null, '/inventories/inventory/:id/hosts/:hostId'
isInitialized: false, );
}; const baseUrl = hostsMatch ? hostsMatch.url : inventoriesMatch.url;
this.loadHost = this.loadHost.bind(this); const hostListUrl = hostsMatch
} ? '/hosts'
: `/inventories/inventory/${inventoriesMatch.params.id}/hosts`;
async componentDidMount() { const loadHost = async () => {
await this.loadHost(); setContentError(null);
this.setState({ isInitialized: true }); setHasContentLoading(true);
}
async componentDidUpdate(prevProps) {
const { location, match } = this.props;
const url = `/hosts/${match.params.id}/`;
if (
prevProps.location.pathname.startsWith(url) &&
prevProps.location !== location &&
location.pathname === `${url}details`
) {
await this.loadHost();
}
}
async loadHost() {
const { match, setBreadcrumb, history, inventory } = this.props;
this.setState({ contentError: null, hasContentLoading: true });
try { try {
const { data } = await HostsAPI.readDetail( const hostId = hostsMatch
match.params.hostId || match.params.id ? hostsMatch.params.id
); : inventoriesMatch.params.hostId;
this.setState({ host: data }); const { data } = await HostsAPI.readDetail(hostId);
setHost(data);
if (history.location.pathname.startsWith('/hosts')) { if (hostsMatch) {
setBreadcrumb(data); setBreadcrumb(data);
} else { } else if (inventoriesMatch) {
setBreadcrumb(inventory, data); setBreadcrumb(inventory, data);
} }
} catch (err) { } catch (error) {
this.setState({ contentError: err }); setContentError(error);
} finally { } finally {
this.setState({ hasContentLoading: false }); setHasContentLoading(false);
} }
};
useEffect(() => {
loadHost();
}, [location]); // eslint-disable-line react-hooks/exhaustive-deps
const tabsArray = [
{
name: i18n._(t`Details`),
link: `${baseUrl}/details`,
id: 0,
},
{
name: i18n._(t`Facts`),
link: `${baseUrl}/facts`,
id: 1,
},
{
name: i18n._(t`Groups`),
link: `${baseUrl}/groups`,
id: 2,
},
{
name: i18n._(t`Completed Jobs`),
link: `${baseUrl}/completed_jobs`,
id: 3,
},
];
if (inventoriesMatch) {
tabsArray.unshift({
name: (
<>
<CaretLeftIcon />
{i18n._(t`Back to Hosts`)}
</>
),
link: hostListUrl,
id: 99,
});
} }
render() { let cardHeader = (
const { location, match, history, i18n } = this.props; <TabbedCardHeader>
const { host, hasContentLoading, isInitialized, contentError } = this.state; <RoutedTabs tabsArray={tabsArray} />
<CardCloseButton linkTo={hostListUrl} />
</TabbedCardHeader>
);
const tabsArray = [ if (location.pathname.endsWith('edit')) {
{ cardHeader = null;
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`,
id: 3,
},
];
if (!history.location.pathname.startsWith('/hosts')) { if (hasContentLoading) {
tabsArray.unshift({ return <ContentLoading />;
name: ( }
<>
<CaretLeftIcon />
{i18n._(t`Back to Hosts`)}
</>
),
link: `/inventories/inventory/${match.params.id}/hosts`,
id: 99,
});
}
let cardHeader = (
<TabbedCardHeader>
<RoutedTabs tabsArray={tabsArray} />
<CardCloseButton linkTo="/hosts" />
</TabbedCardHeader>
);
if (!isInitialized) {
cardHeader = null;
}
if (location.pathname.endsWith('edit')) {
cardHeader = null;
}
if (hasContentLoading) {
return <ContentLoading />;
}
if (!hasContentLoading && contentError) {
return (
<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>
);
}
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
/>
);
if (!hasContentLoading && contentError) {
return ( return (
<Card className="awx-c-card"> <Card className="awx-c-card">
{cardHeader} <ContentError error={contentError}>
<Switch> {contentError.response && contentError.response.status === 404 && (
{redirect} <span>
{host && ( {i18n._(`Host not found.`)}{' '}
<Route <Link to={hostListUrl}>{i18n._(`View all Hosts.`)}</Link>
path={[ </span>
'/hosts/:id/details',
'/inventories/inventory/:id/hosts/:hostId/details',
]}
render={() => (
<HostDetail
host={host}
onUpdateHost={newHost => this.setState({ host: newHost })}
/>
)}
/>
)} )}
)) </ContentError>
{host && (
<Route
path={[
'/hosts/:id/edit',
'/inventories/inventory/:id/hosts/:hostId/edit',
]}
render={() => <HostEdit 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>
); );
} }
const redirect = hostsMatch ? (
<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',
]}
>
<HostDetail
host={host}
onUpdateHost={newHost => setHost(newHost)}
/>
</Route>
)}
{host && (
<Route
path={[
'/hosts/:id/edit',
'/inventories/inventory/:id/hosts/:hostId/edit',
]}
render={() => <HostEdit 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>
<Link to={`${baseUrl}/details`}>
{i18n._(`View Host Details`)}
</Link>
</ContentError>
)
}
/>
</Switch>
</Card>
);
} }
export default withI18n()(withRouter(Host)); export default withI18n()(Host);
export { Host as _Host }; export { Host as _Host };

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { HostsAPI } from '@api'; import { HostsAPI } from '@api';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
@@ -7,39 +8,70 @@ import Host from './Host';
jest.mock('@api'); jest.mock('@api');
const mockMe = {
is_super_user: true,
is_system_auditor: false,
};
describe('<Host />', () => { describe('<Host />', () => {
test('initially renders succesfully', () => { let wrapper;
HostsAPI.readDetail.mockResolvedValue({ data: mockDetails }); let history;
mountWithContexts(<Host setBreadcrumb={() => {}} me={mockMe} />);
HostsAPI.readDetail.mockResolvedValue({
data: { ...mockDetails },
});
afterEach(() => {
wrapper.unmount();
});
test('initially renders succesfully', async () => {
history = createMemoryHistory({
initialEntries: ['/hosts/1/edit'],
});
await act(async () => {
wrapper = mountWithContexts(<Host setBreadcrumb={() => {}} />, {
context: { router: { history } },
});
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(wrapper.find('Host').length).toBe(1);
});
test('should render "Back to Hosts" tab when navigating from inventories', async () => {
history = createMemoryHistory({
initialEntries: ['/inventories/inventory/1/hosts/1'],
});
await act(async () => {
wrapper = mountWithContexts(<Host setBreadcrumb={() => {}} />, {
context: { router: { history } },
});
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(
wrapper
.find('RoutedTabs li')
.first()
.text()
).toBe('Back to Hosts');
});
test('should show content error when api throws error on initial render', async () => {
HostsAPI.readDetail.mockRejectedValueOnce(new Error());
await act(async () => {
wrapper = mountWithContexts(<Host setBreadcrumb={() => {}} />, {
context: { router: { history } },
});
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
}); });
test('should show content error when user attempts to navigate to erroneous route', async () => { test('should show content error when user attempts to navigate to erroneous route', async () => {
const history = createMemoryHistory({ history = createMemoryHistory({
initialEntries: ['/hosts/1/foobar'], initialEntries: ['/hosts/1/foobar'],
}); });
const wrapper = mountWithContexts( await act(async () => {
<Host setBreadcrumb={() => {}} me={mockMe} />, wrapper = mountWithContexts(<Host setBreadcrumb={() => {}} />, {
{ context: { router: { history } },
context: { });
router: { });
history,
route: {
location: history.location,
match: {
params: { id: 1 },
url: '/hosts/1/foobar',
path: '/host/1/foobar',
},
},
},
},
}
);
await waitForElement(wrapper, 'ContentError', el => el.length === 1); await waitForElement(wrapper, 'ContentError', el => el.length === 1);
}); });
}); });

View File

@@ -1,58 +1,42 @@
import React from 'react'; import React, { useState } from 'react';
import { withRouter } from 'react-router-dom'; import { useHistory, useRouteMatch } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { PageSection, Card } from '@patternfly/react-core';
import { CardBody } from '@components/Card'; import { CardBody } from '@components/Card';
import { HostsAPI } from '@api'; import { HostsAPI } from '@api';
import { Config } from '@contexts/Config';
import HostForm from '../shared'; import HostForm from '../shared';
class HostAdd extends React.Component { function HostAdd() {
constructor(props) { const [formError, setFormError] = useState(null);
super(props); const history = useHistory();
this.handleSubmit = this.handleSubmit.bind(this); const hostsMatch = useRouteMatch('/hosts');
this.handleCancel = this.handleCancel.bind(this); const inventoriesMatch = useRouteMatch('/inventories/inventory/:id/hosts');
this.state = { error: '' }; const url = hostsMatch ? hostsMatch.url : inventoriesMatch.url;
}
const handleSubmit = async formData => {
const values = {
...formData,
inventory: inventoriesMatch
? inventoriesMatch.params.id
: formData.inventory,
};
async handleSubmit(values) {
const { history } = this.props;
try { try {
const { data: response } = await HostsAPI.create(values); const { data: response } = await HostsAPI.create(values);
history.push(`/hosts/${response.id}`); history.push(`${url}/${response.id}/details`);
} catch (error) { } catch (error) {
this.setState({ error }); setFormError(error);
} }
} };
handleCancel() { const handleCancel = () => {
const { history } = this.props; history.push(`${url}`);
history.push('/hosts'); };
}
render() { return (
const { error } = this.state; <CardBody>
<HostForm handleSubmit={handleSubmit} handleCancel={handleCancel} />
return ( {formError ? <div>error</div> : ''}
<PageSection> </CardBody>
<Card> );
<CardBody>
<Config>
{({ me }) => (
<HostForm
handleSubmit={this.handleSubmit}
handleCancel={this.handleCancel}
me={me || {}}
/>
)}
</Config>
{error ? <div>error</div> : ''}
</CardBody>
</Card>
</PageSection>
);
}
} }
export { HostAdd as _HostAdd }; export default HostAdd;
export default withI18n()(withRouter(HostAdd));

View File

@@ -8,55 +8,48 @@ import { HostsAPI } from '@api';
jest.mock('@api'); jest.mock('@api');
describe('<HostAdd />', () => { describe('<HostAdd />', () => {
test('handleSubmit should post to api', async () => { let wrapper;
let wrapper; let history;
await act(async () => {
wrapper = mountWithContexts(<HostAdd />);
});
const updatedHostData = {
name: 'new name',
description: 'new description',
inventory: 1,
variables: '---\nfoo: bar',
};
wrapper.find('HostForm').prop('handleSubmit')(updatedHostData);
expect(HostsAPI.create).toHaveBeenCalledWith(updatedHostData);
});
test('should navigate to hosts list when cancel is clicked', async () => { const hostData = {
const history = createMemoryHistory({}); name: 'new name',
let wrapper; description: 'new description',
inventory: 1,
variables: '---\nfoo: bar',
};
beforeEach(async () => {
history = createMemoryHistory({
initialEntries: ['/hosts/1/add'],
});
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<HostAdd />, { wrapper = mountWithContexts(<HostAdd />, {
context: { router: { history } }, context: { router: { history } },
}); });
}); });
});
test('handleSubmit should post to api', async () => {
act(() => {
wrapper.find('HostForm').prop('handleSubmit')(hostData);
});
expect(HostsAPI.create).toHaveBeenCalledWith(hostData);
});
test('should navigate to hosts list when cancel is clicked', async () => {
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
expect(history.location.pathname).toEqual('/hosts'); expect(history.location.pathname).toEqual('/hosts');
}); });
test('successful form submission should trigger redirect', async () => { test('successful form submission should trigger redirect', async () => {
const history = createMemoryHistory({});
const hostData = {
name: 'new name',
description: 'new description',
inventory: 1,
variables: '---\nfoo: bar',
};
HostsAPI.create.mockResolvedValueOnce({ HostsAPI.create.mockResolvedValueOnce({
data: { data: {
id: 5, id: 5,
...hostData, ...hostData,
}, },
}); });
let wrapper;
await act(async () => {
wrapper = mountWithContexts(<HostAdd />, {
context: { router: { history } },
});
});
await waitForElement(wrapper, 'button[aria-label="Save"]'); await waitForElement(wrapper, 'button[aria-label="Save"]');
await wrapper.find('HostForm').invoke('handleSubmit')(hostData); await wrapper.find('HostForm').invoke('handleSubmit')(hostData);
expect(history.location.pathname).toEqual('/hosts/5'); expect(history.location.pathname).toEqual('/hosts/5/details');
}); });
}); });

View File

@@ -1,72 +1,54 @@
import React, { Component } from 'react'; import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom'; import { useHistory, useRouteMatch } from 'react-router-dom';
import { CardBody } from '@components/Card'; import { CardBody } from '@components/Card';
import { HostsAPI } from '@api'; import { HostsAPI } from '@api';
import { Config } from '@contexts/Config';
import HostForm from '../shared'; import HostForm from '../shared';
class HostEdit extends Component { function HostEdit({ host }) {
constructor(props) { const [formError, setFormError] = useState(null);
super(props); const hostsMatch = useRouteMatch('/hosts/:id/edit');
const inventoriesMatch = useRouteMatch(
'/inventories/inventory/:id/hosts/:hostId/edit'
);
const history = useHistory();
let detailsUrl;
this.handleSubmit = this.handleSubmit.bind(this); if (hostsMatch) {
this.handleCancel = this.handleCancel.bind(this); detailsUrl = `/hosts/${hostsMatch.params.id}/details`;
this.handleSuccess = this.handleSuccess.bind(this);
this.state = {
error: '',
};
} }
async handleSubmit(values) { if (inventoriesMatch) {
const { host } = this.props; const kind =
host.summary_fields.inventory.kind === 'smart'
? 'smart_inventory'
: 'inventory';
detailsUrl = `/inventories/${kind}/${inventoriesMatch.params.id}/hosts/${inventoriesMatch.params.hostId}/details`;
}
const handleSubmit = async values => {
try { try {
await HostsAPI.update(host.id, values); await HostsAPI.update(host.id, values);
this.handleSuccess(); history.push(detailsUrl);
} catch (err) { } catch (error) {
this.setState({ error: err }); setFormError(error);
} }
} };
handleCancel() { const handleCancel = () => {
const { history.push(detailsUrl);
host: { id }, };
history,
} = this.props;
history.push(`/hosts/${id}/details`);
}
handleSuccess() { return (
const { <CardBody>
host: { id }, <HostForm
history, host={host}
} = this.props; handleSubmit={handleSubmit}
history.push(`/hosts/${id}/details`); handleCancel={handleCancel}
} />
{formError ? <div>error</div> : null}
render() { </CardBody>
const { host } = this.props; );
const { error } = this.state;
return (
<CardBody>
<Config>
{({ me }) => (
<HostForm
host={host}
handleSubmit={this.handleSubmit}
handleCancel={this.handleCancel}
me={me || {}}
/>
)}
</Config>
{error ? <div>error</div> : null}
</CardBody>
);
}
} }
HostEdit.propTypes = { HostEdit.propTypes = {
@@ -74,4 +56,4 @@ HostEdit.propTypes = {
}; };
export { HostEdit as _HostEdit }; export { HostEdit as _HostEdit };
export default withRouter(HostEdit); export default HostEdit;

View File

@@ -35,7 +35,9 @@ describe('<HostEdit />', () => {
}); });
test('should navigate to host detail when cancel is clicked', () => { test('should navigate to host detail when cancel is clicked', () => {
const history = createMemoryHistory({}); const history = createMemoryHistory({
initialEntries: ['/hosts/1/edit'],
});
const wrapper = mountWithContexts(<HostEdit host={mockData} />, { const wrapper = mountWithContexts(<HostEdit host={mockData} />, {
context: { router: { history } }, context: { router: { history } },
}); });

View File

@@ -2,9 +2,9 @@ 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 { PageSection, Card } from '@patternfly/react-core';
import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs'; import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs';
import HostList from './HostList'; import HostList from './HostList';
@@ -54,11 +54,10 @@ class Hosts extends Component {
<Fragment> <Fragment>
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} /> <Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
<PageSection> <PageSection>
<Switch> <Card>
<Route path={`${match.path}/add`} render={() => <HostAdd />} /> <Switch>
<Route <Route path={`${match.path}/add`} render={() => <HostAdd />} />
path={`${match.path}/:id`} <Route path={`${match.path}/:id`}>
render={() => (
<Config> <Config>
{({ me }) => ( {({ me }) => (
<Host <Host
@@ -67,10 +66,10 @@ class Hosts extends Component {
/> />
)} )}
</Config> </Config>
)} </Route>
/> <Route path={`${match.path}`} render={() => <HostList />} />
<Route path={`${match.path}`} render={() => <HostList />} /> </Switch>
</Switch> </Card>
</PageSection> </PageSection>
</Fragment> </Fragment>
); );

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { func, shape } from 'prop-types'; import { func, shape } from 'prop-types';
import { withRouter } from 'react-router-dom'; import { useRouteMatch } from 'react-router-dom';
import { Formik, Field } from 'formik'; import { Formik, Field } from 'formik';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
@@ -20,6 +20,8 @@ function HostForm({ handleSubmit, handleCancel, host, i18n }) {
host ? host.summary_fields.inventory : '' host ? host.summary_fields.inventory : ''
); );
const hostAddMatch = useRouteMatch('/hosts/add');
return ( return (
<Formik <Formik
initialValues={{ initialValues={{
@@ -47,7 +49,7 @@ function HostForm({ handleSubmit, handleCancel, host, i18n }) {
type="text" type="text"
label={i18n._(t`Description`)} label={i18n._(t`Description`)}
/> />
{!host.id && ( {hostAddMatch && (
<Field <Field
name="inventory" name="inventory"
validate={required( validate={required(
@@ -112,4 +114,4 @@ HostForm.defaultProps = {
}; };
export { HostForm as _HostForm }; export { HostForm as _HostForm };
export default withI18n()(withRouter(HostForm)); export default withI18n()(HostForm);

View File

@@ -1,36 +0,0 @@
import React, { useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { CardBody } from '@components/Card';
import InventoryHostForm from '../shared/InventoryHostForm';
import { InventoriesAPI } from '@api';
function InventoryHostAdd() {
const [formError, setFormError] = useState(null);
const history = useHistory();
const { id } = useParams();
const handleSubmit = async values => {
try {
const { data: response } = await InventoriesAPI.createHost(id, values);
history.push(`/inventories/inventory/${id}/hosts/${response.id}/details`);
} catch (error) {
setFormError(error);
}
};
const handleCancel = () => {
history.push(`/inventories/inventory/${id}/hosts`);
};
return (
<CardBody>
<InventoryHostForm
handleSubmit={handleSubmit}
handleCancel={handleCancel}
/>
{formError ? <div className="formSubmitError">error</div> : ''}
</CardBody>
);
}
export default InventoryHostAdd;

View File

@@ -1,100 +0,0 @@
import React from 'react';
import { Route } from 'react-router-dom';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import { sleep } from '@testUtils/testUtils';
import { InventoriesAPI } from '@api';
import InventoryHostAdd from './InventoryHostAdd';
jest.mock('@api');
describe('<InventoryHostAdd />', () => {
let wrapper;
let history;
const mockHostData = {
name: 'new name',
description: 'new description',
inventory: 1,
variables: '---\nfoo: bar',
};
beforeEach(async () => {
history = createMemoryHistory({
initialEntries: ['/inventories/inventory/1/hosts/add'],
});
await act(async () => {
wrapper = mountWithContexts(
<Route
path="/inventories/inventory/:id/hosts/add"
component={() => <InventoryHostAdd />}
/>,
{
context: {
router: { history, route: { location: history.location } },
},
}
);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
});
afterEach(() => {
wrapper.unmount();
});
test('handleSubmit should post to api', async () => {
InventoriesAPI.createHost.mockResolvedValue({
data: { ...mockHostData },
});
await act(async () => {
wrapper.find('FormField[id="host-name"] input').simulate('change', {
target: { value: 'new name', name: 'name' },
});
wrapper
.find('FormField[id="host-description"] input')
.simulate('change', {
target: { value: 'new description', name: 'description' },
});
wrapper.update();
await sleep(0);
wrapper.find('FormActionGroup').invoke('onSubmit')();
});
wrapper.update();
expect(InventoriesAPI.createHost).toHaveBeenCalledWith('1', {
name: 'new name',
description: 'new description',
variables: '---\n',
});
});
test('handleSubmit should throw an error', async () => {
InventoriesAPI.createHost.mockImplementationOnce(() =>
Promise.reject(new Error())
);
await act(async () => {
wrapper.find('FormField[id="host-name"] input').simulate('change', {
target: { value: 'new name', name: 'name' },
});
wrapper
.find('FormField[id="host-description"] input')
.simulate('change', {
target: { value: 'new description', name: 'description' },
});
});
wrapper.update();
await act(async () => {
wrapper.find('form').simulate('submit');
});
wrapper.update();
expect(wrapper.find('InventoryHostAdd .formSubmitError').length).toBe(1);
});
test('should navigate to inventory hosts list when cancel is clicked', async () => {
wrapper.find('button[aria-label="Cancel"]').simulate('click');
expect(history.location.pathname).toEqual('/inventories/inventory/1/hosts');
});
});

View File

@@ -1 +0,0 @@
export { default } from './InventoryHostAdd';

View File

@@ -1,46 +1,30 @@
import React from 'react'; import React from 'react';
import { Switch, Route, withRouter } from 'react-router-dom'; import { Switch, Route } from 'react-router-dom';
import Host from '../../Host/Host'; import Host from '../../Host/Host';
import InventoryHostList from './InventoryHostList'; import InventoryHostList from './InventoryHostList';
import InventoryHostAdd from '../InventoryHostAdd'; import HostAdd from '../../Host/HostAdd';
function InventoryHosts({ match, setBreadcrumb, i18n, inventory }) { function InventoryHosts({ setBreadcrumb, inventory }) {
return ( return (
<Switch> <Switch>
<Route <Route key="host-add" path="/inventories/inventory/:id/hosts/add">
key="host-add" <HostAdd />
path="/inventories/inventory/:id/hosts/add" </Route>
render={() => <InventoryHostAdd match={match} />}
/>
,
<Route <Route
key="host" key="host"
path="/inventories/inventory/:id/hosts/:hostId" path="/inventories/inventory/:id/hosts/:hostId"
render={() => ( render={() => (
<Host <Host setBreadcrumb={setBreadcrumb} inventory={inventory} />
setBreadcrumb={setBreadcrumb}
match={match}
i18n={i18n}
inventory={inventory}
/>
)} )}
/> />
,
<Route <Route
key="host-list" key="host-list"
path="/inventories/inventory/:id/hosts/" path="/inventories/inventory/:id/hosts/"
render={() => ( render={() => <InventoryHostList setBreadcrumb={setBreadcrumb} />}
<InventoryHostList
match={match}
setBreadcrumb={setBreadcrumb}
i18n={i18n}
/>
)}
/> />
,
</Switch> </Switch>
); );
} }
export default withRouter(InventoryHosts); export default InventoryHosts;