diff --git a/openbis_ng_ui/src/js/common/consts/ids.js b/openbis_ng_ui/src/js/common/consts/ids.js index d6148d853146c110543caa931a53bbb3fa9c0783..861192cce233b931e17fa3928068c6d966af2ffd 100644 --- a/openbis_ng_ui/src/js/common/consts/ids.js +++ b/openbis_ng_ui/src/js/common/consts/ids.js @@ -30,6 +30,7 @@ const PERSONAL_ACCESS_TOKEN_GRID_ID = 'personal_access_token_grid' // browsers const DATABASE_BROWSER_ID = 'database_browser' const TYPE_BROWSER_ID = 'type_browser' +const USER_BROWSER_ID = 'user_browser' export default { // app @@ -65,5 +66,6 @@ export default { // browsers DATABASE_BROWSER_ID, - TYPE_BROWSER_ID + TYPE_BROWSER_ID, + USER_BROWSER_ID } diff --git a/openbis_ng_ui/src/js/components/users/Users.jsx b/openbis_ng_ui/src/js/components/users/Users.jsx index f72928d31d5886ccb45c2f5821ff68227dabf53f..ff37c9faaea02f0e01c53274de1b241d5b0ec049 100644 --- a/openbis_ng_ui/src/js/components/users/Users.jsx +++ b/openbis_ng_ui/src/js/components/users/Users.jsx @@ -2,7 +2,7 @@ import React from 'react' import { withStyles } from '@material-ui/core/styles' import Content from '@src/js/components/common/content/Content.jsx' import ContentTab from '@src/js/components/common/content/ContentTab.jsx' -import UserBrowser from '@src/js/components/users/browser/UserBrowser.jsx' +import UserBrowser from '@src/js/components/users/browser2/UserBrowser.jsx' import UserSearch from '@src/js/components/users/search/UserSearch.jsx' import UserForm from '@src/js/components/users/form/user/UserForm.jsx' import UserGroupForm from '@src/js/components/users/form/usergroup/UserGroupForm.jsx' diff --git a/openbis_ng_ui/src/js/components/users/browser2/UserBrowser.jsx b/openbis_ng_ui/src/js/components/users/browser2/UserBrowser.jsx new file mode 100644 index 0000000000000000000000000000000000000000..697c64eea85af5269ce58344ea57b1c52bc30c82 --- /dev/null +++ b/openbis_ng_ui/src/js/components/users/browser2/UserBrowser.jsx @@ -0,0 +1,78 @@ +import _ from 'lodash' +import React from 'react' +import autoBind from 'auto-bind' +import BrowserWithOpenbis from '@src/js/components/common/browser2/BrowserWithOpenbis.jsx' +import BrowserButtonsAddRemove from '@src/js/components/common/browser2/BrowserButtonsAddRemove.jsx' +import UserBrowserController from '@src/js/components/users/browser2/UserBrowserController.js' +import AppController from '@src/js/components/AppController.js' +import pages from '@src/js/common/consts/pages.js' +import ids from '@src/js/common/consts/ids.js' +import logger from '@src/js/common/logger.js' + +export class UserBrowser extends React.Component { + constructor(props) { + super(props) + autoBind(this) + this.controller = this.props.controller || new UserBrowserController() + } + + componentDidMount() { + this.componentDidUpdate({}) + } + + componentDidUpdate(prevProps) { + if (!_.isEqual(this.props.selectedObject, prevProps.selectedObject)) { + this.controller.selectObject(this.props.selectedObject) + } + + if ( + !_.isEqual( + this.props.lastObjectModifications, + prevProps.lastObjectModifications + ) + ) { + this.controller.reload(this.props.lastObjectModifications) + } + } + + render() { + logger.log(logger.DEBUG, 'UserBrowser.render') + + return ( + <BrowserWithOpenbis + id={ids.USER_BROWSER_ID} + controller={this.controller} + renderFooter={this.renderFooter} + onSelectedChange={selectedObject => { + if (selectedObject) { + AppController.getInstance().objectOpen( + pages.USERS, + selectedObject.type, + selectedObject.id + ) + } + }} + /> + ) + } + + renderFooter() { + return ( + <div> + <BrowserButtonsAddRemove + selectedObject={this.controller.getSelectedObject()} + addEnabled={this.controller.canAddNode()} + removeEnabled={this.controller.canRemoveNode()} + onAdd={this.controller.addNode} + onRemove={this.controller.removeNode} + /> + </div> + ) + } +} + +export default AppController.getInstance().withState(() => ({ + selectedObject: AppController.getInstance().getSelectedObject(pages.USERS), + lastObjectModifications: + AppController.getInstance().getLastObjectModifications() +}))(UserBrowser) diff --git a/openbis_ng_ui/src/js/components/users/browser2/UserBrowserConsts.js b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserConsts.js new file mode 100644 index 0000000000000000000000000000000000000000..dff830968a40534605ce324ce061668237c1420e --- /dev/null +++ b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserConsts.js @@ -0,0 +1,19 @@ +import messages from '@src/js/common/messages.js' + +const TYPE_ROOT = 'root' +const TYPE_WARNING = 'warning' + +const TEXT_USERS = messages.get(messages.USERS) +const TEXT_GROUPS = messages.get(messages.GROUPS) + +function nodeId(...parts) { + return parts.join('__') +} + +export default { + nodeId, + TYPE_ROOT, + TYPE_WARNING, + TEXT_USERS, + TEXT_GROUPS +} diff --git a/openbis_ng_ui/src/js/components/users/browser2/UserBrowserController.js b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserController.js new file mode 100644 index 0000000000000000000000000000000000000000..6e4d6ac55c0dcf583f8731218469936a4a4f9876 --- /dev/null +++ b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserController.js @@ -0,0 +1,42 @@ +import BrowserController from '@src/js/components/common/browser2/BrowserController.js' +import UserBrowserControllerLoadNodePath from '@src/js/components/users/browser2/UserBrowserControllerLoadNodePath.js' +import UserBrowserControllerLoadNodes from '@src/js/components/users/browser2/UserBrowserControllerLoadNodes.js' +import UserBrowserControllerAddNode from '@src/js/components/users/browser2/UserBrowserControllerAddNode.js' +import UserBrowserControllerRemoveNode from '@src/js/components/users/browser2/UserBrowserControllerRemoveNode.js' +import UserBrowserControllerReload from '@src/js/components/users/browser2/UserBrowserControllerReload.js' + +export default class UserBrowserController extends BrowserController { + async doLoadNodePath(params) { + return await new UserBrowserControllerLoadNodePath().doLoadNodePath(params) + } + + async doLoadNodes(params) { + return await new UserBrowserControllerLoadNodes().doLoadNodes(params) + } + + async reload(objectModifications) { + new UserBrowserControllerReload(this).reload(objectModifications) + } + + canAddNode() { + return new UserBrowserControllerAddNode().canAddNode( + this.getSelectedObject() + ) + } + + async addNode() { + await new UserBrowserControllerAddNode().doAddNode(this.getSelectedObject()) + } + + canRemoveNode() { + return new UserBrowserControllerRemoveNode().canRemoveNode( + this.getSelectedObject() + ) + } + + async removeNode() { + await new UserBrowserControllerRemoveNode().doRemoveNode( + this.getSelectedObject() + ) + } +} diff --git a/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerAddNode.js b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerAddNode.js new file mode 100644 index 0000000000000000000000000000000000000000..0719f60746f9f5e594dd48db9b68eb8145642a0f --- /dev/null +++ b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerAddNode.js @@ -0,0 +1,28 @@ +import AppController from '@src/js/components/AppController.js' +import objectType from '@src/js/common/consts/objectType.js' +import pages from '@src/js/common/consts/pages.js' + +const NEW_OBJECT_TYPES = { + [objectType.USER]: objectType.NEW_USER, + [objectType.USER_GROUP]: objectType.NEW_USER_GROUP +} + +export default class TypeBrowserControllerAddNode { + canAddNode(selectedObject) { + return ( + selectedObject && + selectedObject.type === objectType.OVERVIEW && + NEW_OBJECT_TYPES[selectedObject.id] + ) + } + + async doAddNode(selectedObject) { + if (!this.canAddNode(selectedObject)) { + return + } + await AppController.getInstance().objectNew( + pages.USERS, + NEW_OBJECT_TYPES[selectedObject.id] + ) + } +} diff --git a/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerLoadNodePath.js b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerLoadNodePath.js new file mode 100644 index 0000000000000000000000000000000000000000..cd7e4f94bc524d2c752bbd5dfacbfca6e0a4d0c5 --- /dev/null +++ b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerLoadNodePath.js @@ -0,0 +1,85 @@ +import UserBrowserConsts from '@src/js/components/users/browser2/UserBrowserConsts.js' +import openbis from '@src/js/services/openbis.js' +import objectType from '@src/js/common/consts/objectType.js' + +export default class TypeBrowserConstsLoadNodePath { + async doLoadNodePath(params) { + const { object } = params + + if (object.type === objectType.OVERVIEW) { + if (object.id === objectType.USER) { + return this.createFolderPath( + objectType.USER, + UserBrowserConsts.TEXT_USERS + ) + } else if (object.id === objectType.USER_GROUP) { + return this.createFolderPath( + objectType.USER_GROUP, + UserBrowserConsts.TEXT_GROUPS + ) + } + } else if (object.type === objectType.USER) { + const id = new openbis.PersonPermId(object.id) + const fetchOptions = new openbis.PersonFetchOptions() + + const persons = await openbis.getPersons([id], fetchOptions) + const person = persons[object.id] + + return this.createNodePath( + object, + person, + objectType.USER, + UserBrowserConsts.TEXT_USERS + ) + } else if (object.type === objectType.USER_GROUP) { + const id = new openbis.AuthorizationGroupPermId(object.id) + const fetchOptions = new openbis.AuthorizationGroupFetchOptions() + + const groups = await openbis.getAuthorizationGroups([id], fetchOptions) + const group = groups[object.id] + + return this.createNodePath( + object, + group, + objectType.USER_GROUP, + UserBrowserConsts.TEXT_GROUPS + ) + } else { + return null + } + } + + createFolderPath(folderObjectType, folderText) { + return [ + { + id: UserBrowserConsts.nodeId( + UserBrowserConsts.TYPE_ROOT, + folderObjectType + ), + object: { type: objectType.OVERVIEW, id: folderObjectType }, + text: folderText + } + ] + } + + createNodePath(object, loadedObject, folderObjectType, folderText) { + if (loadedObject) { + const folderPath = this.createFolderPath(folderObjectType, folderText) + return [ + ...folderPath, + { + id: UserBrowserConsts.nodeId( + UserBrowserConsts.TYPE_ROOT, + folderObjectType, + folderObjectType, + object.id + ), + object, + text: object.id + } + ] + } else { + return null + } + } +} diff --git a/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerLoadNodes.js b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerLoadNodes.js new file mode 100644 index 0000000000000000000000000000000000000000..f20488a2878bf612cfe4c3680ec8c183ef0871be --- /dev/null +++ b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerLoadNodes.js @@ -0,0 +1,206 @@ +import _ from 'lodash' +import UserBrowserConsts from '@src/js/components/users/browser2/UserBrowserConsts.js' +import openbis from '@src/js/services/openbis.js' +import objectType from '@src/js/common/consts/objectType.js' +import messages from '@src/js/common/messages.js' +import compare from '@src/js/common/compare.js' + +const LOAD_LIMIT = 100 +const TOTAL_LOAD_LIMIT = 500 + +export default class UserBrowserConstsLoadNodesUnfiltered { + async doLoadNodes(params) { + const { node } = params + + if (node.internalRoot) { + return { + nodes: [ + { + id: UserBrowserConsts.TYPE_ROOT, + object: { + type: UserBrowserConsts.TYPE_ROOT + }, + canHaveChildren: true + } + ] + } + } else if (node.object.type === UserBrowserConsts.TYPE_ROOT) { + const [users, groups] = await Promise.all([ + this.searchUsers(params), + this.searchGroups(params) + ]) + + if (params.filter) { + const totalCount = users.totalCount + groups.totalCount + + if (totalCount > TOTAL_LOAD_LIMIT) { + return this.tooManyResultsFound(node) + } + } + + let nodes = [ + this.createUsersNode(node, users), + this.createGroupsNode(node, groups) + ] + + if (params.filter) { + nodes = nodes.filter(node => !_.isEmpty(node.children)) + } + + return { + nodes: nodes + } + } else if (node.object.type === objectType.OVERVIEW) { + let types = null + + if (node.object.id === objectType.USER) { + types = await this.searchUsers(params) + } else if (node.object.id === objectType.USER_GROUP) { + types = await this.searchGroups(params) + } + + if (types) { + return this.createNodes(node, types, node.object.id) + } else { + return { + nodes: [] + } + } + } else { + return null + } + } + + tooManyResultsFound(node) { + return { + nodes: [ + { + id: UserBrowserConsts.nodeId(node.id, UserBrowserConsts.TYPE_WARNING), + message: { + type: 'warning', + text: messages.get(messages.TOO_MANY_FILTERED_RESULTS_FOUND) + }, + selectable: false + } + ] + } + } + + async searchUsers(params) { + const { filter, offset } = params + + const criteria = new openbis.PersonSearchCriteria() + if (filter) { + criteria.withUserId().thatContains(filter) + } + const fetchOptions = new openbis.PersonFetchOptions() + + const result = await openbis.searchPersons(criteria, fetchOptions) + + return { + objects: result.getObjects().map(o => ({ + id: o.getUserId(), + text: o.getUserId() + })), + totalCount: result.getTotalCount(), + filter, + offset + } + } + + async searchGroups(params) { + const { filter, offset } = params + + const criteria = new openbis.AuthorizationGroupSearchCriteria() + if (filter) { + criteria.withCode().thatContains(filter) + } + const fetchOptions = new openbis.AuthorizationGroupFetchOptions() + + const result = await openbis.searchAuthorizationGroups( + criteria, + fetchOptions + ) + + return { + objects: result.getObjects().map(o => ({ + id: o.getCode(), + text: o.getCode() + })), + totalCount: result.getTotalCount(), + filter, + offset + } + } + + createUsersNode(parent, result) { + return this.createFolderNode( + parent, + result, + objectType.USER, + UserBrowserConsts.TEXT_USERS + ) + } + + createGroupsNode(parent, result) { + return this.createFolderNode( + parent, + result, + objectType.USER_GROUP, + UserBrowserConsts.TEXT_GROUPS + ) + } + + createFolderNode(parent, result, folderObjectType, folderText) { + const folderNode = { + id: UserBrowserConsts.nodeId(parent.id, folderObjectType), + text: folderText, + object: { + type: objectType.OVERVIEW, + id: folderObjectType + }, + canHaveChildren: !!result, + selectable: true, + expanded: result && result.filter + } + + if (result) { + folderNode.children = this.createNodes( + folderNode, + result, + folderObjectType + ) + } + + return folderNode + } + + createNodes(parent, result, objectType) { + let objects = result.objects + objects.sort((o1, o2) => compare(o1.text, o2.text)) + objects = objects.slice(result.offset, result.offset + LOAD_LIMIT) + + let nodes = objects.map(object => ({ + id: UserBrowserConsts.nodeId(parent.id, objectType, object.id), + text: object.text, + object: { + type: objectType, + id: object.id + } + })) + + if (_.isEmpty(nodes)) { + return null + } else { + return { + nodes: nodes, + loadMore: { + offset: result.offset + nodes.length, + loadedCount: result.offset + nodes.length, + totalCount: result.totalCount, + append: true + } + } + } + } +} diff --git a/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerReload.js b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerReload.js new file mode 100644 index 0000000000000000000000000000000000000000..b5e9af8443c519384fbca93ffabf36a50290fa1e --- /dev/null +++ b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerReload.js @@ -0,0 +1,16 @@ +import BrowserControllerReload from '@src/js/components/common/browser2/BrowserControllerReload.js' +import objectType from '@src/js/common/consts/objectType.js' +import objectOperation from '@src/js/common/consts/objectOperation.js' + +export default class TypeBrowserControllerReload extends BrowserControllerReload { + constructor(controller) { + super(controller) + } + + doGetObservedModifications() { + return { + [objectType.USER]: [objectOperation.CREATE, objectOperation.DELETE], + [objectType.USER_GROUP]: [objectOperation.CREATE, objectOperation.DELETE] + } + } +} diff --git a/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerRemoveNode.js b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerRemoveNode.js new file mode 100644 index 0000000000000000000000000000000000000000..4788233e1313f83b1644cf0370d5e59556e8b745 --- /dev/null +++ b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerRemoveNode.js @@ -0,0 +1,81 @@ +import AppController from '@src/js/components/AppController.js' +import objectType from '@src/js/common/consts/objectType.js' +import openbis from '@src/js/services/openbis.js' +import pages from '@src/js/common/consts/pages.js' +import messages from '@src/js/common/messages.js' + +const REMOVABLE_OBJECT_TYPES = [objectType.USER, objectType.USER_GROUP] + +export default class TypeBrowserControllerRemoveNode { + canRemoveNode(selectedObject) { + return ( + selectedObject && REMOVABLE_OBJECT_TYPES.includes(selectedObject.type) + ) + } + + async doRemoveNode(selectedObject) { + if (!this.canRemoveNode(selectedObject)) { + return + } + + const { type, id } = selectedObject + const reason = 'deleted via ng_ui' + + return this._prepareRemoveOperations(type, id, reason) + .then(operations => { + const options = new openbis.SynchronousOperationExecutionOptions() + options.setExecuteInOrder(true) + return openbis.executeOperations(operations, options) + }) + .then(() => { + AppController.getInstance().objectDelete(pages.USERS, type, id) + }) + .catch(error => { + if ( + error && + error.message && + error.message.startsWith('Could not commit Hibernate transaction') + ) { + AppController.getInstance().errorChange( + messages.get( + messages.USERS_WHO_REGISTERED_SOME_DATA_CANNOT_BE_REMOVED + ) + ) + } else { + AppController.getInstance().errorChange(error) + } + }) + } + + _prepareRemoveOperations(type, id, reason) { + if (type === objectType.USER_GROUP) { + return this._prepareRemoveUserGroupOperations(id, reason) + } else if (type === objectType.USER) { + return this._prepareRemoveUserOperations(id, reason) + } else { + throw new Error('Unsupported type: ' + type) + } + } + + _prepareRemoveUserGroupOperations(id, reason) { + const options = new openbis.AuthorizationGroupDeletionOptions() + options.setReason(reason) + return Promise.resolve([ + new openbis.DeleteAuthorizationGroupsOperation( + [new openbis.AuthorizationGroupPermId(id)], + options + ) + ]) + } + + _prepareRemoveUserOperations(id, reason) { + const options = new openbis.PersonDeletionOptions() + options.setReason(reason) + return Promise.resolve([ + new openbis.DeletePersonsOperation( + [new openbis.PersonPermId(id)], + options + ) + ]) + } +} diff --git a/openbis_ng_ui/srcTest/js/components/users/wrapper/UsersWrapper.js b/openbis_ng_ui/srcTest/js/components/users/wrapper/UsersWrapper.js index 6244d123d647ab363bf565764563ff93c264ced5..c69b316f85956a61336b49b9fd365653e1a604e0 100644 --- a/openbis_ng_ui/srcTest/js/components/users/wrapper/UsersWrapper.js +++ b/openbis_ng_ui/srcTest/js/components/users/wrapper/UsersWrapper.js @@ -1,5 +1,5 @@ import Content from '@src/js/components/common/content/Content.jsx' -import UserBrowser from '@src/js/components/users/browser/UserBrowser.jsx' +import { UserBrowser } from '@src/js/components/users/browser2/UserBrowser.jsx' import BaseWrapper from '@srcTest/js/components/common/wrapper/BaseWrapper.js' import BrowserWrapper from '@srcTest/js/components/common/browser/wrapper/BrowserWrapper.js' import ContentWrapper from '@srcTest/js/components/common/content/wrapper/ContentWrapper.js'