From c997fcfc2cdf48b19cc9a19a075c118a73c80ac7 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Fri, 15 Nov 2019 16:57:56 -0500 Subject: [PATCH 1/9] Adds Inventory Groups routing --sort of Adds Inventory Groups Add Adds Inventory Groups Edit Adds Inventory Groups Form Adds api module for Groups Adds placeholder file for InventoryGroupsList. This was added to refine routing. Tgere are no tests for this file yet. --- .../src/screens/Inventory/Inventories.jsx | 9 +- .../src/screens/Inventory/Inventory.jsx | 9 +- .../InventoryGroup/InventoryGroup.jsx | 87 ++++++ .../InventoryGroups/InventoryGroup/index.js | 1 + .../InventoryGroupAdd/InventoryGroupAdd.jsx | 36 +++ .../InventoryGroupAdd.test.jsx | 60 ++++ .../InventoryGroupAdd/index.js | 1 + .../InventoryGroupDetail.jsx | 86 ++++++ .../InventoryGroupDetail.test.jsx | 78 +++++ .../InventoryGroupDetail/index.js | 1 + .../InventoryGroupEdit/InventoryGroupEdit.jsx | 33 +++ .../InventroyGroupEdit.test.jsx | 64 ++++ .../InventoryGroupEdit/index.js | 1 + .../InventoryGroupForm/InventoryGroupForm.jsx | 78 +++++ .../InventoryGroupForm.test.jsx | 34 +++ .../InventoryGroupForm/index.js | 1 + .../InventoryGroups/InventoryGroupItem.jsx | 2 +- .../InventoryGroups/InventoryGroups.jsx | 278 +++--------------- .../InventoryGroups/InventoryGroupsList.jsx | 248 ++++++++++++++++ 19 files changed, 865 insertions(+), 242 deletions(-) create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/index.js create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.test.jsx create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/index.js create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.test.jsx create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/index.js create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventroyGroupEdit.test.jsx create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/index.js create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.jsx create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.test.jsx create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/index.js create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx diff --git a/awx/ui_next/src/screens/Inventory/Inventories.jsx b/awx/ui_next/src/screens/Inventory/Inventories.jsx index 1d72507c03..8e48139bc9 100644 --- a/awx/ui_next/src/screens/Inventory/Inventories.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventories.jsx @@ -27,7 +27,7 @@ class Inventories extends Component { }; } - setBreadCrumbConfig = inventory => { + setBreadCrumbConfig = (inventory, group) => { const { i18n } = this.props; if (!inventory) { return; @@ -57,6 +57,13 @@ class Inventories extends Component { ), [`/inventories/inventory/${inventory.id}/sources`]: i18n._(t`Sources`), [`/inventories/inventory/${inventory.id}/groups`]: i18n._(t`Groups`), + [`/inventories/inventory/${inventory.id}/groups/add`]: i18n._( + t`Create New Group` + ), + [`/inventories/inventory/${inventory.id}/groups/${group && + group.id}/details`]: i18n._(t`Details`), + [`/inventories/inventory/${inventory.id}/groups/${group && + group.id}/edit`]: `${group && group.name}`, }; this.setState({ breadcrumbConfig }); }; diff --git a/awx/ui_next/src/screens/Inventory/Inventory.jsx b/awx/ui_next/src/screens/Inventory/Inventory.jsx index 5352b949a4..da49aa6d8e 100644 --- a/awx/ui_next/src/screens/Inventory/Inventory.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventory.jsx @@ -123,7 +123,14 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) { } + render={() => ( + + )} />, { + const loadData = async () => { + try { + const { data } = await GroupsAPI.readDetail(match.params.groupId); + setInventoryGroup(data); + setBreadcrumb(inventory, data); + } catch (err) { + setHasContentError(err); + } finally { + setContentLoading(false); + } + }; + + loadData(); + }, [match.params.groupId, setBreadcrumb, inventory]); + + if (hasContentError) { + return ; + } + if (hasContentLoading) { + return ; + } + return ( + + + {inventoryGroup && [ + { + return ; + }} + />, + { + return ; + }} + />, + + !hasContentLoading && ( + + {match.params.id && ( + + {i18n._(t`View Inventory Details`)} + + )} + + ) + } + />, + ]} + + ); +} + +export { InventoryGroups as _InventoryGroups }; +export default withI18n()(withRouter(InventoryGroups)); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/index.js b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/index.js new file mode 100644 index 0000000000..9de3820a01 --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/index.js @@ -0,0 +1 @@ +export { default } from './InventoryGroup'; diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx new file mode 100644 index 0000000000..f09510a771 --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx @@ -0,0 +1,36 @@ +import React, { useState, useEffect } from 'react'; +import { withI18n } from '@lingui/react'; +import { withRouter } from 'react-router-dom'; +import { GroupsAPI } from '@api'; + +import ContentError from '@components/ContentError'; +import InventoryGroupForm from '../InventoryGroupForm/InventoryGroupForm'; + +function InventoryGroupsAdd({ history, inventory, setBreadcrumb }) { + useEffect(() => setBreadcrumb(inventory), [inventory, setBreadcrumb]); + const [error, setError] = useState(null); + const handleSubmit = async values => { + values.inventory = inventory.id; + try { + const { data } = await GroupsAPI.create(values); + history.push(`/inventories/inventory/${inventory.id}/groups/${data.id}`); + } catch (err) { + setError(err); + } + }; + const handleCancel = () => { + history.push(`/inventories/inventory/${inventory.id}/groups`); + }; + if (error) { + return ; + } + return ( + + ); +} +export default withI18n()(withRouter(InventoryGroupsAdd)); +export { InventoryGroupsAdd as _InventoryGroupsAdd }; diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.test.jsx new file mode 100644 index 0000000000..9155a5054a --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.test.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { GroupsAPI } from '@api'; +import { createMemoryHistory } from 'history'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; + +import InventoryGroupAdd from './InventoryGroupAdd'; + +jest.mock('@api'); + +describe('', () => { + let wrapper; + let history; + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/inventories/1/groups'], + }); + await act(async () => { + wrapper = mountWithContexts( + {}} + inventory={{ inventory: { id: 1 } }} + />, + { + context: { + router: { + history, + }, + }, + } + ); + }); + }); + afterEach(() => { + wrapper.unmount(); + }); + test('InventoryGroupEdit renders successfully', () => { + expect(wrapper.length).toBe(1); + }); + test('cancel should navigate user to Inventory Groups List', async () => { + await act(async () => { + waitForElement(wrapper, 'isLoading', el => el.length === 0); + }); + expect(history.location.pathname).toEqual('/inventories/1/groups'); + }); + test('handleSubmit should call api', async () => { + await act(async () => { + waitForElement(wrapper, 'isLoading', el => el.length === 0); + }); + await act(async () => { + wrapper.find('InventoryGroupForm').prop('handleSubmit')({ + name: 'Bar', + description: 'Ansible', + variables: 'ying: yang', + }); + }); + + expect(GroupsAPI.create).toBeCalled(); + }); +}); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/index.js b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/index.js new file mode 100644 index 0000000000..0e15c69a55 --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/index.js @@ -0,0 +1 @@ +export { default } from './InventoryGroupAdd'; diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx new file mode 100644 index 0000000000..5a593ba5a6 --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx @@ -0,0 +1,86 @@ +import React, { useState } from 'react'; +import { t } from '@lingui/macro'; + +import { CardBody, Button } from '@patternfly/react-core'; +import { withI18n } from '@lingui/react'; +import { withRouter } from 'react-router-dom'; +import styled from 'styled-components'; +import { VariablesInput } from '@components/CodeMirrorInput'; +import ContentError from '@components/ContentError'; + +import { GroupsAPI } from '@api'; +import { DetailList, Detail } from '@components/DetailList'; + +const ActionButtonWrapper = styled.div` + display: flex; + justify-content: flex-end; + margin-top: 20px; + & > :not(:first-child) { + margin-left: 20px; + } +`; +function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { + const [error, setError] = useState(false); + const handleDelete = async () => { + try { + await GroupsAPI.destroy(inventoryGroup.id); + history.push(`/inventories/inventory/${match.params.id}/groups`); + } catch (err) { + setError(err); + } + }; + + if (error) { + return ; + } + return ( + + + + + + + + + + + + + + + + ); +} +export default withI18n()(withRouter(InventoryGroupDetail)); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.test.jsx new file mode 100644 index 0000000000..d1803d1928 --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.test.jsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { GroupsAPI } from '@api'; +import { MemoryRouter, Route } from 'react-router-dom'; +import { act } from 'react-dom/test-utils'; + +import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; + +import InventoryGroupDetail from './InventoryGroupDetail'; + +jest.mock('@api'); +const inventoryGroup = { + name: 'Foo', + description: 'Bar', + variables: 'bizz: buzz', + id: 1, + created: '10:00', + modified: '12:00', + summary_fields: { + created_by: { + username: 'James', + }, + modified_by: { + username: 'Bond', + }, + }, +}; +describe('', () => { + let wrapper; + beforeEach(async () => { + await act(async () => { + wrapper = mountWithContexts( + + ( + + )} + /> + + ); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + }); + afterEach(() => { + wrapper.unmount(); + }); + test('InventoryGroupDetail renders successfully', () => { + expect(wrapper.length).toBe(1); + }); + test('should call api to delete the group', () => { + wrapper.find('button[aria-label="Delete"]').simulate('click'); + expect(GroupsAPI.destroy).toBeCalledWith(1); + }); + test('should navigate user to edit form on edit button click', async () => { + wrapper.find('button[aria-label="Edit"]').prop('onClick'); + expect( + wrapper + .find('Router') + .at(1) + .prop('history').location.pathname + ).toEqual('/inventories/inventory/1/groups/1/edit'); + }); + test('details shoudld render with the proper values', () => { + expect(wrapper.find('Detail[label="Name"]').prop('value')).toBe('Foo'); + expect(wrapper.find('Detail[label="Description"]').prop('value')).toBe( + 'Bar' + ); + expect(wrapper.find('Detail[label="Created"]').prop('value')).toBe( + '10:00 by James' + ); + expect(wrapper.find('Detail[label="Modified"]').prop('value')).toBe( + '12:00 by Bond' + ); + expect(wrapper.find('VariablesInput').prop('value')).toBe('bizz: buzz'); + }); +}); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/index.js b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/index.js new file mode 100644 index 0000000000..155a1c8e10 --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/index.js @@ -0,0 +1 @@ +export { default } from './InventoryGroupDetail'; diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx new file mode 100644 index 0000000000..f9e559c6c3 --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx @@ -0,0 +1,33 @@ +import React, { useState } from 'react'; +import { withI18n } from '@lingui/react'; +import { withRouter } from 'react-router-dom'; +import { GroupsAPI } from '@api'; + +import InventoryGroupForm from '../InventoryGroupForm/InventoryGroupForm'; + +function InventoryGroupEdit({ history, inventoryGroup, inventory, match }) { + const [error, setError] = useState(null); + + const handleSubmit = async values => { + try { + await GroupsAPI.update(match.params.groupId, values); + } catch (err) { + setError(err); + } finally { + history.push(`/inventories/inventory/${inventory.id}/groups`); + } + }; + const handleCancel = () => { + history.push(`/inventories/inventory/${inventory.id}/groups`); + }; + return ( + + ); +} +export default withI18n()(withRouter(InventoryGroupEdit)); +export { InventoryGroupEdit as _InventoryGroupEdit }; diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventroyGroupEdit.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventroyGroupEdit.test.jsx new file mode 100644 index 0000000000..056d99ac79 --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventroyGroupEdit.test.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { GroupsAPI } from '@api'; +import { createMemoryHistory } from 'history'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; + +import InventoryGroupEdit from './InventoryGroupEdit'; + +jest.mock('@api'); +GroupsAPI.readDetail.mockResolvedValue({ + data: { + name: 'Foo', + description: 'Bar', + variables: 'bizz: buzz', + }, +}); +describe('', () => { + let wrapper; + let history; + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/inventories/1/groups'], + }); + await act(async () => { + wrapper = mountWithContexts( + , + { + context: { + router: { + history, + route: { + match: { + params: { groupId: 13 }, + }, + }, + }, + }, + } + ); + }); + }); + afterEach(() => { + wrapper.unmount(); + }); + test('InventoryGroupEdit renders successfully', () => { + expect(wrapper.length).toBe(1); + }); + test('cancel should navigate user to Inventory Groups List', async () => { + await waitForElement(wrapper, 'isLoading', el => el.length === 0); + expect(history.location.pathname).toEqual('/inventories/1/groups'); + }); + test('handleSubmit should call api', async () => { + await waitForElement(wrapper, 'isLoading', el => el.length === 0); + wrapper.find('InventoryGroupForm').prop('handleSubmit')({ + name: 'Bar', + description: 'Ansible', + variables: 'ying: yang', + }); + expect(GroupsAPI.update).toBeCalled(); + }); +}); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/index.js b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/index.js new file mode 100644 index 0000000000..75519c821b --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/index.js @@ -0,0 +1 @@ +export { default } from './InventoryGroupEdit'; diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.jsx new file mode 100644 index 0000000000..0ae49c22d4 --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.jsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { withRouter } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { Formik } from 'formik'; +import { Form, Card, CardBody, CardHeader } from '@patternfly/react-core'; +import { t } from '@lingui/macro'; + +import CardCloseButton from '@components/CardCloseButton'; +import FormRow from '@components/FormRow'; +import FormField from '@components/FormField'; +import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; +import { VariablesField } from '@components/CodeMirrorInput'; +import { required } from '@util/validators'; + +function InventoryGroupForm({ + i18n, + error, + group = {}, + handleSubmit, + handleCancel, + match, +}) { + const initialValues = { + name: group.name || '', + description: group.description || '', + variables: group.variables || '---', + }; + + return ( + + + + + + ( +
+ + + + + + + + + {error ?
error
: null} + + )} + /> +
+
+ ); +} + +export default withI18n()(withRouter(InventoryGroupForm)); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.test.jsx new file mode 100644 index 0000000000..2c3484e1b6 --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.test.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { mountWithContexts } from '@testUtils/enzymeHelpers'; +import InventoryGroupForm from './InventoryGroupForm'; + +const group = { + id: 1, + name: 'Foo', + description: 'Bar', + variables: 'ying: false', +}; +describe('', () => { + let wrapper; + beforeEach(() => { + wrapper = mountWithContexts( + + ); + }); + afterEach(() => { + wrapper.unmount(); + }); + test('initially renders successfully', () => { + expect(wrapper.length).toBe(1); + }); + test('should render values for the fields that have them', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find("FormGroup[label='Name']").length).toBe(1); + expect(wrapper.find("FormGroup[label='Description']").length).toBe(1); + expect(wrapper.find("VariablesField[label='Variables']").length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/index.js b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/index.js new file mode 100644 index 0000000000..090b2c2f8a --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/index.js @@ -0,0 +1 @@ +export { default } from './InventoryGroupForm'; diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupItem.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupItem.jsx index c8f6b78621..91f0b0c4f9 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupItem.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupItem.jsx @@ -27,7 +27,7 @@ function InventoryGroupItem({ onSelect, }) { const labelId = `check-action-${group.id}`; - const detailUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/detail`; + const detailUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/details`; const editUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/edit`; return ( diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx index 49f55cd4d8..0e3d82a8b0 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx @@ -1,250 +1,50 @@ -import React, { useState, useEffect } from 'react'; -import { TrashAltIcon } from '@patternfly/react-icons'; -import { withRouter } from 'react-router-dom'; +import React from 'react'; import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; -import { getQSConfig, parseQueryString } from '@util/qs'; -import { InventoriesAPI, GroupsAPI } from '@api'; -import { Button, Tooltip } from '@patternfly/react-core'; -import AlertModal from '@components/AlertModal'; -import ErrorDetail from '@components/ErrorDetail'; -import DataListToolbar from '@components/DataListToolbar'; -import PaginatedDataList, { - ToolbarAddButton, -} from '@components/PaginatedDataList'; -import styled from 'styled-components'; -import InventoryGroupItem from './InventoryGroupItem'; -import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal'; -const QS_CONFIG = getQSConfig('host', { - page: 1, - page_size: 20, - order_by: 'name', -}); +import { Switch, Route, withRouter } from 'react-router-dom'; -const DeleteButton = styled(Button)` - padding: 5px 8px; +import InventoryGroupAdd from './InventoryGroupAdd/InventoryGroupAdd'; - &:hover { - background-color: #d9534f; - color: white; - } - - &[disabled] { - color: var(--pf-c-button--m-plain--Color); - pointer-events: initial; - cursor: not-allowed; - } -`; - -function cannotDelete(item) { - return !item.summary_fields.user_capabilities.delete; -} - -const useModal = () => { - const [isModalOpen, setIsModalOpen] = useState(false); - - function toggleModal() { - setIsModalOpen(!isModalOpen); - } - - return { - isModalOpen, - toggleModal, - }; -}; - -function InventoryGroups({ i18n, location, match }) { - const [actions, setActions] = useState(null); - const [contentError, setContentError] = useState(null); - const [deletionError, setDeletionError] = useState(null); - const [groupCount, setGroupCount] = useState(0); - const [groups, setGroups] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [selected, setSelected] = useState([]); - const { isModalOpen, toggleModal } = useModal(); - - const inventoryId = match.params.id; - - const fetchGroups = (id, queryString) => { - const params = parseQueryString(QS_CONFIG, queryString); - return InventoriesAPI.readGroups(id, params); - }; - - useEffect(() => { - async function fetchData() { - try { - const [ - { - data: { count, results }, - }, - { - data: { actions: optionActions }, - }, - ] = await Promise.all([ - fetchGroups(inventoryId, location.search), - InventoriesAPI.readGroupsOptions(inventoryId), - ]); - - setGroups(results); - setGroupCount(count); - setActions(optionActions); - } catch (error) { - setContentError(error); - } finally { - setIsLoading(false); - } - } - fetchData(); - }, [inventoryId, location]); - - const handleSelectAll = isSelected => { - setSelected(isSelected ? [...groups] : []); - }; - - const handleSelect = row => { - if (selected.some(s => s.id === row.id)) { - setSelected(selected.filter(s => s.id !== row.id)); - } else { - setSelected(selected.concat(row)); - } - }; - - const renderTooltip = () => { - const itemsUnableToDelete = selected - .filter(cannotDelete) - .map(item => item.name) - .join(', '); - - if (selected.some(cannotDelete)) { - return ( -
- {i18n._( - t`You do not have permission to delete the following Groups: ${itemsUnableToDelete}` - )} -
- ); - } - if (selected.length) { - return i18n._(t`Delete`); - } - return i18n._(t`Select a row to delete`); - }; - - const handleDelete = async option => { - setIsLoading(true); - - try { - /* eslint-disable no-await-in-loop, no-restricted-syntax */ - /* Delete groups sequentially to avoid api integrity errors */ - for (const group of selected) { - if (option === 'delete') { - await GroupsAPI.destroy(+group.id); - } else if (option === 'promote') { - await InventoriesAPI.promoteGroup(inventoryId, +group.id); - } - } - /* eslint-enable no-await-in-loop, no-restricted-syntax */ - } catch (error) { - setDeletionError(error); - } - - toggleModal(); - - try { - const { - data: { count, results }, - } = await fetchGroups(inventoryId, location.search); - setGroups(results); - setGroupCount(count); - } catch (error) { - setContentError(error); - } - - setIsLoading(false); - }; - - const canAdd = - actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); - const isAllSelected = - selected.length > 0 && selected.length === groups.length; +import InventoryGroup from './InventoryGroup/InventoryGroup'; +import InventoryGroupsList from './InventoryGroupsList'; +function InventoryGroups({ setBreadcrumb, inventory, location, match }) { return ( - <> - ( - row.id === item.id)} - onSelect={() => handleSelect(item)} - /> - )} - renderToolbar={props => ( - -
- - - -
- , - canAdd && ( - - ), - ]} - /> - )} - emptyStateControls={ - canAdd && ( - + {[ + { + return ; + }} + />, + { + return ( + + ); + }} + />, + ( + - ) - } - /> - {deletionError && ( - setDeletionError(null)} - > - {i18n._(t`Failed to delete one or more groups.`)} - - - )} - - + )} + />, + ]} + ); } +export { InventoryGroups as _InventoryGroups }; export default withI18n()(withRouter(InventoryGroups)); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx new file mode 100644 index 0000000000..20f2578afe --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx @@ -0,0 +1,248 @@ +import React, { useState, useEffect } from 'react'; +import { TrashAltIcon } from '@patternfly/react-icons'; +import { withRouter } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { getQSConfig, parseQueryString } from '@util/qs'; +import { InventoriesAPI, GroupsAPI } from '@api'; +import { Button, Tooltip } from '@patternfly/react-core'; +import AlertModal from '@components/AlertModal'; +import ErrorDetail from '@components/ErrorDetail'; +import DataListToolbar from '@components/DataListToolbar'; +import PaginatedDataList, { + ToolbarAddButton, +} from '@components/PaginatedDataList'; +import styled from 'styled-components'; +import InventoryGroupItem from './InventoryGroupItem'; +import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal'; + +const QS_CONFIG = getQSConfig('host', { + page: 1, + page_size: 20, + order_by: 'name', +}); + +const DeleteButton = styled(Button)` + padding: 5px 8px; + + &:hover { + background-color: #d9534f; + color: white; + } + + &[disabled] { + color: var(--pf-c-button--m-plain--Color); + pointer-events: initial; + cursor: not-allowed; + } +`; + +function cannotDelete(item) { + return !item.summary_fields.user_capabilities.delete; +} + +const useModal = () => { + const [isModalOpen, setIsModalOpen] = useState(false); + + function toggleModal() { + setIsModalOpen(!isModalOpen); + } + + return { + isModalOpen, + toggleModal, + }; +}; + +function InventoryGroupsList({ i18n, location, match }) { + const [actions, setActions] = useState(null); + const [contentError, setContentError] = useState(null); + const [deletionError, setDeletionError] = useState(null); + const [groupCount, setGroupCount] = useState(0); + const [groups, setGroups] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [selected, setSelected] = useState([]); + const { isModalOpen, toggleModal } = useModal(); + + const inventoryId = match.params.id; + const fetchGroups = (id, queryString) => { + const params = parseQueryString(QS_CONFIG, queryString); + return InventoriesAPI.readGroups(id, params); + }; + + useEffect(() => { + async function fetchData() { + try { + const [ + { + data: { count, results }, + }, + { + data: { actions: optionActions }, + }, + ] = await Promise.all([ + fetchGroups(inventoryId, location.search), + InventoriesAPI.readGroupsOptions(inventoryId), + ]); + + setGroups(results); + setGroupCount(count); + setActions(optionActions); + } catch (error) { + setContentError(error); + } finally { + setIsLoading(false); + } + } + fetchData(); + }, [inventoryId, location]); + + const handleSelectAll = isSelected => { + setSelected(isSelected ? [...groups] : []); + }; + + const handleSelect = row => { + if (selected.some(s => s.id === row.id)) { + setSelected(selected.filter(s => s.id !== row.id)); + } else { + setSelected(selected.concat(row)); + } + }; + + const renderTooltip = () => { + const itemsUnableToDelete = selected + .filter(cannotDelete) + .map(item => item.name) + .join(', '); + + if (selected.some(cannotDelete)) { + return ( +
+ {i18n._( + t`You do not have permission to delete the following Groups: ${itemsUnableToDelete}` + )} +
+ ); + } + if (selected.length) { + return i18n._(t`Delete`); + } + return i18n._(t`Select a row to delete`); + }; + + const handleDelete = async option => { + setIsLoading(true); + + try { + /* eslint-disable no-await-in-loop, no-restricted-syntax */ + /* Delete groups sequentially to avoid api integrity errors */ + for (const group of selected) { + if (option === 'delete') { + await GroupsAPI.destroy(+group.id); + } else if (option === 'promote') { + await InventoriesAPI.promoteGroup(inventoryId, +group.id); + } + } + /* eslint-enable no-await-in-loop, no-restricted-syntax */ + } catch (error) { + setDeletionError(error); + } + + toggleModal(); + + try { + const { + data: { count, results }, + } = await fetchGroups(inventoryId, location.search); + setGroups(results); + setGroupCount(count); + } catch (error) { + setContentError(error); + } + + setIsLoading(false); + }; + + const canAdd = + actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); + const isAllSelected = + selected.length > 0 && selected.length === groups.length; + + return ( + <> + ( + row.id === item.id)} + onSelect={() => handleSelect(item)} + /> + )} + renderToolbar={props => ( + +
+ + + +
+ , + canAdd && ( + + ), + ]} + /> + )} + emptyStateControls={ + canAdd && ( + + ) + } + /> + {deletionError && ( + setDeletionError(null)} + > + {i18n._(t`Failed to delete one or more groups.`)} + + + )} + + + ); +} +export default withI18n()(withRouter(InventoryGroupsList)); From 3ea37e1c798c6d316746e7ac19e56a9ea17d2867 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Tue, 3 Dec 2019 15:49:36 -0500 Subject: [PATCH 2/9] Addresses PR issues Adds Delete Modal for deleting from Details view Adds test for delete modal Addresses styling for Variables label Removes X close button from form --- .../InventoryGroup/InventoryGroup.jsx | 14 ++- .../InventoryGroupDetail.jsx | 86 ++++++++++++++++--- .../InventoryGroupDetail.test.jsx | 18 ++-- .../InventoryGroupForm/InventoryGroupForm.jsx | 9 +- 4 files changed, 95 insertions(+), 32 deletions(-) diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx index 9a4994c8fe..d665c19176 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx @@ -51,14 +51,24 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory }) { key="edit" path="/inventories/inventory/:id/groups/:groupId/edit" render={() => { - return ; + return ( + + ); }} />, { - return ; + return ( + + ); }} />, { try { await GroupsAPI.destroy(inventoryGroup.id); @@ -29,10 +33,42 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { setError(err); } }; - if (error) { return ; } + if (isDeleteModalOpen) { + return ( + setIsDeleteModalOpen(false)} + actions={[ + , + , + ]} + > + {i18n._(t`Are you sure you want to delete:`)} +
+ {inventoryGroup.name} +
+
+ ); + } return ( @@ -41,23 +77,47 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { label={i18n._(t`Description`)} value={inventoryGroup.description} /> + + } + /> - + {i18n._(t`${formatDateString(inventoryGroup.created)} by`)}{' '} + + {inventoryGroup.summary_fields.created_by.username} + + + } /> + {i18n._(t`${formatDateString(inventoryGroup.modified)} by`)}{' '} + + {inventoryGroup.summary_fields.modified_by.username} + + + } /> @@ -75,7 +135,7 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.test.jsx index d1803d1928..2a762aa435 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.test.jsx @@ -13,14 +13,16 @@ const inventoryGroup = { description: 'Bar', variables: 'bizz: buzz', id: 1, - created: '10:00', - modified: '12:00', + created: '2019-12-02T15:58:16.276813Z', + modified: '2019-12-03T20:33:46.207654Z', summary_fields: { created_by: { username: 'James', + id: 13, }, modified_by: { username: 'Bond', + id: 14, }, }, }; @@ -49,8 +51,10 @@ describe('', () => { test('InventoryGroupDetail renders successfully', () => { expect(wrapper.length).toBe(1); }); - test('should call api to delete the group', () => { + test('should open delete modal and then call api to delete the group', () => { wrapper.find('button[aria-label="Delete"]').simulate('click'); + expect(wrapper.find('Modal').length).toBe(1); + wrapper.find('button[aria-label="confirm delete"]').simulate('click'); expect(GroupsAPI.destroy).toBeCalledWith(1); }); test('should navigate user to edit form on edit button click', async () => { @@ -67,12 +71,8 @@ describe('', () => { expect(wrapper.find('Detail[label="Description"]').prop('value')).toBe( 'Bar' ); - expect(wrapper.find('Detail[label="Created"]').prop('value')).toBe( - '10:00 by James' - ); - expect(wrapper.find('Detail[label="Modified"]').prop('value')).toBe( - '12:00 by Bond' - ); + expect(wrapper.find('Detail[label="Created"]').length).toBe(1); + expect(wrapper.find('Detail[label="Modified"]').length).toBe(1); expect(wrapper.find('VariablesInput').prop('value')).toBe('bizz: buzz'); }); }); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.jsx index 0ae49c22d4..f6dea49aee 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.jsx @@ -2,10 +2,9 @@ import React from 'react'; import { withRouter } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { Formik } from 'formik'; -import { Form, Card, CardBody, CardHeader } from '@patternfly/react-core'; +import { Form, Card, CardBody } from '@patternfly/react-core'; import { t } from '@lingui/macro'; -import CardCloseButton from '@components/CardCloseButton'; import FormRow from '@components/FormRow'; import FormField from '@components/FormField'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; @@ -18,7 +17,6 @@ function InventoryGroupForm({ group = {}, handleSubmit, handleCancel, - match, }) { const initialValues = { name: group.name || '', @@ -28,11 +26,6 @@ function InventoryGroupForm({ return ( - - - Date: Mon, 9 Dec 2019 16:06:56 -0500 Subject: [PATCH 3/9] Adds Alert Modal, Breadcrumb, Nested Tabs and Refactors PR. --- .../src/components/RoutedTabs/RoutedTabs.jsx | 11 +- .../src/screens/Inventory/Inventories.jsx | 6 +- .../src/screens/Inventory/Inventory.jsx | 7 +- .../InventoryGroup/InventoryGroup.jsx | 141 +++++++++++------- .../InventoryGroup/InventoryGroup.test.jsx | 70 +++++++++ .../InventoryGroupAdd/InventoryGroupAdd.jsx | 17 +-- .../InventoryGroupAdd.test.jsx | 8 +- .../InventoryGroupDetail.jsx | 45 +++--- .../InventoryGroupDetail.test.jsx | 50 ++++--- .../InventoryGroupEdit/InventoryGroupEdit.jsx | 8 +- 10 files changed, 255 insertions(+), 108 deletions(-) create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.test.jsx diff --git a/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx b/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx index 0e90017b22..964806f05e 100644 --- a/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx +++ b/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx @@ -3,6 +3,7 @@ import { shape, string, number, arrayOf } from 'prop-types'; import { Tab, Tabs as PFTabs } from '@patternfly/react-core'; import { withRouter } from 'react-router-dom'; import styled from 'styled-components'; +import { CaretLeftIcon } from '@patternfly/react-icons'; const Tabs = styled(PFTabs)` --pf-c-tabs__button--PaddingLeft: 20px; @@ -62,7 +63,15 @@ function RoutedTabs(props) { eventKey={tab.id} key={tab.id} link={tab.link} - title={tab.name} + title={ + tab.isNestedTabs ? ( + <> + {tab.name} + + ) : ( + tab.name + ) + } /> ))} diff --git a/awx/ui_next/src/screens/Inventory/Inventories.jsx b/awx/ui_next/src/screens/Inventory/Inventories.jsx index 8e48139bc9..4253078486 100644 --- a/awx/ui_next/src/screens/Inventory/Inventories.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventories.jsx @@ -61,9 +61,11 @@ class Inventories extends Component { t`Create New Group` ), [`/inventories/inventory/${inventory.id}/groups/${group && - group.id}/details`]: i18n._(t`Details`), + group.id}`]: `${group && group.name}`, [`/inventories/inventory/${inventory.id}/groups/${group && - group.id}/edit`]: `${group && group.name}`, + group.id}/details`]: i18n._(t`Group Details`), + [`/inventories/inventory/${inventory.id}/groups/${group && + group.id}/edit`]: i18n._(t`Edit Details`), }; this.setState({ breadcrumbConfig }); }; diff --git a/awx/ui_next/src/screens/Inventory/Inventory.jsx b/awx/ui_next/src/screens/Inventory/Inventory.jsx index da49aa6d8e..f3e78d584b 100644 --- a/awx/ui_next/src/screens/Inventory/Inventory.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventory.jsx @@ -57,7 +57,11 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) { ); - if (location.pathname.endsWith('edit') || location.pathname.endsWith('add')) { + if ( + location.pathname.endsWith('edit') || + location.pathname.endsWith('add') || + location.pathname.includes('groups/') + ) { cardHeader = null; } @@ -127,6 +131,7 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) { diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx index d665c19176..f5fbada3fe 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx @@ -1,18 +1,18 @@ import React, { useEffect, useState } from 'react'; import { t } from '@lingui/macro'; import { withI18n } from '@lingui/react'; +import { CardHeader } from '@patternfly/react-core'; import { Switch, Route, withRouter, Link, Redirect } from 'react-router-dom'; import { GroupsAPI } from '@api'; - +import CardCloseButton from '@components/CardCloseButton'; +import RoutedTabs from '@components/RoutedTabs'; import ContentError from '@components/ContentError'; import ContentLoading from '@components/ContentLoading'; - import InventoryGroupEdit from '../InventoryGroupEdit/InventoryGroupEdit'; - import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail'; -function InventoryGroups({ i18n, match, setBreadcrumb, inventory }) { +function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { const [inventoryGroup, setInventoryGroup] = useState(null); const [hasContentLoading, setContentLoading] = useState(true); const [hasContentError, setHasContentError] = useState(false); @@ -32,64 +32,101 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory }) { loadData(); }, [match.params.groupId, setBreadcrumb, inventory]); - + const tabsArray = [ + { + name: i18n._(t`Return to Groups`), + link: `/inventories/inventory/${inventory.id}/groups`, + id: 99, + isNestedTabs: true, + }, + { + name: i18n._(t`Details`), + link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup && + inventoryGroup.id}/details`, + id: 0, + }, + { + name: i18n._(t`RelatedGroups`), + link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup && + inventoryGroup.id}/nested_groups`, + id: 1, + }, + { + name: i18n._(t`Hosts`), + link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup && + inventoryGroup.id}/nested_hosts`, + id: 2, + }, + ]; if (hasContentError) { return ; } if (hasContentLoading) { return ; } - return ( - - + + - {inventoryGroup && [ - { - return ( - - ); - }} - />, - { - return ( - - ); - }} - />, + + ); + if ( + !history.location.pathname.includes('groups/') || + history.location.pathname.endsWith('edit') + ) { + cardHeader = null; + } + return ( + <> + {cardHeader} + + + {inventoryGroup && [ + { + return ( + + ); + }} + />, + { + return ; + }} + />, + ]} - !hasContentLoading && ( - - {match.params.id && ( - - {i18n._(t`View Inventory Details`)} - - )} - - ) - } - />, - ]} - + render={() => { + return ( + !hasContentLoading && ( + + {inventory && ( + + {i18n._(t`View Inventory Details`)} + + )} + + ) + ); + }} + /> + + ); } diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.test.jsx new file mode 100644 index 0000000000..afec079a6d --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.test.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { GroupsAPI } from '@api'; +import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; + +import InventoryGroup from './InventoryGroup'; + +jest.mock('@api'); +GroupsAPI.readDetail.mockResolvedValue({ + data: { + id: 1, + name: 'Foo', + description: 'Bar', + variables: 'bizz: buzz', + summary_fields: { + created_by: { id: 1, name: 'Athena' }, + modified_by: { id: 1, name: 'Apollo' }, + }, + }, +}); +describe('', () => { + let wrapper; + let history; + const inventory = { id: 1, name: 'Foo' }; + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/inventories/inventory/1/groups/1/details'], + }); + await act(async () => { + wrapper = mountWithContexts( + {}} />, + { + context: { + router: { + history, + route: { + location: history.location, + match: { + params: { id: 1 }, + }, + }, + }, + }, + } + ); + }); + }); + afterEach(() => { + wrapper.unmount(); + }); + test('renders successfully', async () => { + await act(async () => { + waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + expect(wrapper.length).toBe(1); + expect(wrapper.find('button[aria-label="Return to Groups"]').length).toBe( + 1 + ); + }); + test('expect Return to Groups tab to exist', async () => { + await act(async () => { + waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + expect(wrapper.length).toBe(1); + expect(wrapper.find('button[aria-label="Return to Groups"]').length).toBe( + 1 + ); + }); +}); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx index f09510a771..248309b005 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx @@ -2,8 +2,8 @@ import React, { useState, useEffect } from 'react'; import { withI18n } from '@lingui/react'; import { withRouter } from 'react-router-dom'; import { GroupsAPI } from '@api'; +import { Card } from '@patternfly/react-core'; -import ContentError from '@components/ContentError'; import InventoryGroupForm from '../InventoryGroupForm/InventoryGroupForm'; function InventoryGroupsAdd({ history, inventory, setBreadcrumb }) { @@ -21,15 +21,14 @@ function InventoryGroupsAdd({ history, inventory, setBreadcrumb }) { const handleCancel = () => { history.push(`/inventories/inventory/${inventory.id}/groups`); }; - if (error) { - return ; - } return ( - + + + ); } export default withI18n()(withRouter(InventoryGroupsAdd)); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.test.jsx index 9155a5054a..67ca98f99c 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.test.jsx @@ -13,7 +13,7 @@ describe('', () => { let history; beforeEach(async () => { history = createMemoryHistory({ - initialEntries: ['/inventories/1/groups'], + initialEntries: ['/inventories/inventory/1/groups'], }); await act(async () => { wrapper = mountWithContexts( @@ -34,14 +34,16 @@ describe('', () => { afterEach(() => { wrapper.unmount(); }); - test('InventoryGroupEdit renders successfully', () => { + test('InventoryGroupAdd renders successfully', () => { expect(wrapper.length).toBe(1); }); test('cancel should navigate user to Inventory Groups List', async () => { await act(async () => { waitForElement(wrapper, 'isLoading', el => el.length === 0); }); - expect(history.location.pathname).toEqual('/inventories/1/groups'); + expect(history.location.pathname).toEqual( + '/inventories/inventory/1/groups' + ); }); test('handleSubmit should call api', async () => { await act(async () => { diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx index 0835e87c25..7244385471 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx @@ -5,14 +5,21 @@ import { CardBody, Button } from '@patternfly/react-core'; import { withI18n } from '@lingui/react'; import { withRouter, Link } from 'react-router-dom'; import styled from 'styled-components'; -import { VariablesInput } from '@components/CodeMirrorInput'; -import ContentError from '@components/ContentError'; +import { VariablesInput as CodeMirrorInput } from '@components/CodeMirrorInput'; +import ErrorDetail from '@components/ErrorDetail'; import AlertModal from '@components/AlertModal'; import { formatDateString } from '@util/dates'; import { GroupsAPI } from '@api'; import { DetailList, Detail } from '@components/DetailList'; +const VariablesInput = styled(CodeMirrorInput)` + .pf-c-form__label { + font-weight: 600; + font-size: 16px; + } + margin: 20px 0; +`; const ActionButtonWrapper = styled.div` display: flex; justify-content: flex-end; @@ -26,6 +33,7 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const handleDelete = async () => { + setIsDeleteModalOpen(false); try { await GroupsAPI.destroy(inventoryGroup.id); history.push(`/inventories/inventory/${match.params.id}/groups`); @@ -34,7 +42,17 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { } }; if (error) { - return ; + return ( + setError(false)} + > + {i18n._(t`Failed to delete group ${inventoryGroup.name}.`)} + + + ); } if (isDeleteModalOpen) { return ( @@ -77,21 +95,14 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { label={i18n._(t`Description`)} value={inventoryGroup.description} /> - - } - /> + ', () => { let wrapper; + let history; beforeEach(async () => { await act(async () => { + history = createMemoryHistory({ + initialEntries: ['/inventories/inventory/1/groups/1/edit'], + }); wrapper = mountWithContexts( - - ( - - )} - /> - + ( + + )} + />, + { + context: { + router: { history, route: { location: history.location } }, + }, + } ); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); }); @@ -51,20 +57,22 @@ describe('', () => { test('InventoryGroupDetail renders successfully', () => { expect(wrapper.length).toBe(1); }); - test('should open delete modal and then call api to delete the group', () => { - wrapper.find('button[aria-label="Delete"]').simulate('click'); + test('should open delete modal and then call api to delete the group', async () => { + await act(async () => { + wrapper.find('button[aria-label="Delete"]').simulate('click'); + }); + await waitForElement(wrapper, 'Modal', el => el.length === 1); expect(wrapper.find('Modal').length).toBe(1); - wrapper.find('button[aria-label="confirm delete"]').simulate('click'); + await act(async () => { + wrapper.find('button[aria-label="confirm delete"]').simulate('click'); + }); expect(GroupsAPI.destroy).toBeCalledWith(1); }); test('should navigate user to edit form on edit button click', async () => { wrapper.find('button[aria-label="Edit"]').prop('onClick'); - expect( - wrapper - .find('Router') - .at(1) - .prop('history').location.pathname - ).toEqual('/inventories/inventory/1/groups/1/edit'); + expect(history.location.pathname).toEqual( + '/inventories/inventory/1/groups/1/edit' + ); }); test('details shoudld render with the proper values', () => { expect(wrapper.find('Detail[label="Name"]').prop('value')).toBe('Foo'); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx index f9e559c6c3..42d2fcde2a 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx @@ -14,11 +14,15 @@ function InventoryGroupEdit({ history, inventoryGroup, inventory, match }) { } catch (err) { setError(err); } finally { - history.push(`/inventories/inventory/${inventory.id}/groups`); + history.push( + `/inventories/inventory/${inventory.id}/groups/${inventoryGroup.id}/details` + ); } }; const handleCancel = () => { - history.push(`/inventories/inventory/${inventory.id}/groups`); + history.push( + `/inventories/inventory/${inventory.id}/groups/${inventoryGroup.id}/details` + ); }; return ( Date: Tue, 10 Dec 2019 15:08:59 -0500 Subject: [PATCH 4/9] Testing Improvements and Refactoring --- .../InventoryGroup/InventoryGroup.jsx | 44 +++--- .../InventoryGroup/InventoryGroup.test.jsx | 16 +- .../InventoryGroupAdd/InventoryGroupAdd.jsx | 6 +- .../InventoryGroupAdd.test.jsx | 25 ++- .../InventoryGroupDetail.jsx | 144 +++++++++--------- .../InventoryGroupDetail.test.jsx | 4 +- .../InventoryGroupEdit/InventoryGroupEdit.jsx | 11 +- .../InventroyGroupEdit.test.jsx | 25 ++- .../InventoryGroupForm.test.jsx | 1 - .../InventoryGroups/InventoryGroups.jsx | 53 +++---- .../InventoryGroups/InventoryGroups.test.jsx | 10 +- 11 files changed, 173 insertions(+), 166 deletions(-) diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx index f5fbada3fe..b205182c04 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx @@ -14,8 +14,8 @@ import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail'; function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { const [inventoryGroup, setInventoryGroup] = useState(null); - const [hasContentLoading, setContentLoading] = useState(true); - const [hasContentError, setHasContentError] = useState(false); + const [hasContentLoading, setHasContentLoading] = useState(true); + const [contentError, setHasContentError] = useState(null); useEffect(() => { const loadData = async () => { @@ -26,12 +26,18 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { } catch (err) { setHasContentError(err); } finally { - setContentLoading(false); + setHasContentLoading(false); } }; loadData(); - }, [match.params.groupId, setBreadcrumb, inventory]); + }, [ + history.location.pathname, + match.params.groupId, + inventory, + setBreadcrumb, + ]); + const tabsArray = [ { name: i18n._(t`Return to Groups`), @@ -46,7 +52,7 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { id: 0, }, { - name: i18n._(t`RelatedGroups`), + name: i18n._(t`Related Groups`), link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup && inventoryGroup.id}/nested_groups`, id: 1, @@ -58,26 +64,28 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { id: 2, }, ]; - if (hasContentError) { - return ; + if (contentError) { + return ; } if (hasContentLoading) { return ; } - let cardHeader = hasContentLoading ? null : ( - - - - - ); + + let cardHeader = null; if ( - !history.location.pathname.includes('groups/') || - history.location.pathname.endsWith('edit') + history.location.pathname.includes('groups/') && + !history.location.pathname.endsWith('edit') ) { - cardHeader = null; + cardHeader = ( + + + + + ); } + return ( <> {cardHeader} diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.test.jsx index afec079a6d..4c1b50c95b 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.test.jsx @@ -45,26 +45,20 @@ describe('', () => { } ); }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); }); afterEach(() => { wrapper.unmount(); }); test('renders successfully', async () => { - await act(async () => { - waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - }); expect(wrapper.length).toBe(1); - expect(wrapper.find('button[aria-label="Return to Groups"]').length).toBe( - 1 - ); }); - test('expect Return to Groups tab to exist', async () => { - await act(async () => { - waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - }); - expect(wrapper.length).toBe(1); + test('expect all tabs to exist, including Return to Groups', async () => { expect(wrapper.find('button[aria-label="Return to Groups"]').length).toBe( 1 ); + expect(wrapper.find('button[aria-label="Details"]').length).toBe(1); + expect(wrapper.find('button[aria-label="Related Groups"]').length).toBe(1); + expect(wrapper.find('button[aria-label="Hosts"]').length).toBe(1); }); }); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx index 248309b005..720e9d4b3c 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx @@ -7,8 +7,10 @@ import { Card } from '@patternfly/react-core'; import InventoryGroupForm from '../InventoryGroupForm/InventoryGroupForm'; function InventoryGroupsAdd({ history, inventory, setBreadcrumb }) { - useEffect(() => setBreadcrumb(inventory), [inventory, setBreadcrumb]); const [error, setError] = useState(null); + + useEffect(() => setBreadcrumb(inventory), [inventory, setBreadcrumb]); + const handleSubmit = async values => { values.inventory = inventory.id; try { @@ -18,9 +20,11 @@ function InventoryGroupsAdd({ history, inventory, setBreadcrumb }) { setError(err); } }; + const handleCancel = () => { history.push(`/inventories/inventory/${inventory.id}/groups`); }; + return ( ', () => { let history; beforeEach(async () => { history = createMemoryHistory({ - initialEntries: ['/inventories/inventory/1/groups'], + initialEntries: ['/inventories/inventory/1/groups/add'], }); await act(async () => { wrapper = mountWithContexts( - {}} - inventory={{ inventory: { id: 1 } }} + ( + {}} inventory={{ id: 1 }} /> + )} />, { context: { - router: { - history, - }, + router: { history, route: { location: history.location } }, }, } ); @@ -38,17 +40,12 @@ describe('', () => { expect(wrapper.length).toBe(1); }); test('cancel should navigate user to Inventory Groups List', async () => { - await act(async () => { - waitForElement(wrapper, 'isLoading', el => el.length === 0); - }); + wrapper.find('button[aria-label="Cancel"]').simulate('click'); expect(history.location.pathname).toEqual( '/inventories/inventory/1/groups' ); }); test('handleSubmit should call api', async () => { - await act(async () => { - waitForElement(wrapper, 'isLoading', el => el.length === 0); - }); await act(async () => { wrapper.find('InventoryGroupForm').prop('handleSubmit')({ name: 'Bar', diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx index 7244385471..7b383e986f 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx @@ -29,6 +29,9 @@ const ActionButtonWrapper = styled.div` } `; function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { + const { + summary_fields: { created_by, modified_by }, + } = inventoryGroup; const [error, setError] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -41,52 +44,7 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { setError(err); } }; - if (error) { - return ( - setError(false)} - > - {i18n._(t`Failed to delete group ${inventoryGroup.name}.`)} - - - ); - } - if (isDeleteModalOpen) { - return ( - setIsDeleteModalOpen(false)} - actions={[ - , - , - ]} - > - {i18n._(t`Are you sure you want to delete:`)} -
- {inventoryGroup.name} -
-
- ); - } + return ( @@ -104,32 +62,32 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { label={i18n._(t`Variables`)} /> - - {i18n._(t`${formatDateString(inventoryGroup.created)} by`)}{' '} - - {inventoryGroup.summary_fields.created_by.username} - - - } - /> - - {i18n._(t`${formatDateString(inventoryGroup.modified)} by`)}{' '} - - {inventoryGroup.summary_fields.modified_by.username} - - - } - /> + {created_by && created_by.username && ( + + {i18n._(t`${formatDateString(inventoryGroup.created)} by`)}{' '} + + {created_by.username} + + + } + /> + )} + {modified_by && modified_by.username && ( + + {i18n._(t`${formatDateString(inventoryGroup.modified)} by`)}{' '} + + {modified_by.username} + + + } + /> + )} , + , + ]} + > + {i18n._(t`Are you sure you want to delete:`)} +
+ {inventoryGroup.name} +
+ + )} + {error && ( + setError(false)} + > + {i18n._(t`Failed to delete group ${inventoryGroup.name}.`)} + + + )}
); } diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.test.jsx index 8e82fea394..99a017ce32 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.test.jsx @@ -33,7 +33,7 @@ describe('', () => { beforeEach(async () => { await act(async () => { history = createMemoryHistory({ - initialEntries: ['/inventories/inventory/1/groups/1/edit'], + initialEntries: ['/inventories/inventory/1/groups/1/details'], }); wrapper = mountWithContexts( ', () => { expect(GroupsAPI.destroy).toBeCalledWith(1); }); test('should navigate user to edit form on edit button click', async () => { - wrapper.find('button[aria-label="Edit"]').prop('onClick'); + wrapper.find('button[aria-label="Edit"]').simulate('click'); expect(history.location.pathname).toEqual( '/inventories/inventory/1/groups/1/edit' ); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx index 42d2fcde2a..6ff0e58c58 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx @@ -11,19 +11,20 @@ function InventoryGroupEdit({ history, inventoryGroup, inventory, match }) { const handleSubmit = async values => { try { await GroupsAPI.update(match.params.groupId, values); + history.push( + `/inventories/inventory/${inventory.id}/groups/${inventoryGroup.id}` + ); } catch (err) { setError(err); - } finally { - history.push( - `/inventories/inventory/${inventory.id}/groups/${inventoryGroup.id}/details` - ); } }; + const handleCancel = () => { history.push( - `/inventories/inventory/${inventory.id}/groups/${inventoryGroup.id}/details` + `/inventories/inventory/${inventory.id}/groups/${inventoryGroup.id}` ); }; + return ( ', () => { let history; beforeEach(async () => { history = createMemoryHistory({ - initialEntries: ['/inventories/1/groups'], + initialEntries: ['/inventories/inventory/1/groups/2/edit'], }); await act(async () => { wrapper = mountWithContexts( - ( + {}} + inventory={{ id: 1 }} + inventoryGroup={{ id: 2 }} + /> + )} />, { context: { @@ -35,6 +42,7 @@ describe('', () => { match: { params: { groupId: 13 }, }, + location: history.location, }, }, }, @@ -49,11 +57,12 @@ describe('', () => { expect(wrapper.length).toBe(1); }); test('cancel should navigate user to Inventory Groups List', async () => { - await waitForElement(wrapper, 'isLoading', el => el.length === 0); - expect(history.location.pathname).toEqual('/inventories/1/groups'); + wrapper.find('button[aria-label="Cancel"]').simulate('click'); + expect(history.location.pathname).toEqual( + '/inventories/inventory/1/groups/2' + ); }); test('handleSubmit should call api', async () => { - await waitForElement(wrapper, 'isLoading', el => el.length === 0); wrapper.find('InventoryGroupForm').prop('handleSubmit')({ name: 'Bar', description: 'Ansible', diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.test.jsx index 2c3484e1b6..ebf459f76f 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.test.jsx @@ -26,7 +26,6 @@ describe('', () => { expect(wrapper.length).toBe(1); }); test('should render values for the fields that have them', () => { - expect(wrapper.length).toBe(1); expect(wrapper.find("FormGroup[label='Name']").length).toBe(1); expect(wrapper.find("FormGroup[label='Description']").length).toBe(1); expect(wrapper.find("VariablesField[label='Variables']").length).toBe(1); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx index 0e3d82a8b0..e35e2dedb7 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx @@ -11,37 +11,32 @@ import InventoryGroupsList from './InventoryGroupsList'; function InventoryGroups({ setBreadcrumb, inventory, location, match }) { return ( - {[ - { - return ; - }} - />, - { - return ( - - ); - }} - />, - ( - { + return ( + - )} - />, - ]} + ); + }} + /> + ( + + )} + /> + { + return ; + }} + /> ); } diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx index 2b5a7340c0..8c60d8bfbd 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx @@ -4,7 +4,7 @@ import { Route } from 'react-router-dom'; import { createMemoryHistory } from 'history'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { InventoriesAPI, GroupsAPI } from '@api'; -import InventoryGroups from './InventoryGroups'; +import InventoryGroupsList from './InventoryGroupsList'; jest.mock('@api'); @@ -50,7 +50,7 @@ const mockGroups = [ }, ]; -describe('', () => { +describe('', () => { let wrapper; beforeEach(async () => { @@ -75,7 +75,7 @@ describe('', () => { wrapper = mountWithContexts( } + component={() => } />, { context: { @@ -88,7 +88,7 @@ describe('', () => { }); test('initially renders successfully', () => { - expect(wrapper.find('InventoryGroups').length).toBe(1); + expect(wrapper.find('InventoryGroupsList').length).toBe(1); }); test('should fetch groups from api and render them in the list', async () => { @@ -147,7 +147,7 @@ describe('', () => { Promise.reject(new Error()) ); await act(async () => { - wrapper = mountWithContexts(); + wrapper = mountWithContexts(); }); await waitForElement(wrapper, 'ContentError', el => el.length === 1); }); From 210f9577b0d57cec0b58b92dd474ccea2e16bdc4 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Wed, 11 Dec 2019 14:32:47 -0500 Subject: [PATCH 5/9] Fixed filename typo --- .../{InventroyGroupEdit.test.jsx => InventoryGroupEdit.test.jsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/{InventroyGroupEdit.test.jsx => InventoryGroupEdit.test.jsx} (100%) diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventroyGroupEdit.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.test.jsx similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventroyGroupEdit.test.jsx rename to awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.test.jsx From 1942be7dc3c5afa0eebc1ddb627eb1bc36498f0c Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Thu, 12 Dec 2019 11:26:35 -0500 Subject: [PATCH 6/9] Checks for modified and create in Inv Group Deets Also includes refactoring for css over style prop and removed some unnecessary loading checks --- .../InventoryGroup/InventoryGroup.jsx | 16 +++-- .../InventoryGroupDetail.jsx | 60 +++++++++++-------- .../InventoryGroups/InventoryGroupsList.jsx | 2 +- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx index b205182c04..db03a1ecf2 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx @@ -121,15 +121,13 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { path="*" render={() => { return ( - !hasContentLoading && ( - - {inventory && ( - - {i18n._(t`View Inventory Details`)} - - )} - - ) + + {inventory && ( + + {i18n._(t`View Inventory Details`)} + + )} + ); }} /> diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx index 7b383e986f..20d99538b8 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx @@ -31,6 +31,8 @@ const ActionButtonWrapper = styled.div` function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { const { summary_fields: { created_by, modified_by }, + created, + modified, } = inventoryGroup; const [error, setError] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -45,8 +47,36 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { } }; + let createdBy = ''; + if (created) { + if (created_by && created_by.username) { + createdBy = ( + + {i18n._(t`${formatDateString(inventoryGroup.created)} by`)}{' '} + {created_by.username} + + ); + } else { + createdBy = i18n._(t`${formatDateString(inventoryGroup.created)} by`); + } + } + + let modifiedBy = ''; + if (modified) { + if (modified_by && modified_by.username) { + modifiedBy = ( + + {i18n._(t`${formatDateString(inventoryGroup.modified)} by`)}{' '} + {modified_by.username} + + ); + } else { + modifiedBy = i18n._(t`${formatDateString(inventoryGroup.modified)} by`); + } + } + return ( - + - {created_by && created_by.username && ( - - {i18n._(t`${formatDateString(inventoryGroup.created)} by`)}{' '} - - {created_by.username} - - - } - /> - )} - {modified_by && modified_by.username && ( - - {i18n._(t`${formatDateString(inventoryGroup.modified)} by`)}{' '} - - {modified_by.username} - - - } - /> + {createdBy && } + {modifiedBy && ( + )} diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx index 20f2578afe..1840c3815c 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx @@ -16,7 +16,7 @@ import styled from 'styled-components'; import InventoryGroupItem from './InventoryGroupItem'; import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal'; -const QS_CONFIG = getQSConfig('host', { +const QS_CONFIG = getQSConfig('group', { page: 1, page_size: 20, order_by: 'name', From ef5ce0b08207587add3071e05b5cf33ab7d2d551 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Thu, 12 Dec 2019 15:42:36 -0500 Subject: [PATCH 7/9] Flattens Inventory File Structure Remove --- awx/ui_next/src/index.jsx | 4 ++-- .../{InventoryGroups => }/InventoryGroup/InventoryGroup.jsx | 0 .../InventoryGroup/InventoryGroup.test.jsx | 0 .../Inventory/{InventoryGroups => }/InventoryGroup/index.js | 0 .../InventoryGroupAdd/InventoryGroupAdd.jsx | 0 .../InventoryGroupAdd/InventoryGroupAdd.test.jsx | 0 .../{InventoryGroups => }/InventoryGroupAdd/index.js | 0 .../InventoryGroupDetail/InventoryGroupDetail.jsx | 4 ++-- .../InventoryGroupDetail/InventoryGroupDetail.test.jsx | 0 .../{InventoryGroups => }/InventoryGroupDetail/index.js | 0 .../InventoryGroupEdit/InventoryGroupEdit.jsx | 0 .../InventoryGroupEdit/InventoryGroupEdit.test.jsx | 0 .../{InventoryGroups => }/InventoryGroupEdit/index.js | 0 .../InventoryGroupForm/InventoryGroupForm.jsx | 0 .../InventoryGroupForm/InventoryGroupForm.test.jsx | 0 .../{InventoryGroups => }/InventoryGroupForm/index.js | 0 .../src/screens/Inventory/InventoryGroups/InventoryGroups.jsx | 4 ++-- 17 files changed, 6 insertions(+), 6 deletions(-) rename awx/ui_next/src/screens/Inventory/{InventoryGroups => }/InventoryGroup/InventoryGroup.jsx (100%) rename awx/ui_next/src/screens/Inventory/{InventoryGroups => }/InventoryGroup/InventoryGroup.test.jsx (100%) rename awx/ui_next/src/screens/Inventory/{InventoryGroups => }/InventoryGroup/index.js (100%) rename awx/ui_next/src/screens/Inventory/{InventoryGroups => }/InventoryGroupAdd/InventoryGroupAdd.jsx (100%) rename awx/ui_next/src/screens/Inventory/{InventoryGroups => }/InventoryGroupAdd/InventoryGroupAdd.test.jsx (100%) rename awx/ui_next/src/screens/Inventory/{InventoryGroups => }/InventoryGroupAdd/index.js (100%) rename awx/ui_next/src/screens/Inventory/{InventoryGroups => }/InventoryGroupDetail/InventoryGroupDetail.jsx (96%) rename awx/ui_next/src/screens/Inventory/{InventoryGroups => }/InventoryGroupDetail/InventoryGroupDetail.test.jsx (100%) rename awx/ui_next/src/screens/Inventory/{InventoryGroups => }/InventoryGroupDetail/index.js (100%) rename awx/ui_next/src/screens/Inventory/{InventoryGroups => }/InventoryGroupEdit/InventoryGroupEdit.jsx (100%) rename awx/ui_next/src/screens/Inventory/{InventoryGroups => }/InventoryGroupEdit/InventoryGroupEdit.test.jsx (100%) rename awx/ui_next/src/screens/Inventory/{InventoryGroups => }/InventoryGroupEdit/index.js (100%) rename awx/ui_next/src/screens/Inventory/{InventoryGroups => }/InventoryGroupForm/InventoryGroupForm.jsx (100%) rename awx/ui_next/src/screens/Inventory/{InventoryGroups => }/InventoryGroupForm/InventoryGroupForm.test.jsx (100%) rename awx/ui_next/src/screens/Inventory/{InventoryGroups => }/InventoryGroupForm/index.js (100%) diff --git a/awx/ui_next/src/index.jsx b/awx/ui_next/src/index.jsx index 9227e6e7e3..06d5245d81 100644 --- a/awx/ui_next/src/index.jsx +++ b/awx/ui_next/src/index.jsx @@ -15,7 +15,7 @@ import CredentialTypes from '@screens/CredentialType'; import Dashboard from '@screens/Dashboard'; import Hosts from '@screens/Host'; import InstanceGroups from '@screens/InstanceGroup'; -import Inventories from '@screens/Inventory'; +import Inventory from '@screens/Inventory'; import InventoryScripts from '@screens/InventoryScript'; import { Jobs } from '@screens/Job'; import Login from '@screens/Login'; @@ -139,7 +139,7 @@ export function main(render) { { title: i18n._(t`Inventories`), path: '/inventories', - component: Inventories, + component: Inventory, }, { title: i18n._(t`Hosts`), diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.jsx rename to awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/InventoryGroup.test.jsx rename to awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/index.js b/awx/ui_next/src/screens/Inventory/InventoryGroup/index.js similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroup/index.js rename to awx/ui_next/src/screens/Inventory/InventoryGroup/index.js diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupAdd/InventoryGroupAdd.jsx similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.jsx rename to awx/ui_next/src/screens/Inventory/InventoryGroupAdd/InventoryGroupAdd.jsx diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupAdd/InventoryGroupAdd.test.jsx similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/InventoryGroupAdd.test.jsx rename to awx/ui_next/src/screens/Inventory/InventoryGroupAdd/InventoryGroupAdd.test.jsx diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/index.js b/awx/ui_next/src/screens/Inventory/InventoryGroupAdd/index.js similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupAdd/index.js rename to awx/ui_next/src/screens/Inventory/InventoryGroupAdd/index.js diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx similarity index 96% rename from awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx rename to awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx index 20d99538b8..28b662e2ee 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx @@ -57,7 +57,7 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { ); } else { - createdBy = i18n._(t`${formatDateString(inventoryGroup.created)} by`); + createdBy = formatDateString(inventoryGroup.created); } } @@ -71,7 +71,7 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { ); } else { - modifiedBy = i18n._(t`${formatDateString(inventoryGroup.modified)} by`); + modifiedBy = formatDateString(inventoryGroup.modified); } } diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.test.jsx similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/InventoryGroupDetail.test.jsx rename to awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.test.jsx diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/index.js b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/index.js similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupDetail/index.js rename to awx/ui_next/src/screens/Inventory/InventoryGroupDetail/index.js diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/InventoryGroupEdit.jsx similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.jsx rename to awx/ui_next/src/screens/Inventory/InventoryGroupEdit/InventoryGroupEdit.jsx diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/InventoryGroupEdit.test.jsx similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/InventoryGroupEdit.test.jsx rename to awx/ui_next/src/screens/Inventory/InventoryGroupEdit/InventoryGroupEdit.test.jsx diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/index.js b/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/index.js similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupEdit/index.js rename to awx/ui_next/src/screens/Inventory/InventoryGroupEdit/index.js diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupForm/InventoryGroupForm.jsx similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.jsx rename to awx/ui_next/src/screens/Inventory/InventoryGroupForm/InventoryGroupForm.jsx diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupForm/InventoryGroupForm.test.jsx similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/InventoryGroupForm.test.jsx rename to awx/ui_next/src/screens/Inventory/InventoryGroupForm/InventoryGroupForm.test.jsx diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/index.js b/awx/ui_next/src/screens/Inventory/InventoryGroupForm/index.js similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupForm/index.js rename to awx/ui_next/src/screens/Inventory/InventoryGroupForm/index.js diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx index e35e2dedb7..2917f3f96d 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx @@ -3,9 +3,9 @@ import { withI18n } from '@lingui/react'; import { Switch, Route, withRouter } from 'react-router-dom'; -import InventoryGroupAdd from './InventoryGroupAdd/InventoryGroupAdd'; +import InventoryGroupAdd from '../InventoryGroupAdd/InventoryGroupAdd'; -import InventoryGroup from './InventoryGroup/InventoryGroup'; +import InventoryGroup from '../InventoryGroup/InventoryGroup'; import InventoryGroupsList from './InventoryGroupsList'; function InventoryGroups({ setBreadcrumb, inventory, location, match }) { From 4b62d77015bb3b7c634025ef455314a3899b8124 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Fri, 13 Dec 2019 10:05:17 -0500 Subject: [PATCH 8/9] Moves inventoryGroupForm into shared directory Updates InventoryGroups tests Adds ContentError functionalist to catch a case where a use might navigate to an Inventory that isn't associated to the shown inventoryGroup. --- .../Inventory/InventoryEdit/InventoryEdit.jsx | 8 +- .../InventoryGroup/InventoryGroup.jsx | 37 ++- .../InventoryGroup/InventoryGroup.test.jsx | 9 +- .../InventoryGroupAdd/InventoryGroupAdd.jsx | 2 +- .../InventoryGroupDetail.jsx | 12 +- .../InventoryGroupEdit/InventoryGroupEdit.jsx | 2 +- .../Inventory/InventoryGroupForm/index.js | 1 - .../InventoryGroups/InventoryGroups.test.jsx | 224 +++--------------- .../InventoryGroupsList.test.jsx | 217 +++++++++++++++++ .../InventoryGroupForm.jsx | 0 .../InventoryGroupForm.test.jsx | 0 11 files changed, 297 insertions(+), 215 deletions(-) delete mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroupForm/index.js create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.jsx rename awx/ui_next/src/screens/Inventory/{InventoryGroupForm => shared}/InventoryGroupForm.jsx (100%) rename awx/ui_next/src/screens/Inventory/{InventoryGroupForm => shared}/InventoryGroupForm.test.jsx (100%) diff --git a/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx b/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx index 131787ae95..2ec78aef4a 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx @@ -15,7 +15,7 @@ import { getAddedAndRemoved } from '../../../util/lists'; function InventoryEdit({ history, i18n, inventory }) { const [error, setError] = useState(null); const [associatedInstanceGroups, setInstanceGroups] = useState(null); - const [isLoading, setIsLoading] = useState(true); + const [contentLoading, setContentLoading] = useState(true); const [credentialTypeId, setCredentialTypeId] = useState(null); useEffect(() => { @@ -39,11 +39,11 @@ function InventoryEdit({ history, i18n, inventory }) { } catch (err) { setError(err); } finally { - setIsLoading(false); + setContentLoading(false); } }; loadData(); - }, [inventory.id, isLoading, inventory, credentialTypeId]); + }, [inventory.id, contentLoading, inventory, credentialTypeId]); const handleCancel = () => { history.push('/inventories'); @@ -85,7 +85,7 @@ function InventoryEdit({ history, i18n, inventory }) { history.push(`${url}`); } }; - if (isLoading) { + if (contentLoading) { return ; } if (error) { diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx index db03a1ecf2..1c510f4fe6 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx @@ -14,8 +14,8 @@ import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail'; function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { const [inventoryGroup, setInventoryGroup] = useState(null); - const [hasContentLoading, setHasContentLoading] = useState(true); - const [contentError, setHasContentError] = useState(null); + const [contentLoading, setContentLoading] = useState(true); + const [contentError, setContentError] = useState(null); useEffect(() => { const loadData = async () => { @@ -24,9 +24,9 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { setInventoryGroup(data); setBreadcrumb(inventory, data); } catch (err) { - setHasContentError(err); + setContentError(err); } finally { - setHasContentLoading(false); + setContentLoading(false); } }; @@ -64,12 +64,32 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { 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 ; + } + + if ( + inventoryGroup.summary_fields.inventory.id !== parseInt(match.params.id, 10) + ) { + return ( + + {inventoryGroup && ( + + {i18n._(t`View Inventory Groups`)} + + )} + + ); + } + if (contentError) { return ; } - if (hasContentLoading) { - return ; - } let cardHeader = null; if ( @@ -80,12 +100,11 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { ); } - return ( <> {cardHeader} diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx index 4c1b50c95b..6273de12d8 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { GroupsAPI } from '@api'; +import { Route } from 'react-router-dom'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; @@ -14,6 +15,7 @@ GroupsAPI.readDetail.mockResolvedValue({ description: 'Bar', variables: 'bizz: buzz', summary_fields: { + inventory: { id: 1 }, created_by: { id: 1, name: 'Athena' }, modified_by: { id: 1, name: 'Apollo' }, }, @@ -29,7 +31,12 @@ describe('', () => { }); await act(async () => { wrapper = mountWithContexts( - {}} />, + ( + {}} inventory={inventory} /> + )} + />, { context: { router: { diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupAdd/InventoryGroupAdd.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupAdd/InventoryGroupAdd.jsx index 720e9d4b3c..eff8a2fe10 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupAdd/InventoryGroupAdd.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupAdd/InventoryGroupAdd.jsx @@ -4,7 +4,7 @@ import { withRouter } from 'react-router-dom'; import { GroupsAPI } from '@api'; import { Card } from '@patternfly/react-core'; -import InventoryGroupForm from '../InventoryGroupForm/InventoryGroupForm'; +import InventoryGroupForm from '../shared/InventoryGroupForm'; function InventoryGroupsAdd({ history, inventory, setBreadcrumb }) { const [error, setError] = useState(null); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx index 28b662e2ee..8cf97cffc0 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx @@ -33,6 +33,9 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { summary_fields: { created_by, modified_by }, created, modified, + name, + description, + variables, } = inventoryGroup; const [error, setError] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -78,16 +81,13 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { return ( - - + + diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/InventoryGroupEdit.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/InventoryGroupEdit.jsx index 6ff0e58c58..230314ce7c 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/InventoryGroupEdit.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/InventoryGroupEdit.jsx @@ -3,7 +3,7 @@ import { withI18n } from '@lingui/react'; import { withRouter } from 'react-router-dom'; import { GroupsAPI } from '@api'; -import InventoryGroupForm from '../InventoryGroupForm/InventoryGroupForm'; +import InventoryGroupForm from '../shared/InventoryGroupForm'; function InventoryGroupEdit({ history, inventoryGroup, inventory, match }) { const [error, setError] = useState(null); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupForm/index.js b/awx/ui_next/src/screens/Inventory/InventoryGroupForm/index.js deleted file mode 100644 index 090b2c2f8a..0000000000 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupForm/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './InventoryGroupForm'; diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx index 8c60d8bfbd..d5a3665247 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx @@ -1,81 +1,25 @@ import React from 'react'; -import { act } from 'react-dom/test-utils'; import { Route } from 'react-router-dom'; +import { mountWithContexts } from '@testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; -import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; -import { InventoriesAPI, GroupsAPI } from '@api'; -import InventoryGroupsList from './InventoryGroupsList'; +import InventoryGroups from './InventoryGroups'; -jest.mock('@api'); - -const mockGroups = [ - { - id: 1, - type: 'group', - name: 'foo', - inventory: 1, - url: '/api/v2/groups/1', - summary_fields: { - user_capabilities: { - delete: true, - edit: true, - }, - }, - }, - { - id: 2, - type: 'group', - name: 'bar', - inventory: 1, - url: '/api/v2/groups/2', - summary_fields: { - user_capabilities: { - delete: true, - edit: true, - }, - }, - }, - { - id: 3, - type: 'group', - name: 'baz', - inventory: 1, - url: '/api/v2/groups/3', - summary_fields: { - user_capabilities: { - delete: false, - edit: false, - }, - }, - }, -]; - -describe('', () => { - let wrapper; - - beforeEach(async () => { - InventoriesAPI.readGroups.mockResolvedValue({ - data: { - count: mockGroups.length, - results: mockGroups, - }, - }); - InventoriesAPI.readGroupsOptions.mockResolvedValue({ - data: { - actions: { - GET: {}, - POST: {}, - }, - }, - }); +describe('', () => { + test('initially renders successfully', async () => { + let wrapper; const history = createMemoryHistory({ - initialEntries: ['/inventories/inventory/3/groups'], + initialEntries: ['/inventories/inventory/1/groups'], }); + const inventory = { id: 1, name: 'Foo' }; + await act(async () => { wrapper = mountWithContexts( } + component={() => ( + {}} inventory={inventory} /> + )} />, { context: { @@ -84,134 +28,30 @@ describe('', () => { } ); }); - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - }); - - test('initially renders successfully', () => { + expect(wrapper.length).toBe(1); expect(wrapper.find('InventoryGroupsList').length).toBe(1); }); - - test('should fetch groups from api and render them in the list', async () => { - expect(InventoriesAPI.readGroups).toHaveBeenCalled(); - expect(wrapper.find('InventoryGroupItem').length).toBe(3); - }); - - test('should check and uncheck the row item', async () => { - expect( - wrapper.find('PFDataListCheck[id="select-group-1"]').props().checked - ).toBe(false); - + test('test that InventoryGroupsAdd renders', async () => { + const history = createMemoryHistory({ + initialEntries: ['/inventories/inventory/1/groups/add'], + }); + const inventory = { id: 1, name: 'Foo' }; + let wrapper; await act(async () => { - wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')( - true - ); - }); - wrapper.update(); - expect( - wrapper.find('PFDataListCheck[id="select-group-1"]').props().checked - ).toBe(true); - - await act(async () => { - wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')( - false - ); - }); - wrapper.update(); - expect( - wrapper.find('PFDataListCheck[id="select-group-1"]').props().checked - ).toBe(false); - }); - - test('should check all row items when select all is checked', async () => { - wrapper.find('PFDataListCheck').forEach(el => { - expect(el.props().checked).toBe(false); - }); - await act(async () => { - wrapper.find('Checkbox#select-all').invoke('onChange')(true); - }); - wrapper.update(); - wrapper.find('PFDataListCheck').forEach(el => { - expect(el.props().checked).toBe(true); - }); - await act(async () => { - wrapper.find('Checkbox#select-all').invoke('onChange')(false); - }); - wrapper.update(); - wrapper.find('PFDataListCheck').forEach(el => { - expect(el.props().checked).toBe(false); - }); - }); - - test('should show content error when api throws error on initial render', async () => { - InventoriesAPI.readGroupsOptions.mockImplementation(() => - Promise.reject(new Error()) - ); - await act(async () => { - wrapper = mountWithContexts(); - }); - await waitForElement(wrapper, 'ContentError', el => el.length === 1); - }); - - test('should show content error if groups are not successfully fetched from api', async () => { - InventoriesAPI.readGroups.mockImplementation(() => - Promise.reject(new Error()) - ); - await act(async () => { - wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')(); - }); - wrapper.update(); - await act(async () => { - wrapper.find('Toolbar Button[aria-label="Delete"]').invoke('onClick')(); - }); - await waitForElement( - wrapper, - 'InventoryGroupsDeleteModal', - el => el.props().isModalOpen === true - ); - await act(async () => { - wrapper - .find('ModalBoxFooter Button[aria-label="Delete"]') - .invoke('onClick')(); - }); - await waitForElement(wrapper, 'ContentError', el => el.length === 1); - }); - - test('should show error modal when group is not successfully deleted from api', async () => { - GroupsAPI.destroy.mockRejectedValue( - new Error({ - response: { - config: { - method: 'delete', - url: '/api/v2/groups/1', + wrapper = mountWithContexts( + ( + {}} inventory={inventory} /> + )} + />, + { + context: { + router: { history, route: { location: history.location } }, }, - data: 'An error occurred', - }, - }) - ); - await act(async () => { - wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')(); - }); - wrapper.update(); - await act(async () => { - wrapper.find('Toolbar Button[aria-label="Delete"]').invoke('onClick')(); - }); - await waitForElement( - wrapper, - 'InventoryGroupsDeleteModal', - el => el.props().isModalOpen === true - ); - await act(async () => { - wrapper.find('Radio[id="radio-delete"]').invoke('onChange')(); - }); - wrapper.update(); - await act(async () => { - wrapper - .find('ModalBoxFooter Button[aria-label="Delete"]') - .invoke('onClick')(); - }); - await waitForElement(wrapper, { title: 'Error!', variant: 'danger' }); - await act(async () => { - wrapper.find('ModalBoxCloseButton').invoke('onClose')(); + } + ); }); + expect(wrapper.find('InventoryGroupsAdd').length).toBe(1); }); }); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.jsx new file mode 100644 index 0000000000..8c60d8bfbd --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.jsx @@ -0,0 +1,217 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { Route } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; +import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; +import { InventoriesAPI, GroupsAPI } from '@api'; +import InventoryGroupsList from './InventoryGroupsList'; + +jest.mock('@api'); + +const mockGroups = [ + { + id: 1, + type: 'group', + name: 'foo', + inventory: 1, + url: '/api/v2/groups/1', + summary_fields: { + user_capabilities: { + delete: true, + edit: true, + }, + }, + }, + { + id: 2, + type: 'group', + name: 'bar', + inventory: 1, + url: '/api/v2/groups/2', + summary_fields: { + user_capabilities: { + delete: true, + edit: true, + }, + }, + }, + { + id: 3, + type: 'group', + name: 'baz', + inventory: 1, + url: '/api/v2/groups/3', + summary_fields: { + user_capabilities: { + delete: false, + edit: false, + }, + }, + }, +]; + +describe('', () => { + let wrapper; + + beforeEach(async () => { + InventoriesAPI.readGroups.mockResolvedValue({ + data: { + count: mockGroups.length, + results: mockGroups, + }, + }); + InventoriesAPI.readGroupsOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + }, + }); + const history = createMemoryHistory({ + initialEntries: ['/inventories/inventory/3/groups'], + }); + await act(async () => { + wrapper = mountWithContexts( + } + />, + { + context: { + router: { history, route: { location: history.location } }, + }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + test('initially renders successfully', () => { + expect(wrapper.find('InventoryGroupsList').length).toBe(1); + }); + + test('should fetch groups from api and render them in the list', async () => { + expect(InventoriesAPI.readGroups).toHaveBeenCalled(); + expect(wrapper.find('InventoryGroupItem').length).toBe(3); + }); + + test('should check and uncheck the row item', async () => { + expect( + wrapper.find('PFDataListCheck[id="select-group-1"]').props().checked + ).toBe(false); + + await act(async () => { + wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')( + true + ); + }); + wrapper.update(); + expect( + wrapper.find('PFDataListCheck[id="select-group-1"]').props().checked + ).toBe(true); + + await act(async () => { + wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')( + false + ); + }); + wrapper.update(); + expect( + wrapper.find('PFDataListCheck[id="select-group-1"]').props().checked + ).toBe(false); + }); + + test('should check all row items when select all is checked', async () => { + wrapper.find('PFDataListCheck').forEach(el => { + expect(el.props().checked).toBe(false); + }); + await act(async () => { + wrapper.find('Checkbox#select-all').invoke('onChange')(true); + }); + wrapper.update(); + wrapper.find('PFDataListCheck').forEach(el => { + expect(el.props().checked).toBe(true); + }); + await act(async () => { + wrapper.find('Checkbox#select-all').invoke('onChange')(false); + }); + wrapper.update(); + wrapper.find('PFDataListCheck').forEach(el => { + expect(el.props().checked).toBe(false); + }); + }); + + test('should show content error when api throws error on initial render', async () => { + InventoriesAPI.readGroupsOptions.mockImplementation(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts(); + }); + await waitForElement(wrapper, 'ContentError', el => el.length === 1); + }); + + test('should show content error if groups are not successfully fetched from api', async () => { + InventoriesAPI.readGroups.mockImplementation(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')(); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Toolbar Button[aria-label="Delete"]').invoke('onClick')(); + }); + await waitForElement( + wrapper, + 'InventoryGroupsDeleteModal', + el => el.props().isModalOpen === true + ); + await act(async () => { + wrapper + .find('ModalBoxFooter Button[aria-label="Delete"]') + .invoke('onClick')(); + }); + await waitForElement(wrapper, 'ContentError', el => el.length === 1); + }); + + test('should show error modal when group is not successfully deleted from api', async () => { + GroupsAPI.destroy.mockRejectedValue( + new Error({ + response: { + config: { + method: 'delete', + url: '/api/v2/groups/1', + }, + data: 'An error occurred', + }, + }) + ); + await act(async () => { + wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')(); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Toolbar Button[aria-label="Delete"]').invoke('onClick')(); + }); + await waitForElement( + wrapper, + 'InventoryGroupsDeleteModal', + el => el.props().isModalOpen === true + ); + await act(async () => { + wrapper.find('Radio[id="radio-delete"]').invoke('onChange')(); + }); + wrapper.update(); + await act(async () => { + wrapper + .find('ModalBoxFooter Button[aria-label="Delete"]') + .invoke('onClick')(); + }); + await waitForElement(wrapper, { title: 'Error!', variant: 'danger' }); + await act(async () => { + wrapper.find('ModalBoxCloseButton').invoke('onClose')(); + }); + }); +}); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupForm/InventoryGroupForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroupForm/InventoryGroupForm.jsx rename to awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupForm/InventoryGroupForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.test.jsx similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroupForm/InventoryGroupForm.test.jsx rename to awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.test.jsx From 259e53f59d46f3541b0e8ce82d03e9d31269bb28 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Fri, 13 Dec 2019 14:20:52 -0500 Subject: [PATCH 9/9] Fixes failing zuul test --- .../InventoryGroups/InventoryGroups.test.jsx | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx index d5a3665247..935ef7bb04 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx @@ -1,10 +1,11 @@ import React from 'react'; -import { Route } from 'react-router-dom'; import { mountWithContexts } from '@testUtils/enzymeHelpers'; import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; import InventoryGroups from './InventoryGroups'; +jest.mock('@api'); + describe('', () => { test('initially renders successfully', async () => { let wrapper; @@ -15,12 +16,8 @@ describe('', () => { await act(async () => { wrapper = mountWithContexts( - ( - {}} inventory={inventory} /> - )} - />, + {}} inventory={inventory} />, + { context: { router: { history, route: { location: history.location } }, @@ -39,12 +36,7 @@ describe('', () => { let wrapper; await act(async () => { wrapper = mountWithContexts( - ( - {}} inventory={inventory} /> - )} - />, + {}} inventory={inventory} />, { context: { router: { history, route: { location: history.location } },