aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/permissions
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
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')
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/global/app.js35
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.js131
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/global/components/App.js38
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/global/components/PageHeader.js66
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/global/store/actions.js122
-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
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js83
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/components/GroupIcon.js38
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.js98
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.js50
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js95
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js82
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/store/actions.js23
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/store/error.js35
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/store/filter.js29
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/store/groups/byName.js44
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/store/groups/groups.js30
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/store/groups/names.js30
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/store/loading.js33
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/store/query.js30
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/store/rootReducer.js51
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/store/selectedPermission.js29
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/store/users/byLogin.js44
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/store/users/logins.js30
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/shared/store/users/users.js30
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/styles.css17
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;
+}