Add InventoryHostAdd route file

This commit is contained in:
Marliana Lara 2020-03-06 00:51:55 -05:00
parent b560a21ca3
commit da94b2dc9e
No known key found for this signature in database
GPG Key ID: 38C73B40DFA809EE
13 changed files with 350 additions and 113 deletions

View File

@ -1,22 +1,19 @@
import React, { useState } from 'react';
import { func, shape } from 'prop-types';
import { useRouteMatch } from 'react-router-dom';
import { bool, func, shape } from 'prop-types';
import { Formik, useField } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Form, FormGroup } from '@patternfly/react-core';
import FormField, {
FormSubmitError,
FieldTooltip,
} from '@components/FormField';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
import { VariablesField } from '@components/CodeMirrorInput';
import { required } from '@util/validators';
import { InventoryLookup } from '@components/Lookup';
import { FormColumnLayout, FormFullWidthLayout } from '@components/FormLayout';
import { required } from '@util/validators';
const InventoryLookupField = withI18n()(({ i18n, host }) => {
const [inventory, setInventory] = useState(
@ -57,9 +54,14 @@ const InventoryLookupField = withI18n()(({ i18n, host }) => {
);
});
const HostForm = ({ handleCancel, handleSubmit, host, i18n, submitError }) => {
const hostAddMatch = useRouteMatch('/hosts/add');
const HostForm = ({
handleCancel,
handleSubmit,
host,
isInventoryVisible,
i18n,
submitError,
}) => {
return (
<Formik
initialValues={{
@ -87,7 +89,7 @@ const HostForm = ({ handleCancel, handleSubmit, host, i18n, submitError }) => {
type="text"
label={i18n._(t`Description`)}
/>
{hostAddMatch && <InventoryLookupField host={host} />}
{isInventoryVisible && <InventoryLookupField host={host} />}
<FormFullWidthLayout>
<VariablesField
id="host-variables"
@ -95,7 +97,7 @@ const HostForm = ({ handleCancel, handleSubmit, host, i18n, submitError }) => {
label={i18n._(t`Variables`)}
/>
</FormFullWidthLayout>
<FormSubmitError error={submitError} />
{submitError && <FormSubmitError error={submitError} />}
<FormActionGroup
onCancel={handleCancel}
onSubmit={formik.handleSubmit}
@ -111,6 +113,7 @@ HostForm.propTypes = {
handleCancel: func.isRequired,
handleSubmit: func.isRequired,
host: shape({}),
isInventoryVisible: bool,
submitError: shape({}),
};
@ -124,6 +127,7 @@ HostForm.defaultProps = {
inventory: null,
},
},
isInventoryVisible: true,
submitError: null,
};

View File

@ -6,43 +6,43 @@ import HostForm from './HostForm';
jest.mock('@api');
const mockData = {
id: 1,
name: 'Foo',
description: 'Bar',
variables: '---',
inventory: 1,
summary_fields: {
inventory: {
id: 1,
name: 'Test Inv',
},
},
};
describe('<HostForm />', () => {
const meConfig = {
me: {
is_superuser: false,
},
};
const mockData = {
id: 1,
name: 'Foo',
description: 'Bar',
variables: '---',
inventory: 1,
summary_fields: {
inventory: {
id: 1,
name: 'Test Inv',
},
},
};
let wrapper;
const handleSubmit = jest.fn();
const handleCancel = jest.fn();
afterEach(() => {
jest.clearAllMocks();
});
test('changing inputs should update form values', async () => {
let wrapper;
beforeEach(async () => {
await act(async () => {
wrapper = mountWithContexts(
<HostForm
host={mockData}
handleSubmit={jest.fn()}
handleCancel={jest.fn()}
me={meConfig.me}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
/>
);
});
});
afterEach(() => {
jest.clearAllMocks();
wrapper.unmount();
});
test('changing inputs should update form values', async () => {
await act(async () => {
wrapper.find('input#host-name').simulate('change', {
target: { value: 'new foo', name: 'name' },
@ -59,35 +59,30 @@ describe('<HostForm />', () => {
});
test('calls handleSubmit when form submitted', async () => {
const handleSubmit = jest.fn();
const wrapper = mountWithContexts(
<HostForm
host={mockData}
handleSubmit={handleSubmit}
handleCancel={jest.fn()}
me={meConfig.me}
/>
);
expect(handleSubmit).not.toHaveBeenCalled();
await act(async () => {
wrapper.find('button[aria-label="Save"]').simulate('click');
});
expect(handleSubmit).toHaveBeenCalled();
expect(handleSubmit).toHaveBeenCalledTimes(1);
});
test('calls "handleCancel" when Cancel button is clicked', () => {
const handleCancel = jest.fn();
const wrapper = mountWithContexts(
<HostForm
host={mockData}
handleSubmit={jest.fn()}
handleCancel={handleCancel}
me={meConfig.me}
/>
);
expect(handleCancel).not.toHaveBeenCalled();
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
expect(handleCancel).toBeCalled();
expect(handleCancel).toHaveBeenCalledTimes(1);
});
test('should hide inventory lookup field', async () => {
await act(async () => {
wrapper = mountWithContexts(
<HostForm
host={mockData}
handleSubmit={jest.fn()}
handleCancel={jest.fn()}
isInventoryVisible={false}
/>
);
});
expect(wrapper.find('InventoryLookupField').length).toBe(0);
});
});

View File

@ -1,34 +1,24 @@
import React, { useState } from 'react';
import { useHistory, useRouteMatch } from 'react-router-dom';
import { useHistory } from 'react-router-dom';
import { CardBody } from '@components/Card';
import HostForm from '@components/HostForm';
import { HostsAPI } from '@api';
import HostForm from '../shared';
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,
};
try {
const { data: response } = await HostsAPI.create(values);
history.push(`${url}/${response.id}/details`);
const { data: response } = await HostsAPI.create(formData);
history.push(`/hosts/${response.id}/details`);
} catch (error) {
setFormError(error);
}
};
const handleCancel = () => {
history.push(`${url}`);
history.push(`/hosts`);
};
return (

View File

@ -1,27 +1,32 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import HostAdd from './HostAdd';
import { HostsAPI } from '@api';
jest.mock('@api');
const hostData = {
name: 'new name',
description: 'new description',
inventory: 1,
variables: '---\nfoo: bar',
};
HostsAPI.create.mockResolvedValue({
data: {
...hostData,
id: 5,
},
});
describe('<HostAdd />', () => {
let wrapper;
let history;
const hostData = {
name: 'new name',
description: 'new description',
inventory: 1,
variables: '---\nfoo: bar',
};
beforeEach(async () => {
history = createMemoryHistory({
initialEntries: ['/hosts/add'],
});
history = createMemoryHistory();
await act(async () => {
wrapper = mountWithContexts(<HostAdd />, {
context: { router: { history } },
@ -29,13 +34,12 @@ describe('<HostAdd />', () => {
});
});
afterEach(() => {
jest.clearAllMocks();
wrapper.unmount();
});
test('handleSubmit should post to api', async () => {
HostsAPI.create.mockResolvedValueOnce({
data: {
...hostData,
id: 5,
},
});
await act(async () => {
wrapper.find('HostForm').prop('handleSubmit')(hostData);
});
@ -43,21 +47,31 @@ describe('<HostAdd />', () => {
});
test('should navigate to hosts list when cancel is clicked', async () => {
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
await act(async () => {
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
});
expect(history.location.pathname).toEqual('/hosts');
});
test('successful form submission should trigger redirect', async () => {
HostsAPI.create.mockResolvedValueOnce({
data: {
...hostData,
id: 5,
},
});
await waitForElement(wrapper, 'button[aria-label="Save"]');
await act(async () => {
wrapper.find('HostForm').invoke('handleSubmit')(hostData);
});
expect(wrapper.find('FormSubmitError').length).toBe(0);
expect(history.location.pathname).toEqual('/hosts/5/details');
});
test('failed form submission should show an error message', async () => {
const error = {
response: {
data: { detail: 'An error occurred' },
},
};
HostsAPI.create.mockImplementationOnce(() => Promise.reject(error));
await act(async () => {
wrapper.find('HostForm').invoke('handleSubmit')(hostData);
});
wrapper.update();
expect(wrapper.find('FormSubmitError').length).toBe(1);
});
});

View File

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useHistory, useRouteMatch } from 'react-router-dom';
import { CardBody } from '@components/Card';
import HostForm from '@components/HostForm';
import { HostsAPI } from '@api';
import HostForm from '../shared';

View File

@ -30,4 +30,23 @@ describe('<Hosts />', () => {
expect(wrapper.find('BreadcrumbHeading').length).toBe(1);
wrapper.unmount();
});
test('should render Host component', () => {
const history = createMemoryHistory({
initialEntries: ['/hosts/1'],
});
const match = {
path: '/hosts/:id',
url: '/hosts/1',
isExact: true,
};
const wrapper = mountWithContexts(<Hosts />, {
context: { router: { history, route: { match } } },
});
expect(wrapper.find('Host').length).toBe(1);
wrapper.unmount();
});
});

View File

@ -0,0 +1,42 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import { CardBody } from '@components/Card';
import HostForm from '@components/HostForm';
import { HostsAPI } from '@api';
function InventoryHostAdd({ inventory }) {
const [formError, setFormError] = useState(null);
const hostsUrl = `/inventories/inventory/${inventory.id}/hosts`;
const history = useHistory();
const handleSubmit = async formData => {
try {
const values = {
...formData,
inventory: inventory.id,
};
const { data: response } = await HostsAPI.create(values);
history.push(`${hostsUrl}/${response.id}/details`);
} catch (error) {
setFormError(error);
}
};
const handleCancel = () => {
history.push(hostsUrl);
};
return (
<CardBody>
<HostForm
handleSubmit={handleSubmit}
handleCancel={handleCancel}
isInventoryVisible={false}
submitError={formError}
/>
</CardBody>
);
}
export default InventoryHostAdd;

View File

@ -0,0 +1,70 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import InventoryHostAdd from './InventoryHostAdd';
import mockHost from '../shared/data.host.json';
import { HostsAPI } from '@api';
jest.mock('@api');
HostsAPI.create.mockResolvedValue({
data: {
...mockHost,
},
});
describe('<InventoryHostAdd />', () => {
let wrapper;
let history;
beforeAll(async () => {
history = createMemoryHistory();
await act(async () => {
wrapper = mountWithContexts(<InventoryHostAdd inventory={{ id: 3 }} />, {
context: { router: { history } },
});
});
});
afterAll(() => {
jest.clearAllMocks();
wrapper.unmount();
});
test('handleSubmit should post to api', async () => {
await act(async () => {
wrapper.find('HostForm').prop('handleSubmit')(mockHost);
});
expect(HostsAPI.create).toHaveBeenCalledWith(mockHost);
});
test('should navigate to hosts list when cancel is clicked', () => {
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
expect(history.location.pathname).toEqual('/inventories/inventory/3/hosts');
});
test('successful form submission should trigger redirect', async () => {
await act(async () => {
wrapper.find('HostForm').invoke('handleSubmit')(mockHost);
});
expect(wrapper.find('FormSubmitError').length).toBe(0);
expect(history.location.pathname).toEqual(
'/inventories/inventory/3/hosts/2/details'
);
});
test('failed form submission should show an error message', async () => {
const error = {
response: {
data: { detail: 'An error occurred' },
},
};
HostsAPI.create.mockImplementationOnce(() => Promise.reject(error));
await act(async () => {
wrapper.find('HostForm').invoke('handleSubmit')(mockHost);
});
wrapper.update();
expect(wrapper.find('FormSubmitError').length).toBe(1);
});
});

View File

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

View File

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

View File

@ -0,0 +1,25 @@
import React from 'react';
import { createMemoryHistory } from 'history';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import InventoryHosts from './InventoryHosts';
describe('<InventoryHosts />', () => {
test('should render inventory host list', () => {
const history = createMemoryHistory({
initialEntries: ['/inventories/inventory/1/hosts'],
});
const match = {
path: '/inventories/inventory/:id/hosts',
url: '/inventories/inventory/1/hosts',
isExact: true,
};
const wrapper = mountWithContexts(<InventoryHosts />, {
context: { router: { history, route: { match } } },
});
expect(wrapper.find('InventoryHostList').length).toBe(1);
wrapper.unmount();
});
});

View File

@ -0,0 +1,86 @@
{
"id": 2,
"type": "host",
"url": "/api/v2/hosts/2/",
"related": {
"created_by": "/api/v2/users/1/",
"modified_by": "/api/v2/users/1/",
"variable_data": "/api/v2/hosts/2/variable_data/",
"groups": "/api/v2/hosts/2/groups/",
"all_groups": "/api/v2/hosts/2/all_groups/",
"job_events": "/api/v2/hosts/2/job_events/",
"job_host_summaries": "/api/v2/hosts/2/job_host_summaries/",
"activity_stream": "/api/v2/hosts/2/activity_stream/",
"inventory_sources": "/api/v2/hosts/2/inventory_sources/",
"smart_inventories": "/api/v2/hosts/2/smart_inventories/",
"ad_hoc_commands": "/api/v2/hosts/2/ad_hoc_commands/",
"ad_hoc_command_events": "/api/v2/hosts/2/ad_hoc_command_events/",
"insights": "/api/v2/hosts/2/insights/",
"ansible_facts": "/api/v2/hosts/2/ansible_facts/",
"inventory": "/api/v2/inventories/3/",
"last_job": "/api/v2/jobs/3/",
"last_job_host_summary": "/api/v2/job_host_summaries/1/"
},
"summary_fields": {
"inventory": {
"id": 3,
"name": "Mikes Inventory",
"description": "",
"has_active_failures": false,
"total_hosts": 3,
"hosts_with_active_failures": 0,
"total_groups": 0,
"groups_with_active_failures": 0,
"has_inventory_sources": true,
"total_inventory_sources": 1,
"inventory_sources_with_failures": 0,
"organization_id": 3,
"kind": ""
},
"last_job": {
"id": 3,
"name": "Ping",
"description": "",
"finished": "2019-10-28T21:29:08.880572Z",
"status": "successful",
"failed": false,
"job_template_id": 9,
"job_template_name": "Ping"
},
"last_job_host_summary": {
"id": 1,
"failed": false
},
"user_capabilities": {
"edit": true,
"delete": true
},
"groups": {
"count": 0,
"results": []
},
"recent_jobs": [
{
"id": 3,
"name": "Ping",
"status": "successful",
"finished": "2019-10-28T21:29:08.880572Z",
"type": "job"
}
]
},
"created": "2019-10-28T21:26:54.508081Z",
"modified": "2019-10-29T20:18:41.915796Z",
"name": "localhost",
"description": "localhost description",
"inventory": 3,
"enabled": true,
"instance_id": "",
"variables": "---\nansible_connection: local",
"has_active_failures": false,
"has_inventory_sources": false,
"last_job": 3,
"last_job_host_summary": 1,
"insights_system_id": null,
"ansible_facts_modified": null
}