mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 11:00:03 -03:30
Merge pull request #5491 from marshmalien/inv-host-add
Add Inventory Host Add form Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
commit
c5b4681bf4
@ -19,6 +19,10 @@ class Inventories extends InstanceGroupsMixin(Base) {
|
||||
});
|
||||
}
|
||||
|
||||
createHost(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/hosts/`, data);
|
||||
}
|
||||
|
||||
readHosts(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/hosts/`, { params });
|
||||
}
|
||||
|
||||
@ -1,20 +1,10 @@
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
PageSection,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { PageSection, Card, CardBody } from '@patternfly/react-core';
|
||||
import { HostsAPI } from '@api';
|
||||
import { Config } from '@contexts/Config';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
|
||||
import HostForm from '../shared/HostForm';
|
||||
import HostForm from '../shared';
|
||||
|
||||
class HostAdd extends React.Component {
|
||||
constructor(props) {
|
||||
@ -41,16 +31,10 @@ class HostAdd extends React.Component {
|
||||
|
||||
render() {
|
||||
const { error } = this.state;
|
||||
const { i18n } = this.props;
|
||||
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
<CardHeader className="at-u-textRight">
|
||||
<Tooltip content={i18n._(t`Close`)} position="top">
|
||||
<CardCloseButton onClick={this.handleCancel} />
|
||||
</Tooltip>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Config>
|
||||
{({ me }) => (
|
||||
|
||||
@ -35,18 +35,6 @@ describe('<HostAdd />', () => {
|
||||
expect(history.location.pathname).toEqual('/hosts');
|
||||
});
|
||||
|
||||
test('should navigate to hosts list when close (x) is clicked', async () => {
|
||||
const history = createMemoryHistory({});
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<HostAdd />, {
|
||||
context: { router: { history } },
|
||||
});
|
||||
});
|
||||
wrapper.find('button[aria-label="Close"]').invoke('onClick')();
|
||||
expect(history.location.pathname).toEqual('/hosts');
|
||||
});
|
||||
|
||||
test('successful form submission should trigger redirect', async () => {
|
||||
const history = createMemoryHistory({});
|
||||
const hostData = {
|
||||
|
||||
@ -6,7 +6,7 @@ import { CardBody } from '@patternfly/react-core';
|
||||
import { HostsAPI } from '@api';
|
||||
import { Config } from '@contexts/Config';
|
||||
|
||||
import HostForm from '../shared/HostForm';
|
||||
import HostForm from '../shared';
|
||||
|
||||
class HostEdit extends Component {
|
||||
constructor(props) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useState } from 'react';
|
||||
import { func, shape } from 'prop-types';
|
||||
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { Formik, Field } from 'formik';
|
||||
@ -15,120 +15,86 @@ import { VariablesField } from '@components/CodeMirrorInput';
|
||||
import { required } from '@util/validators';
|
||||
import { InventoryLookup } from '@components/Lookup';
|
||||
|
||||
class HostForm extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
function HostForm({ handleSubmit, handleCancel, host, i18n }) {
|
||||
const [inventory, setInventory] = useState(
|
||||
host ? host.summary_fields.inventory : ''
|
||||
);
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
this.state = {
|
||||
formIsValid: true,
|
||||
inventory: props.host.summary_fields.inventory,
|
||||
};
|
||||
}
|
||||
|
||||
handleSubmit(values) {
|
||||
const { handleSubmit } = this.props;
|
||||
|
||||
handleSubmit(values);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { host, handleCancel, i18n } = this.props;
|
||||
const { formIsValid, inventory, error } = this.state;
|
||||
|
||||
const initialValues = !host.id
|
||||
? {
|
||||
name: host.name,
|
||||
description: host.description,
|
||||
inventory: host.inventory || '',
|
||||
variables: host.variables,
|
||||
}
|
||||
: {
|
||||
name: host.name,
|
||||
description: host.description,
|
||||
variables: host.variables,
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={this.handleSubmit}
|
||||
render={formik => (
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
<FormRow>
|
||||
<FormField
|
||||
id="host-name"
|
||||
name="name"
|
||||
type="text"
|
||||
label={i18n._(t`Name`)}
|
||||
validate={required(null, i18n)}
|
||||
isRequired
|
||||
/>
|
||||
<FormField
|
||||
id="host-description"
|
||||
name="description"
|
||||
type="text"
|
||||
label={i18n._(t`Description`)}
|
||||
/>
|
||||
{!host.id && (
|
||||
<Field
|
||||
name="inventory"
|
||||
validate={required(
|
||||
i18n._(t`Select a value for this field`),
|
||||
i18n
|
||||
)}
|
||||
render={({ form }) => (
|
||||
<InventoryLookup
|
||||
value={inventory}
|
||||
onBlur={() => form.setFieldTouched('inventory')}
|
||||
tooltip={i18n._(
|
||||
t`Select the inventory that this host will belong to.`
|
||||
)}
|
||||
isValid={
|
||||
!form.touched.inventory || !form.errors.inventory
|
||||
}
|
||||
helperTextInvalid={form.errors.inventory}
|
||||
onChange={value => {
|
||||
form.setFieldValue('inventory', value.id);
|
||||
this.setState({ inventory: value });
|
||||
}}
|
||||
required
|
||||
touched={form.touched.inventory}
|
||||
error={form.errors.inventory}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<VariablesField
|
||||
id="host-variables"
|
||||
name="variables"
|
||||
label={i18n._(t`Variables`)}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormActionGroup
|
||||
onCancel={handleCancel}
|
||||
onSubmit={formik.handleSubmit}
|
||||
submitDisabled={!formIsValid}
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
name: host.name,
|
||||
description: host.description,
|
||||
inventory: host.inventory || '',
|
||||
variables: host.variables,
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
render={formik => (
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
<FormRow>
|
||||
<FormField
|
||||
id="host-name"
|
||||
name="name"
|
||||
type="text"
|
||||
label={i18n._(t`Name`)}
|
||||
validate={required(null, i18n)}
|
||||
isRequired
|
||||
/>
|
||||
{error ? <div>error</div> : null}
|
||||
</Form>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
<FormField
|
||||
id="host-description"
|
||||
name="description"
|
||||
type="text"
|
||||
label={i18n._(t`Description`)}
|
||||
/>
|
||||
{!host.id && (
|
||||
<Field
|
||||
name="inventory"
|
||||
validate={required(
|
||||
i18n._(t`Select a value for this field`),
|
||||
i18n
|
||||
)}
|
||||
render={({ form }) => (
|
||||
<InventoryLookup
|
||||
value={inventory}
|
||||
onBlur={() => form.setFieldTouched('inventory')}
|
||||
tooltip={i18n._(
|
||||
t`Select the inventory that this host will belong to.`
|
||||
)}
|
||||
isValid={!form.touched.inventory || !form.errors.inventory}
|
||||
helperTextInvalid={form.errors.inventory}
|
||||
onChange={value => {
|
||||
form.setFieldValue('inventory', value.id);
|
||||
setInventory(value);
|
||||
}}
|
||||
required
|
||||
touched={form.touched.inventory}
|
||||
error={form.errors.inventory}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<VariablesField
|
||||
id="host-variables"
|
||||
name="variables"
|
||||
label={i18n._(t`Variables`)}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormActionGroup
|
||||
onCancel={handleCancel}
|
||||
onSubmit={formik.handleSubmit}
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
FormField.propTypes = {
|
||||
label: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired,
|
||||
};
|
||||
|
||||
HostForm.propTypes = {
|
||||
host: PropTypes.shape(),
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
handleCancel: PropTypes.func.isRequired,
|
||||
handleSubmit: func.isRequired,
|
||||
handleCancel: func.isRequired,
|
||||
host: shape({}),
|
||||
};
|
||||
|
||||
HostForm.defaultProps = {
|
||||
|
||||
@ -65,11 +65,7 @@ describe('<HostForm />', () => {
|
||||
expect(handleSubmit).not.toHaveBeenCalled();
|
||||
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||
await sleep(1);
|
||||
expect(handleSubmit).toHaveBeenCalledWith({
|
||||
name: 'Foo',
|
||||
description: 'Bar',
|
||||
variables: '---',
|
||||
});
|
||||
expect(handleSubmit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('calls "handleCancel" when Cancel button is clicked', () => {
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './HostForm';
|
||||
@ -1,8 +1,36 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import InventoryHostForm from '../shared/InventoryHostForm';
|
||||
import { InventoriesAPI } from '@api';
|
||||
|
||||
function InventoryHostAdd() {
|
||||
return <CardBody>Coming soon :)</CardBody>;
|
||||
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;
|
||||
|
||||
@ -0,0 +1,102 @@
|
||||
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 InventoryHostAdd from './InventoryHostAdd';
|
||||
import { InventoriesAPI } from '@api';
|
||||
|
||||
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 },
|
||||
});
|
||||
|
||||
const formik = wrapper.find('Formik').instance();
|
||||
await act(async () => {
|
||||
const changeState = new Promise(resolve => {
|
||||
formik.setState(
|
||||
{
|
||||
values: {
|
||||
...mockHostData,
|
||||
},
|
||||
},
|
||||
() => resolve()
|
||||
);
|
||||
});
|
||||
await changeState;
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('form').simulate('submit');
|
||||
});
|
||||
wrapper.update();
|
||||
expect(InventoriesAPI.createHost).toHaveBeenCalledWith('1', mockHostData);
|
||||
});
|
||||
|
||||
test('handleSubmit should throw an error', async () => {
|
||||
InventoriesAPI.createHost.mockImplementationOnce(() =>
|
||||
Promise.reject(new Error())
|
||||
);
|
||||
const formik = wrapper.find('Formik').instance();
|
||||
await act(async () => {
|
||||
const changeState = new Promise(resolve => {
|
||||
formik.setState(
|
||||
{
|
||||
values: {
|
||||
...mockHostData,
|
||||
},
|
||||
},
|
||||
() => resolve()
|
||||
);
|
||||
});
|
||||
await changeState;
|
||||
});
|
||||
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');
|
||||
});
|
||||
});
|
||||
@ -23,6 +23,7 @@ import { Host } from '@types';
|
||||
function InventoryHostItem(props) {
|
||||
const {
|
||||
detailUrl,
|
||||
editUrl,
|
||||
host,
|
||||
i18n,
|
||||
isSelected,
|
||||
@ -79,7 +80,7 @@ function InventoryHostItem(props) {
|
||||
<ListActionButton
|
||||
variant="plain"
|
||||
component={Link}
|
||||
to={`/hosts/${host.id}/edit`}
|
||||
to={`${editUrl}`}
|
||||
>
|
||||
<PencilAltIcon />
|
||||
</ListActionButton>
|
||||
|
||||
@ -178,7 +178,8 @@ function InventoryHosts({ i18n, location, match }) {
|
||||
<InventoryHostItem
|
||||
key={o.id}
|
||||
host={o}
|
||||
detailUrl={`/hosts/${o.id}/details`}
|
||||
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}
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { func, shape } from 'prop-types';
|
||||
import { Formik } from 'formik';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Form } from '@patternfly/react-core';
|
||||
import FormRow from '@components/FormRow';
|
||||
import FormField from '@components/FormField';
|
||||
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
|
||||
import { VariablesField } from '@components/CodeMirrorInput';
|
||||
import { required } from '@util/validators';
|
||||
|
||||
function InventoryHostForm({ handleSubmit, handleCancel, host, i18n }) {
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
name: host.name,
|
||||
description: host.description,
|
||||
variables: host.variables,
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
render={formik => (
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
<FormRow>
|
||||
<FormField
|
||||
id="host-name"
|
||||
name="name"
|
||||
type="text"
|
||||
label={i18n._(t`Name`)}
|
||||
validate={required(null, i18n)}
|
||||
isRequired
|
||||
/>
|
||||
<FormField
|
||||
id="host-description"
|
||||
name="description"
|
||||
type="text"
|
||||
label={i18n._(t`Description`)}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<VariablesField
|
||||
id="host-variables"
|
||||
name="variables"
|
||||
label={i18n._(t`Variables`)}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormActionGroup
|
||||
onCancel={handleCancel}
|
||||
onSubmit={formik.handleSubmit}
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
InventoryHostForm.propTypes = {
|
||||
handleSubmit: func.isRequired,
|
||||
handleCancel: func.isRequired,
|
||||
host: shape({}),
|
||||
};
|
||||
|
||||
InventoryHostForm.defaultProps = {
|
||||
host: {
|
||||
name: '',
|
||||
description: '',
|
||||
variables: '---\n',
|
||||
},
|
||||
};
|
||||
|
||||
export default withI18n()(InventoryHostForm);
|
||||
@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import { sleep } from '@testUtils/testUtils';
|
||||
import InventoryHostForm from './InventoryHostForm';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
describe('<InventoryHostform />', () => {
|
||||
let wrapper;
|
||||
|
||||
const handleSubmit = jest.fn();
|
||||
const handleCancel = jest.fn();
|
||||
|
||||
const mockHostData = {
|
||||
name: 'foo',
|
||||
description: 'bar',
|
||||
inventory: 1,
|
||||
variables: '---\nfoo: bar',
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventoryHostForm
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleSubmit}
|
||||
host={mockHostData}
|
||||
/>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('should display form fields', () => {
|
||||
expect(wrapper.find('FormGroup[label="Name"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1);
|
||||
expect(wrapper.find('VariablesField').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should call handleSubmit when Submit button is clicked', async () => {
|
||||
expect(handleSubmit).not.toHaveBeenCalled();
|
||||
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||
await sleep(1);
|
||||
expect(handleSubmit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should call handleCancel when Cancel button is clicked', async () => {
|
||||
expect(handleCancel).not.toHaveBeenCalled();
|
||||
wrapper.find('button[aria-label="Cancel"]').simulate('click');
|
||||
await sleep(1);
|
||||
expect(handleCancel).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user