aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/permissions/project
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2016-06-29 09:52:31 +0200
committerStas Vilchik <vilchiks@gmail.com>2016-07-12 10:18:55 +0200
commitd5ca0eb5782c29c613a53b76cfe169dbe4ceab81 (patch)
treeb69d3c2999251dc78f6770332a8687a2e027cfee /server/sonar-web/src/main/js/apps/permissions/project
parent0fbbe800ee3ae1f68df6e5d4c868a2910b981a55 (diff)
downloadsonarqube-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')
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/app.js35
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js163
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/App.js42
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js89
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/store/actions.js124
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/templates/ApplyTemplateTemplate.hbs30
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/views/ApplyTemplateView.js74
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
+ };
+ }
+});