mirror of
https://github.com/ansible/awx.git
synced 2026-01-16 12:20:45 -03:30
Merge pull request #6235 from marshmalien/6142-inv-group-add-host-form
Add Inventory Group Host Add form Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
commit
bbe5789e70
@ -5,10 +5,15 @@ class Groups extends Base {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/groups/';
|
||||
|
||||
this.createHost = this.createHost.bind(this);
|
||||
this.readAllHosts = this.readAllHosts.bind(this);
|
||||
this.disassociateHost = this.disassociateHost.bind(this);
|
||||
}
|
||||
|
||||
createHost(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/hosts/`, data);
|
||||
}
|
||||
|
||||
readAllHosts(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/all_hosts/`, { params });
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ class Inventories extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
setBreadCrumbConfig = (inventory, nestedResource) => {
|
||||
setBreadCrumbConfig = (inventory, nested) => {
|
||||
const { i18n } = this.props;
|
||||
if (!inventory) {
|
||||
return;
|
||||
@ -36,57 +36,42 @@ class Inventories extends Component {
|
||||
const inventoryKind =
|
||||
inventory.kind === 'smart' ? 'smart_inventory' : 'inventory';
|
||||
|
||||
const inventoryPath = `/inventories/${inventoryKind}/${inventory.id}`;
|
||||
const inventoryHostsPath = `/inventories/${inventoryKind}/${inventory.id}/hosts`;
|
||||
const inventoryGroupsPath = `/inventories/${inventoryKind}/${inventory.id}/groups`;
|
||||
|
||||
const breadcrumbConfig = {
|
||||
'/inventories': i18n._(t`Inventories`),
|
||||
'/inventories/inventory/add': i18n._(t`Create New Inventory`),
|
||||
'/inventories/smart_inventory/add': i18n._(t`Create New Smart Inventory`),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}`]: `${inventory.name}`,
|
||||
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/access`]: i18n._(
|
||||
t`Access`
|
||||
),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/completed_jobs`]: i18n._(
|
||||
[inventoryPath]: `${inventory.name}`,
|
||||
[`${inventoryPath}/access`]: i18n._(t`Access`),
|
||||
[`${inventoryPath}/completed_jobs`]: i18n._(t`Completed Jobs`),
|
||||
[`${inventoryPath}/details`]: i18n._(t`Details`),
|
||||
[`${inventoryPath}/edit`]: i18n._(t`Edit Details`),
|
||||
[`${inventoryPath}/sources`]: i18n._(t`Sources`),
|
||||
|
||||
[inventoryHostsPath]: i18n._(t`Hosts`),
|
||||
[`${inventoryHostsPath}/add`]: i18n._(t`Create New Host`),
|
||||
[`${inventoryHostsPath}/${nested?.id}`]: `${nested?.name}`,
|
||||
[`${inventoryHostsPath}/${nested?.id}/edit`]: i18n._(t`Edit Details`),
|
||||
[`${inventoryHostsPath}/${nested?.id}/details`]: i18n._(t`Host Details`),
|
||||
[`${inventoryHostsPath}/${nested?.id}/completed_jobs`]: i18n._(
|
||||
t`Completed Jobs`
|
||||
),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/details`]: i18n._(
|
||||
t`Details`
|
||||
),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/edit`]: i18n._(
|
||||
t`Edit Details`
|
||||
),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/groups`]: i18n._(
|
||||
t`Groups`
|
||||
),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts`]: i18n._(t`Hosts`),
|
||||
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/sources`]: i18n._(
|
||||
t`Sources`
|
||||
[inventoryGroupsPath]: i18n._(t`Groups`),
|
||||
[`${inventoryGroupsPath}/add`]: i18n._(t`Create New Group`),
|
||||
[`${inventoryGroupsPath}/${nested?.id}`]: `${nested?.name}`,
|
||||
[`${inventoryGroupsPath}/${nested?.id}/edit`]: i18n._(t`Edit Details`),
|
||||
[`${inventoryGroupsPath}/${nested?.id}/details`]: i18n._(
|
||||
t`Group Details`
|
||||
),
|
||||
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts/add`]: i18n._(
|
||||
[`${inventoryGroupsPath}/${nested?.id}/nested_hosts`]: i18n._(t`Hosts`),
|
||||
[`${inventoryGroupsPath}/${nested?.id}/nested_hosts/add`]: i18n._(
|
||||
t`Create New Host`
|
||||
),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
|
||||
nestedResource.id}`]: `${nestedResource && nestedResource.name}`,
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
|
||||
nestedResource.id}/edit`]: i18n._(t`Edit Details`),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
|
||||
nestedResource.id}/details`]: i18n._(t`Host Details`),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
|
||||
nestedResource.id}/completed_jobs`]: i18n._(t`Completed Jobs`),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/groups/add`]: i18n._(
|
||||
t`Create New Group`
|
||||
),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
|
||||
nestedResource.id}`]: `${nestedResource && nestedResource.name}`,
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
|
||||
nestedResource.id}/edit`]: i18n._(t`Edit Details`),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
|
||||
nestedResource.id}/details`]: i18n._(t`Group Details`),
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
|
||||
nestedResource.id}`]: `${nestedResource && nestedResource.name}`,
|
||||
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
|
||||
nestedResource.id}/nested_hosts`]: i18n._(t`Hosts`),
|
||||
};
|
||||
this.setState({ breadcrumbConfig });
|
||||
};
|
||||
|
||||
@ -59,70 +59,58 @@ function InventoryGroup({ i18n, setBreadcrumb, inventory }) {
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Details`),
|
||||
link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup &&
|
||||
inventoryGroup.id}/details`,
|
||||
link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup?.id}/details`,
|
||||
id: 0,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Related Groups`),
|
||||
link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup &&
|
||||
inventoryGroup.id}/nested_groups`,
|
||||
link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup?.id}/nested_groups`,
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Hosts`),
|
||||
link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup &&
|
||||
inventoryGroup.id}/nested_hosts`,
|
||||
link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup?.id}/nested_hosts`,
|
||||
id: 2,
|
||||
},
|
||||
];
|
||||
|
||||
// In cases where a user manipulates the url such that they try to navigate to a
|
||||
// Inventory Group that is not associated with the Inventory Id in the Url this
|
||||
// Content Error is thrown. Inventory Groups have a 1:1 relationship to Inventories
|
||||
// thus their Ids must corrolate.
|
||||
|
||||
if (contentLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
if (
|
||||
inventoryGroup.summary_fields.inventory.id !== parseInt(inventoryId, 10)
|
||||
) {
|
||||
return (
|
||||
<ContentError>
|
||||
{inventoryGroup && (
|
||||
<Link to={`/inventories/inventory/${inventory.id}/groups`}>
|
||||
{i18n._(t`View Inventory Groups`)}
|
||||
</Link>
|
||||
)}
|
||||
</ContentError>
|
||||
);
|
||||
}
|
||||
|
||||
if (contentError) {
|
||||
return <ContentError error={contentError} />;
|
||||
}
|
||||
|
||||
let cardHeader = null;
|
||||
// In cases where a user manipulates the url such that they try to navigate to a
|
||||
// Inventory Group that is not associated with the Inventory Id in the Url this
|
||||
// Content Error is thrown. Inventory Groups have a 1:1 relationship to Inventories
|
||||
// thus their Ids must corrolate.
|
||||
|
||||
if (
|
||||
location.pathname.includes('groups/') &&
|
||||
!location.pathname.endsWith('edit')
|
||||
inventoryGroup?.summary_fields?.inventory?.id !== parseInt(inventoryId, 10)
|
||||
) {
|
||||
cardHeader = (
|
||||
<TabbedCardHeader>
|
||||
<RoutedTabs tabsArray={tabsArray} />
|
||||
<CardActions>
|
||||
<CardCloseButton
|
||||
linkTo={`/inventories/inventory/${inventory.id}/groups`}
|
||||
/>
|
||||
</CardActions>
|
||||
</TabbedCardHeader>
|
||||
return (
|
||||
<ContentError isNotFound>
|
||||
<Link to={`/inventories/inventory/${inventory.id}/groups`}>
|
||||
{i18n._(t`View Inventory Groups`)}
|
||||
</Link>
|
||||
</ContentError>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{cardHeader}
|
||||
{['add', 'edit'].some(name => location.pathname.includes(name)) ? null : (
|
||||
<TabbedCardHeader>
|
||||
<RoutedTabs tabsArray={tabsArray} />
|
||||
<CardActions>
|
||||
<CardCloseButton
|
||||
linkTo={`/inventories/inventory/${inventory.id}/groups`}
|
||||
/>
|
||||
</CardActions>
|
||||
</TabbedCardHeader>
|
||||
)}
|
||||
<Switch>
|
||||
<Redirect
|
||||
from="/inventories/inventory/:id/groups/:groupId"
|
||||
@ -139,17 +127,16 @@ function InventoryGroup({ i18n, setBreadcrumb, inventory }) {
|
||||
<Route
|
||||
key="details"
|
||||
path="/inventories/inventory/:id/groups/:groupId/details"
|
||||
render={() => {
|
||||
return <InventoryGroupDetail inventoryGroup={inventoryGroup} />;
|
||||
}}
|
||||
/>,
|
||||
>
|
||||
<InventoryGroupDetail inventoryGroup={inventoryGroup} />
|
||||
</Route>,
|
||||
<Route
|
||||
key="hosts"
|
||||
path="/inventories/inventory/:id/groups/:groupId/nested_hosts"
|
||||
>
|
||||
<InventoryGroupHosts inventoryGroup={inventoryGroup} />
|
||||
</Route>,
|
||||
]}
|
||||
<Route
|
||||
key="hosts"
|
||||
path="/inventories/inventory/:id/groups/:groupId/nested_hosts"
|
||||
>
|
||||
<InventoryGroupHosts />
|
||||
</Route>
|
||||
<Route
|
||||
key="not-found"
|
||||
path="*"
|
||||
|
||||
@ -8,6 +8,14 @@ import { createMemoryHistory } from 'history';
|
||||
import InventoryGroup from './InventoryGroup';
|
||||
|
||||
jest.mock('@api');
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: () => ({
|
||||
id: 1,
|
||||
groupId: 2,
|
||||
}),
|
||||
}));
|
||||
|
||||
GroupsAPI.readDetail.mockResolvedValue({
|
||||
data: {
|
||||
id: 1,
|
||||
@ -23,10 +31,12 @@ GroupsAPI.readDetail.mockResolvedValue({
|
||||
modified: '2020-04-25T01:23:45.678901Z',
|
||||
},
|
||||
});
|
||||
|
||||
describe('<InventoryGroup />', () => {
|
||||
let wrapper;
|
||||
let history;
|
||||
const inventory = { id: 1, name: 'Foo' };
|
||||
|
||||
beforeEach(async () => {
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/inventory/1/groups/1/details'],
|
||||
@ -39,29 +49,20 @@ describe('<InventoryGroup />', () => {
|
||||
<InventoryGroup setBreadcrumb={() => {}} inventory={inventory} />
|
||||
)}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
router: {
|
||||
history,
|
||||
route: {
|
||||
location: history.location,
|
||||
match: {
|
||||
params: { id: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
{ context: { router: { history } } }
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('renders successfully', async () => {
|
||||
expect(wrapper.length).toBe(1);
|
||||
});
|
||||
|
||||
test('expect all tabs to exist, including Back to Groups', async () => {
|
||||
expect(
|
||||
wrapper.find('button[link="/inventories/inventory/1/groups"]').length
|
||||
@ -70,4 +71,27 @@ describe('<InventoryGroup />', () => {
|
||||
expect(wrapper.find('button[aria-label="Related Groups"]').length).toBe(1);
|
||||
expect(wrapper.find('button[aria-label="Hosts"]').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should show content error when user attempts to navigate to erroneous route', async () => {
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/inventory/1/groups/1/foobar'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventoryGroup setBreadcrumb={() => {}} inventory={inventory} />,
|
||||
{ context: { router: { history } } }
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
||||
|
||||
test('should show content error when api throws error on initial render', async () => {
|
||||
GroupsAPI.readDetail.mockImplementationOnce(() =>
|
||||
Promise.reject(new Error())
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryGroup inventory={inventory} />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { CardBody } from '@components/Card';
|
||||
import HostForm from '@components/HostForm';
|
||||
|
||||
import { GroupsAPI } from '@api';
|
||||
|
||||
function InventoryGroupHostAdd({ inventoryGroup }) {
|
||||
const [formError, setFormError] = useState(null);
|
||||
const baseUrl = `/inventories/inventory/${inventoryGroup.inventory}`;
|
||||
const history = useHistory();
|
||||
|
||||
const handleSubmit = async formData => {
|
||||
try {
|
||||
const values = {
|
||||
...formData,
|
||||
inventory: inventoryGroup.inventory,
|
||||
};
|
||||
|
||||
const { data: response } = await GroupsAPI.createHost(
|
||||
inventoryGroup.id,
|
||||
values
|
||||
);
|
||||
history.push(`${baseUrl}/hosts/${response.id}/details`);
|
||||
} catch (error) {
|
||||
setFormError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
history.push(`${baseUrl}/groups/${inventoryGroup.id}/nested_hosts`);
|
||||
};
|
||||
|
||||
return (
|
||||
<CardBody>
|
||||
<HostForm
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={handleCancel}
|
||||
isInventoryVisible={false}
|
||||
submitError={formError}
|
||||
/>
|
||||
</CardBody>
|
||||
);
|
||||
}
|
||||
|
||||
export default InventoryGroupHostAdd;
|
||||
@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import InventoryGroupHostAdd from './InventoryGroupHostAdd';
|
||||
import mockHost from '../shared/data.host.json';
|
||||
import { GroupsAPI } from '@api';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
GroupsAPI.createHost.mockResolvedValue({
|
||||
data: {
|
||||
...mockHost,
|
||||
},
|
||||
});
|
||||
|
||||
describe('<InventoryGroupHostAdd />', () => {
|
||||
let wrapper;
|
||||
let history;
|
||||
|
||||
beforeAll(async () => {
|
||||
history = createMemoryHistory();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventoryGroupHostAdd inventoryGroup={{ id: 123, inventory: 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(GroupsAPI.createHost).toHaveBeenCalledWith(123, mockHost);
|
||||
});
|
||||
|
||||
test('should navigate to inventory group host list when cancel is clicked', () => {
|
||||
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
|
||||
expect(history.location.pathname).toEqual(
|
||||
'/inventories/inventory/3/groups/123/nested_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' },
|
||||
},
|
||||
};
|
||||
GroupsAPI.createHost.mockImplementationOnce(() => Promise.reject(error));
|
||||
await act(async () => {
|
||||
wrapper.find('HostForm').invoke('handleSubmit')(mockHost);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('FormSubmitError').length).toBe(1);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './InventoryGroupHostAdd';
|
||||
@ -1,11 +1,14 @@
|
||||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import InventoryGroupHostAdd from '../InventoryGroupHostAdd';
|
||||
import InventoryGroupHostList from './InventoryGroupHostList';
|
||||
|
||||
function InventoryGroupHosts() {
|
||||
function InventoryGroupHosts({ inventoryGroup }) {
|
||||
return (
|
||||
<Switch>
|
||||
{/* Route to InventoryGroupHostAddForm */}
|
||||
<Route path="/inventories/inventory/:id/groups/:groupId/nested_hosts/add">
|
||||
<InventoryGroupHostAdd inventoryGroup={inventoryGroup} />
|
||||
</Route>
|
||||
<Route path="/inventories/inventory/:id/groups/:groupId/nested_hosts">
|
||||
<InventoryGroupHostList />
|
||||
</Route>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user