mirror of
https://github.com/ansible/awx.git
synced 2024-11-01 08:21:15 +03:00
Merge pull request #5734 from marshmalien/5264-inv-host-edit-form
Add inventory host edit form Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
commit
c6595786f5
@ -1,7 +1,14 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
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 { CaretLeftIcon } from '@patternfly/react-icons';
|
||||
|
||||
@ -12,217 +19,189 @@ import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import HostFacts from './HostFacts';
|
||||
import HostDetail from './HostDetail';
|
||||
|
||||
import HostEdit from './HostEdit';
|
||||
import HostGroups from './HostGroups';
|
||||
import HostCompletedJobs from './HostCompletedJobs';
|
||||
import { HostsAPI } from '@api';
|
||||
|
||||
class Host extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
function Host({ inventory, i18n, setBreadcrumb }) {
|
||||
const [host, setHost] = useState(null);
|
||||
const [contentError, setContentError] = useState(null);
|
||||
const [hasContentLoading, setHasContentLoading] = useState(true);
|
||||
|
||||
this.state = {
|
||||
host: null,
|
||||
hasContentLoading: true,
|
||||
contentError: null,
|
||||
isInitialized: false,
|
||||
};
|
||||
this.loadHost = this.loadHost.bind(this);
|
||||
}
|
||||
const location = useLocation();
|
||||
const hostsMatch = useRouteMatch('/hosts/:id');
|
||||
const inventoriesMatch = useRouteMatch(
|
||||
'/inventories/inventory/:id/hosts/:hostId'
|
||||
);
|
||||
const baseUrl = hostsMatch ? hostsMatch.url : inventoriesMatch.url;
|
||||
const hostListUrl = hostsMatch
|
||||
? '/hosts'
|
||||
: `/inventories/inventory/${inventoriesMatch.params.id}/hosts`;
|
||||
|
||||
async componentDidMount() {
|
||||
await this.loadHost();
|
||||
this.setState({ isInitialized: true });
|
||||
}
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setContentError(null);
|
||||
setHasContentLoading(true);
|
||||
|
||||
async componentDidUpdate(prevProps) {
|
||||
const { location, match } = this.props;
|
||||
const url = `/hosts/${match.params.id}/`;
|
||||
try {
|
||||
const hostId = hostsMatch
|
||||
? hostsMatch.params.id
|
||||
: inventoriesMatch.params.hostId;
|
||||
const { data } = await HostsAPI.readDetail(hostId);
|
||||
setHost(data);
|
||||
|
||||
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 {
|
||||
const { data } = await HostsAPI.readDetail(
|
||||
match.params.hostId || match.params.id
|
||||
);
|
||||
this.setState({ host: data });
|
||||
|
||||
if (history.location.pathname.startsWith('/hosts')) {
|
||||
setBreadcrumb(data);
|
||||
} else {
|
||||
setBreadcrumb(inventory, data);
|
||||
if (hostsMatch) {
|
||||
setBreadcrumb(data);
|
||||
} else if (inventoriesMatch) {
|
||||
setBreadcrumb(inventory, data);
|
||||
}
|
||||
} catch (error) {
|
||||
setContentError(error);
|
||||
} finally {
|
||||
setHasContentLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
this.setState({ contentError: err });
|
||||
} finally {
|
||||
this.setState({ hasContentLoading: false });
|
||||
}
|
||||
})();
|
||||
}, [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() {
|
||||
const { location, match, history, i18n } = this.props;
|
||||
const { host, hasContentLoading, isInitialized, contentError } = this.state;
|
||||
let cardHeader = (
|
||||
<TabbedCardHeader>
|
||||
<RoutedTabs tabsArray={tabsArray} />
|
||||
<CardCloseButton linkTo={hostListUrl} />
|
||||
</TabbedCardHeader>
|
||||
);
|
||||
|
||||
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`Completed Jobs`),
|
||||
link: `${match.url}/completed_jobs`,
|
||||
id: 3,
|
||||
},
|
||||
];
|
||||
if (location.pathname.endsWith('edit')) {
|
||||
cardHeader = null;
|
||||
}
|
||||
|
||||
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 = (
|
||||
<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) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
if (!hasContentLoading && contentError) {
|
||||
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 })}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<ContentError error={contentError}>
|
||||
{contentError.response && contentError.response.status === 404 && (
|
||||
<span>
|
||||
{i18n._(`Host not found.`)}{' '}
|
||||
<Link to={hostListUrl}>{i18n._(`View all Hosts.`)}</Link>
|
||||
</span>
|
||||
)}
|
||||
))
|
||||
{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>
|
||||
</ContentError>
|
||||
</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 };
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { HostsAPI } from '@api';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
@ -7,39 +8,70 @@ import Host from './Host';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
const mockMe = {
|
||||
is_super_user: true,
|
||||
is_system_auditor: false,
|
||||
};
|
||||
|
||||
describe('<Host />', () => {
|
||||
test('initially renders succesfully', () => {
|
||||
HostsAPI.readDetail.mockResolvedValue({ data: mockDetails });
|
||||
mountWithContexts(<Host setBreadcrumb={() => {}} me={mockMe} />);
|
||||
let wrapper;
|
||||
let history;
|
||||
|
||||
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 () => {
|
||||
const history = createMemoryHistory({
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/hosts/1/foobar'],
|
||||
});
|
||||
const wrapper = mountWithContexts(
|
||||
<Host setBreadcrumb={() => {}} me={mockMe} />,
|
||||
{
|
||||
context: {
|
||||
router: {
|
||||
history,
|
||||
route: {
|
||||
location: history.location,
|
||||
match: {
|
||||
params: { id: 1 },
|
||||
url: '/hosts/1/foobar',
|
||||
path: '/host/1/foobar',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<Host setBreadcrumb={() => {}} />, {
|
||||
context: { router: { history } },
|
||||
});
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
||||
});
|
||||
|
@ -1,58 +1,42 @@
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { PageSection, Card } from '@patternfly/react-core';
|
||||
import React, { useState } from 'react';
|
||||
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import { CardBody } from '@components/Card';
|
||||
import { HostsAPI } from '@api';
|
||||
import { Config } from '@contexts/Config';
|
||||
import HostForm from '../shared';
|
||||
|
||||
class HostAdd extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleCancel = this.handleCancel.bind(this);
|
||||
this.state = { error: '' };
|
||||
}
|
||||
function HostAdd() {
|
||||
const [formError, setFormError] = useState(null);
|
||||
const history = useHistory();
|
||||
const hostsMatch = useRouteMatch('/hosts');
|
||||
const inventoriesMatch = useRouteMatch('/inventories/inventory/:id/hosts');
|
||||
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 {
|
||||
const { data: response } = await HostsAPI.create(values);
|
||||
history.push(`/hosts/${response.id}`);
|
||||
history.push(`${url}/${response.id}/details`);
|
||||
} catch (error) {
|
||||
this.setState({ error });
|
||||
setFormError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleCancel() {
|
||||
const { history } = this.props;
|
||||
history.push('/hosts');
|
||||
}
|
||||
const handleCancel = () => {
|
||||
history.push(`${url}`);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { error } = this.state;
|
||||
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Config>
|
||||
{({ me }) => (
|
||||
<HostForm
|
||||
handleSubmit={this.handleSubmit}
|
||||
handleCancel={this.handleCancel}
|
||||
me={me || {}}
|
||||
/>
|
||||
)}
|
||||
</Config>
|
||||
{error ? <div>error</div> : ''}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<CardBody>
|
||||
<HostForm handleSubmit={handleSubmit} handleCancel={handleCancel} />
|
||||
{formError ? <div>error</div> : ''}
|
||||
</CardBody>
|
||||
);
|
||||
}
|
||||
|
||||
export { HostAdd as _HostAdd };
|
||||
export default withI18n()(withRouter(HostAdd));
|
||||
export default HostAdd;
|
||||
|
@ -8,55 +8,48 @@ import { HostsAPI } from '@api';
|
||||
jest.mock('@api');
|
||||
|
||||
describe('<HostAdd />', () => {
|
||||
test('handleSubmit should post to api', async () => {
|
||||
let wrapper;
|
||||
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);
|
||||
});
|
||||
let wrapper;
|
||||
let history;
|
||||
|
||||
test('should navigate to hosts list when cancel is clicked', async () => {
|
||||
const history = createMemoryHistory({});
|
||||
let wrapper;
|
||||
const hostData = {
|
||||
name: 'new name',
|
||||
description: 'new description',
|
||||
inventory: 1,
|
||||
variables: '---\nfoo: bar',
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/hosts/1/add'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<HostAdd />, {
|
||||
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')();
|
||||
expect(history.location.pathname).toEqual('/hosts');
|
||||
});
|
||||
|
||||
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({
|
||||
data: {
|
||||
id: 5,
|
||||
...hostData,
|
||||
},
|
||||
});
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<HostAdd />, {
|
||||
context: { router: { history } },
|
||||
});
|
||||
});
|
||||
await waitForElement(wrapper, 'button[aria-label="Save"]');
|
||||
await wrapper.find('HostForm').invoke('handleSubmit')(hostData);
|
||||
expect(history.location.pathname).toEqual('/hosts/5');
|
||||
expect(history.location.pathname).toEqual('/hosts/5/details');
|
||||
});
|
||||
});
|
||||
|
@ -1,72 +1,54 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
import { HostsAPI } from '@api';
|
||||
import { Config } from '@contexts/Config';
|
||||
|
||||
import HostForm from '../shared';
|
||||
|
||||
class HostEdit extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
function HostEdit({ host }) {
|
||||
const [formError, setFormError] = useState(null);
|
||||
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);
|
||||
this.handleCancel = this.handleCancel.bind(this);
|
||||
this.handleSuccess = this.handleSuccess.bind(this);
|
||||
|
||||
this.state = {
|
||||
error: '',
|
||||
};
|
||||
if (hostsMatch) {
|
||||
detailsUrl = `/hosts/${hostsMatch.params.id}/details`;
|
||||
}
|
||||
|
||||
async handleSubmit(values) {
|
||||
const { host } = this.props;
|
||||
if (inventoriesMatch) {
|
||||
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 {
|
||||
await HostsAPI.update(host.id, values);
|
||||
this.handleSuccess();
|
||||
} catch (err) {
|
||||
this.setState({ error: err });
|
||||
history.push(detailsUrl);
|
||||
} catch (error) {
|
||||
setFormError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleCancel() {
|
||||
const {
|
||||
host: { id },
|
||||
history,
|
||||
} = this.props;
|
||||
history.push(`/hosts/${id}/details`);
|
||||
}
|
||||
const handleCancel = () => {
|
||||
history.push(detailsUrl);
|
||||
};
|
||||
|
||||
handleSuccess() {
|
||||
const {
|
||||
host: { id },
|
||||
history,
|
||||
} = this.props;
|
||||
history.push(`/hosts/${id}/details`);
|
||||
}
|
||||
|
||||
render() {
|
||||
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>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<CardBody>
|
||||
<HostForm
|
||||
host={host}
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={handleCancel}
|
||||
/>
|
||||
{formError ? <div>error</div> : null}
|
||||
</CardBody>
|
||||
);
|
||||
}
|
||||
|
||||
HostEdit.propTypes = {
|
||||
@ -74,4 +56,4 @@ HostEdit.propTypes = {
|
||||
};
|
||||
|
||||
export { HostEdit as _HostEdit };
|
||||
export default withRouter(HostEdit);
|
||||
export default HostEdit;
|
||||
|
@ -35,7 +35,9 @@ describe('<HostEdit />', () => {
|
||||
});
|
||||
|
||||
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} />, {
|
||||
context: { router: { history } },
|
||||
});
|
||||
|
@ -2,9 +2,9 @@ import React, { Component, Fragment } from 'react';
|
||||
import { Route, withRouter, Switch } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { PageSection } from '@patternfly/react-core';
|
||||
|
||||
import { Config } from '@contexts/Config';
|
||||
import { PageSection, Card } from '@patternfly/react-core';
|
||||
import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs';
|
||||
|
||||
import HostList from './HostList';
|
||||
@ -54,11 +54,10 @@ class Hosts extends Component {
|
||||
<Fragment>
|
||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
||||
<PageSection>
|
||||
<Switch>
|
||||
<Route path={`${match.path}/add`} render={() => <HostAdd />} />
|
||||
<Route
|
||||
path={`${match.path}/:id`}
|
||||
render={() => (
|
||||
<Card>
|
||||
<Switch>
|
||||
<Route path={`${match.path}/add`} render={() => <HostAdd />} />
|
||||
<Route path={`${match.path}/:id`}>
|
||||
<Config>
|
||||
{({ me }) => (
|
||||
<Host
|
||||
@ -67,10 +66,10 @@ class Hosts extends Component {
|
||||
/>
|
||||
)}
|
||||
</Config>
|
||||
)}
|
||||
/>
|
||||
<Route path={`${match.path}`} render={() => <HostList />} />
|
||||
</Switch>
|
||||
</Route>
|
||||
<Route path={`${match.path}`} render={() => <HostList />} />
|
||||
</Switch>
|
||||
</Card>
|
||||
</PageSection>
|
||||
</Fragment>
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { func, shape } from 'prop-types';
|
||||
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { Formik, Field } from 'formik';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
@ -20,6 +20,8 @@ function HostForm({ handleSubmit, handleCancel, host, i18n }) {
|
||||
host ? host.summary_fields.inventory : ''
|
||||
);
|
||||
|
||||
const hostAddMatch = useRouteMatch('/hosts/add');
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
@ -47,7 +49,7 @@ function HostForm({ handleSubmit, handleCancel, host, i18n }) {
|
||||
type="text"
|
||||
label={i18n._(t`Description`)}
|
||||
/>
|
||||
{!host.id && (
|
||||
{hostAddMatch && (
|
||||
<Field
|
||||
name="inventory"
|
||||
validate={required(
|
||||
@ -112,4 +114,4 @@ HostForm.defaultProps = {
|
||||
};
|
||||
|
||||
export { HostForm as _HostForm };
|
||||
export default withI18n()(withRouter(HostForm));
|
||||
export default withI18n()(HostForm);
|
||||
|
@ -80,7 +80,13 @@ function Inventory({ i18n, setBreadcrumb }) {
|
||||
}
|
||||
|
||||
if (hasContentLoading) {
|
||||
return <ContentLoading />;
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
<ContentLoading />
|
||||
</Card>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasContentLoading && contentError) {
|
||||
|
@ -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;
|
@ -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');
|
||||
});
|
||||
});
|
@ -1 +0,0 @@
|
||||
export { default } from './InventoryHostAdd';
|
@ -1,46 +1,30 @@
|
||||
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 InventoryHostList from './InventoryHostList';
|
||||
import InventoryHostAdd from '../InventoryHostAdd';
|
||||
import HostAdd from '../../Host/HostAdd';
|
||||
|
||||
function InventoryHosts({ match, setBreadcrumb, i18n, inventory }) {
|
||||
function InventoryHosts({ setBreadcrumb, inventory }) {
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
key="host-add"
|
||||
path="/inventories/inventory/:id/hosts/add"
|
||||
render={() => <InventoryHostAdd match={match} />}
|
||||
/>
|
||||
,
|
||||
<Route key="host-add" path="/inventories/inventory/:id/hosts/add">
|
||||
<HostAdd />
|
||||
</Route>
|
||||
<Route
|
||||
key="host"
|
||||
path="/inventories/inventory/:id/hosts/:hostId"
|
||||
render={() => (
|
||||
<Host
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
match={match}
|
||||
i18n={i18n}
|
||||
inventory={inventory}
|
||||
/>
|
||||
<Host setBreadcrumb={setBreadcrumb} inventory={inventory} />
|
||||
)}
|
||||
/>
|
||||
,
|
||||
<Route
|
||||
key="host-list"
|
||||
path="/inventories/inventory/:id/hosts/"
|
||||
render={() => (
|
||||
<InventoryHostList
|
||||
match={match}
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
i18n={i18n}
|
||||
/>
|
||||
)}
|
||||
render={() => <InventoryHostList setBreadcrumb={setBreadcrumb} />}
|
||||
/>
|
||||
,
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(InventoryHosts);
|
||||
export default InventoryHosts;
|
||||
|
Loading…
Reference in New Issue
Block a user