From c997fcfc2cdf48b19cc9a19a075c118a73c80ac7 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Fri, 15 Nov 2019 16:57:56 -0500 Subject: [PATCH] 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));