]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7840 SONAR-7879 Improve UX on permissions pages
authorStas Vilchik <vilchiks@gmail.com>
Wed, 29 Jun 2016 07:52:31 +0000 (09:52 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Tue, 12 Jul 2016 08:18:55 +0000 (10:18 +0200)
65 files changed:
server/sonar-web/src/main/js/api/permissions.js
server/sonar-web/src/main/js/apps/global-permissions/app.js [deleted file]
server/sonar-web/src/main/js/apps/global-permissions/groups-view.js [deleted file]
server/sonar-web/src/main/js/apps/global-permissions/main.js [deleted file]
server/sonar-web/src/main/js/apps/global-permissions/permission-groups.js [deleted file]
server/sonar-web/src/main/js/apps/global-permissions/permission-users-groups-mixin.js [deleted file]
server/sonar-web/src/main/js/apps/global-permissions/permission-users.js [deleted file]
server/sonar-web/src/main/js/apps/global-permissions/permission.js [deleted file]
server/sonar-web/src/main/js/apps/global-permissions/permissions-list.js [deleted file]
server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-groups.hbs [deleted file]
server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-users.hbs [deleted file]
server/sonar-web/src/main/js/apps/global-permissions/users-view.js [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/styles.css
server/sonar-web/src/main/js/apps/permissions/global/app.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/global/components/App.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/global/components/PageHeader.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/global/store/actions.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/project/app.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/project/components/App.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/project/store/actions.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/project/templates/ApplyTemplateTemplate.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/project/views/ApplyTemplateView.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/components/GroupIcon.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/store/actions.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/store/error.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/store/filter.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/store/groups/byName.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/store/groups/groups.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/store/groups/names.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/store/loading.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/store/query.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/store/rootReducer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/store/selectedPermission.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/store/users/byLogin.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/store/users/logins.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/store/users/users.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/styles.css [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-permissions/app.js [deleted file]
server/sonar-web/src/main/js/apps/project-permissions/apply-template-view.js [deleted file]
server/sonar-web/src/main/js/apps/project-permissions/groups-view.js [deleted file]
server/sonar-web/src/main/js/apps/project-permissions/main.js [deleted file]
server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.js [deleted file]
server/sonar-web/src/main/js/apps/project-permissions/permissions-header.js [deleted file]
server/sonar-web/src/main/js/apps/project-permissions/permissions.js [deleted file]
server/sonar-web/src/main/js/apps/project-permissions/project.js [deleted file]
server/sonar-web/src/main/js/apps/project-permissions/qualifier-filter.js [deleted file]
server/sonar-web/src/main/js/apps/project-permissions/search.js [deleted file]
server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-apply-template.hbs [deleted file]
server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-groups.hbs [deleted file]
server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-users.hbs [deleted file]
server/sonar-web/src/main/js/apps/project-permissions/users-view.js [deleted file]
server/sonar-web/src/main/js/components/ui/Avatar.js
server/sonar-web/src/main/less/components/search.less
server/sonar-web/src/main/less/init/forms.less
server/sonar-web/src/main/less/init/tables.less
server/sonar-web/src/main/webapp/WEB-INF/app/views/project_roles/index.html.erb
server/sonar-web/webpack.config.js

index 273c7872735a3d94ed1df90e0834b9a705d4cb70..80a77e565392f73f8d79f744bc97974c95bcc744 100644 (file)
@@ -21,86 +21,56 @@ import $ from 'jquery';
 import _ from 'underscore';
 import { getJSON, post } from '../helpers/request';
 
+const PAGE_SIZE = 100;
+
 function request (options) {
   return $.ajax(options);
 }
 
-function typeError (method, message) {
-  throw new TypeError(`permissions#${method}: ${message}`);
-}
-
-export function getUsers (data) {
-  const url = window.baseUrl + '/api/permissions/users';
-  return request({ type: 'GET', url, data });
+export function getPermissionUsers (data) {
+  const url = '/api/permissions/users';
+  return getJSON(url, data);
 }
 
-export function grantToUser (permission, user, project) {
-  if (typeof permission !== 'string' || !permission.length) {
-    return typeError('grantToUser', 'please provide permission');
-  }
-  if (typeof user !== 'string' || !user.length) {
-    return typeError('grantToUser', 'please provide user login');
-  }
-
-  const url = window.baseUrl + '/api/permissions/add_user';
-  const data = { permission, login: user };
-  if (project) {
-    data.projectId = project;
+export function grantPermissionToUser (projectKey, login, permission) {
+  const url = '/api/permissions/add_user';
+  const data = { login, permission };
+  if (projectKey) {
+    data.projectKey = projectKey;
   }
-  return request({ type: 'POST', url, data });
+  return post(url, data);
 }
 
-export function revokeFromUser (permission, user, project) {
-  if (typeof permission !== 'string' || !permission.length) {
-    return typeError('revokeFromUser', 'please provide permission');
-  }
-  if (typeof user !== 'string' || !user.length) {
-    return typeError('revokeFromUser', 'please provide user login');
-  }
-
-  const url = window.baseUrl + '/api/permissions/remove_user';
-  const data = { permission, login: user };
-  if (project) {
-    data.projectId = project;
+export function revokePermissionFromUser (projectKey, login, permission) {
+  const url = '/api/permissions/remove_user';
+  const data = { login, permission };
+  if (projectKey) {
+    data.projectKey = projectKey;
   }
-  return request({ type: 'POST', url, data });
+  return post(url, data);
 }
 
-export function getGroups (data) {
-  const url = window.baseUrl + '/api/permissions/groups';
-  return request({ type: 'GET', url, data });
+export function getPermissionGroups (data) {
+  const url = '/api/permissions/groups';
+  return getJSON(url, data);
 }
 
-export function grantToGroup (permission, group, project) {
-  if (typeof permission !== 'string' || !permission.length) {
-    return typeError('grantToGroup', 'please provide permission');
-  }
-  if (typeof group !== 'string' || !group.length) {
-    return typeError('grantToGroup', 'please provide group name');
+export function grantPermissionToGroup (projectKey, groupName, permission) {
+  const url = '/api/permissions/add_group';
+  const data = { groupName, permission };
+  if (projectKey) {
+    data.projectKey = projectKey;
   }
-
-  const url = window.baseUrl + '/api/permissions/add_group';
-  const data = { permission, groupName: group };
-  if (project) {
-    data.projectId = project;
-  }
-  return request({ type: 'POST', url, data });
+  return post(url, data);
 }
 
-export function revokeFromGroup (permission, group, project) {
-  if (typeof permission !== 'string' || !permission.length) {
-    return typeError('revokeFromGroup', 'please provide permission');
+export function revokePermissionFromGroup (projectKey, groupName, permission) {
+  const url = '/api/permissions/remove_group';
+  const data = { groupName, permission };
+  if (projectKey) {
+    data.projectKey = projectKey;
   }
-  if (typeof group !== 'string' || !group.length) {
-    return typeError('revokeFromGroup', 'please provide group name');
-  }
-
-  const url = window.baseUrl + '/api/permissions/remove_group';
-  const data = { permission, groupName: group };
-  if (project) {
-    data.projectId = project;
-  }
-  return request({ type: 'POST', url, data });
+  return post(url, data);
 }
 
 /**
@@ -139,9 +109,9 @@ export function setDefaultPermissionTemplate (templateName, qualifier) {
   return post(url, data);
 }
 
-export function applyTemplateToProject (options) {
-  const url = window.baseUrl + '/api/permissions/apply_template';
-  return request(_.extend({ type: 'POST', url }, options));
+export function applyTemplateToProject (data) {
+  const url = '/api/permissions/apply_template';
+  return post(url, data);
 }
 
 export function bulkApplyTemplateToProject (options) {
@@ -160,3 +130,51 @@ export function removeProjectCreatorFromTemplate (templateName, permission) {
   const data = { templateName, permission };
   return post(url, data);
 }
+
+export function getPermissionsUsersForComponent (projectKey, query = '', permission = null) {
+  const url = '/api/permissions/users';
+  const data = { projectKey, ps: PAGE_SIZE };
+  if (query) {
+    data.q = query;
+  }
+  if (permission) {
+    data.permission = permission;
+  }
+  return getJSON(url, data).then(r => r.users);
+}
+
+export function getPermissionsGroupsForComponent (projectKey, query = '', permission = null) {
+  const url = '/api/permissions/groups';
+  const data = { projectKey, ps: PAGE_SIZE };
+  if (query) {
+    data.q = query;
+  }
+  if (permission) {
+    data.permission = permission;
+  }
+  return getJSON(url, data).then(r => r.groups);
+}
+
+export function getGlobalPermissionsUsers (query = '', permission = null) {
+  const url = '/api/permissions/users';
+  const data = { ps: PAGE_SIZE };
+  if (query) {
+    data.q = query;
+  }
+  if (permission) {
+    data.permission = permission;
+  }
+  return getJSON(url, data).then(r => r.users);
+}
+
+export function getGlobalPermissionsGroups (query = '', permission = null) {
+  const url = '/api/permissions/groups';
+  const data = { ps: PAGE_SIZE };
+  if (query) {
+    data.q = query;
+  }
+  if (permission) {
+    data.permission = permission;
+  }
+  return getJSON(url, data).then(r => r.groups);
+}
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/app.js b/server/sonar-web/src/main/js/apps/global-permissions/app.js
deleted file mode 100644 (file)
index 463b7a1..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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 ReactDOM from 'react-dom';
-import Main from './main';
-
-window.sonarqube.appStarted.then(options => {
-  const el = document.querySelector(options.el);
-  ReactDOM.render(<Main/>, el);
-});
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/groups-view.js b/server/sonar-web/src/main/js/apps/global-permissions/groups-view.js
deleted file mode 100644 (file)
index dac2ad2..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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 Modal from '../../components/common/modals';
-import Template from './templates/global-permissions-groups.hbs';
-import '../../components/SelectList';
-
-function getSearchUrl (permission, project) {
-  let url = window.baseUrl + '/api/permissions/groups?ps=100&permission=' + permission;
-  if (project) {
-    url = url + '&projectId=' + project;
-  }
-  return url;
-}
-
-function getExtra (permission, project) {
-  const extra = { permission };
-  if (project) {
-    extra.projectId = project;
-  }
-  return extra;
-}
-
-export default Modal.extend({
-  template: Template,
-
-  onRender () {
-    Modal.prototype.onRender.apply(this, arguments);
-    new window.SelectList({
-      el: this.$('#global-permissions-groups'),
-      width: '100%',
-      readOnly: false,
-      focusSearch: false,
-      format (item) {
-        return item.name;
-      },
-      queryParam: 'q',
-      searchUrl: getSearchUrl(this.options.permission, this.options.project),
-      selectUrl: window.baseUrl + '/api/permissions/add_group',
-      deselectUrl: window.baseUrl + '/api/permissions/remove_group',
-      extra: getExtra(this.options.permission, this.options.project),
-      selectParameter: 'groupName',
-      selectParameterValue: 'name',
-      parse (r) {
-        this.more = false;
-        return r.groups;
-      }
-    });
-  },
-
-  onDestroy () {
-    this.options.refresh();
-    Modal.prototype.onDestroy.apply(this, arguments);
-  }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/main.js b/server/sonar-web/src/main/js/apps/global-permissions/main.js
deleted file mode 100644 (file)
index 8557102..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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 $ from 'jquery';
-import React from 'react';
-import PermissionsList from './permissions-list';
-import { translate } from '../../helpers/l10n';
-
-export default React.createClass({
-  getInitialState() {
-    return { ready: false, permissions: [] };
-  },
-
-  componentDidMount() {
-    this.requestPermissions();
-  },
-
-  requestPermissions() {
-    const url = window.baseUrl + '/api/permissions/search_global_permissions';
-    $.get(url).done(r => {
-      this.setState({ ready: true, permissions: r.permissions });
-    });
-  },
-
-  renderSpinner () {
-    if (this.state.ready) {
-      return null;
-    }
-    return <i className="spinner"/>;
-  },
-
-  render() {
-    return (
-        <div className="page">
-          <header id="global-permissions-header" className="page-header">
-            <h1 className="page-title">{translate('global_permissions.page')}</h1>
-            {this.renderSpinner()}
-            <p className="page-description">{translate('global_permissions.page.description')}</p>
-          </header>
-          <PermissionsList ready={this.state.ready} permissions={this.state.permissions}/>
-        </div>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/permission-groups.js b/server/sonar-web/src/main/js/apps/global-permissions/permission-groups.js
deleted file mode 100644 (file)
index 38b08fb..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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 PermissionsUsersGroupsMixin from './permission-users-groups-mixin';
-import GroupsView from './groups-view';
-
-export default React.createClass({
-  mixins: [PermissionsUsersGroupsMixin],
-
-  renderUpdateLink() {
-    return (
-        <a onClick={this.updateGroups}
-           className="icon-bullet-list"
-           title="Update Groups"
-           data-toggle="tooltip"
-           href="#"></a>
-    );
-  },
-
-  renderItem(item) {
-    return item.name;
-  },
-
-  renderTitle() {
-    return 'Groups';
-  },
-
-  updateGroups(e) {
-    e.preventDefault();
-    new GroupsView({
-      permission: this.props.permission.key,
-      project: this.props.project,
-      refresh: this.props.refresh
-    }).render();
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/permission-users-groups-mixin.js b/server/sonar-web/src/main/js/apps/global-permissions/permission-users-groups-mixin.js
deleted file mode 100644 (file)
index a83ec2c..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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';
-
-export default {
-  propTypes: {
-    permission: React.PropTypes.object.isRequired,
-    max: React.PropTypes.number.isRequired,
-    items: React.PropTypes.array,
-    total: React.PropTypes.number,
-    refresh: React.PropTypes.func.isRequired
-  },
-
-  renderNotDisplayed() {
-    const notDisplayedCount = this.props.total - this.props.max;
-    return notDisplayedCount > 0 ?
-        <span className="note spacer-right" href="#">and {notDisplayedCount} more</span> : null;
-  },
-
-  renderItems() {
-    const displayed = this.props.items.map(item => {
-      return <li key={item.name} className="spacer-left little-spacer-bottom">{this.renderItem(item)}</li>;
-    });
-    return (
-        <ul className="overflow-hidden bordered-left">
-          {displayed}
-          <li className="spacer-left little-spacer-bottom">
-            {this.renderNotDisplayed()}
-            {this.renderUpdateLink()}
-          </li>
-        </ul>
-    );
-  },
-
-  renderCount() {
-    return (
-        <ul className="overflow-hidden bordered-left">
-          <li className="spacer-left little-spacer-bottom">
-            <span className="spacer-right">{this.props.total}</span>
-            {this.renderUpdateLink()}
-          </li>
-        </ul>
-    );
-  },
-
-  render() {
-    return (
-        <li className="abs-width-400">
-          <div className="pull-left spacer-right">
-            <strong>{this.renderTitle()}</strong>
-          </div>
-          {this.props.items ? this.renderItems() : this.renderCount()}
-        </li>
-    );
-  }
-};
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/permission-users.js b/server/sonar-web/src/main/js/apps/global-permissions/permission-users.js
deleted file mode 100644 (file)
index c04664b..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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 PermissionsUsersGroupsMixin from './permission-users-groups-mixin';
-import UsersView from './users-view';
-
-export default React.createClass({
-  mixins: [PermissionsUsersGroupsMixin],
-
-  renderUpdateLink() {
-    return (
-        <a onClick={this.updateUsers}
-           className="icon-bullet-list"
-           title="Update Users"
-           data-toggle="tooltip"
-           href="#"></a>
-    );
-  },
-
-  renderItem(item) {
-    return item.name;
-  },
-
-  renderTitle() {
-    return 'Users';
-  },
-
-  updateUsers(e) {
-    e.preventDefault();
-    new UsersView({
-      permission: this.props.permission.key,
-      project: this.props.project,
-      refresh: this.props.refresh
-    }).render();
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/permission.js b/server/sonar-web/src/main/js/apps/global-permissions/permission.js
deleted file mode 100644 (file)
index 3e99b67..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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 $ from 'jquery';
-import React from 'react';
-import PermissionUsers from './permission-users';
-import PermissionGroups from './permission-groups';
-
-// Maximum number of displayed groups
-const MAX_ITEMS = 3;
-
-export default React.createClass({
-  propTypes: {
-    permission: React.PropTypes.object.isRequired
-  },
-
-  getInitialState() {
-    return {};
-  },
-
-  componentDidMount() {
-    this.requestUsers();
-    this.requestGroups();
-  },
-
-  requestUsers() {
-    const url = window.baseUrl + '/api/permissions/users';
-    const data = { permission: this.props.permission.key, ps: MAX_ITEMS };
-    if (this.props.project) {
-      data.projectId = this.props.project;
-    }
-    $.get(url, data).done(r => this.setState({ users: r.users, totalUsers: r.paging && r.paging.total }));
-  },
-
-  requestGroups() {
-    const url = window.baseUrl + '/api/permissions/groups';
-    const data = { permission: this.props.permission.key, ps: MAX_ITEMS };
-    if (this.props.project) {
-      data.projectId = this.props.project;
-    }
-    $.get(url, data).done(r => this.setState({ groups: r.groups, totalGroups: r.paging && r.paging.total }));
-  },
-
-  render() {
-    return (
-        <li className="panel panel-vertical" data-id={this.props.permission.key}>
-          <h3>{this.props.permission.name}</h3>
-          <p className="spacer-top" dangerouslySetInnerHTML={{ __html: this.props.permission.description }}/>
-          <ul className="list-inline spacer-top">
-            <PermissionUsers permission={this.props.permission}
-                             project={this.props.project}
-                             max={MAX_ITEMS}
-                             items={this.state.users}
-                             total={this.state.totalUsers || this.props.permission.usersCount}
-                             refresh={this.requestUsers}/>
-            <PermissionGroups permission={this.props.permission}
-                              project={this.props.project}
-                              max={MAX_ITEMS}
-                              items={this.state.groups}
-                              total={this.state.totalGroups || this.props.permission.groupsCount}
-                              refresh={this.requestGroups}/>
-          </ul>
-        </li>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/permissions-list.js b/server/sonar-web/src/main/js/apps/global-permissions/permissions-list.js
deleted file mode 100644 (file)
index a8f62b3..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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 classNames from 'classnames';
-import React from 'react';
-
-import Permission from './permission';
-
-export default React.createClass({
-  propTypes: {
-    permissions: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
-  },
-
-  renderPermissions() {
-    return this.props.permissions.map(permission => {
-      return <Permission key={permission.key} permission={permission} project={this.props.project}/>;
-    });
-  },
-
-  render() {
-    const className = classNames({ 'new-loading': !this.props.ready });
-    return <ul id="global-permissions-list" className={className}>{this.renderPermissions()}</ul>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-groups.hbs b/server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-groups.hbs
deleted file mode 100644 (file)
index d921ae9..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-<div class="modal-head">
-  <h2>Update Groups</h2>
-</div>
-<div class="modal-body">
-  <div class="js-modal-messages"></div>
-  <div id="global-permissions-groups"></div>
-</div>
-<div class="modal-foot">
-  <a href="#" class="js-modal-close" id="global-permissions-groups-done">Done</a>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-users.hbs b/server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-users.hbs
deleted file mode 100644 (file)
index cea5a66..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-<div class="modal-head">
-  <h2>Update Users</h2>
-</div>
-<div class="modal-body">
-  <div class="js-modal-messages"></div>
-  <div id="global-permissions-users"></div>
-</div>
-<div class="modal-foot">
-  <a href="#" class="js-modal-close" id="global-permissions-users-done">Done</a>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/users-view.js b/server/sonar-web/src/main/js/apps/global-permissions/users-view.js
deleted file mode 100644 (file)
index 32456c6..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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 Modal from '../../components/common/modals';
-import Template from './templates/global-permissions-users.hbs';
-import '../../components/SelectList';
-
-function getSearchUrl (permission, project) {
-  let url = window.baseUrl + '/api/permissions/users?ps=100&permission=' + permission;
-  if (project) {
-    url = url + '&projectId=' + project;
-  }
-  return url;
-}
-
-function getExtra (permission, project) {
-  const extra = { permission };
-  if (project) {
-    extra.projectId = project;
-  }
-  return extra;
-}
-
-export default Modal.extend({
-  template: Template,
-
-  onRender () {
-    Modal.prototype.onRender.apply(this, arguments);
-    new window.SelectList({
-      el: this.$('#global-permissions-users'),
-      width: '100%',
-      readOnly: false,
-      focusSearch: false,
-      format (item) {
-        return `${item.name}<br><span class="note">${item.login}</span>`;
-      },
-      queryParam: 'q',
-      searchUrl: getSearchUrl(this.options.permission, this.options.project),
-      selectUrl: window.baseUrl + '/api/permissions/add_user',
-      deselectUrl: window.baseUrl + '/api/permissions/remove_user',
-      extra: getExtra(this.options.permission, this.options.project),
-      selectParameter: 'login',
-      selectParameterValue: 'login',
-      parse (r) {
-        this.more = false;
-        return r.users;
-      }
-    });
-  },
-
-  onDestroy () {
-    this.options.refresh();
-    Modal.prototype.onDestroy.apply(this, arguments);
-  }
-});
-
index a19119a6da30566aa0296cdc5e37bcac6e66184f..02b1e863c831160fb69e5053cc3a4bdf6870e17e 100644 (file)
@@ -3,12 +3,12 @@
 }
 
 .permissions-table > tbody > tr > td {
-  border-bottom: 20px solid #fff !important;
+  border-bottom: 10px solid #fff !important;
 }
 
 .permissions-table .permission-column {
-  width: 130px;
-  white-space: nowrap;
+  width: 12%;
+  min-width: 112px;
 }
 
 .permissions-table .actions-column {
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 (file)
index 0000000..db8e298
--- /dev/null
@@ -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 (file)
index 0000000..fe97b7c
--- /dev/null
@@ -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 (file)
index 0000000..71ff949
--- /dev/null
@@ -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 (file)
index 0000000..23b750a
--- /dev/null
@@ -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 (file)
index 0000000..3a7d1fa
--- /dev/null
@@ -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 (file)
index 0000000..1646df7
--- /dev/null
@@ -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 (file)
index 0000000..880c4d2
--- /dev/null
@@ -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 (file)
index 0000000..d8bc447
--- /dev/null
@@ -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 (file)
index 0000000..39fcaa5
--- /dev/null
@@ -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 (file)
index 0000000..5011dc0
--- /dev/null
@@ -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 (file)
index 0000000..c730767
--- /dev/null
@@ -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 (file)
index 0000000..6aa2537
--- /dev/null
@@ -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 (file)
index 0000000..2d5ec48
--- /dev/null
@@ -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 (file)
index 0000000..63f9746
--- /dev/null
@@ -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 (file)
index 0000000..d7fb6fb
--- /dev/null
@@ -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 (file)
index 0000000..3449153
--- /dev/null
@@ -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 (file)
index 0000000..35a4836
--- /dev/null
@@ -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 (file)
index 0000000..9b25cc7
--- /dev/null
@@ -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 (file)
index 0000000..be7cee4
--- /dev/null
@@ -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 (file)
index 0000000..6fe0ec7
--- /dev/null
@@ -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 (file)
index 0000000..6934582
--- /dev/null
@@ -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 (file)
index 0000000..c1475aa
--- /dev/null
@@ -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 (file)
index 0000000..8eba086
--- /dev/null
@@ -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 (file)
index 0000000..8451a26
--- /dev/null
@@ -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 (file)
index 0000000..b7ff6d1
--- /dev/null
@@ -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 (file)
index 0000000..49a755a
--- /dev/null
@@ -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 (file)
index 0000000..26dbd36
--- /dev/null
@@ -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 (file)
index 0000000..c4e9517
--- /dev/null
@@ -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 (file)
index 0000000..a571966
--- /dev/null
@@ -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 (file)
index 0000000..b0bc1e3
--- /dev/null
@@ -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 (file)
index 0000000..021a674
--- /dev/null
@@ -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 (file)
index 0000000..02b1e86
--- /dev/null
@@ -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;
+}
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/app.js b/server/sonar-web/src/main/js/apps/project-permissions/app.js
deleted file mode 100644 (file)
index c90cd10..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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 $ from 'jquery';
-import React from 'react';
-import ReactDOM from 'react-dom';
-import Main from './main';
-
-function requestPermissionTemplates () {
-  return $.get(window.baseUrl + '/api/permissions/search_templates');
-}
-
-window.sonarqube.appStarted.then(options => {
-  requestPermissionTemplates().done(r => {
-    const el = document.querySelector(options.el);
-    ReactDOM.render(<Main permissionTemplates={r.permissionTemplates}
-                          componentId={window.sonarqube.componentId}
-                          rootQualifiers={options.rootQualifiers}/>, el);
-  });
-});
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/apply-template-view.js b/server/sonar-web/src/main/js/apps/project-permissions/apply-template-view.js
deleted file mode 100644 (file)
index b97c4e4..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * 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 _ from 'underscore';
-import ModalForm from '../../components/common/modal-form';
-import { applyTemplateToProject, bulkApplyTemplateToProject } from '../../api/permissions';
-import Template from './templates/project-permissions-apply-template.hbs';
-
-export default ModalForm.extend({
-  template: Template,
-
-  onRender () {
-    ModalForm.prototype.onRender.apply(this, arguments);
-    this.$('#project-permissions-template').select2({
-      width: '250px',
-      minimumResultsForSearch: 20
-    });
-  },
-
-  onFormSubmit () {
-    ModalForm.prototype.onFormSubmit.apply(this, arguments);
-    const that = this;
-    const permissionTemplate = this.$('#project-permissions-template').val();
-    this.disableForm();
-
-    if (this.options.project) {
-      applyTemplateToProject({
-        data: { projectId: this.options.project.id, templateId: permissionTemplate }
-      }).done(function () {
-        that.options.refresh();
-        that.destroy();
-      }).fail(function (jqXHR) {
-        that.enableForm();
-        that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
-      });
-    } else {
-      const data = { templateId: permissionTemplate };
-      if (this.options.query) {
-        data.q = this.options.query;
-      }
-      if (this.options.filter && this.options.filter !== '__ALL__') {
-        data.qualifier = this.options.filter;
-      }
-
-      bulkApplyTemplateToProject({ data }).done(function () {
-        that.options.refresh();
-        that.destroy();
-      }).fail(function (jqXHR) {
-        that.enableForm();
-        that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
-      });
-    }
-  },
-
-  serializeData () {
-    return _.extend(ModalForm.prototype.serializeData.apply(this, arguments), {
-      permissionTemplates: this.options.permissionTemplates,
-      project: this.options.project,
-      projectsCount: _.size(this.options.projects)
-    });
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/groups-view.js b/server/sonar-web/src/main/js/apps/project-permissions/groups-view.js
deleted file mode 100644 (file)
index 30fe799..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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 _ from 'underscore';
-import Modal from '../../components/common/modals';
-import '../../components/SelectList';
-import Template from './templates/project-permissions-groups.hbs';
-
-function getSearchUrl (permission, project) {
-  return `${window.baseUrl}/api/permissions/groups?ps=100&permission=${permission}&projectId=${project}`;
-}
-
-export default Modal.extend({
-  template: Template,
-
-  onRender () {
-    Modal.prototype.onRender.apply(this, arguments);
-    new window.SelectList({
-      el: this.$('#project-permissions-groups'),
-      width: '100%',
-      readOnly: false,
-      focusSearch: false,
-      format (item) {
-        return item.name;
-      },
-      queryParam: 'q',
-      searchUrl: getSearchUrl(this.options.permission, this.options.project),
-      selectUrl: window.baseUrl + '/api/permissions/add_group',
-      deselectUrl: window.baseUrl + '/api/permissions/remove_group',
-      extra: {
-        permission: this.options.permission,
-        projectId: this.options.project
-      },
-      selectParameter: 'groupName',
-      selectParameterValue: 'name',
-      parse (r) {
-        this.more = false;
-        return r.groups;
-      }
-    });
-  },
-
-  onDestroy () {
-    if (this.options.refresh) {
-      this.options.refresh();
-    }
-    Modal.prototype.onDestroy.apply(this, arguments);
-  },
-
-  serializeData () {
-    return _.extend(Modal.prototype.serializeData.apply(this, arguments), {
-      projectName: this.options.projectName
-    });
-  }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/main.js b/server/sonar-web/src/main/js/apps/project-permissions/main.js
deleted file mode 100644 (file)
index eb947f8..0000000
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * 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 $ from 'jquery';
-import _ from 'underscore';
-import React from 'react';
-
-import Permissions from './permissions';
-import PermissionsFooter from './permissions-footer';
-import Search from './search';
-import ApplyTemplateView from './apply-template-view';
-import { translate } from '../../helpers/l10n';
-import { TooltipsContainer } from '../../components/mixins/tooltips-mixin';
-import '../permission-templates/styles.css';
-
-const PERMISSIONS_ORDER = ['user', 'codeviewer', 'issueadmin', 'admin', 'scan'];
-
-export default React.createClass({
-  propTypes: {
-    permissionTemplates: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
-  },
-
-  getInitialState() {
-    return { ready: false, permissions: [], projects: [], total: 0, filter: '__ALL__' };
-  },
-
-  componentDidMount() {
-    this.requestPermissions();
-  },
-
-  sortPermissions(permissions) {
-    return _.sortBy(permissions, p => PERMISSIONS_ORDER.indexOf(p.key));
-  },
-
-  mergePermissionsToProjects(projects, basePermissions) {
-    return projects.map(project => {
-      // it's important to keep the order of the project permissions the same as the order of base permissions
-      const permissions = basePermissions.map(basePermission => {
-        const projectPermission = _.findWhere(project.permissions, { key: basePermission.key });
-        return _.extend({ usersCount: 0, groupsCount: 0 }, basePermission, projectPermission);
-      });
-      return _.extend({}, project, { permissions });
-    });
-  },
-
-  requestPermissions(page = 1, query = '', filter = this.state.filter) {
-    const url = window.baseUrl + '/api/permissions/search_project_permissions';
-    let data = { p: page, q: query };
-    if (filter !== '__ALL__') {
-      data.qualifier = filter;
-    }
-    if (this.props.componentId) {
-      data = { projectId: this.props.componentId };
-    }
-    this.setState({ ready: false }, () => {
-      $.get(url, data).done(r => {
-        const permissions = this.sortPermissions(r.permissions);
-        let projects = this.mergePermissionsToProjects(r.projects, permissions);
-        if (page > 1) {
-          projects = [].concat(this.state.projects, projects);
-        }
-        this.setState({
-          ready: true,
-          projects,
-          permissions,
-          total: r.paging.total,
-          page: r.paging.pageIndex,
-          query,
-          filter
-        });
-      });
-    });
-  },
-
-  loadMore() {
-    this.requestPermissions(this.state.page + 1, this.state.query);
-  },
-
-  search(query) {
-    this.requestPermissions(1, query);
-  },
-
-  handleFilter(filter) {
-    this.requestPermissions(1, this.state.query, filter);
-  },
-
-  refresh() {
-    this.requestPermissions(1, this.state.query);
-  },
-
-  bulkApplyTemplate(e) {
-    e.preventDefault();
-    new ApplyTemplateView({
-      query: this.state.query,
-      filter: this.state.filter,
-      permissionTemplates: this.props.permissionTemplates,
-      refresh: () => this.requestPermissions(1, this.state.query, this.state.filter)
-    }).render();
-  },
-
-  renderBulkApplyButton() {
-    if (this.props.componentId) {
-      return null;
-    }
-    return (
-        <button onClick={this.bulkApplyTemplate} className="js-bulk-apply-template">Bulk Apply Template</button>
-    );
-  },
-
-  renderSpinner () {
-    if (this.state.ready) {
-      return null;
-    }
-    return <i className="spinner"/>;
-  },
-
-  render() {
-    return (
-        <TooltipsContainer>
-          <div className="page">
-            <header id="project-permissions-header" className="page-header">
-              <h1 className="page-title">{translate('roles.page')}</h1>
-              {this.renderSpinner()}
-              <div className="page-actions">
-                {this.renderBulkApplyButton()}
-              </div>
-              <p className="page-description">
-                {translate('roles.page.description2')}
-              </p>
-            </header>
-
-            <Search {...this.props}
-                filter={this.state.filter}
-                search={this.search}
-                onFilter={this.handleFilter}/>
-
-            <Permissions
-                ready={this.state.ready}
-                projects={this.state.projects}
-                permissions={this.state.permissions}
-                permissionTemplates={this.props.permissionTemplates}
-                refresh={this.refresh}/>
-
-            <PermissionsFooter {...this.props}
-                ready={this.state.ready}
-                count={this.state.projects.length}
-                total={this.state.total}
-                loadMore={this.loadMore}/>
-          </div>
-        </TooltipsContainer>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.js b/server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.js
deleted file mode 100644 (file)
index 1e247dc..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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 classNames from 'classnames';
-import React from 'react';
-
-export default React.createClass({
-  propTypes: {
-    count: React.PropTypes.number.isRequired,
-    total: React.PropTypes.number.isRequired,
-    loadMore: React.PropTypes.func.isRequired
-  },
-
-  handleLoadMore (e) {
-    e.preventDefault();
-    this.props.loadMore();
-  },
-
-  render() {
-    if (this.props.componentId) {
-      return null;
-    }
-    const hasMore = this.props.total > this.props.count;
-    const loadMoreLink = <a onClick={this.handleLoadMore} className="spacer-left" href="#">show more</a>;
-    const className = classNames('spacer-top note text-center', { 'new-loading': !this.props.ready });
-    return (
-        <footer className={className}>
-          {this.props.count}/{this.props.total} shown
-          {hasMore ? loadMoreLink : null}
-        </footer>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/permissions-header.js b/server/sonar-web/src/main/js/apps/project-permissions/permissions-header.js
deleted file mode 100644 (file)
index 281616a..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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';
-
-export default React.createClass({
-  propTypes: {
-    permissions: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
-  },
-
-  render() {
-    const cells = this.props.permissions.map(p => (
-        <th key={p.key} className="permission-column">
-          {p.name}
-          <i
-              className="icon-help little-spacer-left"
-              title={p.description}
-              data-toggle="tooltip"/>
-        </th>
-    ));
-
-    return (
-        <thead>
-          <tr>
-            <th>&nbsp;</th>
-            {cells}
-            <th className="actions-column">&nbsp;</th>
-          </tr>
-        </thead>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/permissions.js b/server/sonar-web/src/main/js/apps/project-permissions/permissions.js
deleted file mode 100644 (file)
index c10323e..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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 classNames from 'classnames';
-import React from 'react';
-
-import PermissionsHeader from './permissions-header';
-import Project from './project';
-
-export default React.createClass({
-  propTypes: {
-    projects: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
-    permissions: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
-    permissionTemplates: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
-    refresh: React.PropTypes.func.isRequired
-  },
-
-  render() {
-    const projects = this.props.projects.map(p => {
-      return <Project
-          key={p.id}
-          project={p}
-          permissionTemplates={this.props.permissionTemplates}
-          refresh={this.props.refresh}/>;
-    });
-    const className = classNames(
-        'data zebra permissions-table',
-        { 'new-loading': !this.props.ready });
-    return (
-        <table id="projects" className={className}>
-          <PermissionsHeader permissions={this.props.permissions}/>
-          <tbody>{projects}</tbody>
-        </table>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/project.js b/server/sonar-web/src/main/js/apps/project-permissions/project.js
deleted file mode 100644 (file)
index 2a3c77a..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * 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 UsersView from './users-view';
-import GroupsView from './groups-view';
-import ApplyTemplateView from './apply-template-view';
-import { getComponentUrl } from '../../helpers/urls';
-import QualifierIcon from '../../components/shared/qualifier-icon';
-
-export default React.createClass({
-  propTypes: {
-    project: React.PropTypes.object.isRequired,
-    permissionTemplates: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
-    refresh: React.PropTypes.func.isRequired
-  },
-
-  showGroups(permission, e) {
-    e.preventDefault();
-    new GroupsView({
-      permission,
-      project: this.props.project.id,
-      projectName: this.props.project.name,
-      refresh: this.props.refresh
-    }).render();
-  },
-
-  showUsers(permission, e) {
-    e.preventDefault();
-    new UsersView({
-      permission,
-      project: this.props.project.id,
-      projectName: this.props.project.name,
-      refresh: this.props.refresh
-    }).render();
-  },
-
-  applyTemplate(e) {
-    e.preventDefault();
-    new ApplyTemplateView({
-      permissionTemplates: this.props.permissionTemplates,
-      project: this.props.project,
-      refresh: this.props.refresh
-    }).render();
-  },
-
-  render() {
-    const permissions = this.props.project.permissions.map(p => {
-      return (
-          <td key={p.key}>
-            <table>
-              <tbody>
-              <tr>
-                <td className="spacer-right">Users</td>
-                <td className="spacer-left bordered-left">{p.usersCount}</td>
-                <td className="spacer-left">
-                  <a onClick={this.showUsers.bind(this, p.key)} className="icon-bullet-list" title="Update Users"
-                     data-toggle="tooltip" href="#"></a>
-                </td>
-              </tr>
-              <tr>
-                <td className="spacer-right">Groups</td>
-                <td className="spacer-left bordered-left">{p.groupsCount}</td>
-                <td className="spacer-left">
-                  <a onClick={this.showGroups.bind(this, p.key)} className="icon-bullet-list" title="Update Users"
-                     data-toggle="tooltip" href="#"></a>
-                </td>
-              </tr>
-              </tbody>
-            </table>
-          </td>
-      );
-    });
-    return (
-        <tr>
-          <td>
-            <span className="little-spacer-right">
-              <QualifierIcon qualifier={this.props.project.qualifier}/>
-            </span>
-            <a href={getComponentUrl(this.props.project.key)}>{this.props.project.name}</a>
-          </td>
-          {permissions}
-          <td className="thin nowrap text-right">
-            <button onClick={this.applyTemplate} className="js-apply-template">Apply Template</button>
-          </td>
-        </tr>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/qualifier-filter.js b/server/sonar-web/src/main/js/apps/project-permissions/qualifier-filter.js
deleted file mode 100644 (file)
index 3056cdc..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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 { translate } from '../../helpers/l10n';
-
-import RadioToggle from '../../components/controls/RadioToggle';
-
-const rootQualifiersToOptions = (qualifiers) => {
-  return qualifiers.map(q => {
-    return {
-      value: q,
-      label: translate('qualifiers', q)
-    };
-  });
-};
-
-export const QualifierFilter = ({ rootQualifiers, filter, onFilter }) => {
-  const options = [{ value: '__ALL__', label: 'All' }, ...rootQualifiersToOptions(rootQualifiers)];
-
-  return (
-      <div className="display-inline-block text-top nowrap big-spacer-right">
-        <RadioToggle value={filter}
-                     options={options}
-                     name="qualifier"
-                     onCheck={onFilter}/>
-      </div>
-  );
-};
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/search.js b/server/sonar-web/src/main/js/apps/project-permissions/search.js
deleted file mode 100644 (file)
index 1b19515..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * 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 _ from 'underscore';
-import React from 'react';
-
-import { QualifierFilter } from './qualifier-filter';
-
-export default React.createClass({
-  propTypes: {
-    search: React.PropTypes.func.isRequired
-  },
-
-  componentWillMount () {
-    this.search = _.debounce(this.search, 250);
-  },
-
-  onSubmit(e) {
-    e.preventDefault();
-    this.search();
-  },
-
-  search() {
-    const q = this.refs.input.value;
-    this.props.search(q);
-  },
-
-  render() {
-    if (this.props.componentId) {
-      return null;
-    }
-    return (
-        <div className="panel panel-vertical bordered-bottom spacer-bottom">
-
-          {this.props.rootQualifiers.length > 1 && <QualifierFilter filter={this.props.filter}
-                                                                    rootQualifiers={this.props.rootQualifiers}
-                                                                    onFilter={this.props.onFilter}/>}
-
-          <form onSubmit={this.onSubmit} className="search-box display-inline-block text-top">
-            <button className="search-box-submit button-clean">
-              <i className="icon-search"></i>
-            </button>
-            <input onChange={this.search}
-                   ref="input"
-                   className="search-box-input"
-                   type="search"
-                   placeholder="Search"/>
-          </form>
-        </div>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-apply-template.hbs b/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-apply-template.hbs
deleted file mode 100644 (file)
index 25f4943..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-<form id="project-permissions-apply-template-form" autocomplete="off">
-  <div class="modal-head">
-    {{#if project}}
-      <h2>Apply Permission Template to "{{project.name}}"</h2>
-    {{else}}
-      <h2>Bulk Apply Permission Template</h2>
-    {{/if}}
-  </div>
-  <div class="modal-body">
-    <div class="js-modal-messages"></div>
-
-    <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>
-  </div>
-  <div class="modal-foot">
-    <button id="project-permissions-apply-template">Apply</button>
-    <a href="#" class="js-modal-close">Cancel</a>
-  </div>
-</form>
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-groups.hbs b/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-groups.hbs
deleted file mode 100644 (file)
index 68ceacf..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-<div class="modal-head">
-  <h2>Update Groups of "{{projectName}}"</h2>
-</div>
-<div class="modal-body">
-  <div class="js-modal-messages"></div>
-  <div id="project-permissions-groups"></div>
-</div>
-<div class="modal-foot">
-  <a href="#" class="js-modal-close">Done</a>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-users.hbs b/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-users.hbs
deleted file mode 100644 (file)
index b21ac81..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-<div class="modal-head">
-  <h2>Update Users of "{{projectName}}"</h2>
-</div>
-<div class="modal-body">
-  <div class="js-modal-messages"></div>
-  <div id="project-permissions-users"></div>
-</div>
-<div class="modal-foot">
-  <a href="#" class="js-modal-close">Done</a>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/users-view.js b/server/sonar-web/src/main/js/apps/project-permissions/users-view.js
deleted file mode 100644 (file)
index 04399fa..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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 _ from 'underscore';
-import Modal from '../../components/common/modals';
-import '../../components/SelectList';
-import Template from './templates/project-permissions-users.hbs';
-
-export default Modal.extend({
-  template: Template,
-
-  onRender () {
-    Modal.prototype.onRender.apply(this, arguments);
-    const searchUrl = window.baseUrl + '/api/permissions/users?ps=100&permission=' + this.options.permission +
-        '&projectId=' + this.options.project;
-    new window.SelectList({
-      searchUrl,
-      el: this.$('#project-permissions-users'),
-      width: '100%',
-      readOnly: false,
-      focusSearch: false,
-      format (item) {
-        return `${item.name}<br><span class="note">${item.login}</span>`;
-      },
-      queryParam: 'q',
-      selectUrl: window.baseUrl + '/api/permissions/add_user',
-      deselectUrl: window.baseUrl + '/api/permissions/remove_user',
-      extra: {
-        permission: this.options.permission,
-        projectId: this.options.project
-      },
-      selectParameter: 'login',
-      selectParameterValue: 'login',
-      parse (r) {
-        this.more = false;
-        return r.users;
-      }
-    });
-  },
-
-  onDestroy () {
-    if (this.options.refresh) {
-      this.options.refresh();
-    }
-    Modal.prototype.onDestroy.apply(this, arguments);
-  },
-
-  serializeData () {
-    return _.extend(Modal.prototype.serializeData.apply(this, arguments), {
-      projectName: this.options.projectName
-    });
-  }
-});
-
index f7b78320c8553ce8a3537ecdcbc04e90acefd8e6..b52d9ff23efb37d9ebd1ef2c89c69800aba43770 100644 (file)
  */
 import React from 'react';
 import md5 from 'blueimp-md5';
+import classNames from 'classnames';
 
 export default class Avatar extends React.Component {
   static propTypes = {
     email: React.PropTypes.string,
-    size: React.PropTypes.number.isRequired
+    size: React.PropTypes.number.isRequired,
+    className: React.PropTypes.string
   };
 
   render () {
@@ -37,8 +39,10 @@ export default class Avatar extends React.Component {
         .replace('{EMAIL_MD5}', emailHash)
         .replace('{SIZE}', this.props.size * 2);
 
+    const className = classNames(this.props.className, 'rounded');
+
     return (
-        <img className="rounded"
+        <img className={className}
              src={url}
              width={this.props.size}
              height={this.props.size}
index 3ff1f28a928b460c11fed133480e71ba131260ac..164052e30f47aadeea400627c11b515d8218f722 100644 (file)
     top: 1px;
   }
 }
+
+.search-box-input-note {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  line-height: 1;
+  color: #777;
+  font-size: @smallFontSize;
+  white-space: nowrap;
+}
+
index d0cd92873ebd93c1a646b66e26c7b33a165d0887..ffb3a31395a869f6361c69b44e56aeacb1851b32 100644 (file)
@@ -137,8 +137,10 @@ input[type="submit"].button-red {
 .button-clean:focus {
   margin: 0;
   padding: 0;
+  line-height: 1;
   border: none;
   background: transparent;
+  box-shadow: none;
   color: @baseFontColor;
 }
 
index d5802264ca9f110038e24c98714ef464c9b716ff..905993a73c2b6340d4720e455774854164f7b885 100644 (file)
@@ -47,7 +47,7 @@ table.data > thead:after {
 
 table.data > thead > tr > th {
   vertical-align: top;
-  line-height: 24px;
+  line-height: 18px;
   padding: 8px 10px;
   border-bottom: 1px solid @barBorderColor;
   font-weight: 500;
index 2ffe436320a8dfb4218111ba58159fb7b09a62f0..df0126ffd091829d2c4a609d1f5e6fa433b20121 100644 (file)
@@ -1,6 +1,3 @@
 <% content_for :extra_script do %>
-  <script>
-    window.sonarqube.componentId = '<%= @project.uuid -%>';
-  </script>
   <script src="<%= ApplicationController.root_context -%>/js/bundles/project-permissions.js?v=<%= sonar_version -%>"></script>
 <% end %>
index c70942bef7481988ee94ec0e8a6069d6a045b059..d64f64ab975abd89695b2c235f14315b35549bcb 100644 (file)
@@ -36,7 +36,7 @@ module.exports = {
     'component-measures': './src/main/js/apps/component-measures/app.js',
     'custom-measures': './src/main/js/apps/custom-measures/app.js',
     'dashboard': './src/main/js/apps/dashboard/app.js',
-    'global-permissions': './src/main/js/apps/global-permissions/app.js',
+    'global-permissions': './src/main/js/apps/permissions/global/app.js',
     'groups': './src/main/js/apps/groups/app.js',
     'issues': './src/main/js/apps/issues/app.js',
     'maintenance': './src/main/js/apps/maintenance/app.js',
@@ -45,7 +45,7 @@ module.exports = {
     'metrics': './src/main/js/apps/metrics/app.js',
     'overview': './src/main/js/apps/overview/app.js',
     'permission-templates': './src/main/js/apps/permission-templates/app.js',
-    'project-permissions': './src/main/js/apps/project-permissions/app.js',
+    'project-permissions': './src/main/js/apps/permissions/project/app.js',
     'projects': './src/main/js/apps/projects/app.js',
     'quality-gates': './src/main/js/apps/quality-gates/app.js',
     'quality-profiles': './src/main/js/apps/quality-profiles/app.js',
@@ -92,7 +92,10 @@ module.exports = {
       { test: require.resolve('jquery'), loader: 'expose?$!expose?jQuery' },
       { test: require.resolve('underscore'), loader: 'expose?_' },
       { test: require.resolve('backbone'), loader: 'expose?Backbone' },
-      { test: require.resolve('backbone.marionette'), loader: 'expose?Marionette' },
+      {
+        test: require.resolve('backbone.marionette'),
+        loader: 'expose?Marionette'
+      },
       { test: require.resolve('d3'), loader: 'expose?d3' },
       { test: require.resolve('react'), loader: 'expose?React' },
       { test: require.resolve('react-dom'), loader: 'expose?ReactDOM' },