-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1f1d0c3
commit 92e5124
Showing
3 changed files
with
277 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 115 additions & 5 deletions
120
awx/ui_next/src/screens/Inventory/SmartInventoryEdit/SmartInventoryEdit.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
160 changes: 160 additions & 0 deletions
160
awx/ui_next/src/screens/Inventory/SmartInventoryEdit/SmartInventoryEdit.test.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
); | ||
}); | ||
}); |