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/project | |
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/project')
7 files changed, 557 insertions, 0 deletions
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 + }; + } +}); |