Repository: couchdb-fauxton Updated Branches: refs/heads/master 258ec71fb -> 25630f4e4
Redux: Permissions - Use Redux for Flux flow Integrates Redux into CouchDB Fauxton to softmigrate our stores to Redux. Removes the Backbone models and introduces testing with Jest. Additional Highlights: - Bluebird for Promises (bye jQuery deferreds) - uses the WHATWG fetch API - 1 file per component Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/25630f4e Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/25630f4e Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/25630f4e Branch: refs/heads/master Commit: 25630f4e48a55e983ea6aff095b034cc2a728251 Parents: 258ec71 Author: Robert Kowalski <robertkowal...@apache.org> Authored: Thu Nov 3 15:09:51 2016 +0100 Committer: Robert Kowalski <robertkowal...@apache.org> Committed: Mon Dec 12 17:56:52 2016 +0100 ---------------------------------------------------------------------- .eslintrc | 3 +- __mocks__/fileMock.js | 1 + __mocks__/styleMock.js | 1 + .../permissions/__tests__/actions-test.js | 112 +++++++++ .../permissions/__tests__/container-test.js | 57 +++++ .../permissions/__tests__/helpers-test.js | 84 +++++++ .../__tests__/permissionsScreen-test.js | 97 ++++++++ app/addons/permissions/actions.js | 132 +++++++---- app/addons/permissions/actiontypes.js | 8 +- app/addons/permissions/base.js | 3 + app/addons/permissions/components.react.jsx | 234 ------------------- .../permissions/components/Permissions.js | 48 ++++ .../permissions/components/PermissionsItem.js | 36 +++ .../permissions/components/PermissionsScreen.js | 85 +++++++ .../components/PermissionsSection.js | 163 +++++++++++++ .../container/PermissionsContainer.js | 40 ++++ app/addons/permissions/helpers.js | 39 ++++ app/addons/permissions/layout.js | 4 +- app/addons/permissions/reducers.js | 67 ++++++ app/addons/permissions/resources.js | 83 ------- app/addons/permissions/routes.js | 34 +-- app/addons/permissions/stores.js | 104 --------- app/addons/permissions/tests/actionsSpec.js | 122 ---------- .../permissions/tests/componentsSpec.react.jsx | 135 ----------- .../permissions/tests/nightwatch/permissions.js | 40 ++++ app/addons/permissions/tests/resourceSpec.js | 69 ------ app/core/base.js | 2 + app/helpers.js | 1 + app/main.js | 23 +- jest-config.json | 12 +- jest-setup.js | 18 ++ package.json | 10 +- 32 files changed, 1042 insertions(+), 825 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/.eslintrc ---------------------------------------------------------------------- diff --git a/.eslintrc b/.eslintrc index 0b5439d..c642ec6 100644 --- a/.eslintrc +++ b/.eslintrc @@ -73,7 +73,8 @@ "after": true, "define": true, "expect": true, - "prettyPrint": true + "prettyPrint": true, + "jest": true } } http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/__mocks__/fileMock.js ---------------------------------------------------------------------- diff --git a/__mocks__/fileMock.js b/__mocks__/fileMock.js new file mode 100644 index 0000000..86059f3 --- /dev/null +++ b/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = 'test-file-stub'; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/__mocks__/styleMock.js ---------------------------------------------------------------------- diff --git a/__mocks__/styleMock.js b/__mocks__/styleMock.js new file mode 100644 index 0000000..f053ebf --- /dev/null +++ b/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/__tests__/actions-test.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/__tests__/actions-test.js b/app/addons/permissions/__tests__/actions-test.js new file mode 100644 index 0000000..5f74223 --- /dev/null +++ b/app/addons/permissions/__tests__/actions-test.js @@ -0,0 +1,112 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import { + setPermissionOnObject, + deletePermissionFromObject +} from '../actions'; + + +describe('Permissions Actions', () => { + + describe('deleting roles', () => { + it('throws if a role is not in permissions', () => { + const p = { + admins: { + names: ['abc'], + roles: [] + }, + members: { + names: [], + roles: [] + } + }; + + expect(() => { + deletePermissionFromObject(p, 'admins', 'names', 'pizza'); + }).toThrow(); + }); + + it('deletes roles', () => { + const p = { + admins: { + names: ['abc', 'furbie'], + roles: [] + }, + members: { + names: [], + roles: [] + } + }; + + const res = deletePermissionFromObject(p, 'admins', 'names', 'abc'); + + expect(res).toEqual({ + admins: { + names: ['furbie'], + roles: [] + }, + members: { + names: [], + roles: [] + } + }); + }); + + }); + + describe('adding roles', () => { + it('throws if a role is already in permissions', () => { + const p = { + admins: { + names: ['abc'], + roles: [] + }, + members: { + names: [], + roles: [] + } + }; + + expect(() => { + setPermissionOnObject(p, 'admins', 'names', 'abc'); + }).toThrow(); + }); + + it('adds if not already present', () => { + const p = { + admins: { + names: ['abc'], + roles: [] + }, + members: { + names: [], + roles: [] + } + }; + + const res = setPermissionOnObject(p, 'admins', 'names', 'test123'); + + expect(res).toEqual({ + admins: { + names: ['abc', 'test123'], + roles: [] + }, + members: { + names: [], + roles: [] + } + }); + }); + + }); +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/__tests__/container-test.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/__tests__/container-test.js b/app/addons/permissions/__tests__/container-test.js new file mode 100644 index 0000000..82ae677 --- /dev/null +++ b/app/addons/permissions/__tests__/container-test.js @@ -0,0 +1,57 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import { receivedPermissions } from '../actions'; + +import React from 'react'; +import { mount } from 'enzyme'; + +import { createStore, applyMiddleware } from 'redux'; + +import thunk from 'redux-thunk'; +import { Provider } from 'react-redux'; + +import reducer from '../reducers'; +import PermissionsContainer from '../container/PermissionsContainer'; + +describe('Permissions Container', () => { + + it('renders with new results', () => { + + fetch.mockResponse( + JSON.stringify({}) + ); + + const middlewares = [thunk]; + const store = createStore( + reducer, + applyMiddleware(...middlewares) + ); + + const wrapper = mount( + <Provider store={store}> + <PermissionsContainer url="http://example.com/abc" /> + </Provider> + ); + + store.dispatch( + receivedPermissions({ + admins: { names: ['banana'], roles: [] } + }) + ); + + const item = wrapper + .find('.permissions__admins .permissions__entry'); + + expect(item.text()).toContain('banana'); + }); +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/__tests__/helpers-test.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/__tests__/helpers-test.js b/app/addons/permissions/__tests__/helpers-test.js new file mode 100644 index 0000000..a3fd54b --- /dev/null +++ b/app/addons/permissions/__tests__/helpers-test.js @@ -0,0 +1,84 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import { + isValueAlreadySet, + addValueToPermissions +} from '../helpers'; + +describe('Permissions - Helpers', () => { + + describe('isValueAlreadySet', () => { + + it('returns false if object does not have properties', () => { + + let permissions = {}; + expect( + isValueAlreadySet(permissions, 'admins', 'names', 'rocko') + ).toBe(false); + + permissions = { admins: {} }; + expect( + isValueAlreadySet(permissions, 'admins', 'names', 'rocko') + ).toBe(false); + + permissions = { admins: { names: [] } }; + expect( + isValueAlreadySet(permissions, 'admins', 'names', 'rocko') + ).toBe(false); + }); + + it('confirms existing properties', () => { + + const permissions = { admins: { names: ['michelle', 'rocko', 'garren'] } }; + + expect( + isValueAlreadySet(permissions, 'admins', 'names', 'rocko') + ).toBe(true); + }); + + }); + + describe('addValueToPermissions', () => { + + it('adds values, even if properties not set', () => { + + const permissions = {}; + expect( + addValueToPermissions(permissions, 'admins', 'names', 'rocko') + ).toEqual({ + admins: { + names: ['rocko'] + } + }); + }); + + it('adds values', () => { + + const permissions = { + admins: { + names: ['rocko'] + } + }; + + expect( + addValueToPermissions(permissions, 'admins', 'names', 'garren') + ).toEqual({ + admins: { + names: ['rocko', 'garren'] + } + }); + }); + + }); + +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/__tests__/permissionsScreen-test.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/__tests__/permissionsScreen-test.js b/app/addons/permissions/__tests__/permissionsScreen-test.js new file mode 100644 index 0000000..2283a47 --- /dev/null +++ b/app/addons/permissions/__tests__/permissionsScreen-test.js @@ -0,0 +1,97 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import React from 'react'; +import { mount } from 'enzyme'; + +import PermissionsScreen from '../components/PermissionsScreen'; + + +describe('PermissionsScreen', () => { + + it('add permississon: does not dispatch if value already exists', () => { + + const security = { + admins: { names: ['abc'], roles: [] }, + members: { names: [], roles: [] } + }; + const stub = jest.fn(); + + const wrapper = mount( + <PermissionsScreen + adminRoles={security.admins.roles} + adminNames={security.admins.names} + memberRoles={security.members.roles} + memberNames={security.members.names} + security={security} + dispatch={stub} /> + ); + + wrapper + .find('.permissions__admins .permissions-add-user input') + .simulate('change', {target: {value: 'abc'}}); + + wrapper + .find('.permissions__admins .permissions-add-user') + .simulate('submit'); + + expect(stub).not.toHaveBeenCalled(); + }); + + it('add permississon: dispatches if values does not exist', () => { + + const security = { + admins: { names: ['pineapple'], roles: [] }, + members: { names: [], roles: [] } + }; + const stub = jest.fn(); + + const wrapper = mount( + <PermissionsScreen security={security} dispatch={stub} /> + ); + + wrapper + .find('.permissions__admins .permissions-add-user input') + .simulate('change', {target: {value: 'mango'}}); + + wrapper + .find('.permissions__admins .permissions-add-user') + .simulate('submit'); + + expect(stub).toHaveBeenCalled(); + }); + + it('remove permississon: dispatches', () => { + + const security = { + admins: { names: ['pineapple'], roles: [] }, + members: { names: [], roles: [] } + }; + const stub = jest.fn(); + + const wrapper = mount( + <PermissionsScreen + adminRoles={security.admins.roles} + adminNames={security.admins.names} + memberRoles={security.members.roles} + memberNames={security.members.names} + security={security} + dispatch={stub} /> + ); + + wrapper + .find('.permissions__admins .permissions__entry button') + .simulate('click'); + + expect(stub).toHaveBeenCalled(); + }); +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/actions.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/actions.js b/app/addons/permissions/actions.js index f11ff09..0a49cc0 100644 --- a/app/addons/permissions/actions.js +++ b/app/addons/permissions/actions.js @@ -12,70 +12,104 @@ import FauxtonAPI from "../../core/api"; import ActionTypes from "./actiontypes"; -import Stores from "./stores"; -var permissionsStore = Stores.permissionsStore; +import Promise from 'bluebird'; +import 'whatwg-fetch'; +import { isValueAlreadySet, addValueToPermissions } from './helpers'; -export default { - fetchPermissions: function (database, security) { - FauxtonAPI.dispatch({ - type: ActionTypes.PERMISSIONS_FETCHING, - database: database, - security: security - }); +import { + PERMISSIONS_UPDATE +} from './actiontypes'; - FauxtonAPI.when([database.fetch(), security.fetch()]).then(function () { - this.editPermissions(database, security); - }.bind(this)); - }, - editPermissions: function (database, security) { - FauxtonAPI.dispatch({ - type: ActionTypes.PERMISSIONS_EDIT, - database: database, - security: security - }); - }, - addItem: function (options) { - var check = permissionsStore.getSecurity().canAddItem(options.value, options.type, options.section); +export const receivedPermissions = json => { + return { + type: PERMISSIONS_UPDATE, + permissions: json + }; +}; - if (check.error) { - FauxtonAPI.addNotification({ - msg: check.msg, - type: 'error' - }); - return; - } +export const fetchPermissions = url => dispatch => { + return fetch(url, { headers: { 'Accept': 'application/json' }}) + .then(res => res.json()) + .then(json => dispatch(receivedPermissions(json))); +}; - FauxtonAPI.dispatch({ - type: ActionTypes.PERMISSIONS_ADD_ITEM, - options: options - }); +export const setPermissionOnObject = (p, section, type, value) => { + if (isValueAlreadySet(p, section, type, value)) { + throw new Error('Role/Name has already been added'); + } - this.savePermissions(); + const res = addValueToPermissions(p, section, type, value); - }, - removeItem: function (options) { - FauxtonAPI.dispatch({ - type: ActionTypes.PERMISSIONS_REMOVE_ITEM, - options: options - }); - this.savePermissions(); - }, + return res; +}; + +export const deletePermissionFromObject = (p, section, type, value) => { + if (!isValueAlreadySet(p, section, type, value)) { + throw new Error('Role/Name does not exist'); + } + + p[section][type] = p[section][type].filter((el) => { + return el !== value; + }); - savePermissions: function () { - permissionsStore.getSecurity().save().then(function () { + return p; +}; + +export const updatePermission = (url, permissions, section, type, value) => dispatch => { + const res = setPermissionOnObject(permissions, section, type, value); + + updatePermissionUnsafe(url, permissions, dispatch) + .catch((err) => { FauxtonAPI.addNotification({ - msg: 'Database permissions has been updated.' + msg: err.message, + type: 'error' }); - }, function (xhr) { - if (!xhr && !xhr.responseJSON) { return;} + }); +}; + +export const deletePermission = (url, permissions, section, type, value) => dispatch => { + const res = deletePermissionFromObject(permissions, section, type, value); + updatePermissionUnsafe(url, permissions, dispatch) + .catch((err) => { FauxtonAPI.addNotification({ - msg: 'Could not update permissions - reason: ' + xhr.responseJSON.reason, + msg: err.message, type: 'error' }); }); - } +}; + +export const updatePermissionUnsafe = (url, p, dispatch) => { + return fetch(url, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + credentials: 'include', + method: 'PUT', + body: JSON.stringify(p) + }) + .then((res) => res.json()) + .then((json) => { + if (!json.ok) { + throw new Error(json.reason); + } + return json; + }) + .then((json) => { + FauxtonAPI.addNotification({ + msg: 'Database permissions has been updated.' + }); + + return dispatch(receivedPermissions(p)); + }) + .catch((error) => { + FauxtonAPI.addNotification({ + msg: 'Could not update permissions - reason: ' + error, + type: 'error' + }); + }); }; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/actiontypes.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/actiontypes.js b/app/addons/permissions/actiontypes.js index 132da81..f474fbd 100644 --- a/app/addons/permissions/actiontypes.js +++ b/app/addons/permissions/actiontypes.js @@ -10,9 +10,5 @@ // License for the specific language governing permissions and limitations under // the License. -export default { - PERMISSIONS_EDIT: 'PERMISSIONS_EDIT', - PERMISSIONS_FETCHING: 'PERMISSIONS_FETCHING', - PERMISSIONS_ADD_ITEM: 'PERMISSIONS_ADD_ITEM', - PERMISSIONS_REMOVE_ITEM: 'PERMISSIONS_REMOVE_ITEM' -}; + +export const PERMISSIONS_UPDATE = 'PERMISSIONS_UPDATE'; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/base.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/base.js b/app/addons/permissions/base.js index d6ddf5d..5123112 100644 --- a/app/addons/permissions/base.js +++ b/app/addons/permissions/base.js @@ -13,8 +13,11 @@ import app from "../../app"; import FauxtonAPI from "../../core/api"; import Permissions from "./routes"; +import reducer from './reducers'; import "./assets/less/permissions.less"; Permissions.initialize = function () {}; +FauxtonAPI.reducers.push(reducer); + export default Permissions; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/components.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/permissions/components.react.jsx b/app/addons/permissions/components.react.jsx deleted file mode 100644 index 4b5d307..0000000 --- a/app/addons/permissions/components.react.jsx +++ /dev/null @@ -1,234 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy of -// the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. - -import app from "../../app"; -import FauxtonAPI from "../../core/api"; -import React from "react"; -import Components from "../components/react-components.react"; -import Stores from "./stores"; -import Actions from "./actions"; -var LoadLines = Components.LoadLines; -var permissionsStore = Stores.permissionsStore; -var getDocUrl = app.helpers.getDocUrl; - -var PermissionsItem = React.createClass({ - - removeItem: function (e) { - this.props.removeItem({ - value: this.props.item, - type: this.props.type, - section: this.props.section - }); - }, - - render: function () { - return ( - <li> - <span>{this.props.item}</span> - <button onClick={this.removeItem} type="button" className="pull-right close">Ã</button> - </li> - ); - } - -}); - -var PermissionsSection = React.createClass({ - getInitialState: function () { - return { - newRole: '', - newName: '' - }; - }, - - getHelp: function () { - if (this.props.section === 'admins') { - return 'Database members can access the database. If no members are defined, the database is public. '; - } - - return 'Database members can access the database. If no members are defined, the database is public. '; - }, - - isEmptyValue: function (value, type) { - if (!_.isEmpty(value)) { - return false; - } - FauxtonAPI.addNotification({ - msg: 'Cannot add an empty value for ' + type + '.', - type: 'warning' - }); - - return true; - }, - - addNames: function (e) { - e.preventDefault(); - if (this.isEmptyValue(this.state.newName, 'names')) { - return; - } - this.props.addItem({ - type: 'names', - section: this.props.section, - value: this.state.newName - }); - - this.setState({newName: ''}); - }, - - addRoles: function (e) { - e.preventDefault(); - if (this.isEmptyValue(this.state.newRole, 'roles')) { - return; - } - this.props.addItem({ - type: 'roles', - section: this.props.section, - value: this.state.newRole - }); - - this.setState({newRole: ''}); - }, - - getItems: function (items, type) { - return _.map(items, function (item, i) { - return <PermissionsItem key={i} item={item} section={this.props.section} type={type} removeItem={this.props.removeItem} />; - }, this); - }, - - getNames: function () { - return this.getItems(this.props.names, 'names'); - }, - - getRoles: function () { - return this.getItems(this.props.roles, 'roles'); - }, - - nameChange: function (e) { - this.setState({newName: e.target.value}); - }, - - roleChange: function (e) { - this.setState({newRole: e.target.value}); - }, - - render: function () { - return ( - <div> - <header className="page-header"> - <h3>{this.props.section}</h3> - <p className="help"> - {this.getHelp()} - <a className="help-link" data-bypass="true" href={getDocUrl('DB_PERMISSION')} target="_blank"> - <i className="icon-question-sign"></i> - </a> - </p> - </header> - <div className="row-fluid"> - <div className="span6"> - <header> - <h4>Users</h4> - <p>Specify users who will have {this.props.section} access to this database.</p> - </header> - <form onSubmit={this.addNames} className="permission-item-form permissions-add-user form-inline"> - <input onChange={this.nameChange} value={this.state.newName} type="text" className="item input-small" placeholder="Add User" /> - <button type="submit" className="btn btn-success"><i className="icon fonticon-plus-circled" /> Add User</button> - </form> - <ul className="clearfix unstyled permission-items span10"> - {this.getNames()} - </ul> - </div> - <div className="span6"> - <header> - <h4>Roles</h4> - <p>Users with any of the following role(s) will have {this.props.section} access.</p> - </header> - <form onSubmit={this.addRoles} className="permission-item-form permissions-add-role form-inline"> - <input onChange={this.roleChange} value={this.state.newRole} type="text" className="item input-small" placeholder="Add Role" /> - <button type="submit" className="btn btn-success"><i className="icon fonticon-plus-circled" /> Add Role</button> - </form> - <ul className="unstyled permission-items span10"> - {this.getRoles()} - </ul> - </div> - </div> - </div> - ); - } - -}); - -var PermissionsController = React.createClass({ - - getStoreState: function () { - return { - isLoading: permissionsStore.isLoading(), - adminRoles: permissionsStore.getAdminRoles(), - adminNames: permissionsStore.getAdminNames(), - memberRoles: permissionsStore.getMemberRoles(), - memberNames: permissionsStore.getMemberNames(), - }; - }, - - getInitialState: function () { - return this.getStoreState(); - }, - - componentDidMount: function () { - permissionsStore.on('change', this.onChange, this); - }, - - componentWillUnmount: function () { - permissionsStore.off('change', this.onChange); - }, - - onChange: function () { - this.setState(this.getStoreState()); - }, - - addItem: function (options) { - Actions.addItem(options); - }, - - removeItem: function (options) { - Actions.removeItem(options); - }, - - render: function () { - if (this.state.isLoading) { - return <LoadLines />; - } - - return ( - <div className="permissions-page flex-body"> - <div id="sections"> - <PermissionsSection roles={this.state.adminRoles} - names={this.state.adminNames} - addItem={this.addItem} - removeItem={this.removeItem} - section={'admins'} /> - <PermissionsSection - roles={this.state.memberRoles} - names={this.state.memberNames} - addItem={this.addItem} - removeItem={this.removeItem} - section={'members'} /> - </div> - </div> - ); - } - -}); - -export default { - PermissionsController: PermissionsController, - PermissionsSection: PermissionsSection, - PermissionsItem: PermissionsItem -}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/components/Permissions.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/components/Permissions.js b/app/addons/permissions/components/Permissions.js new file mode 100644 index 0000000..49bc9c8 --- /dev/null +++ b/app/addons/permissions/components/Permissions.js @@ -0,0 +1,48 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import React, { Component, PropTypes } from 'react'; + +import PermissionsScreen from './PermissionsScreen'; + +import { fetchPermissions } from '../actions'; +import { LoadLines } from '../../components/components/loadlines'; + + +export default class Permissions extends Component { + + constructor (props) { + super(props); + } + + componentDidMount() { + const { dispatch, url } = this.props; + dispatch(fetchPermissions(url)); + } + + render () { + const { isLoading } = this.props; + + return ( + isLoading ? <LoadLines /> : <PermissionsScreen {...this.props} /> + ); + } +}; + +Permissions.propTypes = { + isLoading: PropTypes.bool.isRequired, + adminRoles: React.PropTypes.array.isRequired, + adminNames: React.PropTypes.array.isRequired, + memberNames: React.PropTypes.array.isRequired, + memberRoles: React.PropTypes.array.isRequired, + security: React.PropTypes.object.isRequired +}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/components/PermissionsItem.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/components/PermissionsItem.js b/app/addons/permissions/components/PermissionsItem.js new file mode 100644 index 0000000..6c03456 --- /dev/null +++ b/app/addons/permissions/components/PermissionsItem.js @@ -0,0 +1,36 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import React, { Component, PropTypes } from 'react'; + +const PermissionsItem = ({removeItem, section, type, value}) =>Â { + + return ( + <li className="permissions__entry"> + <span>{value}</span> + <button + onClick={() => removeItem(section, type, value)} + type="button" + className="pull-right close" + > + Ã + </button> + </li> + ); +}; + +PermissionsItem.propTypes = { + value: React.PropTypes.string.isRequired, + removeItem: PropTypes.func.isRequired, +}; + +export default PermissionsItem; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/components/PermissionsScreen.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/components/PermissionsScreen.js b/app/addons/permissions/components/PermissionsScreen.js new file mode 100644 index 0000000..12bf0ad --- /dev/null +++ b/app/addons/permissions/components/PermissionsScreen.js @@ -0,0 +1,85 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import FauxtonAPI from '../../../core/api'; +import React, { Component, PropTypes } from 'react'; + +import PermissionsSection from './PermissionsSection'; +import { updatePermission, deletePermission } from '../actions'; +import { isValueAlreadySet } from '../helpers'; + +export default class PermissionsScreen extends Component { + + constructor (props) { + super(props); + + this.addItem = this.addItem.bind(this); + this.removeItem = this.removeItem.bind(this); + } + + addItem ({ section, type, value }) { + + if (isValueAlreadySet(this.props.security, section, type, value)) { + FauxtonAPI.addNotification({ + msg: 'Role/Name has already been added', + type: 'error' + }); + + return null; + } + + this.props.dispatch( + updatePermission(this.props.url, this.props.security, section, type, value) + ); + } + + removeItem (section, type, value) { + + this.props.dispatch( + deletePermission(this.props.url, this.props.security, section, type, value) + ); + } + + render () { + const { + adminRoles, + adminNames, + memberRoles, + memberNames + } = this.props; + + return ( + <div className="permissions-page flex-body"> + <div> + <PermissionsSection + roles={adminRoles} + names={adminNames} + addItem={this.addItem} + removeItem={this.removeItem} + section="admins" /> + + <PermissionsSection + roles={memberRoles} + names={memberNames} + addItem={this.addItem} + removeItem={this.removeItem} + section="members" /> + </div> + </div> + ); + } + +}; + +PermissionsScreen.propTypes = { + security: React.PropTypes.object.isRequired +}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/components/PermissionsSection.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/components/PermissionsSection.js b/app/addons/permissions/components/PermissionsSection.js new file mode 100644 index 0000000..95eb9bf --- /dev/null +++ b/app/addons/permissions/components/PermissionsSection.js @@ -0,0 +1,163 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import React, { Component, PropTypes } from 'react'; + +import FauxtonAPI from '../../../core/api'; +import app from '../../../app'; +import _ from 'lodash'; + +import PermissionsItem from './PermissionsItem'; + + +const getDocUrl = app.helpers.getDocUrl; + +const PermissionsSection = React.createClass({ + getInitialState: function () { + return { + newRole: '', + newName: '' + }; + }, + + getDefaultProps: function () { + return { + names: [], + roles: [] + }; + }, + + getHelp: function () { + if (this.props.section === 'admins') { + return 'Database members can access the database. If no members are defined, the database is public. '; + } + + return 'Database members can access the database. If no members are defined, the database is public. '; + }, + + isEmptyValue: function (value, type) { + if (!_.isEmpty(value)) { + return false; + } + FauxtonAPI.addNotification({ + msg: 'Cannot add an empty value for ' + type + '.', + type: 'error' + }); + + return true; + }, + + addNames: function (e) { + e.preventDefault(); + if (this.isEmptyValue(this.state.newName, 'names')) { + return; + } + this.props.addItem({ + type: 'names', + section: this.props.section, + value: this.state.newName + }); + + this.setState({newName: ''}); + }, + + addRoles: function (e) { + e.preventDefault(); + if (this.isEmptyValue(this.state.newRole, 'roles')) { + return; + } + this.props.addItem({ + type: 'roles', + section: this.props.section, + value: this.state.newRole + }); + + this.setState({newRole: ''}); + }, + + getItems: function (items, type) { + return items.map((item, i) => { + return <PermissionsItem + key={i} + value={item} + section={this.props.section} + type={type} + removeItem={this.props.removeItem} />; + }); + }, + + getNames: function () { + return this.getItems(this.props.names, 'names'); + }, + + getRoles: function () { + return this.getItems(this.props.roles, 'roles'); + }, + + nameChange: function (e) { + this.setState({newName: e.target.value}); + }, + + roleChange: function (e) { + this.setState({newRole: e.target.value}); + }, + + render: function () { + + const { section } = this.props; + + return ( + <div className={"permissions__" + section}> + <header className="page-header"> + <h3>{section}</h3> + <p className="help"> + {this.getHelp()} + <a className="help-link" data-bypass="true" href={getDocUrl('DB_PERMISSION')} target="_blank"> + <i className="icon-question-sign"></i> + </a> + </p> + </header> + <div className="row-fluid"> + <div className="span6"> + <header> + <h4>Users</h4> + <p>Specify users who will have {this.props.section} access to this database.</p> + </header> + <form onSubmit={this.addNames} className="permission-item-form permissions-add-user form-inline"> + <input onChange={this.nameChange} value={this.state.newName} type="text" className="item input-small" placeholder="Add User" /> + <button type="submit" className="btn btn-success"><i className="icon fonticon-plus-circled" /> Add User</button> + </form> + <ul className="unstyled permission-items span10"> + {this.getNames()} + </ul> + </div> + <div className="span6"> + <header> + <h4>Roles</h4> + <p>Users with any of the following role(s) will have {this.props.section} access.</p> + </header> + <form onSubmit={this.addRoles} className="permission-item-form permissions-add-role form-inline"> + <input onChange={this.roleChange} value={this.state.newRole} type="text" className="item input-small" placeholder="Add Role" /> + <button type="submit" className="btn btn-success"><i className="icon fonticon-plus-circled" /> Add Role</button> + </form> + <ul className="unstyled permission-items span10"> + {this.getRoles()} + </ul> + </div> + </div> + </div> + ); + } + +}); + +export default PermissionsSection; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/container/PermissionsContainer.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/container/PermissionsContainer.js b/app/addons/permissions/container/PermissionsContainer.js new file mode 100644 index 0000000..0360ee7 --- /dev/null +++ b/app/addons/permissions/container/PermissionsContainer.js @@ -0,0 +1,40 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import { connect } from 'react-redux'; + +import Permissions from '../components/Permissions'; + +import { + getIsLoading, + getSecurity, + getAdminRoles, + getAdminNames, + getMemberNames, + getMemberRoles +} from '../reducers'; + + +const mapStateToProps = (state) => { + return { + isLoading: getIsLoading(state), + adminRoles: getAdminRoles(state), + adminNames: getAdminNames(state), + memberNames: getMemberNames(state), + memberRoles: getMemberRoles(state), + security: getSecurity(state) + }; +}; + +export default connect( + mapStateToProps +)(Permissions); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/helpers.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/helpers.js b/app/addons/permissions/helpers.js new file mode 100644 index 0000000..6658f8c --- /dev/null +++ b/app/addons/permissions/helpers.js @@ -0,0 +1,39 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +export function isValueAlreadySet (p, section, type, value) { + + if (!p[section]) { + return false; + } + + if (!p[section][type]) { + return false; + } + + return p[section][type].indexOf(value) !== -1; +} + +export function addValueToPermissions (p, section, type, value) { + + if (!p[section]) { + p[section] = {}; + } + + if (!p[section][type]) { + p[section][type] = []; + } + + p[section][type].push(value); + + return p; +} http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/layout.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/layout.js b/app/addons/permissions/layout.js index 0ddeee0..84bd27c 100644 --- a/app/addons/permissions/layout.js +++ b/app/addons/permissions/layout.js @@ -13,7 +13,7 @@ import React from 'react'; import FauxtonAPI from "../../core/api"; import {TabsSidebarHeader} from '../documents/layouts'; -import Permissions from "./components.react"; +import PermissionsContainer from './container/PermissionsContainer'; import SidebarComponents from "../documents/sidebar/sidebar.react"; export const PermissionsLayout = ({docURL, database, endpoint, dbName, dropDownLinks}) => { @@ -32,7 +32,7 @@ export const PermissionsLayout = ({docURL, database, endpoint, dbName, dropDownL <SidebarComponents.SidebarController /> </aside> <section id="dashboard-content" className="flex-layout flex-col"> - <Permissions.PermissionsController /> + <PermissionsContainer url={endpoint} /> </section> </div> </div> http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/reducers.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/reducers.js b/app/addons/permissions/reducers.js new file mode 100644 index 0000000..3bf70b7 --- /dev/null +++ b/app/addons/permissions/reducers.js @@ -0,0 +1,67 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import { + PERMISSIONS_UPDATE +} from './actiontypes'; + +const initialState = { + isLoading: true, + security: {}, + adminRoles: [], + adminNames: [], + memberNames: [], + memberRoles: [] +}; + +export default function permissions (state = initialState, action) { + switch (action.type) { + + case PERMISSIONS_UPDATE: + const { permissions } = action; + return Object.assign({}, state, { + isLoading: false, + + security: permissions, + adminRoles: getRoles('admins', permissions), + adminNames: getNames('admins', permissions), + memberRoles: getRoles('members', permissions), + memberNames: getNames('members', permissions) + }); + + default: + return state; + } +}; + +function getRoles (type, permissions) { + if (!permissions[type]) { + return []; + } + + return permissions[type].roles ? permissions[type].roles : []; +} + +function getNames (type, permissions) { + if (!permissions[type]) { + return []; + } + + return permissions[type].names ? permissions[type].names : []; +} + +export const getIsLoading = state => state.isLoading; +export const getSecurity = state => state.security; +export const getAdminRoles = state => state.adminRoles; +export const getAdminNames = state => state.adminNames; +export const getMemberNames = state => state.memberNames; +export const getMemberRoles = state => state.memberRoles; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/resources.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/resources.js b/app/addons/permissions/resources.js deleted file mode 100644 index d4a4b58..0000000 --- a/app/addons/permissions/resources.js +++ /dev/null @@ -1,83 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy of -// the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. - -import app from "../../app"; -import FauxtonAPI from "../../core/api"; -var Permissions = FauxtonAPI.addon(); - -Permissions.Security = Backbone.Model.extend({ - defaults: { - admins: { names: [], roles: [] }, - members: { names: [], roles: [] } - }, - - isNew: function () { - return false; - }, - - initialize: function (attrs, options) { - this.database = options.database; - }, - - url: function () { - return window.location.origin + '/' + this.database.safeID() + '/_security'; - }, - - documentation: FauxtonAPI.constants.DOC_URLS.DB_PERMISSION, - - addItem: function (value, type, section) { - var sectionValues = this.get(section); - - var check = this.canAddItem(value, type, section); - if (check.error) { return check;} - - sectionValues[type].push(value); - return this.set(section, sectionValues); - }, - - canAddItem: function (value, type, section) { - var sectionValues = this.get(section); - - if (!sectionValues || !sectionValues[type]) { - return { - error: true, - msg: 'Section ' + section + ' does not exist' - }; - } - - if (sectionValues[type].indexOf(value) > -1) { - return { - error: true, - msg: 'Role/Name has already been added' - }; - } - - return { - error: false - }; - }, - - removeItem: function (value, type, section) { - var sectionValues = this.get(section); - var types = sectionValues[type]; - var indexOf = _.indexOf(types, value); - - if (indexOf === -1) { return;} - - types.splice(indexOf, 1); - sectionValues[type] = types; - return this.set(section, sectionValues); - } - -}); - -export default Permissions; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/routes.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/routes.js b/app/addons/permissions/routes.js index bd87d59..5025daa 100644 --- a/app/addons/permissions/routes.js +++ b/app/addons/permissions/routes.js @@ -13,48 +13,48 @@ import app from "../../app"; import FauxtonAPI from "../../core/api"; import Databases from "../databases/base"; -import Resources from "./resources"; + import Actions from "./actions"; import BaseRoute from "../documents/shared-routes"; import Layout from './layout'; import React from 'react'; const PermissionsRouteObject = BaseRoute.extend({ + roles: ['fx_loggedIn'], routes: { 'database/:database/permissions': 'permissions' }, initialize: function (route, options) { - var docOptions = app.getParams(); - docOptions.include_docs = true; + const docOptions = app.getParams(); - this.initViews(options[0]); + docOptions.include_docs = true; }, - initViews: function (databaseName) { - this.database = new Databases.Model({ id: databaseName }); - this.security = new Resources.Security(null, { - database: this.database - }); + permissions: function (databaseId) { + + // XXX magic inheritance props we need to maintain for BaseRoute + this.database = new Databases.Model({ id: databaseId }); + // XXX magic methods we have to call - originating from BaseRoute.extend this.createDesignDocsCollection(); this.addSidebar('permissions'); - }, - permissions: function () { - Actions.fetchPermissions(this.database, this.security); const crumbs = [ - { name: this.database.id, link: Databases.databaseUrl(this.database)}, + { name: this.database.id, link: Databases.databaseUrl(databaseId)}, { name: 'Permissions' } ]; + + const url = FauxtonAPI.urls('permissions', 'server', databaseId); + return <Layout - docURL={this.security.documentation} - endpoint={this.security.url('apiurl')} + docURL={FauxtonAPI.constants.DOC_URLS.DB_PERMISSION} + endpoint={url} dbName={this.database.id} dropDownLinks={crumbs} - database={this.database} - />; + database={this.database} />; + } }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/stores.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/stores.js b/app/addons/permissions/stores.js deleted file mode 100644 index 9737cd0..0000000 --- a/app/addons/permissions/stores.js +++ /dev/null @@ -1,104 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy of -// the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. - -import FauxtonAPI from "../../core/api"; -import ActionTypes from "./actiontypes"; -var Stores = {}; - -Stores.PermissionsStore = FauxtonAPI.Store.extend({ - initialize: function () { - this._isLoading = true; - }, - - isLoading: function () { - return this._isLoading; - }, - - editPermissions: function (database, security) { - this._database = database; - this._security = security; - this._isLoading = false; - }, - - getItem: function (section, type) { - if (this._isLoading) {return [];} - - return this._security.get(section)[type]; - }, - - getDatabase: function () { - return this._database; - }, - - getSecurity: function () { - return this._security; - }, - - getAdminRoles: function () { - return this.getItem('admins', 'roles'); - }, - - getAdminNames: function () { - return this.getItem('admins', 'names'); - }, - - getMemberNames: function () { - return this.getItem('members', 'names'); - }, - - getMemberRoles: function () { - return this.getItem('members', 'roles'); - }, - - addItem: function (options) { - this._security.addItem(options.value, options.type, options.section); - }, - - removeItem: function (options) { - this._security.removeItem(options.value, options.type, options.section); - }, - - dispatch: function (action) { - switch (action.type) { - case ActionTypes.PERMISSIONS_FETCHING: - this._isLoading = true; - this.triggerChange(); - break; - - case ActionTypes.PERMISSIONS_EDIT: - this.editPermissions(action.database, action.security); - this.triggerChange(); - break; - - case ActionTypes.PERMISSIONS_ADD_ITEM: - this.addItem(action.options); - this.triggerChange(); - break; - - case ActionTypes.PERMISSIONS_REMOVE_ITEM: - this.removeItem(action.options); - this.triggerChange(); - break; - - default: - return; - // do nothing - } - } - -}); - -Stores.permissionsStore = new Stores.PermissionsStore(); - -Stores.permissionsStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.permissionsStore.dispatch); - -export default Stores; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/tests/actionsSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/tests/actionsSpec.js b/app/addons/permissions/tests/actionsSpec.js deleted file mode 100644 index 4424e04..0000000 --- a/app/addons/permissions/tests/actionsSpec.js +++ /dev/null @@ -1,122 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy of -// the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. - -import FauxtonAPI from "../../../core/api"; -import Databases from "../../databases/base"; -import Stores from "../stores"; -import Permissions from "../resources"; -import Actions from "../actions"; -import testUtils from "../../../../test/mocha/testUtils"; -import sinon from "sinon"; -var assert = testUtils.assert; -var restore = testUtils.restore; -var store = Stores.permissionsStore; - -describe('Permissions Actions', function () { - var getSecuritystub; - - beforeEach(function () { - var databaseName = 'permissions-test'; - var database = new Databases.Model({ id: databaseName }); - Actions.editPermissions( - database, - new Permissions.Security(null, { - database: database - }) - ); - - - var promise = FauxtonAPI.Deferred(); - getSecuritystub = sinon.stub(store, 'getSecurity'); - getSecuritystub.returns({ - canAddItem: function () { return {error: true};}, - save: function () { - return promise; - } - }); - }); - - afterEach(function () { - restore(store.getSecurity); - }); - - describe('add Item', function () { - - afterEach(function () { - restore(FauxtonAPI.addNotification); - restore(Actions.savePermissions); - restore(store.getSecurity); - }); - - it('does not save item if cannot add it', function () { - var spy = sinon.spy(FauxtonAPI, 'addNotification'); - var spy2 = sinon.spy(Actions, 'savePermissions'); - - Actions.addItem({ - value: 'boom', - type: 'names', - section: 'members' - }); - - assert.ok(spy.calledOnce); - assert.notOk(spy2.calledOnce); - }); - - it('save items', function () { - var spy = sinon.spy(FauxtonAPI, 'addNotification'); - var spy2 = sinon.spy(Actions, 'savePermissions'); - - var promise = FauxtonAPI.Deferred(); - getSecuritystub.returns({ - canAddItem: function () { return {error: false};}, - save: function () { - return promise; - } - }); - - Actions.addItem({ - value: 'boom', - type: 'names', - section: 'members' - }); - - assert.ok(spy2.calledOnce); - assert.notOk(spy.calledOnce); - }); - }); - - describe('remove item', function () { - - afterEach(function () { - restore(Actions.savePermissions); - }); - - it('saves item', function () { - Actions.addItem({ - value: 'boom', - type: 'names', - section: 'members' - }); - - var spy = sinon.spy(Actions, 'savePermissions'); - - Actions.removeItem({ - value: 'boom', - type: 'names', - section: 'members' - }); - - assert.ok(spy.calledOnce); - }); - - }); -}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/tests/componentsSpec.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/permissions/tests/componentsSpec.react.jsx b/app/addons/permissions/tests/componentsSpec.react.jsx deleted file mode 100644 index 1b90088..0000000 --- a/app/addons/permissions/tests/componentsSpec.react.jsx +++ /dev/null @@ -1,135 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy of -// the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. -import FauxtonAPI from "../../../core/api"; -import Databases from "../../databases/base"; -import Permissions from "../resources"; -import Views from "../components.react"; -import Actions from "../actions"; -import utils from "../../../../test/mocha/testUtils"; -import React from "react"; -import ReactDOM from "react-dom"; -import sinon from "sinon"; -import { mount } from 'enzyme'; -var assert = utils.assert; -var restore = utils.restore; - -FauxtonAPI.router = new FauxtonAPI.Router([]); - -describe('Permissions Components', () => { - - beforeEach(() => { - var savePermissionsStub = sinon.stub(Actions, 'savePermissions'); - }); - - afterEach(() => { - restore(Actions.savePermissions); - }); - - describe('Permissions Controller', () => { - afterEach(() => { - restore(Actions.addItem); - restore(Actions.removeItem); - }); - - it('on Add triggers add action', () => { - var spy = sinon.spy(Actions, 'addItem'); - const el = mount(<Views.PermissionsController />); - el.instance().addItem({}); - assert.ok(spy.calledOnce); - }); - - it('on Remove triggers remove action', () => { - var spy = sinon.spy(Actions, 'removeItem'); - const el = mount(<Views.PermissionsController />); - el.instance().removeItem({ - value: 'boom', - type: 'names', - section: 'members' - }); - assert.ok(spy.calledOnce); - }); - - }); - - describe('PermissionsSection', () => { - - it('adds user on submit', () => { - const addSpy = sinon.spy(); - const el = mount(<Views.PermissionsSection section={'members'} roles={[]} names={[]} addItem={addSpy} />); - el.find('.permissions-add-user input').simulate('change', { - target: { - value: 'newusername' - } - }); - - el.find('.permissions-add-user').simulate('submit'); - - var options = addSpy.args[0][0]; - assert.ok(addSpy.calledOnce); - assert.equal(options.type, "names"); - assert.equal(options.section, "members"); - }); - - it('adds role on submit', () => { - const addSpy = sinon.spy(); - const el = mount(<Views.PermissionsSection section={'members'} roles={[]} names={[]} addItem={addSpy} />); - - el.find('.permissions-add-role input').simulate('change', { - target: { - value: 'newrole' - } - }); - - el.find('.permissions-add-role').simulate('submit'); - - var options = addSpy.args[0][0]; - assert.ok(addSpy.calledOnce); - assert.equal(options.type, "roles"); - assert.equal(options.section, "members"); - }); - - it('stores new name on change', () => { - const addSpy = sinon.spy(); - var newName = 'newName'; - const el = mount(<Views.PermissionsSection section={'members'} roles={[]} names={[]} addItem={addSpy} />); - el.find('.permissions-add-user .item').simulate('change', { - target: { - value: newName - } - }); - - assert.equal(el.state().newName, newName); - }); - - it('stores new role on change', () => { - var newRole = 'newRole'; - const addSpy = sinon.spy(); - const el = mount(<Views.PermissionsSection section={'members'} roles={[]} names={[]} addItem={addSpy} />); - el.find('.permissions-add-role .item').simulate('change', { - target: { - value: newRole - } - }); - assert.equal(el.state().newRole, newRole); - }); - }); - - describe('PermissionsItem', () => { - - it('triggers remove on click', () => { - const removeSpy = sinon.spy(); - const el = mount(<Views.PermissionsItem section={'members'} item={'test-item'} removeItem={removeSpy} />); - el.find('.close').simulate('click'); - assert.ok(removeSpy.calledOnce); - }); - }); -}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/tests/nightwatch/permissions.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/tests/nightwatch/permissions.js b/app/addons/permissions/tests/nightwatch/permissions.js new file mode 100644 index 0000000..3092bec --- /dev/null +++ b/app/addons/permissions/tests/nightwatch/permissions.js @@ -0,0 +1,40 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + + +module.exports = { + + 'CouchDB Database Permissions Test' : (client) => { + + const waitTime = client.globals.maxWaitTime; + const newDatabaseName = client.globals.testDatabaseName; + const baseUrl = client.globals.test_settings.launch_url; + + client + .loginToGUI() + .url(baseUrl + '/#/database/' + newDatabaseName + '/permissions') + + .waitForElementVisible('.permissions__admins', waitTime, false) + + .setValue('.permissions__admins [placeholder="Add User"]', 'blergie') + .clickWhenVisible('.permissions__admins .permissions-add-user button') + + .waitForElementVisible('.permissions__admins .permissions__entry', waitTime, false) + .assert.containsText('.permissions__entry span', 'blergie') + + .url(baseUrl + '/#/database/' + newDatabaseName + '/permissions') + .waitForElementVisible('.permissions__admins .permissions__entry', waitTime, false) + .assert.containsText('.permissions__entry span', 'blergie') + + .end(); + } +}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/tests/resourceSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/tests/resourceSpec.js b/app/addons/permissions/tests/resourceSpec.js deleted file mode 100644 index 7a77710..0000000 --- a/app/addons/permissions/tests/resourceSpec.js +++ /dev/null @@ -1,69 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy of -// the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. -import FauxtonAPI from "../../../core/api"; -import Models from "../resources"; -import testUtils from "../../../../test/mocha/testUtils"; -var assert = testUtils.assert; - -describe('Permissions', function () { - - describe('#addItem', function () { - var security; - - beforeEach(function () { - security = new Models.Security(null, {database: 'fakedb'}); - }); - - it('Should add value to section', function () { - - security.addItem('_user', 'names', 'admins'); - assert.equal(security.get('admins').names[0], '_user'); - }); - - it('Should handle incorrect type', function () { - security.addItem('_user', 'asdasd', 'admins'); - }); - - it('Should handle incorrect section', function () { - security.addItem('_user', 'names', 'Asdasd'); - }); - - it('Should reject duplicates', function () { - security.addItem('_user', 'names', 'admins'); - security.addItem('_user', 'names', 'admins'); - assert.equal(security.get('admins').names.length, 1); - }); - }); - - describe('#removeItem', function () { - var security; - - beforeEach(function () { - security = new Models.Security(null, {database: 'fakedb'}); - }); - - it('removes value from section', function () { - security.addItem('_user', 'names', 'admins'); - security.removeItem('_user', 'names', 'admins'); - - assert.equal(security.get('admins').names.length, 0); - }); - - it('ignores non-existing value', function () { - security.addItem('_user', 'names', 'admins'); - security.removeItem('wrong_user', 'names', 'admins'); - assert.equal(security.get('admins').names.length, 1); - }); - - }); - -}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/core/base.js ---------------------------------------------------------------------- diff --git a/app/core/base.js b/app/core/base.js index ef3d909..00e4f19 100644 --- a/app/core/base.js +++ b/app/core/base.js @@ -157,4 +157,6 @@ FauxtonAPI.setSession = function (newSession) { return FauxtonAPI.session.fetchUser(); }; +FauxtonAPI.reducers = []; + export default FauxtonAPI; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/helpers.js ---------------------------------------------------------------------- diff --git a/app/helpers.js b/app/helpers.js index f80a3f0..9310ffc 100644 --- a/app/helpers.js +++ b/app/helpers.js @@ -21,6 +21,7 @@ import constants from "./constants"; import utils from "./core/utils"; import d3 from "d3"; import moment from "moment"; +import _ from 'lodash'; var Helpers = {}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/main.js ---------------------------------------------------------------------- diff --git a/app/main.js b/app/main.js index 0a1c7e0..7f092da 100644 --- a/app/main.js +++ b/app/main.js @@ -19,6 +19,9 @@ import Backbone from 'backbone'; import $ from 'jquery'; import AppWrapper from './addons/fauxton/appwrapper'; +import { createStore, applyMiddleware } from 'redux'; +import thunk from 'redux-thunk'; +import { Provider } from 'react-redux'; app.addons = LoadAddons; FauxtonAPI.router = app.router = new FauxtonAPI.Router(app.addons); @@ -54,4 +57,22 @@ $(document).on("click", "a:not([data-bypass])", function (evt) { } }); -ReactDOM.render(<AppWrapper router={app.router}/>, document.getElementById('app')); + +const reducer = FauxtonAPI.reducers.reduce((el, acc) => { + acc[el] = el; + return acc; +}, {}); + +const middlewares = [thunk]; + +const store = createStore( + reducer, + applyMiddleware(...middlewares) +); + +ReactDOM.render( + <Provider store={store}> + <AppWrapper router={app.router}/> + </Provider>, + document.getElementById('app') +); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/jest-config.json ---------------------------------------------------------------------- diff --git a/jest-config.json b/jest-config.json index 3321768..7f817ce 100644 --- a/jest-config.json +++ b/jest-config.json @@ -1,3 +1,13 @@ { - "testPathDirs": ["app"] + "testPathDirs": ["app"], + + "setupTestFrameworkScriptFile": "jest-setup.js", + + "moduleNameMapper": { + "bootstrap": "<rootDir>/assets/js/libs/bootstrap", + "underscore": "lodash", + + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|swf|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js", + "\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js" + } } http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/jest-setup.js ---------------------------------------------------------------------- diff --git a/jest-setup.js b/jest-setup.js new file mode 100644 index 0000000..35502e6 --- /dev/null +++ b/jest-setup.js @@ -0,0 +1,18 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +const jest = require('jest'); + +window.$ = window.jQuery = require('jquery'); +jest.mock('zeroclipboard', () => {}); + +global.fetch = require('jest-fetch-mock'); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/package.json ---------------------------------------------------------------------- diff --git a/package.json b/package.json index 2f141a6..bcd4fcb 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,15 @@ "enzyme": "^2.4.1", "es5-shim": "4.5.4", "jest": "^17.0.3", + "jest-fetch-mock": "^1.0.6", "mocha": "~3.1.2", "mocha-loader": "^1.0.0", "mocha-phantomjs": "git+https://github.com/garrensmith/mocha-phantomjs.git", "nightwatch": "~0.9.0", "phantomjs-prebuilt": "^2.1.7", "react-addons-test-utils": "~15.4.1", + "redux-devtools": "^3.3.1", + "redux-mock-store": "^1.2.1", "sinon": "git+https://github.com/sinonjs/sinon.git", "url-polyfill": "github/url-polyfill" }, @@ -41,6 +44,7 @@ "babel-register": "^6.4.3", "backbone": "^1.1.0", "base-64": "^0.1.0", + "bluebird": "^3.4.6", "brace": "^0.7.0", "chai": "^3.5.0", "clean-css": "^3.4.9", @@ -82,7 +86,10 @@ "react-addons-css-transition-group": "~15.4.1", "react-bootstrap": "^0.30.7", "react-dom": "~15.4.1", + "react-redux": "^4.4.5", "react-select": "1.0.0-rc.2", + "redux": "^3.6.0", + "redux-thunk": "^2.1.0", "request": "^2.54.0", "semver": "^5.1.0", "send": "^0.13.1", @@ -96,6 +103,7 @@ "visualizeRevTree": "git+https://github.com/neojski/visualizeRevTree.git#gh-pages", "webpack": "^1.12.12", "webpack-dev-server": "^1.14.1", + "whatwg-fetch": "^2.0.1", "zeroclipboard": "^2.2.0" }, "scripts": { @@ -104,7 +112,7 @@ "webpack:test": "webpack --debug --progress --colors --config ./webpack.config.test.js", "webpack:release": "webpack --debug --progress --colors --config ./webpack.config.release.js", "jest": "jest --config ./jest-config.json", - "test": "npm run jest && grunt test", + "test": "grunt test && npm run jest", "phantomjs": "./node_modules/.bin/mocha-phantomjs --debug=false --ssl-protocol=sslv2 --web-security=false --ignore-ssl-errors=true ./test/runner.html", "couchdebug": "grunt couchdebug", "couchdb": "grunt couchdb",