From a1002b03fa33c591b6952358f33c5ca3de067f2b Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 27 Mar 2019 17:27:27 -0400 Subject: [PATCH 1/5] Add roles modal to org access list --- __tests__/components/AddResourceRole.test.jsx | 260 ++++++++++++++++++ __tests__/components/CheckboxCard.test.jsx | 17 ++ .../components/SelectResourceStep.test.jsx | 170 ++++++++++++ __tests__/components/SelectRoleStep.test.jsx | 63 +++++ __tests__/components/SelectableCard.test.jsx | 27 ++ package-lock.json | 12 +- src/api.js | 18 ++ src/components/AddRole/AddResourceRole.jsx | 258 +++++++++++++++++ src/components/AddRole/CheckboxCard.jsx | 50 ++++ src/components/AddRole/SelectResourceStep.jsx | 216 +++++++++++++++ src/components/AddRole/SelectRoleStep.jsx | 69 +++++ src/components/AddRole/SelectableCard.jsx | 39 +++ src/components/AddRole/styles.scss | 28 ++ .../DataListToolbar/DataListToolbar.jsx | 7 +- src/components/Lookup/Lookup.jsx | 2 +- src/components/Pagination/styles.scss | 1 + src/components/SelectedList/SelectedList.jsx | 56 +++- src/index.jsx | 1 + .../components/OrganizationAccessList.jsx | 52 +++- 19 files changed, 1322 insertions(+), 24 deletions(-) create mode 100644 __tests__/components/AddResourceRole.test.jsx create mode 100644 __tests__/components/CheckboxCard.test.jsx create mode 100644 __tests__/components/SelectResourceStep.test.jsx create mode 100644 __tests__/components/SelectRoleStep.test.jsx create mode 100644 __tests__/components/SelectableCard.test.jsx create mode 100644 src/components/AddRole/AddResourceRole.jsx create mode 100644 src/components/AddRole/CheckboxCard.jsx create mode 100644 src/components/AddRole/SelectResourceStep.jsx create mode 100644 src/components/AddRole/SelectRoleStep.jsx create mode 100644 src/components/AddRole/SelectableCard.jsx create mode 100644 src/components/AddRole/styles.scss diff --git a/__tests__/components/AddResourceRole.test.jsx b/__tests__/components/AddResourceRole.test.jsx new file mode 100644 index 0000000000..19cf538b91 --- /dev/null +++ b/__tests__/components/AddResourceRole.test.jsx @@ -0,0 +1,260 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { I18nProvider } from '@lingui/react'; +import AddResourceRole from '../../src/components/AddRole/AddResourceRole'; + +describe('', () => { + const readUsers = jest.fn().mockResolvedValue({ + data: { + count: 2, + results: [ + { id: 1, username: 'foo' }, + { id: 2, username: 'bar' } + ] + } + }); + const readTeams = jest.fn(); + const createUserRole = jest.fn(); + const createTeamRole = jest.fn(); + const api = { readUsers, readTeams, createUserRole, createTeamRole }; + const roles = { + admin_role: { + description: 'Can manage all aspects of the organization', + id: 1, + name: 'Admin' + }, + execute_role: { + description: 'May run any executable resources in the organization', + id: 2, + name: 'Execute' + } + }; + test('initially renders without crashing', () => { + mount( + + + + ); + }); + test('handleRoleCheckboxClick properly updates state', () => { + const wrapper = mount( + + + + ).find('AddResourceRole'); + wrapper.setState({ + selectedRoleRows: [ + { + description: 'Can manage all aspects of the organization', + name: 'Admin', + id: 1 + } + ] + }); + wrapper.instance().handleRoleCheckboxClick({ + description: 'Can manage all aspects of the organization', + name: 'Admin', + id: 1 + }); + expect(wrapper.state('selectedRoleRows')).toEqual([]); + wrapper.instance().handleRoleCheckboxClick({ + description: 'Can manage all aspects of the organization', + name: 'Admin', + id: 1 + }); + expect(wrapper.state('selectedRoleRows')).toEqual([{ + description: 'Can manage all aspects of the organization', + name: 'Admin', + id: 1 + }]); + }); + test('handleResourceCheckboxClick properly updates state', () => { + const wrapper = mount( + + + + ).find('AddResourceRole'); + wrapper.setState({ + selectedResourceRows: [ + { + id: 1, + username: 'foobar' + } + ] + }); + wrapper.instance().handleResourceCheckboxClick({ + id: 1, + username: 'foobar' + }); + expect(wrapper.state('selectedResourceRows')).toEqual([]); + wrapper.instance().handleResourceCheckboxClick({ + id: 1, + username: 'foobar' + }); + expect(wrapper.state('selectedResourceRows')).toEqual([{ + id: 1, + username: 'foobar' + }]); + }); + test('clicking user/team cards updates state', () => { + const spy = jest.spyOn(AddResourceRole.prototype, 'handleResourceSelect'); + const wrapper = mount( + + + + ).find('AddResourceRole'); + const selectableCardWrapper = wrapper.find('SelectableCard'); + expect(selectableCardWrapper.length).toBe(2); + selectableCardWrapper.first().simulate('click'); + expect(spy).toHaveBeenCalledWith('users'); + expect(wrapper.state('selectedResource')).toBe('users'); + selectableCardWrapper.at(1).simulate('click'); + expect(spy).toHaveBeenCalledWith('teams'); + expect(wrapper.state('selectedResource')).toBe('teams'); + }); + test('readUsers and readTeams call out to corresponding api functions', () => { + const wrapper = mount( + + + + ).find('AddResourceRole'); + wrapper.instance().readUsers({ + foo: 'bar' + }); + expect(readUsers).toHaveBeenCalledWith({ + foo: 'bar' + }); + wrapper.instance().readTeams({ + foo: 'bar' + }); + expect(readTeams).toHaveBeenCalledWith({ + foo: 'bar' + }); + }); + test('handleResourceSelect clears out selected lists and sets selectedResource', () => { + const wrapper = mount( + + + + ).find('AddResourceRole'); + wrapper.setState({ + selectedResource: 'teams', + selectedResourceRows: [ + { + id: 1, + username: 'foobar' + } + ], + selectedRoleRows: [ + { + description: 'Can manage all aspects of the organization', + id: 1, + name: 'Admin' + } + ] + }); + wrapper.instance().handleResourceSelect('users'); + expect(wrapper.state()).toEqual({ + selectedResource: 'users', + selectedResourceRows: [], + selectedRoleRows: [] + }); + wrapper.instance().handleResourceSelect('teams'); + expect(wrapper.state()).toEqual({ + selectedResource: 'teams', + selectedResourceRows: [], + selectedRoleRows: [] + }); + }); + test('handleWizardSave makes correct api calls, calls onSave when done', async () => { + const handleSave = jest.fn(); + const wrapper = mount( + + + + ).find('AddResourceRole'); + wrapper.setState({ + selectedResource: 'users', + selectedResourceRows: [ + { + id: 1, + username: 'foobar' + } + ], + selectedRoleRows: [ + { + description: 'Can manage all aspects of the organization', + id: 1, + name: 'Admin' + }, + { + description: 'May run any executable resources in the organization', + id: 2, + name: 'Execute' + } + ] + }); + await wrapper.instance().handleWizardSave(); + expect(createUserRole).toHaveBeenCalledTimes(2); + expect(handleSave).toHaveBeenCalled(); + wrapper.setState({ + selectedResource: 'teams', + selectedResourceRows: [ + { + id: 1, + name: 'foobar' + } + ], + selectedRoleRows: [ + { + description: 'Can manage all aspects of the organization', + id: 1, + name: 'Admin' + }, + { + description: 'May run any executable resources in the organization', + id: 2, + name: 'Execute' + } + ] + }); + await wrapper.instance().handleWizardSave(); + expect(createTeamRole).toHaveBeenCalledTimes(2); + expect(handleSave).toHaveBeenCalled(); + }); +}); diff --git a/__tests__/components/CheckboxCard.test.jsx b/__tests__/components/CheckboxCard.test.jsx new file mode 100644 index 0000000000..de02dca288 --- /dev/null +++ b/__tests__/components/CheckboxCard.test.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import CheckboxCard from '../../src/components/AddRole/CheckboxCard'; + +describe('', () => { + let wrapper; + test('initially renders without crashing', () => { + wrapper = mount( + + ); + expect(wrapper.length).toBe(1); + wrapper.unmount(); + }); +}); diff --git a/__tests__/components/SelectResourceStep.test.jsx b/__tests__/components/SelectResourceStep.test.jsx new file mode 100644 index 0000000000..33af41429b --- /dev/null +++ b/__tests__/components/SelectResourceStep.test.jsx @@ -0,0 +1,170 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { I18nProvider } from '@lingui/react'; +import SelectResourceStep from '../../src/components/AddRole/SelectResourceStep'; + +describe('', () => { + const columns = [ + { name: 'Username', key: 'username', isSortable: true } + ]; + afterEach(() => { + jest.restoreAllMocks(); + }); + test('initially renders without crashing', () => { + mount( + + + + ); + }); + test('fetches resources on mount', async () => { + const handleSearch = jest.fn().mockResolvedValue({ + data: { + count: 2, + results: [ + { id: 1, username: 'foo' }, + { id: 2, username: 'bar' } + ] + } + }); + mount( + + + + ); + expect(handleSearch).toHaveBeenCalledWith({ + is_superuser: false, + order_by: 'username', + page: 1, + page_size: 5 + }); + }); + test('readResourceList properly adds rows to state', async () => { + const selectedResourceRows = [ + { + id: 1, + username: 'foo' + } + ]; + const handleSearch = jest.fn().mockResolvedValue({ + data: { + count: 2, + results: [ + { id: 1, username: 'foo' }, + { id: 2, username: 'bar' } + ] + } + }); + const wrapper = await mount( + + + + ).find('SelectResourceStep'); + await wrapper.instance().readResourceList({ + page: 1, + order_by: '-username' + }); + expect(handleSearch).toHaveBeenCalledWith({ + is_superuser: false, + order_by: '-username', + page: 1 + }); + expect(wrapper.state('resources')).toEqual([ + { id: 1, username: 'foo' }, + { id: 2, username: 'bar' } + ]); + }); + test('handleSetPage calls readResourceList with correct params', () => { + const spy = jest.spyOn(SelectResourceStep.prototype, 'readResourceList'); + const wrapper = mount( + + + + ).find('SelectResourceStep'); + wrapper.setState({ sortOrder: 'descending' }); + wrapper.instance().handleSetPage(2); + expect(spy).toHaveBeenCalledWith({ page: 2, page_size: 5, order_by: '-username' }); + wrapper.setState({ sortOrder: 'ascending' }); + wrapper.instance().handleSetPage(2); + expect(spy).toHaveBeenCalledWith({ page: 2, page_size: 5, order_by: 'username' }); + }); + test('handleSort calls readResourceList with correct params', () => { + const spy = jest.spyOn(SelectResourceStep.prototype, 'readResourceList'); + const wrapper = mount( + + + + ).find('SelectResourceStep'); + wrapper.instance().handleSort('username', 'descending'); + expect(spy).toHaveBeenCalledWith({ page: 1, page_size: 5, order_by: '-username' }); + wrapper.instance().handleSort('username', 'ascending'); + expect(spy).toHaveBeenCalledWith({ page: 1, page_size: 5, order_by: 'username' }); + }); + test('clicking on row fires callback with correct params', () => { + const handleRowClick = jest.fn(); + const wrapper = mount( + + + + ); + const selectResourceStepWrapper = wrapper.find('SelectResourceStep'); + selectResourceStepWrapper.setState({ + resources: [ + { id: 1, username: 'foo' } + ] + }); + const checkboxListItemWrapper = wrapper.find('CheckboxListItem'); + expect(checkboxListItemWrapper.length).toBe(1); + checkboxListItemWrapper.first().find('input[type="checkbox"]').simulate('change', { target: { checked: true } }); + expect(handleRowClick).toHaveBeenCalledWith({ id: 1, username: 'foo' }); + }); +}); diff --git a/__tests__/components/SelectRoleStep.test.jsx b/__tests__/components/SelectRoleStep.test.jsx new file mode 100644 index 0000000000..857ec0a9f6 --- /dev/null +++ b/__tests__/components/SelectRoleStep.test.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import SelectRoleStep from '../../src/components/AddRole/SelectRoleStep'; + +describe('', () => { + let wrapper; + const roles = { + project_admin_role: { + id: 1, + name: 'Project Admin', + description: 'Can manage all projects of the organization' + }, + execute_role: { + id: 2, + name: 'Execute', + description: 'May run any executable resources in the organization' + } + }; + const selectedRoles = [ + { + id: 1, + name: 'Project Admin', + description: 'Can manage all projects of the organization' + } + ]; + const selectedResourceRows = [ + { + id: 1, + name: 'foo' + } + ]; + test('initially renders without crashing', () => { + wrapper = mount( + + ); + expect(wrapper.length).toBe(1); + wrapper.unmount(); + }); + test('clicking role fires onRolesClick callback', () => { + const onRolesClick = jest.fn(); + wrapper = mount( + + ); + const CheckboxCards = wrapper.find('CheckboxCard'); + expect(CheckboxCards.length).toBe(2); + CheckboxCards.first().prop('onSelect')(); + expect(onRolesClick).toBeCalledWith({ + id: 1, + name: 'Project Admin', + description: 'Can manage all projects of the organization' + }); + wrapper.unmount(); + }); +}); diff --git a/__tests__/components/SelectableCard.test.jsx b/__tests__/components/SelectableCard.test.jsx new file mode 100644 index 0000000000..b9608f6333 --- /dev/null +++ b/__tests__/components/SelectableCard.test.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import SelectableCard from '../../src/components/AddRole/SelectableCard'; + +describe('', () => { + let wrapper; + const onClick = jest.fn(); + test('initially renders without crashing when not selected', () => { + wrapper = mount( + + ); + expect(wrapper.length).toBe(1); + wrapper.unmount(); + }); + test('initially renders without crashing when selected', () => { + wrapper = mount( + + ); + expect(wrapper.length).toBe(1); + wrapper.unmount(); + }); +}); diff --git a/package-lock.json b/package-lock.json index 8f85c50e78..356499ff37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2219,7 +2219,7 @@ }, "ansi-colors": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "requires": { "ansi-wrap": "^0.1.0" @@ -3239,12 +3239,12 @@ }, "babel-plugin-syntax-class-properties": { "version": "6.13.0", - "resolved": "http://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=" }, "babel-plugin-syntax-flow": { "version": "6.18.0", - "resolved": "http://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", "integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=" }, "babel-plugin-syntax-jsx": { @@ -5421,7 +5421,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -6640,7 +6640,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -10625,7 +10625,7 @@ }, "kind-of": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=" }, "kleur": { diff --git a/src/api.js b/src/api.js index fcf8403fd4..bda5fd5319 100644 --- a/src/api.js +++ b/src/api.js @@ -5,6 +5,8 @@ const API_V2 = `${API_ROOT}v2/`; const API_CONFIG = `${API_V2}config/`; const API_ORGANIZATIONS = `${API_V2}organizations/`; const API_INSTANCE_GROUPS = `${API_V2}instance_groups/`; +const API_USERS = `${API_V2}users/`; +const API_TEAMS = `${API_V2}teams/`; const LOGIN_CONTENT_TYPE = 'application/x-www-form-urlencoded'; @@ -140,6 +142,22 @@ class APIClient { disassociate (url, id) { return this.http.post(url, { id, disassociate: true }); } + + readUsers (params) { + return this.http.get(API_USERS, { params }); + } + + readTeams (params) { + return this.http.get(API_TEAMS, { params }); + } + + createUserRole (userId, roleId) { + return this.http.post(`${API_USERS}${userId}/roles/`, { id: roleId }); + } + + createTeamRole (teamId, roleId) { + return this.http.post(`${API_TEAMS}${teamId}/roles/`, { id: roleId }); + } } export default APIClient; diff --git a/src/components/AddRole/AddResourceRole.jsx b/src/components/AddRole/AddResourceRole.jsx new file mode 100644 index 0000000000..d24250730b --- /dev/null +++ b/src/components/AddRole/AddResourceRole.jsx @@ -0,0 +1,258 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { I18n, i18nMark } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { + BackgroundImageSrc, + Wizard +} from '@patternfly/react-core'; + +import SelectResourceStep from './SelectResourceStep'; +import SelectRoleStep from './SelectRoleStep'; +import SelectableCard from './SelectableCard'; + +class AddResourceRole extends React.Component { + constructor (props) { + super(props); + + this.state = { + selectedResource: null, + selectedResourceRows: [], + selectedRoleRows: [] + }; + + this.handleResourceCheckboxClick = this.handleResourceCheckboxClick.bind(this); + this.handleResourceSelect = this.handleResourceSelect.bind(this); + this.handleRoleCheckboxClick = this.handleRoleCheckboxClick.bind(this); + this.handleWizardSave = this.handleWizardSave.bind(this); + this.readTeams = this.readTeams.bind(this); + this.readUsers = this.readUsers.bind(this); + } + + handleResourceCheckboxClick (user) { + const { selectedResourceRows } = this.state; + + const selectedIndex = selectedResourceRows + .findIndex(selectedRow => selectedRow.id === user.id); + + if (selectedIndex > -1) { + selectedResourceRows.splice(selectedIndex, 1); + this.setState({ selectedResourceRows }); + } else { + this.setState(prevState => ({ + selectedResourceRows: [...prevState.selectedResourceRows, user] + })); + } + } + + handleRoleCheckboxClick (role) { + const { selectedRoleRows } = this.state; + + const selectedIndex = selectedRoleRows + .findIndex(selectedRow => selectedRow.id === role.id); + + if (selectedIndex > -1) { + selectedRoleRows.splice(selectedIndex, 1); + this.setState({ selectedRoleRows }); + } else { + this.setState(prevState => ({ + selectedRoleRows: [...prevState.selectedRoleRows, role] + })); + } + } + + handleResourceSelect (resourceType) { + this.setState({ + selectedResource: resourceType, + selectedResourceRows: [], + selectedRoleRows: [] + }); + } + + async handleWizardSave () { + const { + onSave, + api + } = this.props; + const { + selectedResourceRows, + selectedRoleRows, + selectedResource + } = this.state; + + try { + const roleRequests = []; + + for (let i = 0; i < selectedResourceRows.length; i++) { + for (let j = 0; j < selectedRoleRows.length; j++) { + if (selectedResource === 'users') { + roleRequests.push( + api.createUserRole(selectedResourceRows[i].id, selectedRoleRows[j].id) + ); + } else if (selectedResource === 'teams') { + roleRequests.push( + api.createTeamRole(selectedResourceRows[i].id, selectedRoleRows[j].id) + ); + } + } + } + + await Promise.all(roleRequests); + onSave(); + } catch (err) { + // TODO: handle this error + } + } + + async readUsers (queryParams) { + const { api } = this.props; + return api.readUsers(queryParams); + } + + async readTeams (queryParams) { + const { api } = this.props; + return api.readTeams(queryParams); + } + + render () { + const { + selectedResource, + selectedResourceRows, + selectedRoleRows + } = this.state; + const { + onClose, + roles + } = this.props; + + const images = { + [BackgroundImageSrc.xs]: '/assets/images/pfbg_576.jpg', + [BackgroundImageSrc.xs2x]: '/assets/images/pfbg_576@2x.jpg', + [BackgroundImageSrc.sm]: '/assets/images/pfbg_768.jpg', + [BackgroundImageSrc.sm2x]: '/assets/images/pfbg_768@2x.jpg', + [BackgroundImageSrc.lg]: '/assets/images/pfbg_2000.jpg', + [BackgroundImageSrc.filter]: '/assets/images/background-filter.svg#image_overlay' + }; + + const userColumns = [ + { name: i18nMark('Username'), key: 'username', isSortable: true } + ]; + + const teamColumns = [ + { name: i18nMark('Name'), key: 'name', isSortable: true } + ]; + + const steps = [ + { + name: i18nMark('Select Users Or Teams'), + component: ( + + {({ i18n }) => ( +
+ this.handleResourceSelect('users')} + /> + this.handleResourceSelect('teams')} + /> +
+ )} +
+ ), + enableNext: selectedResource !== null + }, + { + name: i18nMark('Select items from list'), + component: ( + + {({ i18n }) => ( + + {selectedResource === 'users' && ( + + )} + {selectedResource === 'teams' && ( + + )} + + )} + + ), + enableNext: selectedResourceRows.length > 0 + }, + { + name: i18nMark('Apply roles'), + component: ( + + {({ i18n }) => ( + + )} + + ), + enableNext: selectedRoleRows.length > 0 + } + ]; + + return ( + + {({ i18n }) => ( + + )} + + ); + } +} + +AddResourceRole.propTypes = { + onClose: PropTypes.func.isRequired, + onSave: PropTypes.func.isRequired, + roles: PropTypes.shape() +}; + +AddResourceRole.defaultProps = { + roles: {} +}; + +export default AddResourceRole; diff --git a/src/components/AddRole/CheckboxCard.jsx b/src/components/AddRole/CheckboxCard.jsx new file mode 100644 index 0000000000..5486fa05f3 --- /dev/null +++ b/src/components/AddRole/CheckboxCard.jsx @@ -0,0 +1,50 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { + Checkbox +} from '@patternfly/react-core'; + +class CheckboxCard extends Component { + render () { + const { name, description, isSelected, onSelect, itemId } = this.props; + return ( +
+ +
{name}
+
{description}
+ + )} + value={itemId} + /> +
+ ); + } +} + +CheckboxCard.propTypes = { + name: PropTypes.string.isRequired, + description: PropTypes.string, + isSelected: PropTypes.bool, + onSelect: PropTypes.func, + itemId: PropTypes.number.isRequired +}; + +CheckboxCard.defaultProps = { + description: '', + isSelected: false, + onSelect: null +}; + +export default CheckboxCard; diff --git a/src/components/AddRole/SelectResourceStep.jsx b/src/components/AddRole/SelectResourceStep.jsx new file mode 100644 index 0000000000..962277aec1 --- /dev/null +++ b/src/components/AddRole/SelectResourceStep.jsx @@ -0,0 +1,216 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; + +import { i18nMark } from '@lingui/react'; + +import { + EmptyState, + EmptyStateBody, + EmptyStateIcon, + Title, +} from '@patternfly/react-core'; + +import { CubesIcon } from '@patternfly/react-icons'; + +import CheckboxListItem from '../ListItem'; +import DataListToolbar from '../DataListToolbar'; +import Pagination from '../Pagination'; +import SelectedList from '../SelectedList'; + +class SelectResourceStep extends React.Component { + constructor (props) { + super(props); + + const { sortedColumnKey } = this.props; + + this.state = { + count: null, + error: false, + page: 1, + page_size: 5, + resources: [], + sortOrder: 'ascending', + sortedColumnKey + }; + + this.handleSetPage = this.handleSetPage.bind(this); + this.handleSort = this.handleSort.bind(this); + this.readResourceList = this.readResourceList.bind(this); + } + + componentDidMount () { + const { page_size, page, sortedColumnKey } = this.state; + + this.readResourceList({ page_size, page, order_by: sortedColumnKey }); + } + + handleSetPage (pageNumber) { + const { page_size, sortedColumnKey, sortOrder } = this.state; + const page = parseInt(pageNumber, 10); + + let order_by = sortedColumnKey; + + if (sortOrder === 'descending') { + order_by = `-${order_by}`; + } + + this.readResourceList({ page_size, page, order_by }); + } + + handleSort (sortedColumnKey, sortOrder) { + const { page_size } = this.state; + + let order_by = sortedColumnKey; + + if (sortOrder === 'descending') { + order_by = `-${order_by}`; + } + + this.readResourceList({ page: 1, page_size, order_by }); + } + + async readResourceList (queryParams) { + const { onSearch, defaultSearchParams } = this.props; + const { page, order_by } = queryParams; + + let sortOrder = 'ascending'; + let sortedColumnKey = order_by; + + if (order_by.startsWith('-')) { + sortOrder = 'descending'; + sortedColumnKey = order_by.substring(1); + } + + this.setState({ error: false }); + + try { + const { data } = await onSearch(Object.assign(queryParams, defaultSearchParams)); + const { count, results } = data; + + const stateToUpdate = { + count, + page, + resources: results, + sortOrder, + sortedColumnKey + }; + + this.setState(stateToUpdate); + } catch (err) { + this.setState({ error: true }); + } + } + + render () { + const { + count, + error, + page, + page_size, + resources, + sortOrder, + sortedColumnKey + } = this.state; + + const { + columns, + displayKey, + emptyListBody, + emptyListTitle, + onRowClick, + selectedLabel, + selectedResourceRows, + title + } = this.props; + + return ( + + + {(resources.length === 0) ? ( + + + + {emptyListTitle} + + + {emptyListBody} + + + ) : ( + + + {title} + + +
    + {resources.map(i => ( + item.id === i.id)} + itemId={i.id} + key={i.id} + name={i[displayKey]} + onSelect={() => onRowClick(i)} + /> + ))} +
+ +
+ )} +
+ {selectedResourceRows.length > 0 && ( + + )} + { error ?
error
: '' } +
+ ); + } +} + +SelectResourceStep.propTypes = { + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + defaultSearchParams: PropTypes.shape(), + displayKey: PropTypes.string, + emptyListBody: PropTypes.string, + emptyListTitle: PropTypes.string, + onRowClick: PropTypes.func, + onSearch: PropTypes.func.isRequired, + selectedLabel: PropTypes.string, + selectedResourceRows: PropTypes.arrayOf(PropTypes.object), + sortedColumnKey: PropTypes.string, + title: PropTypes.string +}; + +SelectResourceStep.defaultProps = { + defaultSearchParams: {}, + displayKey: 'name', + emptyListBody: i18nMark('Please add items to populate this list'), + emptyListTitle: i18nMark('No Items Found'), + onRowClick: () => {}, + selectedLabel: i18nMark('Selected Items'), + selectedResourceRows: [], + sortedColumnKey: 'name', + title: '' +}; + +export default SelectResourceStep; diff --git a/src/components/AddRole/SelectRoleStep.jsx b/src/components/AddRole/SelectRoleStep.jsx new file mode 100644 index 0000000000..e0d052d76a --- /dev/null +++ b/src/components/AddRole/SelectRoleStep.jsx @@ -0,0 +1,69 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; + +import { i18nMark } from '@lingui/react'; + +import CheckboxCard from './CheckboxCard'; +import SelectedList from '../SelectedList'; + +class RolesStep extends React.Component { + render () { + const { + onRolesClick, + roles, + selectedListKey, + selectedListLabel, + selectedResourceRows, + selectedRoleRows + } = this.props; + + return ( + +
+ {selectedResourceRows.length > 0 && ( + + )} +
+
+ {Object.keys(roles).map(role => ( + item.id === roles[role].id) + } + key={roles[role].id} + name={roles[role].name} + onSelect={() => onRolesClick(roles[role])} + /> + ))} +
+
+ ); + } +} + +RolesStep.propTypes = { + onRolesClick: PropTypes.func, + roles: PropTypes.objectOf(PropTypes.object).isRequired, + selectedListKey: PropTypes.string, + selectedListLabel: PropTypes.string, + selectedResourceRows: PropTypes.arrayOf(PropTypes.object), + selectedRoleRows: PropTypes.arrayOf(PropTypes.object) +}; + +RolesStep.defaultProps = { + onRolesClick: () => {}, + selectedListKey: 'name', + selectedListLabel: i18nMark('Selected'), + selectedResourceRows: [], + selectedRoleRows: [] +}; + +export default RolesStep; diff --git a/src/components/AddRole/SelectableCard.jsx b/src/components/AddRole/SelectableCard.jsx new file mode 100644 index 0000000000..150bae29ad --- /dev/null +++ b/src/components/AddRole/SelectableCard.jsx @@ -0,0 +1,39 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +class SelectableCard extends Component { + render () { + const { + label, + onClick, + isSelected + } = this.props; + return ( +
+
+
{label}
+
+ ); + } +} + +SelectableCard.propTypes = { + label: PropTypes.string, + onClick: PropTypes.func.isRequired, + isSelected: PropTypes.bool +}; + +SelectableCard.defaultProps = { + label: '', + isSelected: false +}; + +export default SelectableCard; diff --git a/src/components/AddRole/styles.scss b/src/components/AddRole/styles.scss new file mode 100644 index 0000000000..21a8a73989 --- /dev/null +++ b/src/components/AddRole/styles.scss @@ -0,0 +1,28 @@ +.awx-selectableCard { + min-width: 200px; + border: 1px solid var(--pf-global--BorderColor); + border-radius: var(--pf-global--BorderRadius--sm); + margin-right: 20px; + font-weight: bold; + display: flex; + + .awx-selectableCard__indicator { + display: flex; + flex: 0 0 10px; + } + + .awx-selectableCard__label { + display: flex; + flex: 1; + align-items: center; + padding: 20px; + } +} + +.awx-selectableCard.awx-selectableCard__selected { + border-color: var(--pf-global--active-color--100); + + .awx-selectableCard__indicator { + background-color: var(--pf-global--active-color--100); + } +} \ No newline at end of file diff --git a/src/components/DataListToolbar/DataListToolbar.jsx b/src/components/DataListToolbar/DataListToolbar.jsx index d77ab68ae3..cc5b387902 100644 --- a/src/components/DataListToolbar/DataListToolbar.jsx +++ b/src/components/DataListToolbar/DataListToolbar.jsx @@ -39,6 +39,7 @@ class DataListToolbar extends React.Component { isAllSelected, isLookup, isCompact, + noLeftMargin, onSort, onSearch, onCompact, @@ -54,7 +55,7 @@ class DataListToolbar extends React.Component {
- + { showSelectAll && ( @@ -152,6 +153,7 @@ DataListToolbar.propTypes = { addUrl: PropTypes.string, columns: PropTypes.arrayOf(PropTypes.object).isRequired, isAllSelected: PropTypes.bool, + noLeftMargin: PropTypes.bool, onSearch: PropTypes.func, onSelectAll: PropTypes.func, onSort: PropTypes.func, @@ -178,7 +180,8 @@ DataListToolbar.defaultProps = { onCompact: null, onExpand: null, isCompact: false, - add: null + add: null, + noLeftMargin: false }; export default DataListToolbar; diff --git a/src/components/Lookup/Lookup.jsx b/src/components/Lookup/Lookup.jsx index f4f77cae6b..6d74e96e55 100644 --- a/src/components/Lookup/Lookup.jsx +++ b/src/components/Lookup/Lookup.jsx @@ -218,7 +218,7 @@ class Lookup extends React.Component { columns={columns} onSearch={this.onSearch} onSort={this.onSort} - isLookup + noLeftMargin />
    {results.map(i => ( diff --git a/src/components/Pagination/styles.scss b/src/components/Pagination/styles.scss index 78141870ba..e238b25266 100644 --- a/src/components/Pagination/styles.scss +++ b/src/components/Pagination/styles.scss @@ -5,6 +5,7 @@ --awx-pagination--disabled-Color: #C2C2CA; border-top: 1px solid var(--awx-pagination--BorderColor); + border-bottom: 1px solid var(--awx-pagination--BorderColor); background-color: var(--awx-pagination--BackgroundColor); height: 55px; display: flex; diff --git a/src/components/SelectedList/SelectedList.jsx b/src/components/SelectedList/SelectedList.jsx index eb6bd995d0..bf3b295d6d 100644 --- a/src/components/SelectedList/SelectedList.jsx +++ b/src/components/SelectedList/SelectedList.jsx @@ -1,9 +1,10 @@ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { Chip } from '@patternfly/react-core'; +import BasicChip from '../BasicChip/BasicChip'; import VerticalSeparator from '../VerticalSeparator'; const selectedRowStyling = { @@ -35,7 +36,14 @@ class SelectedList extends Component { }; render () { - const { label, selected, showOverflowAfter, onRemove } = this.props; + const { + label, + selected, + showOverflowAfter, + onRemove, + displayKey, + isReadOnly + } = this.props; const { showOverflow } = this.state; return (
    @@ -46,16 +54,33 @@ class SelectedList extends Component {
    - {selected - .slice(0, showOverflow ? selected.length : showOverflowAfter) - .map(selectedItem => ( - onRemove(selectedItem)} - > - {selectedItem.name} - - ))} + {isReadOnly ? ( + + {selected + .slice(0, showOverflow ? selected.length : showOverflowAfter) + .map(selectedItem => ( + + )) + } + + ) : ( + + {selected + .slice(0, showOverflow ? selected.length : showOverflowAfter) + .map(selectedItem => ( + onRemove(selectedItem)} + > + {selectedItem[displayKey]} + + )) + } + + )} {( !showOverflow && selected.length > showOverflowAfter @@ -76,15 +101,20 @@ class SelectedList extends Component { } SelectedList.propTypes = { + displayKey: PropTypes.string, label: PropTypes.string, - onRemove: PropTypes.func.isRequired, + onRemove: PropTypes.func, selected: PropTypes.arrayOf(PropTypes.object).isRequired, showOverflowAfter: PropTypes.number, + isReadOnly: PropTypes.bool }; SelectedList.defaultProps = { + displayKey: 'name', label: 'Selected', + onRemove: () => null, showOverflowAfter: 5, + isReadOnly: false }; export default SelectedList; diff --git a/src/index.jsx b/src/index.jsx index 099ab7a584..4654c25ed2 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -15,6 +15,7 @@ import './app.scss'; import './components/Pagination/styles.scss'; import './components/DataListToolbar/styles.scss'; import './components/SelectedList/styles.scss'; +import './components/AddRole/styles.scss'; import { Config } from './contexts/Config'; diff --git a/src/pages/Organizations/components/OrganizationAccessList.jsx b/src/pages/Organizations/components/OrganizationAccessList.jsx index 5aecddd279..3897cae617 100644 --- a/src/pages/Organizations/components/OrganizationAccessList.jsx +++ b/src/pages/Organizations/components/OrganizationAccessList.jsx @@ -6,6 +6,10 @@ import { TextContent, TextVariants, Chip, Button } from '@patternfly/react-core'; +import { + PlusIcon, +} from '@patternfly/react-icons'; + import { I18n, i18nMark } from '@lingui/react'; import { t, Trans } from '@lingui/macro'; @@ -19,6 +23,7 @@ import { withNetwork } from '../../../contexts/Network'; import AlertModal from '../../../components/AlertModal'; import Pagination from '../../../components/Pagination'; import DataListToolbar from '../../../components/DataListToolbar'; +import AddResourceRole from '../../../components/AddRole/AddResourceRole'; import { parseQueryString, @@ -109,6 +114,7 @@ class OrganizationAccessList extends React.Component { deleteRoleId: null, deleteResourceId: null, results: [], + isModalOpen: false }; this.fetchOrgAccessList = this.fetchOrgAccessList.bind(this); @@ -119,6 +125,8 @@ class OrganizationAccessList extends React.Component { this.handleWarning = this.handleWarning.bind(this); this.hideWarning = this.hideWarning.bind(this); this.confirmDelete = this.confirmDelete.bind(this); + this.handleModalToggle = this.handleModalToggle.bind(this); + this.handleSuccessfulRoleAdd = this.handleSuccessfulRoleAdd.bind(this); } componentDidMount () { @@ -282,6 +290,22 @@ class OrganizationAccessList extends React.Component { }); } + handleSuccessfulRoleAdd () { + this.handleModalToggle(); + const queryParams = this.getQueryParams(); + try { + this.fetchOrgAccessList(queryParams); + } catch (error) { + this.setState({ error }); + } + } + + handleModalToggle () { + this.setState((prevState) => ({ + isModalOpen: !prevState.isModalOpen, + })); + } + hideWarning () { this.setState({ showWarning: false }); } @@ -303,8 +327,13 @@ class OrganizationAccessList extends React.Component { sortOrder, warningMsg, warningTitle, - showWarning + showWarning, + isModalOpen } = this.state; + const { + api, + organization + } = this.props; return ( {({ i18n }) => ( @@ -328,6 +357,25 @@ class OrganizationAccessList extends React.Component { columns={this.columns} onSearch={() => { }} onSort={this.onSort} + add={( + + + {isModalOpen && ( + + )} + + )} /> {showWarning && ( Date: Thu, 28 Mar 2019 09:53:59 -0400 Subject: [PATCH 2/5] Removes the need to pass default search params to the select resource step --- __tests__/components/AddResourceRole.test.jsx | 3 ++- __tests__/components/SelectResourceStep.test.jsx | 11 ----------- src/components/AddRole/AddResourceRole.jsx | 5 +---- src/components/AddRole/SelectResourceStep.jsx | 6 ++---- 4 files changed, 5 insertions(+), 20 deletions(-) diff --git a/__tests__/components/AddResourceRole.test.jsx b/__tests__/components/AddResourceRole.test.jsx index 19cf538b91..68a3b03fad 100644 --- a/__tests__/components/AddResourceRole.test.jsx +++ b/__tests__/components/AddResourceRole.test.jsx @@ -147,7 +147,8 @@ describe('', () => { foo: 'bar' }); expect(readUsers).toHaveBeenCalledWith({ - foo: 'bar' + foo: 'bar', + is_superuser: false }); wrapper.instance().readTeams({ foo: 'bar' diff --git a/__tests__/components/SelectResourceStep.test.jsx b/__tests__/components/SelectResourceStep.test.jsx index 33af41429b..5097a5ce03 100644 --- a/__tests__/components/SelectResourceStep.test.jsx +++ b/__tests__/components/SelectResourceStep.test.jsx @@ -15,9 +15,6 @@ describe('', () => { ', () => { ', () => { ); expect(handleSearch).toHaveBeenCalledWith({ - is_superuser: false, order_by: 'username', page: 1, page_size: 5 @@ -77,9 +70,6 @@ describe('', () => { ', () => { order_by: '-username' }); expect(handleSearch).toHaveBeenCalledWith({ - is_superuser: false, order_by: '-username', page: 1 }); diff --git a/src/components/AddRole/AddResourceRole.jsx b/src/components/AddRole/AddResourceRole.jsx index d24250730b..1221cb6e9d 100644 --- a/src/components/AddRole/AddResourceRole.jsx +++ b/src/components/AddRole/AddResourceRole.jsx @@ -106,7 +106,7 @@ class AddResourceRole extends React.Component { async readUsers (queryParams) { const { api } = this.props; - return api.readUsers(queryParams); + return api.readUsers(Object.assign(queryParams, { is_superuser: false })); } async readTeams (queryParams) { @@ -174,9 +174,6 @@ class AddResourceRole extends React.Component { {selectedResource === 'users' && ( Date: Mon, 22 Apr 2019 17:15:32 -0400 Subject: [PATCH 3/5] Rebase and incorporates feedback --- __tests__/components/AddResourceRole.test.jsx | 124 ++++++++---------- __tests__/components/CheckboxCard.test.jsx | 5 +- .../components/SelectResourceStep.test.jsx | 120 ++++++++--------- __tests__/components/SelectRoleStep.test.jsx | 4 +- __tests__/components/SelectableCard.test.jsx | 6 +- src/components/AddRole/AddResourceRole.jsx | 23 +++- src/components/AddRole/SelectResourceStep.jsx | 39 +++--- src/components/AddRole/styles.scss | 2 +- src/components/SelectedList/SelectedList.jsx | 12 +- 9 files changed, 161 insertions(+), 174 deletions(-) diff --git a/__tests__/components/AddResourceRole.test.jsx b/__tests__/components/AddResourceRole.test.jsx index 68a3b03fad..f439a320d3 100644 --- a/__tests__/components/AddResourceRole.test.jsx +++ b/__tests__/components/AddResourceRole.test.jsx @@ -1,6 +1,6 @@ import React from 'react'; -import { mount } from 'enzyme'; -import { I18nProvider } from '@lingui/react'; +import { shallow } from 'enzyme'; +import { mountWithContexts } from '../enzymeHelpers'; import AddResourceRole from '../../src/components/AddRole/AddResourceRole'; describe('', () => { @@ -30,28 +30,24 @@ describe('', () => { } }; test('initially renders without crashing', () => { - mount( - - - + shallow( + {}} + onSave={() => {}} + roles={roles} + /> ); }); test('handleRoleCheckboxClick properly updates state', () => { - const wrapper = mount( - - - - ).find('AddResourceRole'); + const wrapper = shallow( + {}} + onSave={() => {}} + roles={roles} + /> + ); wrapper.setState({ selectedRoleRows: [ { @@ -79,16 +75,14 @@ describe('', () => { }]); }); test('handleResourceCheckboxClick properly updates state', () => { - const wrapper = mount( - - - - ).find('AddResourceRole'); + const wrapper = shallow( + {}} + onSave={() => {}} + roles={roles} + /> + ); wrapper.setState({ selectedResourceRows: [ { @@ -113,15 +107,13 @@ describe('', () => { }); test('clicking user/team cards updates state', () => { const spy = jest.spyOn(AddResourceRole.prototype, 'handleResourceSelect'); - const wrapper = mount( - - - + const wrapper = mountWithContexts( + {}} + onSave={() => {}} + api={api} + roles={roles} + /> ).find('AddResourceRole'); const selectableCardWrapper = wrapper.find('SelectableCard'); expect(selectableCardWrapper.length).toBe(2); @@ -133,16 +125,14 @@ describe('', () => { expect(wrapper.state('selectedResource')).toBe('teams'); }); test('readUsers and readTeams call out to corresponding api functions', () => { - const wrapper = mount( - - - - ).find('AddResourceRole'); + const wrapper = shallow( + {}} + onSave={() => {}} + roles={roles} + /> + ); wrapper.instance().readUsers({ foo: 'bar' }); @@ -158,16 +148,14 @@ describe('', () => { }); }); test('handleResourceSelect clears out selected lists and sets selectedResource', () => { - const wrapper = mount( - - - - ).find('AddResourceRole'); + const wrapper = shallow( + {}} + onSave={() => {}} + roles={roles} + /> + ); wrapper.setState({ selectedResource: 'teams', selectedResourceRows: [ @@ -199,15 +187,13 @@ describe('', () => { }); test('handleWizardSave makes correct api calls, calls onSave when done', async () => { const handleSave = jest.fn(); - const wrapper = mount( - - - + const wrapper = mountWithContexts( + {}} + onSave={handleSave} + roles={roles} + /> ).find('AddResourceRole'); wrapper.setState({ selectedResource: 'users', diff --git a/__tests__/components/CheckboxCard.test.jsx b/__tests__/components/CheckboxCard.test.jsx index de02dca288..7b689e0dd9 100644 --- a/__tests__/components/CheckboxCard.test.jsx +++ b/__tests__/components/CheckboxCard.test.jsx @@ -1,17 +1,16 @@ import React from 'react'; -import { mount } from 'enzyme'; +import { shallow } from 'enzyme'; import CheckboxCard from '../../src/components/AddRole/CheckboxCard'; describe('', () => { let wrapper; test('initially renders without crashing', () => { - wrapper = mount( + wrapper = shallow( ); expect(wrapper.length).toBe(1); - wrapper.unmount(); }); }); diff --git a/__tests__/components/SelectResourceStep.test.jsx b/__tests__/components/SelectResourceStep.test.jsx index 5097a5ce03..0c207b871e 100644 --- a/__tests__/components/SelectResourceStep.test.jsx +++ b/__tests__/components/SelectResourceStep.test.jsx @@ -1,6 +1,6 @@ import React from 'react'; -import { mount } from 'enzyme'; -import { I18nProvider } from '@lingui/react'; +import { shallow } from 'enzyme'; +import { mountWithContexts } from '../enzymeHelpers'; import SelectResourceStep from '../../src/components/AddRole/SelectResourceStep'; describe('', () => { @@ -11,16 +11,14 @@ describe('', () => { jest.restoreAllMocks(); }); test('initially renders without crashing', () => { - mount( - - - + shallow( + {}} + onSearch={() => {}} + sortedColumnKey="username" + /> ); }); test('fetches resources on mount', async () => { @@ -33,16 +31,14 @@ describe('', () => { ] } }); - mount( - - - + mountWithContexts( + {}} + onSearch={handleSearch} + sortedColumnKey="username" + /> ); expect(handleSearch).toHaveBeenCalledWith({ order_by: 'username', @@ -66,17 +62,15 @@ describe('', () => { ] } }); - const wrapper = await mount( - - - + const wrapper = await mountWithContexts( + {}} + onSearch={handleSearch} + selectedResourceRows={selectedResourceRows} + sortedColumnKey="username" + /> ).find('SelectResourceStep'); await wrapper.instance().readResourceList({ page: 1, @@ -93,17 +87,15 @@ describe('', () => { }); test('handleSetPage calls readResourceList with correct params', () => { const spy = jest.spyOn(SelectResourceStep.prototype, 'readResourceList'); - const wrapper = mount( - - - + const wrapper = mountWithContexts( + {}} + onSearch={() => {}} + selectedResourceRows={[]} + sortedColumnKey="username" + /> ).find('SelectResourceStep'); wrapper.setState({ sortOrder: 'descending' }); wrapper.instance().handleSetPage(2); @@ -114,17 +106,15 @@ describe('', () => { }); test('handleSort calls readResourceList with correct params', () => { const spy = jest.spyOn(SelectResourceStep.prototype, 'readResourceList'); - const wrapper = mount( - - - + const wrapper = mountWithContexts( + {}} + onSearch={() => {}} + selectedResourceRows={[]} + sortedColumnKey="username" + /> ).find('SelectResourceStep'); wrapper.instance().handleSort('username', 'descending'); expect(spy).toHaveBeenCalledWith({ page: 1, page_size: 5, order_by: '-username' }); @@ -133,17 +123,15 @@ describe('', () => { }); test('clicking on row fires callback with correct params', () => { const handleRowClick = jest.fn(); - const wrapper = mount( - - - + const wrapper = mountWithContexts( + {}} + selectedResourceRows={[]} + sortedColumnKey="username" + /> ); const selectResourceStepWrapper = wrapper.find('SelectResourceStep'); selectResourceStepWrapper.setState({ diff --git a/__tests__/components/SelectRoleStep.test.jsx b/__tests__/components/SelectRoleStep.test.jsx index 857ec0a9f6..6e323e2936 100644 --- a/__tests__/components/SelectRoleStep.test.jsx +++ b/__tests__/components/SelectRoleStep.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { mount } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import SelectRoleStep from '../../src/components/AddRole/SelectRoleStep'; describe('', () => { @@ -30,7 +30,7 @@ describe('', () => { } ]; test('initially renders without crashing', () => { - wrapper = mount( + wrapper = shallow( ', () => { let wrapper; const onClick = jest.fn(); test('initially renders without crashing when not selected', () => { - wrapper = mount( + wrapper = shallow( @@ -15,7 +15,7 @@ describe('', () => { wrapper.unmount(); }); test('initially renders without crashing when selected', () => { - wrapper = mount( + wrapper = shallow( )} {selectedResource === 'teams' && ( @@ -192,9 +204,8 @@ class AddResourceRole extends React.Component { emptyListTitle={i18n._(t`No Teams Found`)} onRowClick={this.handleResourceCheckboxClick} onSearch={this.readTeams} - selectedLabel={i18n._(t`Selected Teams`)} + selectedLabel={i18n._(t`Selected`)} selectedResourceRows={selectedResourceRows} - title={i18n._(t`Teams`)} /> )} @@ -212,7 +223,7 @@ class AddResourceRole extends React.Component { onRolesClick={this.handleRoleCheckboxClick} roles={roles} selectedListKey={selectedResource === 'users' ? 'username' : 'name'} - selectedListLabel={selectedResource === 'users' ? i18n._(t`Selected Users`) : i18n._(t`Selected Teams`)} + selectedListLabel={i18n._(t`Selected`)} selectedResourceRows={selectedResourceRows} selectedRoleRows={selectedRoleRows} /> @@ -234,7 +245,7 @@ class AddResourceRole extends React.Component { onClose={onClose} onSave={this.handleWizardSave} steps={steps} - title={i18n._(t`Add Roles`)} + title={wizardTitle} /> )} diff --git a/src/components/AddRole/SelectResourceStep.jsx b/src/components/AddRole/SelectResourceStep.jsx index 9f15478631..f921913005 100644 --- a/src/components/AddRole/SelectResourceStep.jsx +++ b/src/components/AddRole/SelectResourceStep.jsx @@ -17,6 +17,14 @@ import DataListToolbar from '../DataListToolbar'; import Pagination from '../Pagination'; import SelectedList from '../SelectedList'; +const paginationStyling = { + paddingLeft: '0', + justifyContent: 'flex-end', + borderRight: '1px solid #ebebeb', + borderBottom: '1px solid #ebebeb', + borderTop: '0' +}; + class SelectResourceStep extends React.Component { constructor (props) { super(props); @@ -119,8 +127,7 @@ class SelectResourceStep extends React.Component { emptyListTitle, onRowClick, selectedLabel, - selectedResourceRows, - title + selectedResourceRows } = this.props; return ( @@ -138,9 +145,15 @@ class SelectResourceStep extends React.Component { ) : ( - - {title} - + {selectedResourceRows.length > 0 && ( + + )} )} - {selectedResourceRows.length > 0 && ( - - )} { error ?
    error
    : '' } ); @@ -196,8 +201,7 @@ SelectResourceStep.propTypes = { onSearch: PropTypes.func.isRequired, selectedLabel: PropTypes.string, selectedResourceRows: PropTypes.arrayOf(PropTypes.object), - sortedColumnKey: PropTypes.string, - title: PropTypes.string + sortedColumnKey: PropTypes.string }; SelectResourceStep.defaultProps = { @@ -207,8 +211,7 @@ SelectResourceStep.defaultProps = { onRowClick: () => {}, selectedLabel: i18nMark('Selected Items'), selectedResourceRows: [], - sortedColumnKey: 'name', - title: '' + sortedColumnKey: 'name' }; export default SelectResourceStep; diff --git a/src/components/AddRole/styles.scss b/src/components/AddRole/styles.scss index 21a8a73989..6349d03421 100644 --- a/src/components/AddRole/styles.scss +++ b/src/components/AddRole/styles.scss @@ -8,7 +8,7 @@ .awx-selectableCard__indicator { display: flex; - flex: 0 0 10px; + flex: 0 0 5px; } .awx-selectableCard__label { diff --git a/src/components/SelectedList/SelectedList.jsx b/src/components/SelectedList/SelectedList.jsx index bf3b295d6d..485eaa615c 100644 --- a/src/components/SelectedList/SelectedList.jsx +++ b/src/components/SelectedList/SelectedList.jsx @@ -45,6 +45,7 @@ class SelectedList extends Component { isReadOnly } = this.props; const { showOverflow } = this.state; + const visibleItems = selected.slice(0, showOverflow ? selected.length : showOverflowAfter); return (
    @@ -56,20 +57,19 @@ class SelectedList extends Component {
    {isReadOnly ? ( - {selected - .slice(0, showOverflow ? selected.length : showOverflowAfter) + {visibleItems .map(selectedItem => ( + > + {selectedItem[displayKey]} + )) } ) : ( - {selected - .slice(0, showOverflow ? selected.length : showOverflowAfter) + {visibleItems .map(selectedItem => ( Date: Tue, 23 Apr 2019 11:04:44 -0400 Subject: [PATCH 4/5] Fix rebase errors --- src/pages/Organizations/screens/Organization/Organization.jsx | 4 +++- .../Organizations/screens/Organization/OrganizationAccess.jsx | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/Organizations/screens/Organization/Organization.jsx b/src/pages/Organizations/screens/Organization/Organization.jsx index ada25966eb..f4152d54af 100644 --- a/src/pages/Organizations/screens/Organization/Organization.jsx +++ b/src/pages/Organizations/screens/Organization/Organization.jsx @@ -158,7 +158,9 @@ class Organization extends Component { ( - + )} /> ); } From df87681e6d80c1cf18181c2a480464543bd7437f Mon Sep 17 00:00:00 2001 From: mabashian Date: Tue, 23 Apr 2019 12:20:08 -0400 Subject: [PATCH 5/5] Fix linting errors --- src/components/DataListToolbar/DataListToolbar.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/DataListToolbar/DataListToolbar.jsx b/src/components/DataListToolbar/DataListToolbar.jsx index cc5b387902..738f396b3f 100644 --- a/src/components/DataListToolbar/DataListToolbar.jsx +++ b/src/components/DataListToolbar/DataListToolbar.jsx @@ -37,7 +37,6 @@ class DataListToolbar extends React.Component { showDelete, showSelectAll, isAllSelected, - isLookup, isCompact, noLeftMargin, onSort,