diff --git a/e2e_tests/integration/0.index.spec.js b/e2e_tests/integration/0.index.spec.js index 76deaa68d19..f51ea70eecd 100644 --- a/e2e_tests/integration/0.index.spec.js +++ b/e2e_tests/integration/0.index.spec.js @@ -114,6 +114,25 @@ describe('Neo4j Browser', () => { cy.get('[data-test-id="sidebarMetaItem"]', { timeout: 30000 }).should(p => { expect(p).to.have.length.above(17) }) + cy.get('[data-test-id="drawerDB"]').click() + }) + it('displays user info in sidebar (when connected)', () => { + cy.executeCommand(':clear') + cy.get('[data-test-id="drawerDB"]').click() + cy.get('[data-test-id="user-details-username"]').should('contain', 'neo4j') + cy.get('[data-test-id="user-details-roles"]').should('contain', 'admin') + cy.executeCommand(':clear') + cy.executeCommand(':server disconnect') + cy.get('[data-test-id="user-details-username"]').should('have.length', 0) + cy.get('[data-test-id="user-details-roles"]').should('have.length', 0) + cy.connect( + 'neo4j', + Cypress.config.password + ) + cy.executeCommand(':clear') + cy.get('[data-test-id="user-details-username"]').should('contain', 'neo4j') + cy.get('[data-test-id="user-details-roles"]').should('contain', 'admin') + cy.get('[data-test-id="drawerDB"]').click() }) it('will clear local storage when clicking "Clear local data"', () => { const scriptName = 'foo' @@ -139,4 +158,11 @@ describe('Neo4j Browser', () => { // once data is cleared the user is logged out and the connect form is displayed cy.get('input[data-test-id="boltaddress"]') }) + it('displays no user info in sidebar (when not connected)', () => { + cy.executeCommand(':clear') + cy.get('[data-test-id="drawerDB"]').click() + cy.get('[data-test-id="user-details-username"]').should('have.length', 0) + cy.get('[data-test-id="user-details-roles"]').should('have.length', 0) + cy.get('[data-test-id="drawerDB"]').click() + }) }) diff --git a/e2e_tests/integration/bolt.spec.js b/e2e_tests/integration/bolt.spec.js index bccc9c561b5..708c5ee3b03 100644 --- a/e2e_tests/integration/bolt.spec.js +++ b/e2e_tests/integration/bolt.spec.js @@ -76,4 +76,14 @@ describe('Bolt connections', () => { '.' ) }) + it('displays user info in sidebar (when connected)', () => { + cy.executeCommand(':clear') + cy.get('[data-test-id="drawerDB"]').click() + cy.get('[data-test-id="user-details-username"]').should( + 'contain', + 'no-roles' + ) + cy.get('[data-test-id="user-details-roles"]').should('contain', '-') + cy.get('[data-test-id="drawerDB"]').click() + }) }) diff --git a/src/browser/modules/DatabaseInfo/DatabaseInfo.jsx b/src/browser/modules/DatabaseInfo/DatabaseInfo.jsx index 130d5449aa0..619d5ecacdb 100644 --- a/src/browser/modules/DatabaseInfo/DatabaseInfo.jsx +++ b/src/browser/modules/DatabaseInfo/DatabaseInfo.jsx @@ -22,8 +22,9 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import { withBus } from 'react-suber' import { executeCommand } from 'shared/modules/commands/commandsDuck' +import { getCurrentUser } from 'shared/modules/currentUser/currentUserDuck' import { LabelItems, RelationshipItems, PropertyItems } from './MetaItems' -import UserDetails from './UserDetails' +import { UserDetails } from './UserDetails' import DatabaseKernelInfo from './DatabaseKernelInfo' import { Drawer, DrawerBody, DrawerHeader } from 'browser-components/drawer' @@ -47,12 +48,11 @@ export class DatabaseInfo extends Component { labels = [], relationshipTypes = [], properties = [], - userDetails, databaseKernelInfo, - onItemClick, nodes, relationships - } = this.props + } = this.props.meta + const { user, onItemClick } = this.props return ( @@ -85,7 +85,7 @@ export class DatabaseInfo extends Component { onMoreClick={this.onMoreClick.bind(this)('properties')} moreStep={this.state.moreStep} /> - + { - return state.meta || {} + return { meta: state.meta, user: getCurrentUser(state) } } const mapDispatchToProps = (_, ownProps) => { return { @@ -109,5 +109,8 @@ const mapDispatchToProps = (_, ownProps) => { } export default withBus( - connect(mapStateToProps, mapDispatchToProps)(DatabaseInfo) + connect( + mapStateToProps, + mapDispatchToProps + )(DatabaseInfo) ) diff --git a/src/browser/modules/DatabaseInfo/UserDetails.jsx b/src/browser/modules/DatabaseInfo/UserDetails.jsx index d1a9dd6da90..013af352ea5 100644 --- a/src/browser/modules/DatabaseInfo/UserDetails.jsx +++ b/src/browser/modules/DatabaseInfo/UserDetails.jsx @@ -19,10 +19,6 @@ */ import React, { Component } from 'react' -import { connect } from 'react-redux' -import { withBus } from 'react-suber' -import { CYPHER_REQUEST } from 'shared/modules/cypher/cypherDuck' -import { executeCommand } from 'shared/modules/commands/commandsDuck' import Render from 'browser-components/Render' import { @@ -33,41 +29,8 @@ import { import { StyledTable, StyledKey, StyledValue, Link } from './styled' export class UserDetails extends Component { - constructor (props) { - super(props) - this.state = { - userDetails: props.userDetails || {} - } - } - fetchUserData () { - this.props.bus.self( - CYPHER_REQUEST, - { query: 'CALL dbms.security.showCurrentUser()' }, - response => { - if (!response.success) return - const result = response.result - const keys = result.records[0].keys - this.setState({ - userDetails: { - username: keys.includes('username') - ? result.records[0].get('username') - : '-', - roles: keys.includes('roles') - ? result.records[0].get('roles') - : ['admin'] - } - }) - } - ) - } - componentWillMount (props) { - this.fetchUserData() - } - componentWillReceiveProps (props) { - this.fetchUserData() - } render () { - const userDetails = this.state.userDetails + const userDetails = this.props.user if (userDetails.username) { const mappedRoles = userDetails.roles.length > 0 ? userDetails.roles.join(', ') : '-' @@ -82,11 +45,15 @@ export class UserDetails extends Component { Username: - {userDetails.username} + + {userDetails.username} + Roles: - {mappedRoles} + + {mappedRoles} + @@ -94,7 +61,8 @@ export class UserDetails extends Component { - this.props.onItemClick(':server user add')} + this.props.onItemClick(':server user add') + } > :server user add @@ -111,14 +79,3 @@ export class UserDetails extends Component { } } } - -const mapDispatchToProps = (dispatch, ownProps) => { - return { - onItemClick: cmd => { - const action = executeCommand(cmd) - ownProps.bus.send(action.type, action) - } - } -} - -export default withBus(connect(null, mapDispatchToProps)(UserDetails)) diff --git a/src/browser/modules/User/UserAdd.jsx b/src/browser/modules/User/UserAdd.jsx index 0e9b6c1d8d6..4a9f898c4f7 100644 --- a/src/browser/modules/User/UserAdd.jsx +++ b/src/browser/modules/User/UserAdd.jsx @@ -289,4 +289,9 @@ const mapStateToProps = state => { } } -export default withBus(connect(mapStateToProps, null)(UserAdd)) +export default withBus( + connect( + mapStateToProps, + null + )(UserAdd) +) diff --git a/src/browser/modules/User/UserInfo.jsx b/src/browser/modules/User/UserInfo.jsx deleted file mode 100644 index 87e64629534..00000000000 --- a/src/browser/modules/User/UserInfo.jsx +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2002-2018 "Neo4j, Inc" - * Network Engine for Objects in Lund AB [http://neotechnology.com] - * - * This file is part of Neo4j. - * - * Neo4j is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, { Component } from 'react' -import { connect } from 'react-redux' -import { withBus } from 'react-suber' -import { CYPHER_REQUEST } from 'shared/modules/cypher/cypherDuck' -import { - getCurrentUser, - updateCurrentUser -} from 'shared/modules/currentUser/currentUserDuck' - -export class UserInfoComponent extends Component { - constructor (props) { - super(props) - this.state = { user: this.props.info } - } - extractUserNameAndRolesFromBolt (r) { - return { - username: r.records[0]._fields[0], - roles: r.records[0]._fields[1] - } - } - componentWillReceiveProps (newProps) { - this.setState({ user: newProps.info }) - } - componentWillMount () { - this.props.bus.self( - CYPHER_REQUEST, - 'CALL dbms.showCurrentUser', - response => { - if (!response.success) return - const user = this.extractUserNameAndRolesFromBolt(response) - this.props.updateCurrentUser(user.username, user.roles) - } - ) - } - render () { - const currentUser = this.state.user - const result = currentUser == null ? null : JSON.stringify(currentUser) - return ( -
-

User Information

-

Current user: {result}

-
- ) - } -} - -const mapStateToProps = state => { - return { - info: getCurrentUser(state) - } -} - -const mapDispatchToProps = dispatch => { - return { - updateCurrentUser: (username, roles) => { - dispatch(updateCurrentUser(username, roles)) - } - } -} - -const UserInfo = withBus( - connect(mapStateToProps, mapDispatchToProps)(UserInfoComponent) -) -export default UserInfo diff --git a/src/browser/modules/User/UserList.jsx b/src/browser/modules/User/UserList.jsx index b9033a846e4..78df9ceda6d 100644 --- a/src/browser/modules/User/UserList.jsx +++ b/src/browser/modules/User/UserList.jsx @@ -34,6 +34,7 @@ import { StyledTable, StyledTh } from 'browser-components/DataTables' import { StyledButtonContainer } from './styled' import FrameTemplate from '../Stream/FrameTemplate' +import { forceFetch } from 'shared/modules/currentUser/currentUserDuck' export class UserList extends Component { constructor (props) { @@ -57,6 +58,7 @@ export class UserList extends Component { this.setState({ userList: this.extractUserNameAndRolesFromBolt(response.result) }) + this.props.bus.send(forceFetch().type, forceFetch()) } } ) diff --git a/src/shared/modules/currentUser/currentUserDuck.js b/src/shared/modules/currentUser/currentUserDuck.js index c1616098d0d..12e6ca50508 100644 --- a/src/shared/modules/currentUser/currentUserDuck.js +++ b/src/shared/modules/currentUser/currentUserDuck.js @@ -18,47 +18,47 @@ * along with this program. If not, see . */ +import Rx from 'rxjs/Rx' +import bolt from 'services/bolt/bolt' +import { shouldUseCypherThread } from 'shared/modules/settings/settingsDuck' import { APP_START } from 'shared/modules/app/appDuck' +import { + CONNECTION_SUCCESS, + DISCONNECTION_SUCCESS +} from 'shared/modules/connections/connectionsDuck' export const NAME = 'user' export const UPDATE_CURRENT_USER = NAME + '/UPDATE_CURRENT_USER' +export const FORCE_FETCH = NAME + '/FORCE_FETCH' +export const CLEAR = NAME + '/CLEAR' const initialState = { - info: null + username: '', + roles: [] } /** * Selectors -*/ + */ export function getCurrentUser (state) { - return Object.assign({}, state[NAME]) -} - -/** - * Helpers -*/ -function updateCurrentUserInfo (state, info) { - return Object.assign({}, state, { info: info }) + return state[NAME] } /** * Reducer -*/ + */ export default function user (state = initialState, action) { if (action.type === APP_START) { state = { ...initialState, ...state } } switch (action.type) { + case CLEAR: + return { ...initialState } case UPDATE_CURRENT_USER: - const info = action.info - if (info) { - return updateCurrentUserInfo(state, action.info) - } else { - return state - } - + const { username, roles } = action + return { username, roles } default: return state } @@ -68,9 +68,45 @@ export default function user (state = initialState, action) { export function updateCurrentUser (username, roles) { return { type: UPDATE_CURRENT_USER, - info: { - username, - roles - } + username, + roles + } +} + +export function forceFetch () { + return { + type: FORCE_FETCH } } + +// Epics +export const getCurrentUserEpic = (some$, store) => + some$ + .ofType(CONNECTION_SUCCESS) + .merge(some$.ofType(FORCE_FETCH)) + .mergeMap(() => + Rx.Observable.fromPromise( + bolt.directTransaction( + 'CALL dbms.security.showCurrentUser()', + {}, + { useCypherThread: shouldUseCypherThread(store.getState()) } + ) + ) + .catch(e => ({ type: CLEAR })) + .map(result => { + if (!result) return { type: CLEAR } + const keys = result.records[0].keys + + const username = keys.includes('username') + ? result.records[0].get('username') + : '-' + const roles = keys.includes('roles') + ? result.records[0].get('roles') + : ['admin'] + + return updateCurrentUser(username, roles) + }) + ) + +export const clearCurrentUserOnDisconnectEpic = (some$, store) => + some$.ofType(DISCONNECTION_SUCCESS).mapTo({ type: CLEAR }) diff --git a/src/shared/modules/currentUser/currentUserDuck.test.js b/src/shared/modules/currentUser/currentUserDuck.test.js index e6271a7174c..17f6b3a6569 100644 --- a/src/shared/modules/currentUser/currentUserDuck.test.js +++ b/src/shared/modules/currentUser/currentUserDuck.test.js @@ -25,28 +25,19 @@ import { dehydrate } from 'services/duckUtils' describe('user reducer current info', () => { test('handles unknown action type', () => { const action = { - type: 'UNKNOWN', - info: {} + type: 'UNKNOWN' } const nextState = reducer(undefined, action) - expect(dehydrate(nextState)).toEqual({ info: null }) - }) - test('should have no info', () => { - const action = { - type: currentUser.UPDATE_CURRENT_USER, - info: null - } - const nextState = reducer(undefined, action) - expect(Object.keys(nextState).indexOf('info')).toBeGreaterThan(-1) - expect(nextState.info).toEqual(null) + expect(dehydrate(nextState)).toEqual({ username: '', roles: [] }) }) test('should set info', () => { const action = { type: currentUser.UPDATE_CURRENT_USER, - info: { username: 'username', roles: ['king'] } + username: 'username', + roles: ['king'] } const nextState = reducer({ a: 'b' }, action) - expect(nextState.info).toEqual({ username: 'username', roles: ['king'] }) + expect(nextState).toEqual({ username: 'username', roles: ['king'] }) }) }) @@ -57,7 +48,7 @@ describe('User info actions', () => { const expectedUser = { username, roles } expect(currentUser.updateCurrentUser(username, roles)).toEqual({ type: currentUser.UPDATE_CURRENT_USER, - info: expectedUser + ...expectedUser }) }) }) diff --git a/src/shared/rootEpic.js b/src/shared/rootEpic.js index e0eacfbca2b..5ae218b6394 100644 --- a/src/shared/rootEpic.js +++ b/src/shared/rootEpic.js @@ -80,6 +80,10 @@ import { eventFiredEpic } from './modules/udc/udcDuck' import { maxFramesConfigEpic } from './modules/stream/streamDuck' +import { + getCurrentUserEpic, + clearCurrentUserOnDisconnectEpic +} from './modules/currentUser/currentUserDuck' export default combineEpics( handleCommandEpic, @@ -128,5 +132,7 @@ export default combineEpics( trackSyncLogoutEpic, trackConnectsEpic, eventFiredEpic, - maxFramesConfigEpic + maxFramesConfigEpic, + getCurrentUserEpic, + clearCurrentUserOnDisconnectEpic )