diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-06-29 09:52:31 +0200 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2016-07-12 10:18:55 +0200 |
commit | d5ca0eb5782c29c613a53b76cfe169dbe4ceab81 (patch) | |
tree | b69d3c2999251dc78f6770332a8687a2e027cfee /server/sonar-web/src/main/js/apps/permissions | |
parent | 0fbbe800ee3ae1f68df6e5d4c868a2910b981a55 (diff) | |
download | sonarqube-d5ca0eb5782c29c613a53b76cfe169dbe4ceab81.tar.gz sonarqube-d5ca0eb5782c29c613a53b76cfe169dbe4ceab81.zip |
SONAR-7840 SONAR-7879 Improve UX on permissions pages
Diffstat (limited to 'server/sonar-web/src/main/js/apps/permissions')
32 files changed, 1850 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/apps/permissions/global/app.js b/server/sonar-web/src/main/js/apps/permissions/global/app.js new file mode 100644 index 00000000000..db8e2981d9c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/global/app.js @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { render } from 'react-dom'; +import { Provider } from 'react-redux'; +import App from './components/App'; +import configureStore from '../../../components/store/configureStore'; +import rootReducer from '../shared/store/rootReducer'; + +window.sonarqube.appStarted.then(options => { + const el = document.querySelector(options.el); + const store = configureStore(rootReducer); + render(( + <Provider store={store}> + <App/> + </Provider> + ), el); +}); diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.js b/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.js new file mode 100644 index 00000000000..fe97b7c3233 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.js @@ -0,0 +1,131 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { connect } from 'react-redux'; +import SearchForm from '../../shared/components/SearchForm'; +import HoldersList from '../../shared/components/HoldersList'; +import { + loadHolders, + grantToUser, + revokeFromUser, + grantToGroup, + revokeFromGroup, + updateFilter, + updateQuery, + selectPermission +} from '../store/actions'; +import { + getUsers, + getGroups, + getQuery, + getFilter, + getSelectedPermission +} from '../../shared/store/rootReducer'; +import { translate } from '../../../../helpers/l10n'; + +const PERMISSIONS_ORDER = [ + 'admin', + 'profileadmin', + 'gateadmin', + 'shareDashboard', + 'scan', + 'provisioning' +]; + +class AllHoldersList extends React.Component { + componentDidMount () { + this.props.loadHolders(); + } + + handleToggleUser (user, permission) { + const hasPermission = user.permissions.includes(permission); + + if (hasPermission) { + this.props.revokePermissionFromUser(user.login, permission); + } else { + this.props.grantPermissionToUser(user.login, permission); + } + } + + handleToggleGroup (group, permission) { + const hasPermission = group.permissions.includes(permission); + + if (hasPermission) { + this.props.revokePermissionFromGroup(group.name, permission); + } else { + this.props.grantPermissionToGroup(group.name, permission); + } + } + + render () { + const permissions = PERMISSIONS_ORDER.map(p => ({ + key: p, + name: translate('global_permissions', p), + description: translate('global_permissions', p, 'desc') + })); + + return ( + <HoldersList + permissions={permissions} + selectedPermission={this.props.selectedPermission} + users={this.props.users} + groups={this.props.groups} + onSelectPermission={this.props.onSelectPermission} + onToggleUser={this.handleToggleUser.bind(this)} + onToggleGroup={this.handleToggleGroup.bind(this)}> + + <SearchForm + query={this.props.query} + filter={this.props.filter} + onSearch={this.props.onSearch} + onFilter={this.props.onFilter}/> + + </HoldersList> + ); + } +} + +const mapStateToProps = state => ({ + users: getUsers(state), + groups: getGroups(state), + query: getQuery(state), + filter: getFilter(state), + selectedPermission: getSelectedPermission(state) +}); + +const mapDispatchToProps = dispatch => ({ + loadHolders: () => dispatch(loadHolders()), + onSearch: query => dispatch(updateQuery(query)), + onFilter: filter => dispatch(updateFilter(filter)), + onSelectPermission: permission => dispatch(selectPermission(permission)), + grantPermissionToUser: (login, permission) => + dispatch(grantToUser(login, permission)), + revokePermissionFromUser: (login, permission) => + dispatch(revokeFromUser(login, permission)), + grantPermissionToGroup: (groupName, permission) => + dispatch(grantToGroup(groupName, permission)), + revokePermissionFromGroup: (groupName, permission) => + dispatch(revokeFromGroup(groupName, permission)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(AllHoldersList); diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/App.js b/server/sonar-web/src/main/js/apps/permissions/global/components/App.js new file mode 100644 index 00000000000..71ff949fd87 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/App.js @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import PageHeader from './PageHeader'; +import AllHoldersList from './AllHoldersList'; +import PageError from '../../shared/components/PageError'; +import '../../styles.css'; + +// TODO helmet + +export default class App extends React.Component { + render () { + return ( + <div className="page page-limited"> + <PageHeader/> + <PageError/> + <AllHoldersList/> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/PageHeader.js b/server/sonar-web/src/main/js/apps/permissions/global/components/PageHeader.js new file mode 100644 index 00000000000..23b750a2dd2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/PageHeader.js @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { connect } from 'react-redux'; +import { translate } from '../../../../helpers/l10n'; +import { loadHolders } from '../store/actions'; +import { isLoading } from '../../shared/store/rootReducer'; + +class PageHeader extends React.Component { + static propTypes = { + loadHolders: React.PropTypes.func.isRequired, + loading: React.PropTypes.bool + }; + + static defaultProps = { + loading: false + }; + + render () { + return ( + <header className="page-header"> + <h1 className="page-title"> + {translate('global_permissions.page')} + </h1> + + {this.props.loading && ( + <i className="spinner"/> + )} + + <div className="page-description"> + {translate('global_permissions.page.description')} + </div> + </header> + ); + } +} + +const mapStateToProps = state => ({ + loading: isLoading(state) +}); + +const mapDispatchToProps = dispatch => ({ + loadHolders: () => dispatch(loadHolders()) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(PageHeader); diff --git a/server/sonar-web/src/main/js/apps/permissions/global/store/actions.js b/server/sonar-web/src/main/js/apps/permissions/global/store/actions.js new file mode 100644 index 00000000000..3a7d1fa158d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/global/store/actions.js @@ -0,0 +1,122 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as api from '../../../../api/permissions'; +import { parseError } from '../../../code/utils'; +import { raiseError } from '../../shared/store/actions'; +import { + getQuery, + getFilter, + getSelectedPermission +} from '../../shared/store/rootReducer'; + +export const loadHolders = () => (dispatch, getState) => { + const query = getQuery(getState()); + const filter = getFilter(getState()); + const selectedPermission = getSelectedPermission(getState()); + + dispatch({ type: 'REQUEST_HOLDERS', query }); + + const requests = []; + + if (filter !== 'groups') { + requests.push(api.getGlobalPermissionsUsers(query, selectedPermission)); + } else { + requests.push(Promise.resolve([])); + } + + if (filter !== 'users') { + requests.push(api.getGlobalPermissionsGroups(query, selectedPermission)); + } else { + requests.push(Promise.resolve([])); + } + + return Promise.all(requests).then(responses => ( + dispatch({ + type: 'RECEIVE_HOLDERS_SUCCESS', + users: responses[0], + groups: responses[1], + query + }) + )).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; + +export const updateQuery = (query = '') => dispatch => { + dispatch({ type: 'UPDATE_QUERY', query }); + if (query.length === 0 || query.length > 2) { + dispatch(loadHolders()); + } +}; + +export const updateFilter = filter => dispatch => { + dispatch({ type: 'UPDATE_FILTER', filter }); + dispatch(loadHolders()); +}; + +export const selectPermission = permission => (dispatch, getState) => { + const selectedPermission = getSelectedPermission(getState()); + if (selectedPermission !== permission) { + dispatch({ type: 'SELECT_PERMISSION', permission }); + } else { + dispatch({ type: 'SELECT_PERMISSION', permission: null }); + } + dispatch(loadHolders()); +}; + +export const grantToUser = (login, permission) => dispatch => { + api.grantPermissionToUser(null, login, permission).then(() => { + dispatch({ type: 'GRANT_PERMISSION_TO_USER', login, permission }); + }).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; + +export const revokeFromUser = (login, permission) => dispatch => { + api.revokePermissionFromUser(null, login, permission).then(() => { + dispatch({ type: 'REVOKE_PERMISSION_TO_USER', login, permission }); + }).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; + +export const grantToGroup = (groupName, permission) => dispatch => { + api.grantPermissionToGroup(null, groupName, permission).then(() => { + dispatch({ + type: 'GRANT_PERMISSION_TO_GROUP', + groupName, + permission + }); + }).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; + +export const revokeFromGroup = (groupName, permission) => dispatch => { + api.revokePermissionFromGroup(null, groupName, permission).then(() => { + dispatch({ + type: 'REVOKE_PERMISSION_FROM_GROUP', + groupName, + permission + }); + }).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; diff --git a/server/sonar-web/src/main/js/apps/permissions/project/app.js b/server/sonar-web/src/main/js/apps/permissions/project/app.js new file mode 100644 index 00000000000..1646df7ef24 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/app.js @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { render } from 'react-dom'; +import { Provider } from 'react-redux'; +import App from './components/App'; +import configureStore from '../../../components/store/configureStore'; +import rootReducer from '../shared/store/rootReducer'; + +window.sonarqube.appStarted.then(options => { + const el = document.querySelector(options.el); + const store = configureStore(rootReducer); + render(( + <Provider store={store}> + <App project={options.component}/> + </Provider> + ), el); +}); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js b/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js new file mode 100644 index 00000000000..880c4d2a195 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js @@ -0,0 +1,163 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { connect } from 'react-redux'; +import SearchForm from '../../shared/components/SearchForm'; +import HoldersList from '../../shared/components/HoldersList'; +import { + loadHolders, + grantToUser, + revokeFromUser, + grantToGroup, + revokeFromGroup, + updateQuery, + updateFilter, + selectPermission +} from '../store/actions'; +import { + getUsers, + getGroups, + getQuery, + getFilter, + getSelectedPermission +} from '../../shared/store/rootReducer'; +import { translate } from '../../../../helpers/l10n'; + +export const PERMISSIONS_ORDER = [ + 'user', + 'codeviewer', + 'issueadmin', + 'admin', + 'scan' +]; + +class AllHoldersList extends React.Component { + static propTypes = { + project: React.PropTypes.object.isRequired + }; + + componentDidMount () { + this.props.loadHolders(this.props.project.key); + } + + handleSearch (query) { + this.props.onSearch(this.props.project.key, query); + } + + handleFilter (filter) { + this.props.onFilter(this.props.project.key, filter); + } + + handleToggleUser (user, permission) { + const hasPermission = user.permissions.includes(permission); + + if (hasPermission) { + this.props.revokePermissionFromUser( + this.props.project.key, + user.login, + permission + ); + } else { + this.props.grantPermissionToUser( + this.props.project.key, + user.login, + permission + ); + } + } + + handleToggleGroup (group, permission) { + const hasPermission = group.permissions.includes(permission); + + if (hasPermission) { + this.props.revokePermissionFromGroup( + this.props.project.key, + group.name, + permission + ); + } else { + this.props.grantPermissionToGroup( + this.props.project.key, + group.name, + permission + ); + } + } + + handleSelectPermission (permission) { + this.props.onSelectPermission(this.props.project.key, permission); + } + + render () { + const permissions = PERMISSIONS_ORDER.map(p => ({ + key: p, + name: translate('projects_role', p), + description: translate('projects_role', p, 'desc') + })); + + return ( + <HoldersList + permissions={permissions} + selectedPermission={this.props.selectedPermission} + users={this.props.users} + groups={this.props.groups} + onSelectPermission={this.handleSelectPermission.bind(this)} + onToggleUser={this.handleToggleUser.bind(this)} + onToggleGroup={this.handleToggleGroup.bind(this)}> + + <SearchForm + query={this.props.query} + filter={this.props.filter} + onSearch={this.handleSearch.bind(this)} + onFilter={this.handleFilter.bind(this)}/> + + </HoldersList> + ); + } +} + +const mapStateToProps = state => ({ + users: getUsers(state), + groups: getGroups(state), + query: getQuery(state), + filter: getFilter(state), + selectedPermission: getSelectedPermission(state) +}); + +const mapDispatchToProps = dispatch => ({ + loadHolders: projectKey => dispatch(loadHolders(projectKey)), + onSearch: (projectKey, query) => dispatch(updateQuery(projectKey, query)), + onFilter: (projectKey, filter) => dispatch(updateFilter(projectKey, filter)), + onSelectPermission: (projectKey, permission) => + dispatch(selectPermission(projectKey, permission)), + grantPermissionToUser: (projectKey, login, permission) => + dispatch(grantToUser(projectKey, login, permission)), + revokePermissionFromUser: (projectKey, login, permission) => + dispatch(revokeFromUser(projectKey, login, permission)), + grantPermissionToGroup: (projectKey, groupName, permission) => + dispatch(grantToGroup(projectKey, groupName, permission)), + revokePermissionFromGroup: (projectKey, groupName, permission) => + dispatch(revokeFromGroup(projectKey, groupName, permission)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(AllHoldersList); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js new file mode 100644 index 00000000000..d8bc447fa08 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import PageHeader from './PageHeader'; +import AllHoldersList from './AllHoldersList'; +import PageError from '../../shared/components/PageError'; +import '../../styles.css'; + +// TODO helmet + +export default class App extends React.Component { + static propTypes = { + project: React.PropTypes.object.isRequired + }; + + render () { + return ( + <div className="page page-limited"> + <PageHeader project={this.props.project}/> + <PageError/> + <AllHoldersList project={this.props.project}/> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js new file mode 100644 index 00000000000..39fcaa5e925 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js @@ -0,0 +1,89 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { connect } from 'react-redux'; +import { translate } from '../../../../helpers/l10n'; +import ApplyTemplateView from '../views/ApplyTemplateView'; +import { loadHolders } from '../store/actions'; +import { isLoading } from '../../shared/store/rootReducer'; + +class PageHeader extends React.Component { + static propTypes = { + project: React.PropTypes.object.isRequired, + loadHolders: React.PropTypes.func.isRequired, + loading: React.PropTypes.bool + }; + + static defaultProps = { + loading: false + }; + + componentWillMount () { + this.handleApplyTemplate = this.handleApplyTemplate.bind(this); + } + + handleApplyTemplate (e) { + e.preventDefault(); + e.target.blur(); + const { project, loadHolders } = this.props; + new ApplyTemplateView({ project }) + .on('done', () => loadHolders(project.key)) + .render(); + } + + render () { + return ( + <header className="page-header"> + <h1 className="page-title"> + {translate('permissions.page')} + </h1> + + {this.props.loading && ( + <i className="spinner"/> + )} + + <div className="page-actions"> + <button + className="js-apply-template" + onClick={this.handleApplyTemplate}> + Apply Template + </button> + </div> + + <div className="page-description"> + {translate('roles.page.description2')} + </div> + </header> + ); + } +} + +const mapStateToProps = state => ({ + loading: isLoading(state) +}); + +const mapDispatchToProps = dispatch => ({ + loadHolders: projectKey => dispatch(loadHolders(projectKey)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(PageHeader); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/store/actions.js b/server/sonar-web/src/main/js/apps/permissions/project/store/actions.js new file mode 100644 index 00000000000..5011dc075ad --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/store/actions.js @@ -0,0 +1,124 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as api from '../../../../api/permissions'; +import { parseError } from '../../../code/utils'; +import { raiseError } from '../../shared/store/actions'; +import { + getQuery, + getFilter, + getSelectedPermission +} from '../../shared/store/rootReducer'; + +export const loadHolders = projectKey => (dispatch, getState) => { + const query = getQuery(getState()); + const filter = getFilter(getState()); + const selectedPermission = getSelectedPermission(getState()); + + dispatch({ type: 'REQUEST_HOLDERS', query }); + + const requests = []; + + if (filter !== 'groups') { + requests.push(api.getPermissionsUsersForComponent(projectKey, query, + selectedPermission)); + } else { + requests.push(Promise.resolve([])); + } + + if (filter !== 'users') { + requests.push(api.getPermissionsGroupsForComponent(projectKey, query, + selectedPermission)); + } else { + requests.push(Promise.resolve([])); + } + + return Promise.all(requests).then(responses => ( + dispatch({ + type: 'RECEIVE_HOLDERS_SUCCESS', + users: responses[0], + groups: responses[1], + query + }) + )).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; + +export const updateQuery = (projectKey, query = '') => dispatch => { + dispatch({ type: 'UPDATE_QUERY', query }); + if (query.length === 0 || query.length > 2) { + dispatch(loadHolders(projectKey)); + } +}; + +export const updateFilter = (projectKey, filter) => dispatch => { + dispatch({ type: 'UPDATE_FILTER', filter }); + dispatch(loadHolders(projectKey)); +}; + +export const selectPermission = (projectKey, permission) => (dispatch, getState) => { + const selectedPermission = getSelectedPermission(getState()); + if (selectedPermission !== permission) { + dispatch({ type: 'SELECT_PERMISSION', permission }); + } else { + dispatch({ type: 'SELECT_PERMISSION', permission: null }); + } + dispatch(loadHolders(projectKey)); +}; + +export const grantToUser = (projectKey, login, permission) => dispatch => { + api.grantPermissionToUser(projectKey, login, permission).then(() => { + dispatch({ type: 'GRANT_PERMISSION_TO_USER', login, permission }); + }).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; + +export const revokeFromUser = (projectKey, login, permission) => dispatch => { + api.revokePermissionFromUser(projectKey, login, permission).then(() => { + dispatch({ type: 'REVOKE_PERMISSION_TO_USER', login, permission }); + }).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; + +export const grantToGroup = (projectKey, groupName, permission) => dispatch => { + api.grantPermissionToGroup(projectKey, groupName, permission).then(() => { + dispatch({ + type: 'GRANT_PERMISSION_TO_GROUP', + groupName, + permission + }); + }).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; + +export const revokeFromGroup = (projectKey, groupName, permission) => dispatch => { + api.revokePermissionFromGroup(projectKey, groupName, permission).then(() => { + dispatch({ + type: 'REVOKE_PERMISSION_FROM_GROUP', + groupName, + permission + }); + }).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; diff --git a/server/sonar-web/src/main/js/apps/permissions/project/templates/ApplyTemplateTemplate.hbs b/server/sonar-web/src/main/js/apps/permissions/project/templates/ApplyTemplateTemplate.hbs new file mode 100644 index 00000000000..c7307670cbe --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/templates/ApplyTemplateTemplate.hbs @@ -0,0 +1,30 @@ +<form id="project-permissions-apply-template-form" autocomplete="off"> + <div class="modal-head"> + <h2>Apply Permission Template to "{{project.name}}"</h2> + </div> + + <div class="modal-body"> + <div class="js-modal-messages"></div> + {{#notNull permissionTemplates}} + <div class="modal-field"> + <label for="project-permissions-template"> + Template<em class="mandatory">*</em> + </label> + <select id="project-permissions-template"> + {{#each permissionTemplates}} + <option value="{{id}}">{{name}}</option> + {{/each}} + </select> + </div> + {{else}} + <i class="spinner"></i> + {{/notNull}} + </div> + + <div class="modal-foot"> + {{#notNull permissionTemplates}} + <button id="project-permissions-apply-template">Apply</button> + {{/notNull}} + <a href="#" class="js-modal-close">Cancel</a> + </div> +</form> diff --git a/server/sonar-web/src/main/js/apps/permissions/project/views/ApplyTemplateView.js b/server/sonar-web/src/main/js/apps/permissions/project/views/ApplyTemplateView.js new file mode 100644 index 00000000000..6aa2537c3f6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/views/ApplyTemplateView.js @@ -0,0 +1,74 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import ModalForm from '../../../../components/common/modal-form'; +import { + applyTemplateToProject, + getPermissionTemplates +} from '../../../../api/permissions'; +import Template from '../templates/ApplyTemplateTemplate.hbs'; + +export default ModalForm.extend({ + template: Template, + + initialize () { + this.loadPermissionTemplates(); + }, + + loadPermissionTemplates () { + return getPermissionTemplates().then(r => { + this.permissionTemplates = r.permissionTemplates; + this.render(); + }); + }, + + onRender () { + ModalForm.prototype.onRender.apply(this, arguments); + this.$('#project-permissions-template').select2({ + width: '250px', + minimumResultsForSearch: 20 + }); + }, + + onFormSubmit () { + ModalForm.prototype.onFormSubmit.apply(this, arguments); + const permissionTemplate = this.$('#project-permissions-template').val(); + this.disableForm(); + + applyTemplateToProject({ + projectKey: this.options.project.key, + templateId: permissionTemplate + }).then(() => { + this.trigger('done'); + this.destroy(); + }).catch(function (e) { + e.response.json().then(r => { + this.showErrors(r.errors, r.warnings); + this.enableForm(); + }); + }); + }, + + serializeData () { + return { + permissionTemplates: this.permissionTemplates, + project: this.options.project + }; + } +}); diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js new file mode 100644 index 00000000000..2d5ec48e164 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js @@ -0,0 +1,83 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import GroupIcon from './GroupIcon'; + +export default class GroupHolder extends React.Component { + static propTypes = { + group: React.PropTypes.object.isRequired, + permissions: React.PropTypes.array.isRequired, + selectedPermission: React.PropTypes.string, + permissionsOrder: React.PropTypes.array.isRequired, + onToggle: React.PropTypes.func.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + handleClick (permission, e) { + e.preventDefault(); + e.target.blur(); + this.props.onToggle(this.props.group, permission); + } + + render () { + const { selectedPermission } = this.props; + const permissionCells = this.props.permissionsOrder.map(p => ( + <td key={p.key} + className="text-center text-middle" + style={{ backgroundColor: p.key === selectedPermission ? '#d9edf7' : 'transparent' }}> + <button + className="button-clean" + onClick={this.handleClick.bind(this, p.key)}> + {this.props.permissions.includes(p.key) ? ( + <i className="icon-checkbox icon-checkbox-checked"/> + ) : ( + <i className="icon-checkbox"/> + )} + </button> + </td> + )); + + const { group } = this.props; + + return ( + <tr> + <td className="nowrap"> + <div className="display-inline-block text-middle big-spacer-right"> + <GroupIcon/> + </div> + <div className="display-inline-block text-middle"> + <div> + <strong>{group.name}</strong> + </div> + <div className="little-spacer-top" + style={{ whiteSpace: 'normal' }}> + {group.description} + </div> + </div> + </td> + {permissionCells} + </tr> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupIcon.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupIcon.js new file mode 100644 index 00000000000..63f9746f107 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupIcon.js @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; + +const GroupIcon = () => { + /* eslint max-len: 0 */ + return ( + <div style={{ padding: '4px 3px 0' }}> + <svg xmlns="http://www.w3.org/2000/svg" + width="30" + height="28" + viewBox="0 0 480 448"> + <path + fill="#aaa" + d="M148.25 224q-40.5 1.25-66.25 32H48.5Q28 256 14 245.875T0 216.25Q0 128 31 128q1.5 0 10.875 5.25t24.375 10.625T96 149.25q16.75 0 33.25-5.75Q128 152.75 128 160q0 34.75 20.25 64zM416 383.25q0 30-18.25 47.375T349.25 448h-218.5q-30.25 0-48.5-17.375T64 383.25q0-13.25.875-25.875t3.5-27.25T75 303t10.75-24.375 15.5-20.25T122.625 245t27.875-5q2.5 0 10.75 5.375t18.25 12 26.75 12T240 274.75t33.75-5.375 26.75-12 18.25-12T329.5 240q15.25 0 27.875 5t21.375 13.375 15.5 20.25T405 303t6.625 27.125 3.5 27.25.875 25.875zM160 64q0 26.5-18.75 45.25T96 128t-45.25-18.75T32 64t18.75-45.25T96 0t45.25 18.75T160 64zm176 96q0 39.75-28.125 67.875T240 256t-67.875-28.125T144 160t28.125-67.875T240 64t67.875 28.125T336 160zm144 56.25q0 19.5-14 29.625T431.5 256H398q-25.75-30.75-66.25-32Q352 194.75 352 160q0-7.25-1.25-16.5 16.5 5.75 33.25 5.75 14.75 0 29.75-5.375t24.375-10.625T449 128q31 0 31 88.25zM448 64q0 26.5-18.75 45.25T384 128t-45.25-18.75T320 64t18.75-45.25T384 0t45.25 18.75T448 64z"/> + </svg> + </div> + ); +}; + +export default GroupIcon; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.js new file mode 100644 index 00000000000..d7fb6fb6364 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.js @@ -0,0 +1,98 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import UserHolder from './UserHolder'; +import GroupHolder from './GroupHolder'; + +export default class HoldersList extends React.Component { + static propTypes = { + permissions: React.PropTypes.array.isRequired, + users: React.PropTypes.array.isRequired, + groups: React.PropTypes.array.isRequired, + selectedPermission: React.PropTypes.string, + onSelectPermission: React.PropTypes.func.isRequired, + onToggleUser: React.PropTypes.func.isRequired, + onToggleGroup: React.PropTypes.func.isRequired + }; + + handlePermissionClick (permission, e) { + e.preventDefault(); + e.target.blur(); + this.props.onSelectPermission(permission); + } + + renderTableHeader () { + const { selectedPermission } = this.props; + const cells = this.props.permissions.map(p => ( + <th key={p.key} + className="permission-column text-center" + style={{ backgroundColor: p.key === selectedPermission ? '#d9edf7' : 'transparent' }}> + <a href="#" onClick={this.handlePermissionClick.bind(this, p.key)}> + {p.name} + </a> + <i className="icon-help little-spacer-left" + title={p.description} + data-toggle="tooltip"/> + </th> + )); + return ( + <thead> + <tr> + <td className="bordered-bottom"> + {this.props.children} + </td> + {cells} + </tr> + </thead> + ); + } + + render () { + const users = this.props.users.map(user => ( + <UserHolder + key={'user-' + user.login} + user={user} + permissions={user.permissions} + selectedPermission={this.props.selectedPermission} + permissionsOrder={this.props.permissions} + onToggle={this.props.onToggleUser}/> + )); + + const groups = this.props.groups.map(group => ( + <GroupHolder + key={'group-' + group.id} + group={group} + permissions={group.permissions} + selectedPermission={this.props.selectedPermission} + permissionsOrder={this.props.permissions} + onToggle={this.props.onToggleGroup}/> + )); + + return ( + <table className="data zebra permissions-table"> + {this.renderTableHeader()} + <tbody> + {users} + {groups} + </tbody> + </table> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.js new file mode 100644 index 00000000000..3449153a921 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.js @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { connect } from 'react-redux'; +import { getError } from '../store/rootReducer'; + +class PageError extends React.Component { + static propTypes = { + message: React.PropTypes.string + }; + + render () { + const { message } = this.props; + + if (!message) { + return null; + } + + return ( + <div className="alert alert-danger"> + {message} + </div> + ); + } +} + +const mapStateToProps = state => ({ + message: getError(state) +}); + +export default connect( + mapStateToProps +)(PageError); diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js new file mode 100644 index 00000000000..35a4836ded4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js @@ -0,0 +1,95 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import RadioToggle from '../../../../components/controls/RadioToggle'; +import { translate, translateWithParameters } from '../../../../helpers/l10n'; + +export default class SearchForm extends React.Component { + static propTypes = { + query: React.PropTypes.string, + filter: React.PropTypes.oneOf(['all', 'users', 'groups']), + onSearch: React.PropTypes.func, + onFilter: React.PropTypes.func + }; + + componentWillMount () { + this.handleSubmit = this.handleSubmit.bind(this); + this.handleSearch = this.handleSearch.bind(this); + } + + handleSubmit (e) { + e.preventDefault(); + this.handleSearch(); + } + + handleSearch () { + const { value } = this.refs.searchInput; + this.props.onSearch(value); + } + + handleFilter (filter) { + this.props.onFilter(filter); + } + + render () { + const { query, filter } = this.props; + + const filterOptions = [ + { value: 'all', label: 'All' }, + { value: 'users', label: 'Users' }, + { value: 'groups', label: 'Groups' } + ]; + + return ( + <div> + + <RadioToggle + value={filter} + options={filterOptions} + name="users-or-groups" + onCheck={this.handleFilter.bind(this)}/> + + <form + className="search-box display-inline-block text-middle big-spacer-left" + onSubmit={this.handleSubmit}> + <button className="search-box-submit button-clean"> + <i className="icon-search"></i> + </button> + <input + ref="searchInput" + value={query} + className="search-box-input" + style={{ width: 100 }} + type="search" + placeholder={translate('search_verb')} + onChange={this.handleSearch.bind(this)}/> + {query.length > 0 && query.length < 3 && ( + <div className="search-box-input-note tooltip bottom fade in"> + <div className="tooltip-inner"> + {translateWithParameters('select2.tooShort', 3)} + </div> + <div className="tooltip-arrow" style={{ left: 23 }}/> + </div> + )} + </form> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js new file mode 100644 index 00000000000..9b25cc74db5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js @@ -0,0 +1,82 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import Avatar from '../../../../components/ui/Avatar'; + +export default class UserHolder extends React.Component { + static propTypes = { + user: React.PropTypes.object.isRequired, + permissions: React.PropTypes.array.isRequired, + selectedPermission: React.PropTypes.string, + permissionsOrder: React.PropTypes.array.isRequired, + onToggle: React.PropTypes.func.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + handleClick (permission, e) { + e.preventDefault(); + e.target.blur(); + this.props.onToggle(this.props.user, permission); + } + + render () { + const { selectedPermission } = this.props; + const permissionCells = this.props.permissionsOrder.map(p => ( + <td key={p.key} + className="text-center text-middle" + style={{ backgroundColor: p.key === selectedPermission ? '#d9edf7' : 'transparent' }}> + <button + className="button-clean" + onClick={this.handleClick.bind(this, p.key)}> + {this.props.permissions.includes(p.key) ? ( + <i className="icon-checkbox icon-checkbox-checked"/> + ) : ( + <i className="icon-checkbox"/> + )} + </button> + </td> + )); + + const { user } = this.props; + + return ( + <tr> + <td className="nowrap"> + <Avatar + email={user.email} + size={36} + className="text-middle big-spacer-right"/> + <div className="display-inline-block text-middle"> + <div> + <strong>{user.name}</strong> + <span className="note spacer-left">{user.login}</span> + </div> + <div className="little-spacer-top">{user.email}</div> + </div> + </td> + {permissionCells} + </tr> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/actions.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/actions.js new file mode 100644 index 00000000000..be7cee4a4b7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/actions.js @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const raiseError = message => ({ + type: 'ERROR', + message +}); diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/error.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/error.js new file mode 100644 index 00000000000..6fe0ec764c3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/error.js @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +const error = (state = null, action = {}) => { + switch (action.type) { + case 'RECEIVE_HOLDERS_SUCCESS': + case 'GRANT_PERMISSION_TO_USER': + case 'REVOKE_PERMISSION_TO_USER': + case 'GRANT_PERMISSION_TO_GROUP': + case 'REVOKE_PERMISSION_FROM_GROUP': + return null; + case 'ERROR': + return action.message; + default: + return state; + } +}; + +export default error; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/filter.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/filter.js new file mode 100644 index 00000000000..6934582b425 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/filter.js @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +const filter = (state = 'all', action = {}) => { + switch (action.type) { + case 'UPDATE_FILTER': + return action.filter; + default: + return state; + } +}; + +export default filter; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/byName.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/byName.js new file mode 100644 index 00000000000..c1475aa6763 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/byName.js @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import keyBy from 'lodash/keyBy'; + +const byName = (state = {}, action = {}) => { + if (action.type === 'RECEIVE_HOLDERS_SUCCESS') { + const newGroups = keyBy(action.groups, 'name'); + return { ...state, ...newGroups }; + } else if (action.type === 'GRANT_PERMISSION_TO_GROUP') { + const newGroup = { ...state[action.groupName] }; + newGroup.permissions = [...newGroup.permissions, action.permission]; + return { ...state, [action.groupName]: newGroup }; + } else if (action.type === 'REVOKE_PERMISSION_FROM_GROUP') { + const newGroup = { ...state[action.groupName] }; + newGroup.permissions = newGroup.permissions + .filter(p => p !== action.permission); + return { ...state, [action.groupName]: newGroup }; + } else { + return state; + } +}; + +export default byName; + +export const getGroups = state => state; + +export const getGroupByName = (state, name) => state[name]; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/groups.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/groups.js new file mode 100644 index 00000000000..8eba086373f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/groups.js @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { combineReducers } from 'redux'; +import byName, { getGroupByName } from './byName'; +import names, { getNames } from './names'; + +export default combineReducers({ + byName, + names +}); + +export const getGroups = state => + getNames(state.names).map(name => getGroupByName(state.byName, name)); diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/names.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/names.js new file mode 100644 index 00000000000..8451a26999e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/names.js @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +const names = (state = [], action = {}) => { + if (action.type === 'RECEIVE_HOLDERS_SUCCESS') { + return action.groups.map(group => group.name); + } else { + return state; + } +}; + +export default names; + +export const getNames = state => state; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/loading.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/loading.js new file mode 100644 index 00000000000..b7ff6d172bc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/loading.js @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +const loading = (state = false, action = {}) => { + switch (action.type) { + case 'REQUEST_HOLDERS': + return true; + case 'RECEIVE_HOLDERS_SUCCESS': + return false; + default: + return state; + } +}; + +export default loading; + +export const isLoading = state => state; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/query.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/query.js new file mode 100644 index 00000000000..49a755a3fee --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/query.js @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +const query = (state = '', action = {}) => { + switch (action.type) { + case 'UPDATE_QUERY': + case 'REQUEST_HOLDERS': + return action.query; + default: + return state; + } +}; + +export default query; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/rootReducer.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/rootReducer.js new file mode 100644 index 00000000000..26dbd367379 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/rootReducer.js @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { combineReducers } from 'redux'; +import users, * as fromUsers from './users/users'; +import groups, * as fromGroups from './groups/groups'; +import loading, * as fromLoading from './loading'; +import query from './query'; +import filter from './filter'; +import selectedPermission from './selectedPermission'; +import error from './error'; + +export default combineReducers({ + users, + groups, + loading, + query, + filter, + selectedPermission, + error +}); + +export const getUsers = state => fromUsers.getUsers(state.users); + +export const getGroups = state => fromGroups.getGroups(state.groups); + +export const isLoading = state => fromLoading.isLoading(state.loading); + +export const getQuery = state => state.query; + +export const getFilter = state => state.filter; + +export const getSelectedPermission = state => state.selectedPermission; + +export const getError = state => state.error; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/selectedPermission.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/selectedPermission.js new file mode 100644 index 00000000000..c4e951745d7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/selectedPermission.js @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +const selectedPermission = (state = null, action = {}) => { + switch (action.type) { + case 'SELECT_PERMISSION': + return action.permission; + default: + return state; + } +}; + +export default selectedPermission; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/users/byLogin.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/users/byLogin.js new file mode 100644 index 00000000000..a571966cd92 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/users/byLogin.js @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import keyBy from 'lodash/keyBy'; + +const byLogin = (state = {}, action = {}) => { + if (action.type === 'RECEIVE_HOLDERS_SUCCESS') { + const newUsers = keyBy(action.users, 'login'); + return { ...state, ...newUsers }; + } else if (action.type === 'GRANT_PERMISSION_TO_USER') { + const newUser = { ...state[action.login] }; + newUser.permissions = [...newUser.permissions, action.permission]; + return { ...state, [action.login]: newUser }; + } else if (action.type === 'REVOKE_PERMISSION_TO_USER') { + const newUser = { ...state[action.login] }; + newUser.permissions = newUser.permissions + .filter(p => p !== action.permission); + return { ...state, [action.login]: newUser }; + } else { + return state; + } +}; + +export default byLogin; + +export const getUsers = state => state; + +export const getUserByLogin = (state, login) => state[login]; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/users/logins.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/users/logins.js new file mode 100644 index 00000000000..b0bc1e31ca0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/users/logins.js @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +const logins = (state = [], action = {}) => { + if (action.type === 'RECEIVE_HOLDERS_SUCCESS') { + return action.users.map(user => user.login); + } else { + return state; + } +}; + +export default logins; + +export const getLogins = state => state; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/users/users.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/users/users.js new file mode 100644 index 00000000000..021a674951a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/users/users.js @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { combineReducers } from 'redux'; +import byLogin, { getUserByLogin } from './byLogin'; +import logins, { getLogins } from './logins'; + +export default combineReducers({ + byLogin, + logins +}); + +export const getUsers = state => + getLogins(state.logins).map(login => getUserByLogin(state.byLogin, login)); diff --git a/server/sonar-web/src/main/js/apps/permissions/styles.css b/server/sonar-web/src/main/js/apps/permissions/styles.css new file mode 100644 index 00000000000..02b1e863c83 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/styles.css @@ -0,0 +1,17 @@ +.permissions-table { + table-layout: fixed; +} + +.permissions-table > tbody > tr > td { + border-bottom: 10px solid #fff !important; +} + +.permissions-table .permission-column { + width: 12%; + min-width: 112px; +} + +.permissions-table .actions-column { + width: 130px; + text-align: right; +} |