diff --git a/web/client/actions/__tests__/usergroups-test.js b/web/client/actions/__tests__/usergroups-test.js new file mode 100644 index 0000000000..38f3d82d48 --- /dev/null +++ b/web/client/actions/__tests__/usergroups-test.js @@ -0,0 +1,255 @@ +/** + * Copyright 2016, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const expect = require('expect'); +const assign = require('object-assign'); +const { + GETGROUPS, + STATUS_SUCCESS, + STATUS_ERROR, + getUserGroups, + editGroup, + EDITGROUP, + changeGroupMetadata, + EDITGROUPDATA, + saveGroup, + UPDATEGROUP, + deleteGroup, + DELETEGROUP, + STATUS_DELETED, + searchUsers, + SEARCHUSERS +} = require('../usergroups'); +let GeoStoreDAO = require('../../api/GeoStoreDAO'); +let oldAddBaseUri = GeoStoreDAO.addBaseUrl; + +describe('Test correctness of the usergroups actions', () => { + beforeEach(() => { + GeoStoreDAO.addBaseUrl = (options) => { + return assign(options, {baseURL: 'base/web/client/test-resources/geostore/'}); + }; + }); + + afterEach(() => { + GeoStoreDAO.addBaseUrl = oldAddBaseUri; + }); + it('get UserGroups', (done) => { + const retFun = getUserGroups('usergroups.json', {params: {start: 0, limit: 10}}); + expect(retFun).toExist(); + let count = 0; + retFun((action) => { + expect(action.type).toBe(GETGROUPS); + count++; + if (count === 2) { + expect(action.status).toBe(STATUS_SUCCESS); + expect(action.groups).toExist(); + expect(action.groups[0]).toExist(); + expect(action.groups[0].groupName).toExist(); + done(); + } + + }, () => ({ + userGroups: { + searchText: "*" + } + })); + + }); + it('getUserGroups error', (done) => { + const retFun = getUserGroups('MISSING_LINK', {params: {start: 0, limit: 10}}); + expect(retFun).toExist(); + let count = 0; + retFun((action) => { + expect(action.type).toBe(GETGROUPS); + count++; + if (count === 2) { + expect(action.status).toBe(STATUS_ERROR); + expect(action.error).toExist(); + done(); + } + + }); + + }); + it('edit UserGroup', (done) => { + const retFun = editGroup({id: 1}); + expect(retFun).toExist(); + let count = 0; + retFun((action) => { + expect(action.type).toBe(EDITGROUP); + count++; + if (count === 2) { + expect(action.group).toExist(); + expect(action.status).toBe("success"); + done(); + } + }); + }, {security: {user: {role: "ADMIN"}}}); + + it('edit UserGroup new', (done) => { + let template = {groupName: "hello"}; + const retFun = editGroup(template); + expect(retFun).toExist(); + let count = 0; + retFun((action) => { + expect(action.type).toBe(EDITGROUP); + count++; + if (count === 1) { + expect(action.group).toExist(); + expect(action.group).toBe(template); + done(); + } + }); + }); + it('edit UserGroup error', (done) => { + const retFun = editGroup({id: 99999}); + expect(retFun).toExist(); + let count = 0; + retFun((action) => { + expect(action.type).toBe(EDITGROUP); + count++; + if (count === 2) { + expect(action.error).toExist(); + expect(action.status).toBe("error"); + done(); + } + }); + }); + + it('change usergroup metadata', () => { + const action = changeGroupMetadata("groupName", "New Group Name"); + expect(action).toExist(); + expect(action.type).toBe(EDITGROUPDATA); + expect(action.key).toBe("groupName"); + expect(action.newValue).toBe("New Group Name"); + + }); + + it('update usergroup', (done) => { + // 1# is a workaround to skip the trailing slash of the request + // that can not be managed by the test-resources + const retFun = saveGroup({id: "1#", newUsers: [{id: 100, name: "name1"}]}); + expect(retFun).toExist(); + let count = 0; + retFun((action) => { + if (action.type) { + expect(action.type).toBe(UPDATEGROUP); + } + count++; + if (count === 2) { + expect(action.group).toExist(); + expect(action.status).toBe("saved"); + } + if (count === 3) { + // the third call is for update list + done(); + } + }); + }); + it('create usergroup', (done) => { + GeoStoreDAO.addBaseUrl = (options) => { + return assign(options, {baseURL: 'base/web/client/test-resources/geostore/usergroups/newGroup.txt#'}); + }; + const retFun = saveGroup({groupName: "TEST"}); + expect(retFun).toExist(); + let count = 0; + retFun((action) => { + if (action.type) { + expect(action.type).toBe(UPDATEGROUP); + } + count++; + if (count === 2) { + expect(action.group).toExist(); + expect(action.group.id).toExist(); + expect(action.group.id).toBe(1); + expect(action.status).toBe("created"); + } + if (count === 3) { + // the third call is for update list + done(); + } + }); + }); + it('create usergroup with groups', (done) => { + GeoStoreDAO.addBaseUrl = (options) => { + return assign(options, {baseURL: 'base/web/client/test-resources/geostore/usergroups/newGroup.txt#'}); + }; + const retFun = saveGroup({groupName: "TEST", newUsers: [{id: 100, name: "name1"}]}); + expect(retFun).toExist(); + let count = 0; + retFun((action) => { + if (action.type) { + expect(action.type).toBe(UPDATEGROUP); + } + count++; + if (count === 2) { + expect(action.group).toExist(); + expect(action.group.id).toExist(); + expect(action.group.id).toBe(1); + expect(action.status).toBe("created"); + } + if (count === 3) { + // the third call is for update list + done(); + } + }); + }); + + it('delete Group', (done) => { + let confirm = deleteGroup(1); + expect(confirm).toExist(); + expect(confirm.status).toBe("confirm"); + const retFun = deleteGroup(1, "delete"); + expect(retFun).toExist(); + let count = 0; + retFun((action) => { + if (action.type) { + expect(action.type).toBe(DELETEGROUP); + } + count++; + if (count === 2) { + expect(action.status).toExist(); + expect(action.status).toBe(STATUS_DELETED); + expect(action.id).toBe(1); + done(); + } + if (count === 3) { + // the third call is for update list + done(); + } + }); + }); + it('search users', (done) => { + const retFun = searchUsers('users.json', 0, 10, {params: {start: 0, limit: 10}}, ""); + expect(retFun).toExist(); + let count = 0; + retFun((action) => { + expect(action.type).toBe(SEARCHUSERS); + count++; + if (count === 2) { + expect(action.users).toExist(); + expect(action.users[0]).toExist(); + expect(action.users[0].groups).toExist(); + done(); + } + }); + }); + it('search users', (done) => { + const retFun = searchUsers('MISSING_LINK', {params: {start: 0, limit: 10}}); + expect(retFun).toExist(); + let count = 0; + retFun((action) => { + expect(action.type).toBe(SEARCHUSERS); + count++; + if (count === 2) { + expect(action.error).toExist(); + done(); + } + }); + }); +}); diff --git a/web/client/actions/usergroups.js b/web/client/actions/usergroups.js new file mode 100644 index 0000000000..3cab0db5b4 --- /dev/null +++ b/web/client/actions/usergroups.js @@ -0,0 +1,330 @@ +/** + * Copyright 2015, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const GETGROUPS = 'GROUPMANAGER_GETGROUPS'; +const EDITGROUP = 'GROUPMANAGER_EDITGROUP'; +const EDITGROUPDATA = 'GROUPMANAGER_EDITGROUP_DATA'; +const UPDATEGROUP = 'GROUPMANAGER_UPDATE_GROUP'; +const DELETEGROUP = 'GROUPMANAGER_DELETEGROUP'; +const SEARCHTEXTCHANGED = 'GROUPMANAGER_SEARCHTEXTCHANGED'; +const SEARCHUSERS = 'GROUPMANAGER_SEARCHUSERS'; +const STATUS_LOADING = "loading"; +const STATUS_SUCCESS = "success"; +const STATUS_ERROR = "error"; +// const STATUS_NEW = "new"; +const STATUS_SAVING = "saving"; +const STATUS_SAVED = "saved"; +const STATUS_CREATING = "creating"; +const STATUS_CREATED = "created"; +const STATUS_DELETED = "deleted"; + +/* +const USERGROUPMANAGER_UPDATE_GROUP = 'USERMANAGER_UPDATE_GROUP'; +const USERGROUPMANAGER_DELETE_GROUP = 'USERMANAGER_DELETE_GROUP'; +const USERGROUPMANAGER_SEARCH_TEXT_CHANGED = 'USERGROUPMANAGER_SEARCH_TEXT_CHANGED'; +*/ +const API = require('../api/GeoStoreDAO'); +const {get/*, assign*/} = require('lodash'); + +function getUserGroupsLoading(text, start, limit) { + return { + type: GETGROUPS, + status: STATUS_LOADING, + searchText: text, + start, + limit + }; +} +function getUserGroupSuccess(text, start, limit, groups, totalCount) { + return { + type: GETGROUPS, + status: STATUS_SUCCESS, + searchText: text, + start, + limit, + groups, + totalCount + + }; +} +function getUserGroupError(text, start, limit, error) { + return { + type: GETGROUPS, + status: STATUS_ERROR, + searchText: text, + start, + limit, + error + }; +} +function getUserGroups(searchText, options) { + let params = options && options.params; + let start; + let limit; + if (params) { + start = params.start; + limit = params.limit; + } + return (dispatch, getState) => { + let text = searchText; + let state = getState && getState(); + if (state) { + let oldText = get(state, "usergroups.searchText"); + text = searchText || oldText || "*"; + start = ( (start !== null && start !== undefined) ? start : (get(state, "usergroups.start") || 0)); + limit = limit || get(state, "usergroups.limit") || 12; + } + dispatch(getUserGroupsLoading(text, start, limit)); + + return API.getGroups(text, {...options, params: {start, limit}}).then((response) => { + let groups; + // this because _.get returns an array with an undefined element isntead of null + if (!response || !response.ExtGroupList || !response.ExtGroupList.Group) { + groups = []; + } else { + groups = get(response, "ExtGroupList.Group"); + } + + let totalCount = get(response, "ExtGroupList.GroupCount"); + groups = Array.isArray(groups) ? groups : [groups]; + dispatch(getUserGroupSuccess(text, start, limit, groups, totalCount)); + }).catch((error) => { + dispatch(getUserGroupError(text, start, limit, error)); + }); + }; +} +function editGroupLoading(group) { + return { + type: EDITGROUP, + status: STATUS_LOADING, + group + }; +} + +function editGroupSuccess(group) { + return { + type: EDITGROUP, + status: STATUS_SUCCESS, + group + }; +} + +function editGroupError(group, error) { + return { + type: EDITGROUP, + status: STATUS_ERROR, + group, + error + }; +} + +function editNewGroup(group) { + return { + type: EDITGROUP, + group + }; +} +// NOTE: not support on server side now for editing groups +function editGroup(group, options ={params: {includeattributes: true}} ) { + return (dispatch) => { + if (group && group.id) { + dispatch(editGroupLoading(group)); + return API.getGroup(group.id, options).then((groupLoaded) => { + // the service returns restUsers = "", skip this to avoid overriding + dispatch(editGroupSuccess(groupLoaded)); + }).catch((error) => { + dispatch(editGroupError(group, error)); + }); + } + dispatch(editNewGroup(group)); + }; +} +function changeGroupMetadata(key, newValue) { + return { + type: EDITGROUPDATA, + key, + newValue + }; +} + +function savingGroup(group) { + return { + type: UPDATEGROUP, + status: STATUS_SAVING, + group + }; +} + +function savedGroup(group) { + return { + type: UPDATEGROUP, + status: STATUS_SAVED, + group: group + }; +} + +function saveError(group, error) { + return { + type: UPDATEGROUP, + status: STATUS_ERROR, + group, + error + }; +} + +function creatingGroup(group) { + return { + type: UPDATEGROUP, + status: STATUS_CREATING, + group + }; +} + +function groupCreated(id, group) { + return { + type: UPDATEGROUP, + status: STATUS_CREATED, + group: { ...group, id} + }; +} + +function createError(group, error) { + return { + type: UPDATEGROUP, + status: STATUS_ERROR, + group, + error + }; +} +function saveGroup(group, options = {}) { + return (dispatch) => { + if (group && group.id) { + dispatch(savingGroup(group)); + return API.updateGroupMembers(group, options).then((groupDetails) => { + dispatch(savedGroup(groupDetails)); + dispatch(getUserGroups()); + }).catch((error) => { + dispatch(saveError(group, error)); + }); + } + // create Group + dispatch(creatingGroup(group)); + return API.createGroup(group, options).then((id) => { + dispatch(groupCreated(id, group)); + dispatch(getUserGroups()); + }).catch((error) => { + dispatch(createError(group, error)); + }); + + }; +} + +function deletingGroup(id) { + return { + type: DELETEGROUP, + status: "deleting", + id + }; +} +function deleteGroupSuccess(id) { + return { + type: DELETEGROUP, + status: STATUS_DELETED, + id + }; +} +function deleteGroupError(id, error) { + return { + type: DELETEGROUP, + status: STATUS_ERROR, + id, + error + }; +} + +function closeDelete(status, id) { + return { + type: DELETEGROUP, + status, + id + }; +} +function deleteGroup(id, status = "confirm") { + if (status === "confirm" || status === "cancelled") { + return closeDelete(status, id); + } else if ( status === "delete") { + return (dispatch) => { + dispatch(deletingGroup(id)); + API.deleteGroup(id).then(() => { + dispatch(deleteGroupSuccess(id)); + dispatch(getUserGroups()); + }).catch((error) => { + dispatch(deleteGroupError(id, error)); + }); + }; + } +} + +function groupSearchTextChanged(text) { + return { + type: SEARCHTEXTCHANGED, + text + }; +} +function searchUsersSuccessLoading() { + return { + type: SEARCHUSERS, + status: STATUS_LOADING + }; +} +function searchUsersSuccess(users) { + return { + type: SEARCHUSERS, + status: STATUS_SUCCESS, + users + }; +} +function searchUsersError(error) { + return { + type: SEARCHUSERS, + status: STATUS_ERROR, + error + }; +} +function searchUsers(text ="*", start = 0, limit = 5, options = {}, jollyChar = "*") { + return (dispatch) => { + dispatch(searchUsersSuccessLoading(text, start, limit)); + return API.getUsers(jollyChar + text + jollyChar, {...options, params: {start, limit}}).then((response) => { + let users; + // this because _.get returns an array with an undefined element instead of null + if (!response || !response.ExtUserList || !response.ExtUserList.User) { + users = []; + } else { + users = get(response, "ExtUserList.User"); + } + users = Array.isArray(users) ? users : [users]; + dispatch(searchUsersSuccess(users)); + }).catch((error) => { + dispatch(searchUsersError(error)); + }); + }; +} + +module.exports = { + getUserGroups, GETGROUPS, + editGroup, EDITGROUP, + changeGroupMetadata, EDITGROUPDATA, + groupSearchTextChanged, SEARCHTEXTCHANGED, + searchUsers, SEARCHUSERS, + saveGroup, UPDATEGROUP, + deleteGroup, DELETEGROUP, + STATUS_SUCCESS, + STATUS_LOADING, + STATUS_ERROR, + STATUS_DELETED +}; diff --git a/web/client/api/GeoStoreDAO.js b/web/client/api/GeoStoreDAO.js index 4a724b63f4..d3149b9721 100644 --- a/web/client/api/GeoStoreDAO.js +++ b/web/client/api/GeoStoreDAO.js @@ -223,7 +223,70 @@ var Api = { deleteUser: function(id, options = {}) { let url = "users/user/" + id; return axios.delete(url, this.addBaseUrl(parseOptions(options))).then(function(response) {return response.data; }); + }, + getGroups: function(textSearch, options = {}) { + let url = "extjs/search/groups" + (textSearch ? "/" + textSearch : ""); + return axios.get(url, this.addBaseUrl(parseOptions(options))).then(function(response) {return response.data; }); + }, + getGroup: function(id, options = {}) { + let url = "usergroups/group/" + id; + return axios.get(url, this.addBaseUrl(parseOptions(options))).then(function(response) { + let groupLoaded = response.data.UserGroup; + let users = groupLoaded && groupLoaded.restUsers && groupLoaded.restUsers.User; + return {...groupLoaded, users: users && (Array.isArray(users) ? users : [users]) || []}; + }); + }, + createGroup: function(group, options) { + let url = "usergroups/"; + let groupId; + return axios.post(url, {UserGroup: {...group}}, this.addBaseUrl(parseOptions(options))) + .then(function(response) { + groupId = response.data; + return Api.updateGroupMembers({...group, id: groupId}, options); + }).then(() => groupId); + }, + updateGroupMembers: function(group, options) { + // No GeoStore API to update group name and description. only update new users + if (group.newUsers) { + let restUsers = group.users || (group.restUsers && group.restUsers.User) || []; + restUsers = Array.isArray(restUsers) ? restUsers : [restUsers]; + // old users not present in the new users list + let toRemove = restUsers.filter( (user) => group.newUsers.findIndex( u => u.id === user.id) < 0); + // new users not present in the old users list + let toAdd = group.newUsers.filter( (user) => restUsers.findIndex( u => u.id === user.id) < 0); + + // create callbacks + let removeCallbacks = toRemove.map( (user) => () => this.removeUserFromGroup(user.id, group.id, options) ); + let addCallbacks = toAdd.map( (user) => () => this.addUserToGroup(user.id, group.id), options ); + let requests = [...(removeCallbacks.map( call => call.call(this))), ...(addCallbacks.map(call => call()))]; + return axios.all(requests).then(() => { + return { + ...group, + newUsers: null, + restUsers: { User: group.newUsers}, + users: group.newUsers + }; + }); + } + return new Promise( (resolve) => { + resolve({ + ...group + }); + }); + }, + deleteGroup: function(id, options={}) { + let url = "usergroups/group/" + id; + return axios.delete(url, this.addBaseUrl(parseOptions(options))).then(function(response) {return response.data; }); + }, + addUserToGroup(userId, groupId, options = {}) { + let url = "/usergroups/group/" + userId + "/" + groupId + "/"; + return axios.post(url, null, this.addBaseUrl(parseOptions(options))); + }, + removeUserFromGroup(userId, groupId, options = {}) { + let url = "/usergroups/group/" + userId + "/" + groupId + "/"; + return axios.delete(url, this.addBaseUrl(parseOptions(options))); } + }; module.exports = Api; diff --git a/web/client/components/manager/users/GroupCard.jsx b/web/client/components/manager/users/GroupCard.jsx new file mode 100644 index 0000000000..642d6ec54b --- /dev/null +++ b/web/client/components/manager/users/GroupCard.jsx @@ -0,0 +1,72 @@ +/** + * Copyright 2016, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const React = require('react'); +// const Message = require('../I18N/Message'); +const GridCard = require('../../misc/GridCard'); +const {Button, Glyphicon} = require('react-bootstrap'); +const Message = require('../../../components/I18N/Message'); + + +// const ConfirmModal = require('./modals/ConfirmModal'); + +require('./style/usercard.css'); + +const GroupCard = React.createClass({ + propTypes: { + // props + style: React.PropTypes.object, + group: React.PropTypes.object, + innerItemStyle: React.PropTypes.object, + actions: React.PropTypes.array + }, + getDefaultProps() { + return { + style: { + background: "#F7F4ED", + position: "relative", + backgroundSize: "cover", + backgroundPosition: "center", + backgroundRepeat: "repeat-x" + }, + innerItemStyle: {"float": "left", margin: "10px"} + }; + }, + renderStatus() { + return (
+
+ {this.props.group.enabled ? + : + } +
); + }, + renderAvatar() { + return (
); + }, + renderDescription() { + return (
+
+
{this.props.group.description ? this.props.group.description : }
+
); + }, + render() { + return ( + + {this.renderAvatar()} + {this.renderStatus()} + {this.renderDescription()} + + ); + } +}); + +module.exports = GroupCard; diff --git a/web/client/components/manager/users/GroupDialog.jsx b/web/client/components/manager/users/GroupDialog.jsx new file mode 100644 index 0000000000..1db428c68f --- /dev/null +++ b/web/client/components/manager/users/GroupDialog.jsx @@ -0,0 +1,228 @@ +/** + * Copyright 2016, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + /** + * Copyright 2016, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const React = require('react'); +const UsersTable = require('./UsersTable'); +const {Alert, Tabs, Tab, Button, Glyphicon, Input} = require('react-bootstrap'); + +const Dialog = require('../../../components/misc/Dialog'); +const assign = require('object-assign'); +const Message = require('../../../components/I18N/Message'); +const Spinner = require('react-spinkit'); +const Select = require("react-select"); +require('./style/userdialog.css'); + /** + * A Modal window to show password reset form + */ +const GroupDialog = React.createClass({ + propTypes: { + // props + group: React.PropTypes.object, + users: React.PropTypes.array, + availableUsers: React.PropTypes.array, + searchUsers: React.PropTypes.func, + availableUsersLoading: React.PropTypes.bool, + show: React.PropTypes.bool, + onClose: React.PropTypes.func, + onChange: React.PropTypes.func, + onSave: React.PropTypes.func, + modal: React.PropTypes.bool, + closeGlyph: React.PropTypes.string, + style: React.PropTypes.object, + buttonSize: React.PropTypes.string, + inputStyle: React.PropTypes.object + }, + getDefaultProps() { + return { + group: {}, + availableUsers: [], + onClose: () => {}, + onChange: () => {}, + onSave: () => {}, + options: {}, + useModal: true, + closeGlyph: "", + style: {}, + buttonSize: "large", + includeCloseButton: true, + inputStyle: { + height: "32px", + width: "260px", + marginTop: "3px", + marginBottom: "20px", + padding: "5px", + border: "1px solid #078AA3" + } + }; + }, + getCurrentGroupMembers() { + return this.props.group && (this.props.group.newUsers || this.props.group.users) || []; + }, + renderGeneral() { + return (
+ } + onChange={this.handleChange} + value={this.props.group && this.props.group.groupName}/> + } + onChange={this.handleChange} + value={this.props.group && this.props.group.description || ""}/> +
); + }, + + renderSaveButtonContent() { + let defaultMessage = this.props.group && this.props.group.id ? : ; + let messages = { + error: defaultMessage, + success: defaultMessage, + modified: defaultMessage, + save: , + saving: , + saved: , + creating: , + created: + }; + let message = messages[status] || defaultMessage; + return [this.isSaving() ? : null, message]; + }, + renderButtons() { + return [ + , + + + ]; + }, + + renderError() { + let error = this.props.group && this.props.group.status === "error"; + if ( error ) { + let lastError = this.props.group && this.props.group.lastError; + return {lastError && lastError.statusText}; + } + + }, + renderMembers() { + let members = this.getCurrentGroupMembers(); + if (!members || members.length === 0) { + return (
); + } + // NOTE: faking group Id + return ( u1.name > u2.name)} onRemove={(user) => { + let id = user.id; + let newUsers = this.getCurrentGroupMembers().filter(u => u.id !== id); + this.props.onChange("newUsers", newUsers); + }}/>); + }, + renderMembersTab() { + let availableUsers = this.props.availableUsers.filter((user) => this.getCurrentGroupMembers().findIndex( member => member.id === user.id) < 0).map(u => ({value: u.id, label: u.name})); + return (
+ +
{this.renderMembers()}
+
+ +