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
|
||||
* Used to put into url bar when modal opens
|
||||
* @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
|
||||
*/
|
||||
export function toQueryString(config, searchParams = {}) {
|
||||
@ -54,7 +54,7 @@ export function toQueryString(config, searchParams = {}) {
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function toHostFilter(searchParams = {}) {
|
||||
|
@ -1,10 +1,120 @@
|
||||
import React, { Component } from 'react';
|
||||
import { PageSection } from '@patternfly/react-core';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
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 {
|
||||
render() {
|
||||
return <PageSection>Coming soon :)</PageSection>;
|
||||
function SmartInventoryEdit({ inventory }) {
|
||||
const history = useHistory();
|
||||
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;
|
||||
|
@ -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