mirror of
https://github.com/ansible/awx.git
synced 2024-10-30 22:21:13 +03:00
Add smart inventory edit form
This commit is contained in:
parent
6a304dce55
commit
8e6d475a9d
@ -29,7 +29,7 @@ export function toSearchParams(string = '') {
|
|||||||
* Convert params object to an encoded namespaced url query string
|
* Convert params object to an encoded namespaced url query string
|
||||||
* Used to put into url bar when modal opens
|
* Used to put into url bar when modal opens
|
||||||
* @param {object} config Config object for namespacing params
|
* @param {object} config Config object for namespacing params
|
||||||
* @param {object} obj A string or array of strings keyed by query param key
|
* @param {object} searchParams A string or array of strings keyed by query param key
|
||||||
* @return {string} URL query string
|
* @return {string} URL query string
|
||||||
*/
|
*/
|
||||||
export function toQueryString(config, searchParams = {}) {
|
export function toQueryString(config, searchParams = {}) {
|
||||||
@ -54,7 +54,7 @@ export function toQueryString(config, searchParams = {}) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert params object to host filter string
|
* Convert params object to host filter string
|
||||||
* @param {object} obj A string or array of strings keyed by query param key
|
* @param {object} searchParams A string or array of strings keyed by query param key
|
||||||
* @return {string} Host filter string
|
* @return {string} Host filter string
|
||||||
*/
|
*/
|
||||||
export function toHostFilter(searchParams = {}) {
|
export function toHostFilter(searchParams = {}) {
|
||||||
|
@ -1,10 +1,120 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { PageSection } from '@patternfly/react-core';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { Inventory } from '../../../types';
|
||||||
|
import { getAddedAndRemoved } from '../../../util/lists';
|
||||||
|
import useRequest from '../../../util/useRequest';
|
||||||
|
import { InventoriesAPI } from '../../../api';
|
||||||
|
import { CardBody } from '../../../components/Card';
|
||||||
|
import ContentError from '../../../components/ContentError';
|
||||||
|
import ContentLoading from '../../../components/ContentLoading';
|
||||||
|
import SmartInventoryForm from '../shared/SmartInventoryForm';
|
||||||
|
|
||||||
class SmartInventoryEdit extends Component {
|
function SmartInventoryEdit({ inventory }) {
|
||||||
render() {
|
const history = useHistory();
|
||||||
return <PageSection>Coming soon :)</PageSection>;
|
const detailsUrl = `/inventories/smart_inventory/${inventory.id}/details`;
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: contentError,
|
||||||
|
isLoading: hasContentLoading,
|
||||||
|
request: fetchInstanceGroups,
|
||||||
|
result: instanceGroups,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const {
|
||||||
|
data: { results },
|
||||||
|
} = await InventoriesAPI.readInstanceGroups(inventory.id);
|
||||||
|
return results;
|
||||||
|
}, [inventory.id]),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchInstanceGroups();
|
||||||
|
}, [fetchInstanceGroups]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: submitError,
|
||||||
|
request: submitRequest,
|
||||||
|
result: submitResult,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(
|
||||||
|
async (values, groupsToAssociate, groupsToDisassociate) => {
|
||||||
|
const { data } = await InventoriesAPI.update(inventory.id, values);
|
||||||
|
await Promise.all(
|
||||||
|
groupsToAssociate.map(id =>
|
||||||
|
InventoriesAPI.associateInstanceGroup(inventory.id, id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await Promise.all(
|
||||||
|
groupsToDisassociate.map(id =>
|
||||||
|
InventoriesAPI.disassociateInstanceGroup(inventory.id, id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
[inventory.id]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (submitResult) {
|
||||||
|
history.push({
|
||||||
|
pathname: detailsUrl,
|
||||||
|
search: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [submitResult, detailsUrl, history]);
|
||||||
|
|
||||||
|
const handleSubmit = async form => {
|
||||||
|
const { instance_groups, organization, ...remainingForm } = form;
|
||||||
|
|
||||||
|
const { added, removed } = getAddedAndRemoved(
|
||||||
|
instanceGroups,
|
||||||
|
instance_groups
|
||||||
|
);
|
||||||
|
const addedIds = added.map(({ id }) => id);
|
||||||
|
const removedIds = removed.map(({ id }) => id);
|
||||||
|
|
||||||
|
await submitRequest(
|
||||||
|
{
|
||||||
|
organization: organization?.id,
|
||||||
|
...remainingForm,
|
||||||
|
},
|
||||||
|
addedIds,
|
||||||
|
removedIds
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
history.push({
|
||||||
|
pathname: detailsUrl,
|
||||||
|
search: '',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (hasContentLoading) {
|
||||||
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (contentError) {
|
||||||
|
return <ContentError error={contentError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardBody>
|
||||||
|
<SmartInventoryForm
|
||||||
|
inventory={inventory}
|
||||||
|
instanceGroups={instanceGroups}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
submitError={submitError}
|
||||||
|
/>
|
||||||
|
</CardBody>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SmartInventoryEdit.propTypes = {
|
||||||
|
inventory: Inventory.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default SmartInventoryEdit;
|
export default SmartInventoryEdit;
|
||||||
|
@ -0,0 +1,160 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import SmartInventoryEdit from './SmartInventoryEdit';
|
||||||
|
import mockSmartInventory from '../shared/data.smart_inventory.json';
|
||||||
|
import {
|
||||||
|
InventoriesAPI,
|
||||||
|
OrganizationsAPI,
|
||||||
|
InstanceGroupsAPI,
|
||||||
|
} from '../../../api';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useParams: () => ({
|
||||||
|
id: 2,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
jest.mock('../../../api/models/Inventories');
|
||||||
|
jest.mock('../../../api/models/Organizations');
|
||||||
|
jest.mock('../../../api/models/InstanceGroups');
|
||||||
|
OrganizationsAPI.read.mockResolvedValue({ data: { results: [], count: 0 } });
|
||||||
|
InstanceGroupsAPI.read.mockResolvedValue({ data: { results: [], count: 0 } });
|
||||||
|
|
||||||
|
const mockSmartInv = Object.assign(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
...mockSmartInventory,
|
||||||
|
organization: {
|
||||||
|
id: mockSmartInventory.organization,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('<SmartInventoryEdit />', () => {
|
||||||
|
let history;
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
InventoriesAPI.associateInstanceGroup.mockResolvedValue();
|
||||||
|
InventoriesAPI.disassociateInstanceGroup.mockResolvedValue();
|
||||||
|
InventoriesAPI.update.mockResolvedValue({ data: mockSmartInv });
|
||||||
|
InventoriesAPI.readOptions.mockResolvedValue({
|
||||||
|
data: { actions: { POST: true } },
|
||||||
|
});
|
||||||
|
InventoriesAPI.readInstanceGroups.mockResolvedValue({
|
||||||
|
data: { count: 0, results: [{ id: 10 }, { id: 20 }] },
|
||||||
|
});
|
||||||
|
history = createMemoryHistory({
|
||||||
|
initialEntries: [`/inventories/smart_inventory/${mockSmartInv.id}/edit`],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SmartInventoryEdit inventory={{ ...mockSmartInv }} />,
|
||||||
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fetch related instance groups on initial render', async () => {
|
||||||
|
expect(InventoriesAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('save button should be enabled for users with POST capability', () => {
|
||||||
|
expect(wrapper.find('Button[aria-label="Save"]').prop('isDisabled')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should post to the api when submit is clicked', async () => {
|
||||||
|
expect(InventoriesAPI.update).toHaveBeenCalledTimes(0);
|
||||||
|
expect(InventoriesAPI.associateInstanceGroup).toHaveBeenCalledTimes(0);
|
||||||
|
expect(InventoriesAPI.disassociateInstanceGroup).toHaveBeenCalledTimes(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('SmartInventoryForm').invoke('onSubmit')({
|
||||||
|
...mockSmartInv,
|
||||||
|
instance_groups: [{ id: 10 }, { id: 30 }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(InventoriesAPI.update).toHaveBeenCalledTimes(1);
|
||||||
|
expect(InventoriesAPI.associateInstanceGroup).toHaveBeenCalledTimes(1);
|
||||||
|
expect(InventoriesAPI.disassociateInstanceGroup).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('successful form submission should trigger redirect to details', async () => {
|
||||||
|
expect(wrapper.find('FormSubmitError').length).toBe(0);
|
||||||
|
expect(history.location.pathname).toEqual(
|
||||||
|
'/inventories/smart_inventory/2/details'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to inventory details when cancel is clicked', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
|
||||||
|
});
|
||||||
|
expect(history.location.pathname).toEqual(
|
||||||
|
'/inventories/smart_inventory/2/details'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unsuccessful form submission should show an error message', async () => {
|
||||||
|
const error = {
|
||||||
|
response: {
|
||||||
|
data: { detail: 'An error occurred' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
InventoriesAPI.update.mockImplementationOnce(() => Promise.reject(error));
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SmartInventoryEdit inventory={{ ...mockSmartInv }} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(wrapper.find('FormSubmitError').length).toBe(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('SmartInventoryForm').invoke('onSubmit')({});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('FormSubmitError').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw content error', async () => {
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(0);
|
||||||
|
InventoriesAPI.readInstanceGroups.mockImplementationOnce(() =>
|
||||||
|
Promise.reject(new Error())
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SmartInventoryEdit inventory={{ ...mockSmartInv }} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('save button should be disabled for users without POST capability', async () => {
|
||||||
|
InventoriesAPI.readOptions.mockResolvedValue({
|
||||||
|
data: { actions: { POST: false } },
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SmartInventoryEdit inventory={{ ...mockSmartInv }} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Save"]').prop('isDisabled')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user