From 585ca082e330af99ae7bd26b3a77366ad3138375 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Thu, 28 May 2020 10:20:15 -0400 Subject: [PATCH] Improves naming and updates resource list and adds search functionality --- .../components/AddRole/AddResourceRole.jsx | 4 +- .../components/AddRole/SelectResourceStep.jsx | 202 ++++++++---------- .../AddRole/SelectResourceStep.test.jsx | 99 +++------ .../UserAndTeamAccessAdd.jsx | 9 +- .../UserAndTeamAccessAdd.test.jsx | 11 +- .../getResourceAccessConfig.js} | 14 +- .../index.js | 0 .../Team/TeamAccess/TeamAccessList.jsx | 2 +- .../User/UserAccess/UserAccessList.jsx | 2 +- 9 files changed, 147 insertions(+), 196 deletions(-) rename awx/ui_next/src/components/{UserAccessAdd => UserAndTeamAccessAdd}/UserAndTeamAccessAdd.jsx (94%) rename awx/ui_next/src/components/{UserAccessAdd => UserAndTeamAccessAdd}/UserAndTeamAccessAdd.test.jsx (94%) rename awx/ui_next/src/components/{UserAccessAdd/resources.data.jsx => UserAndTeamAccessAdd/getResourceAccessConfig.js} (90%) rename awx/ui_next/src/components/{UserAccessAdd => UserAndTeamAccessAdd}/index.js (100%) diff --git a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx index 639a6e8128..5727a78c74 100644 --- a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx +++ b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx @@ -258,7 +258,7 @@ class AddResourceRole extends React.Component { sortColumns={userSortColumns} displayKey="username" onRowClick={this.handleResourceCheckboxClick} - onSearch={readUsers} + fetchItems={readUsers} selectedLabel={i18n._(t`Selected`)} selectedResourceRows={selectedResourceRows} sortedColumnKey="username" @@ -269,7 +269,7 @@ class AddResourceRole extends React.Component { searchColumns={teamSearchColumns} sortColumns={teamSortColumns} onRowClick={this.handleResourceCheckboxClick} - onSearch={readTeams} + fetchItems={readTeams} selectedLabel={i18n._(t`Selected`)} selectedResourceRows={selectedResourceRows} /> diff --git a/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx b/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx index 9427bca17b..461327587e 100644 --- a/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx +++ b/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx @@ -1,8 +1,10 @@ -import React, { Fragment } from 'react'; +import React, { Fragment, useCallback, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { withRouter } from 'react-router-dom'; +import { withRouter, useLocation } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; +import useRequest from '../../util/useRequest'; + import { SearchColumns, SortColumns } from '../../types'; import PaginatedDataList from '../PaginatedDataList'; import DataListToolbar from '../DataListToolbar'; @@ -10,124 +12,94 @@ import CheckboxListItem from '../CheckboxListItem'; import SelectedList from '../SelectedList'; import { getQSConfig, parseQueryString } from '../../util/qs'; -class SelectResourceStep extends React.Component { - constructor(props) { - super(props); +const QS_Config = sortColumns => { + return getQSConfig('resource', { + page: 1, + page_size: 5, + order_by: `${ + sortColumns.filter(col => col.key === 'name').length ? 'name' : 'username' + }`, + }); +}; +function SelectResourceStep({ + searchColumns, + sortColumns, + displayKey, + onRowClick, + selectedLabel, + selectedResourceRows, + fetchItems, + i18n, +}) { + const location = useLocation(); - this.state = { - isInitialized: false, - count: null, - error: false, + const { + isLoading, + error, + request: readResourceList, + result: { resources, itemCount }, + } = useRequest( + useCallback(async () => { + const queryParams = parseQueryString( + QS_Config(sortColumns), + location.search + ); + + const { + data: { count, results }, + } = await fetchItems(queryParams); + return { resources: results, itemCount: count }; + }, [location, fetchItems, sortColumns]), + { resources: [], - }; - - this.qsConfig = getQSConfig('resource', { - page: 1, - page_size: 5, - order_by: `${ - props.sortColumns.filter(col => col.key === 'name').length - ? 'name' - : 'username' - }`, - }); - } - - componentDidMount() { - this.readResourceList(); - } - - componentDidUpdate(prevProps) { - const { location } = this.props; - if (location !== prevProps.location) { - this.readResourceList(); + itemCount: 0, } - } + ); - async readResourceList() { - const { onSearch, location } = this.props; - const queryParams = parseQueryString(this.qsConfig, location.search); + useEffect(() => { + readResourceList(); + }, [readResourceList]); - this.setState({ - isLoading: true, - error: false, - }); - try { - const { data } = await onSearch(queryParams); - const { count, results } = data; - - this.setState({ - resources: results, - count, - isInitialized: true, - isLoading: false, - error: false, - }); - } catch (err) { - this.setState({ - isLoading: false, - error: true, - }); - } - } - - render() { - const { isInitialized, isLoading, count, error, resources } = this.state; - - const { - searchColumns, - sortColumns, - displayKey, - onRowClick, - selectedLabel, - selectedResourceRows, - i18n, - } = this.props; - - return ( - - {isInitialized && ( - -
- {i18n._( - t`Choose the resources that will be receiving new roles. You'll be able to select the roles to apply in the next step. Note that the resources chosen here will receive all roles chosen in the next step.` - )} -
- {selectedResourceRows.length > 0 && ( - - )} - ( - i.id === item.id)} - itemId={item.id} - key={item.id} - name={item[displayKey]} - label={item[displayKey]} - onSelect={() => onRowClick(item)} - onDeselect={() => onRowClick(item)} - /> - )} - renderToolbar={props => } - showPageSizeOptions={false} - /> -
+ return ( + +
+ {i18n._( + t`Choose the resources that will be receiving new roles. You'll be able to select the roles to apply in the next step. Note that the resources chosen here will receive all roles chosen in the next step.` )} - {error ?
error
: ''} - - ); - } +
+ {selectedResourceRows.length > 0 && ( + + )} + ( + i.id === item.id)} + itemId={item.id} + key={item.id} + name={item[displayKey]} + label={item[displayKey]} + onSelect={() => onRowClick(item)} + onDeselect={() => onRowClick(item)} + /> + )} + renderToolbar={props => } + showPageSizeOptions={false} + /> +
+ ); } SelectResourceStep.propTypes = { @@ -135,7 +107,7 @@ SelectResourceStep.propTypes = { sortColumns: SortColumns, displayKey: PropTypes.string, onRowClick: PropTypes.func, - onSearch: PropTypes.func.isRequired, + fetchItems: PropTypes.func.isRequired, selectedLabel: PropTypes.string, selectedResourceRows: PropTypes.arrayOf(PropTypes.object), }; diff --git a/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx b/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx index e925044ed5..d309ea706f 100644 --- a/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx +++ b/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx @@ -1,7 +1,11 @@ import React from 'react'; -import { createMemoryHistory } from 'history'; +import { act } from 'react-dom/test-utils'; + import { shallow } from 'enzyme'; -import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import { + mountWithContexts, + waitForElement, +} from '../../../testUtils/enzymeHelpers'; import { sleep } from '../../../testUtils/testUtils'; import SelectResourceStep from './SelectResourceStep'; @@ -30,12 +34,12 @@ describe('', () => { sortColumns={sortColumns} displayKey="username" onRowClick={() => {}} - onSearch={() => {}} + fetchItems={() => {}} /> ); }); - test('fetches resources on mount', async () => { + test('fetches resources on mount and adds items to list', async () => { const handleSearch = jest.fn().mockResolvedValue({ data: { count: 2, @@ -45,61 +49,24 @@ describe('', () => { ], }, }); - mountWithContexts( - {}} - onSearch={handleSearch} - /> - ); + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + {}} + fetchItems={handleSearch} + /> + ); + }); expect(handleSearch).toHaveBeenCalledWith({ order_by: 'username', page: 1, page_size: 5, }); - }); - - test('readResourceList properly adds rows to state', async () => { - const selectedResourceRows = [{ id: 1, username: 'foo', url: 'item/1' }]; - const handleSearch = jest.fn().mockResolvedValue({ - data: { - count: 2, - results: [ - { id: 1, username: 'foo', url: 'item/1' }, - { id: 2, username: 'bar', url: 'item/2' }, - ], - }, - }); - const history = createMemoryHistory({ - initialEntries: [ - '/organizations/1/access?resource.page=1&resource.order_by=-username', - ], - }); - const wrapper = mountWithContexts( - {}} - onSearch={handleSearch} - selectedResourceRows={selectedResourceRows} - />, - { - context: { router: { history, route: { location: history.location } } }, - } - ).find('SelectResourceStep'); - await wrapper.instance().readResourceList(); - expect(handleSearch).toHaveBeenCalledWith({ - order_by: '-username', - page: 1, - page_size: 5, - }); - expect(wrapper.state('resources')).toEqual([ - { id: 1, username: 'foo', url: 'item/1' }, - { id: 2, username: 'bar', url: 'item/2' }, - ]); + waitForElement(wrapper, 'CheckBoxListItem', el => el.length === 2); }); test('clicking on row fires callback with correct params', async () => { @@ -111,20 +78,24 @@ describe('', () => { { id: 2, username: 'bar', url: 'item/2' }, ], }; - const wrapper = mountWithContexts( - ({ data })} - selectedResourceRows={[]} - /> - ); + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + ({ data })} + selectedResourceRows={[]} + /> + ); + }); await sleep(0); wrapper.update(); const checkboxListItemWrapper = wrapper.find('CheckboxListItem'); expect(checkboxListItemWrapper.length).toBe(2); + checkboxListItemWrapper .first() .find('input[type="checkbox"]') diff --git a/awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.jsx b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx similarity index 94% rename from awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.jsx rename to awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx index ad2c254f06..ceac1a32f5 100644 --- a/awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.jsx +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx @@ -11,7 +11,7 @@ import Wizard from '../Wizard/Wizard'; import useSelected from '../../util/useSelected'; import SelectResourceStep from '../AddRole/SelectResourceStep'; import SelectRoleStep from '../AddRole/SelectRoleStep'; -import getResourceTypes from './resources.data'; +import getResourceAccessConfig from './getResourceAccessConfig'; const Grid = styled.div` display: grid; @@ -28,7 +28,7 @@ function UserAndTeamAccessAdd({ apiModel, onClose, }) { - const [selectedResourceType, setSelectedResourceType] = useState(); + const [selectedResourceType, setSelectedResourceType] = useState(null); const [stepIdReached, setStepIdReached] = useState(1); const { id: userId } = useParams(); const { @@ -70,7 +70,7 @@ function UserAndTeamAccessAdd({ name: i18n._(t`Add resource type`), component: ( - {getResourceTypes(i18n).map(resource => ( + {getResourceAccessConfig(i18n).map(resource => ( ), + enableNext: selectedResourceType !== null, }, { id: 2, @@ -94,7 +95,7 @@ function UserAndTeamAccessAdd({ sortColumns={selectedResourceType.sortColumns} displayKey="name" onRowClick={handleResourceSelect} - onSearch={selectedResourceType.fetchItems} + fetchItems={selectedResourceType.fetchItems} selectedLabel={i18n._(t`Selected`)} selectedResourceRows={resourcesSelected} sortedColumnKey="username" diff --git a/awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.test.jsx b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx similarity index 94% rename from awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.test.jsx rename to awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx index 0a703a7024..b80c24a493 100644 --- a/awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.test.jsx +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx @@ -65,9 +65,10 @@ describe('', () => { expect(wrapper.find('PFWizard').length).toBe(1); }); test('should disable steps', async () => { + expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true); expect( wrapper - .find('WizardNavItem[text="Select ttems from list"]') + .find('WizardNavItem[text="Select items from list"]') .prop('isDisabled') ).toBe(true); expect( @@ -93,7 +94,7 @@ describe('', () => { ).toBe(false); expect( wrapper - .find('WizardNavItem[text="Select ttems from list"]') + .find('WizardNavItem[text="Select items from list"]') .prop('isDisabled') ).toBe(false); expect( @@ -119,6 +120,12 @@ describe('', () => { await act(async () => wrapper.find('Button[type="submit"]').prop('onClick')() ); + expect(JobTemplatesAPI.read).toHaveBeenCalledWith({ + order_by: 'name', + page: 1, + page_size: 5, + }); + await waitForElement(wrapper, 'SelectResourceStep', el => el.length > 0); expect(JobTemplatesAPI.read).toHaveBeenCalled(); await act(async () => diff --git a/awx/ui_next/src/components/UserAccessAdd/resources.data.jsx b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js similarity index 90% rename from awx/ui_next/src/components/UserAccessAdd/resources.data.jsx rename to awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js index 0000036c59..718476e70e 100644 --- a/awx/ui_next/src/components/UserAccessAdd/resources.data.jsx +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js @@ -8,7 +8,7 @@ import { OrganizationsAPI, } from '../../api'; -export default function getResourceTypes(i18n) { +export default function getResourceAccessConfig(i18n) { return [ { selectedResource: 'jobTemplate', @@ -38,7 +38,7 @@ export default function getResourceTypes(i18n) { key: 'name', }, ], - fetchItems: () => JobTemplatesAPI.read(), + fetchItems: queryParams => JobTemplatesAPI.read(queryParams), }, { selectedResource: 'workflowJobTemplate', @@ -68,7 +68,7 @@ export default function getResourceTypes(i18n) { key: 'name', }, ], - fetchItems: () => WorkflowJobTemplatesAPI.read(), + fetchItems: queryParams => WorkflowJobTemplatesAPI.read(queryParams), }, { selectedResource: 'credential', @@ -109,7 +109,7 @@ export default function getResourceTypes(i18n) { key: 'name', }, ], - fetchItems: () => CredentialsAPI.read(), + fetchItems: queryParams => CredentialsAPI.read(queryParams), }, { selectedResource: 'inventory', @@ -135,7 +135,7 @@ export default function getResourceTypes(i18n) { key: 'name', }, ], - fetchItems: () => InventoriesAPI.read(), + fetchItems: queryParams => InventoriesAPI.read(queryParams), }, { selectedResource: 'project', @@ -176,7 +176,7 @@ export default function getResourceTypes(i18n) { key: 'name', }, ], - fetchItems: () => ProjectsAPI.read(), + fetchItems: queryParams => ProjectsAPI.read(queryParams), }, { selectedResource: 'organization', @@ -202,7 +202,7 @@ export default function getResourceTypes(i18n) { key: 'name', }, ], - fetchItems: () => OrganizationsAPI.read(), + fetchItems: queryParams => OrganizationsAPI.read(queryParams), }, ]; } diff --git a/awx/ui_next/src/components/UserAccessAdd/index.js b/awx/ui_next/src/components/UserAndTeamAccessAdd/index.js similarity index 100% rename from awx/ui_next/src/components/UserAccessAdd/index.js rename to awx/ui_next/src/components/UserAndTeamAccessAdd/index.js diff --git a/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.jsx b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.jsx index 6c6f2c2910..516c090cdd 100644 --- a/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.jsx +++ b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.jsx @@ -12,7 +12,7 @@ import DataListToolbar from '../../../components/DataListToolbar'; import PaginatedDataList from '../../../components/PaginatedDataList'; import { getQSConfig, parseQueryString } from '../../../util/qs'; import TeamAccessListItem from './TeamAccessListItem'; -import UserAndTeamAccessAdd from '../../../components/UserAccessAdd/UserAndTeamAccessAdd'; +import UserAndTeamAccessAdd from '../../../components/UserAndTeamAccessAdd/UserAndTeamAccessAdd'; const QS_CONFIG = getQSConfig('team', { page: 1, diff --git a/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx b/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx index b44a83f303..e48fa6f70d 100644 --- a/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx +++ b/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx @@ -10,7 +10,7 @@ import useRequest from '../../../util/useRequest'; import PaginatedDataList from '../../../components/PaginatedDataList'; import DatalistToolbar from '../../../components/DataListToolbar'; import UserAccessListItem from './UserAccessListItem'; -import UserAndTeamAccessAdd from '../../../components/UserAccessAdd/UserAndTeamAccessAdd'; +import UserAndTeamAccessAdd from '../../../components/UserAndTeamAccessAdd/UserAndTeamAccessAdd'; const QS_CONFIG = getQSConfig('roles', { page: 1,