]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6797 rewrite project permissions page
authorStas Vilchik <vilchiks@gmail.com>
Thu, 20 Aug 2015 13:39:03 +0000 (15:39 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Mon, 24 Aug 2015 07:22:49 +0000 (09:22 +0200)
20 files changed:
server/sonar-web/Gruntfile.coffee
server/sonar-web/src/main/js/apps/nav/settings/settings-nav.jsx
server/sonar-web/src/main/js/apps/project-permissions/app.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-permissions/groups-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-permissions/main.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-permissions/permissions-header.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-permissions/permissions.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-permissions/project.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-permissions/search.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-groups.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-users.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-permissions/users-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/Url.jsx [new file with mode: 0644]
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/roles_controller.rb
server/sonar-web/src/main/webapp/WEB-INF/app/views/permission_templates/index.html.erb
server/sonar-web/src/main/webapp/WEB-INF/app/views/roles/projects.html.erb
server/sonar-web/src/test/json/permissions/project-permissions.json [new file with mode: 0644]
server/sonar-web/test/intern.js
server/sonar-web/test/medium/project-permissions.spec.js [new file with mode: 0644]

index 050967793ab9dd9bb24bd051cacd25bb5ef2720d..240adc7e64206cb39207f077900cc77657950fb9 100644 (file)
@@ -146,6 +146,7 @@ module.exports = (grunt) ->
           'build-app:metrics'
           'build-app:nav'
           'build-app:overview'
+          'build-app:project-permissions'
           'build-app:provisioning'
           'build-app:quality-gates'
           'build-app:quality-profiles'
@@ -241,6 +242,9 @@ module.exports = (grunt) ->
           '<%= BUILD_PATH %>/js/apps/global-permissions/templates.js': [
             '<%= SOURCE_PATH %>/js/apps/global-permissions/templates/**/*.hbs'
           ]
+          '<%= BUILD_PATH %>/js/apps/project-permissions/templates.js': [
+            '<%= SOURCE_PATH %>/js/apps/project-permissions/templates/**/*.hbs'
+          ]
 
 
     clean:
index 20f1ce5a08d1544ef2ab579d3f27a9d4875a5462..760034e73846f7d6769a9ef4ceb1d247cd1750c2 100644 (file)
@@ -47,6 +47,7 @@ export default React.createClass({
                 {this.renderLink('/groups', window.t('user_groups.page'))}
                 {this.renderLink('/roles/global', window.t('global_permissions.page'))}
                 {this.renderLink('/roles/projects', window.t('roles.page'))}
+                {this.renderLink('/permission_templates', window.t('permission_templates'))}
               </ul>
             </li>
 
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/app.jsx b/server/sonar-web/src/main/js/apps/project-permissions/app.jsx
new file mode 100644 (file)
index 0000000..28dc73b
--- /dev/null
@@ -0,0 +1,13 @@
+import React from 'react';
+import Main from './main';
+
+const $ = jQuery;
+
+export default {
+  start(options) {
+    window.requestMessages().done(() => {
+      var el = document.querySelector(options.el);
+      React.render(<Main/>, el);
+    });
+  }
+};
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
new file mode 100644 (file)
index 0000000..ba98ca7
--- /dev/null
@@ -0,0 +1,43 @@
+define([
+  'components/common/modals',
+  'components/common/select-list',
+  './templates'
+], function (Modal) {
+
+  return Modal.extend({
+    template: Templates['project-permissions-groups'],
+
+    onRender: function () {
+      this._super();
+      new window.SelectList({
+        el: this.$('#project-permissions-groups'),
+        width: '100%',
+        readOnly: false,
+        focusSearch: false,
+        format: function (item) {
+          return item.name;
+        },
+        queryParam: 'q',
+        searchUrl: baseUrl + '/api/permissions/groups?ps=100&permission=' + this.options.permission + '&projectId=' + this.options.project,
+        selectUrl: baseUrl + '/api/permissions/add_group',
+        deselectUrl: baseUrl + '/api/permissions/remove_group',
+        extra: {
+          permission: this.options.permission,
+          projectId: this.options.project
+        },
+        selectParameter: 'groupName',
+        selectParameterValue: 'name',
+        parse: function (r) {
+          this.more = false;
+          return r.groups;
+        }
+      });
+    },
+
+    onDestroy: function () {
+      this.options.refresh && this.options.refresh();
+      this._super();
+    }
+  });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/main.jsx b/server/sonar-web/src/main/js/apps/project-permissions/main.jsx
new file mode 100644 (file)
index 0000000..96b77f7
--- /dev/null
@@ -0,0 +1,80 @@
+import React from 'react';
+import Permissions from './permissions';
+import PermissionsFooter from './permissions-footer';
+import Search from './search';
+
+let $ = jQuery;
+
+export default React.createClass({
+  getInitialState() {
+    return { permissions: [], projects: [], total: 0 };
+  },
+
+  componentDidMount() {
+    this.requestPermissions();
+  },
+
+  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
+      let permissions = basePermissions.map(basePermission => {
+        let projectPermission = _.findWhere(project.permissions, { key: basePermission.key });
+        if (!projectPermission) {
+          throw new Error(`Project "${project.name} [${project.key}]" doesn't have permission "${basePermission.key}"`);
+        }
+        return _.extend({}, basePermission, projectPermission);
+      });
+      return _.extend({}, project, { permissions: permissions });
+    });
+  },
+
+  requestPermissions(page = 1, query = '') {
+    let url = `${window.baseUrl}/api/permissions/search_project_permissions`;
+    let data = { p: page, q: query };
+    $.get(url, data).done(r => {
+      let projects = this.mergePermissionsToProjects(r.projects, r.permissions);
+      if (page > 1) {
+        projects = [].concat(this.state.projects, projects);
+      }
+      this.setState({
+        projects: projects,
+        permissions: r.permissions,
+        total: r.paging.total,
+        page: r.paging.pageIndex,
+        query: query
+      });
+    });
+  },
+
+  loadMore() {
+    this.requestPermissions(this.state.page + 1, this.state.query);
+  },
+
+  search(query) {
+    this.requestPermissions(1, query);
+  },
+
+  render() {
+    return (
+        <div className="page">
+          <header id="project-permissions-header" className="page-header">
+            <h1 className="page-title">{window.t('roles.page')}</h1>
+            <p className="page-description">{window.t('roles.page.description2')}</p>
+          </header>
+
+          <Search
+              search={this.search}/>
+
+          <Permissions
+              projects={this.state.projects}
+              permissions={this.state.permissions}
+              refresh={this.requestPermissions}/>
+
+          <PermissionsFooter
+              count={this.state.projects.length}
+              total={this.state.total}
+              loadMore={this.loadMore}/>
+        </div>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.jsx b/server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.jsx
new file mode 100644 (file)
index 0000000..f4913a7
--- /dev/null
@@ -0,0 +1,20 @@
+import React from 'react';
+
+export default React.createClass({
+  propTypes:{
+    count: React.PropTypes.number.isRequired,
+    total: React.PropTypes.number.isRequired,
+    loadMore: React.PropTypes.func.isRequired
+  },
+
+  render() {
+    let hasMore = this.props.total > this.props.count;
+    let loadMoreLink = <a onClick={this.props.loadMore} className="spacer-left" href="#">show more</a>;
+    return (
+        <footer className="spacer-top note text-center">
+          {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.jsx b/server/sonar-web/src/main/js/apps/project-permissions/permissions-header.jsx
new file mode 100644 (file)
index 0000000..a68cca2
--- /dev/null
@@ -0,0 +1,26 @@
+import React from 'react';
+
+export default React.createClass({
+  propTypes: {
+    permissions: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
+  },
+
+  render() {
+    let cellWidth = (80 / this.props.permissions.length) + '%';
+    let cells = this.props.permissions.map(p => {
+      return (
+          <th key={p.key} style={{ width: cellWidth }}>
+            {p.name}<br/><span className="small">{p.description}</span>
+          </th>
+      );
+    });
+    return (
+        <thead>
+        <tr>
+          <th style={{ width: '20%' }}>&nbsp;</th>
+          {cells}
+        </tr>
+        </thead>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/permissions.jsx b/server/sonar-web/src/main/js/apps/project-permissions/permissions.jsx
new file mode 100644 (file)
index 0000000..4638c20
--- /dev/null
@@ -0,0 +1,23 @@
+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,
+    refresh: React.PropTypes.func.isRequired
+  },
+
+  render() {
+    let projects = this.props.projects.map(p => {
+      return <Project key={p.uuid} project={p} refresh={this.props.refresh}/>
+    });
+    return (
+        <table id="projects" className="data zebra">
+          <PermissionsHeader permissions={this.props.permissions}/>
+          <tbody>{projects}</tbody>
+        </table>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/project.jsx b/server/sonar-web/src/main/js/apps/project-permissions/project.jsx
new file mode 100644 (file)
index 0000000..31c03a1
--- /dev/null
@@ -0,0 +1,66 @@
+import React from 'react';
+import UsersView from './users-view';
+import GroupsView from './groups-view';
+import {getProjectUrl} from '../../helpers/Url';
+
+export default React.createClass({
+  propTypes: {
+    project: React.PropTypes.object.isRequired,
+    refresh: React.PropTypes.func.isRequired
+  },
+
+  showGroups(permission, e) {
+    e.preventDefault();
+    new GroupsView({
+      permission: permission,
+      project: this.props.project.uuid,
+      refresh: this.props.refresh
+    }).render();
+  },
+
+  showUsers(permission, e) {
+    e.preventDefault();
+    new UsersView({
+      permission: permission,
+      project: this.props.project.uuid,
+      refresh: this.props.refresh
+    }).render();
+  },
+
+  render() {
+    let permissions = this.props.project.permissions.map(p => {
+      return (
+          <td key={p.key}>
+            <table>
+              <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>
+            </table>
+          </td>
+      );
+    });
+    return (
+        <tr>
+          <td>
+            <strong>
+              <a href={getProjectUrl(this.props.project.key)}>{this.props.project.name}</a>
+            </strong>
+          </td>
+          {permissions}
+        </tr>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/search.jsx b/server/sonar-web/src/main/js/apps/project-permissions/search.jsx
new file mode 100644 (file)
index 0000000..3506d78
--- /dev/null
@@ -0,0 +1,34 @@
+import React from 'react';
+
+export default React.createClass({
+  propTypes: {
+    search: React.PropTypes.func.isRequired
+  },
+
+  componentWillMount: function () {
+    this.search = _.debounce(this.search, 250);
+  },
+
+  onSubmit(e) {
+    e.preventDefault();
+    this.search();
+  },
+
+  search() {
+    let q = React.findDOMNode(this.refs.input).value;
+    this.props.search(q);
+  },
+
+  render() {
+    return (
+        <div className="panel panel-vertical bordered-bottom spacer-bottom">
+          <form onSubmit={this.onSubmit} className="search-box">
+            <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-groups.hbs b/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-groups.hbs
new file mode 100644 (file)
index 0000000..c5f551e
--- /dev/null
@@ -0,0 +1,10 @@
+<div class="modal-head">
+  <h2>Update Groups</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
new file mode 100644 (file)
index 0000000..acfd4ea
--- /dev/null
@@ -0,0 +1,10 @@
+<div class="modal-head">
+  <h2>Update Users</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
new file mode 100644 (file)
index 0000000..6a71583
--- /dev/null
@@ -0,0 +1,43 @@
+define([
+  'components/common/modals',
+  'components/common/select-list',
+  './templates'
+], function (Modal) {
+
+  return Modal.extend({
+    template: Templates['project-permissions-users'],
+
+    onRender: function () {
+      this._super();
+      new window.SelectList({
+        el: this.$('#project-permissions-users'),
+        width: '100%',
+        readOnly: false,
+        focusSearch: false,
+        format: function (item) {
+          return item.name + '<br><span class="note">' + item.login + '</span>';
+        },
+        queryParam: 'q',
+        searchUrl: baseUrl + '/api/permissions/users?ps=100&permission=' + this.options.permission + '&projectId=' + this.options.project,
+        selectUrl: baseUrl + '/api/permissions/add_user',
+        deselectUrl: baseUrl + '/api/permissions/remove_user',
+        extra: {
+          permission: this.options.permission,
+          projectId: this.options.project
+        },
+        selectParameter: 'login',
+        selectParameterValue: 'login',
+        parse: function (r) {
+          this.more = false;
+          return r.users;
+        }
+      });
+    },
+
+    onDestroy: function () {
+      this.options.refresh && this.options.refresh();
+      this._super();
+    }
+  });
+
+});
diff --git a/server/sonar-web/src/main/js/helpers/Url.jsx b/server/sonar-web/src/main/js/helpers/Url.jsx
new file mode 100644 (file)
index 0000000..e88ddb5
--- /dev/null
@@ -0,0 +1,6 @@
+export function getProjectUrl(project) {
+  if (typeof project !== 'string') {
+    throw new TypeError("Project ID or KEY should be passed");
+  }
+  return `${window.baseUrl}/overview?id=${encodeURIComponent(project)}`;
+}
index 01abb33a4231e5c195ae593b4e95c1d67a822ee8..5cdcf35824555ee26401876c23173a50d54bd3d2 100644 (file)
@@ -33,23 +33,6 @@ class RolesController < ApplicationController
   # GET /roles/projects
   def projects
     access_denied unless has_role?(:admin)
-
-    params['pageSize'] = 25
-    params['qualifiers'] ||= 'TRK'
-    @query_result = Internal.component_api.findWithUncompleteProjects(params)
-
-    @available_qualifiers = java_facade.getQualifiersWithProperty('hasRolePolicy').collect { |qualifier| [message("qualifiers.#{qualifier}"), qualifier] }.to_a.sort
-
-    # For the moment, we return projects from rails models, but it should be replaced to return java components (this will need methods on ComponentQueryResult to return roles from component)
-    @projects = Project.all(
-        :include => ['user_roles','group_roles'],
-        :conditions => ['kee in (?)', @query_result.components().to_a.collect{|component| component.key()}],
-        # Even if components are already sorted, we must sort them again as this SQL query will not keep order
-        :order => 'lower(name)'
-    )
-    @components_names = params[:names]
-    @components_keys = params[:keys]
-    @components_qualifiers = params[:qualifiers]
   end
 
   # GET /roles/edit_users[?resource=<resource>]
index 8f7b4840298af583551d3581eb4c10ab3fb08fe3..965847fcb40200668054fb66eb2d13d52f9e5925 100644 (file)
@@ -5,12 +5,6 @@
 <div class="page">
   <header class="page-header">
     <h1 class="page-title"><%= message 'roles.page' -%></h1>
-    <p class="page-description"><%= message('roles.page.description') -%></p>
-  </header>
-
-  <%= render :partial => 'roles/tabs', :locals => {:selected_tab => 'Permission templates'} %>
-  <br/>
-  <header class="page-header">
     <div class="page-actions">
       <div class="button-group">
         <%= link_to message('permission_template.set_default_templates'), {:action => :default_templates_form, :qualifiers => @root_qualifiers},
@@ -18,6 +12,7 @@
         <%= link_to message('create'), {:action => :create_form}, :id => 'create-link-permission-template', :class => 'open-modal link-action button' %>
       </div>
     </div>
+    <p class="page-description"><%= message('roles.page.description') -%></p>
   </header>
   <table class="data width100" id="permission-templates">
     <thead>
index b383932b5a5d7a13f7bbaa4ad974d19931f0122e..db45ae6982ecd369b1a7f3a62c140c5670358e32 100644 (file)
@@ -1,125 +1,7 @@
-<% content_for :script do %>
-  <script>require(['components/common/select-list']);</script>
+<% content_for :extra_script do %>
+  <script>
+    require(['apps/project-permissions/app'], function (App) {
+      App.start({ el: '#content' });
+    });
+  </script>
 <% end %>
-
-<div class="page">
-  <header class="page-header">
-    <h1 class="page-title"><%= message 'roles.page' -%></h1>
-    <p class="page-description"><%= message('roles.page.description2') -%></p>
-  </header>
-
-  <%= render :partial => 'roles/tabs', :locals => {:selected_tab => 'Projects'} %>
-
-  <div class="tabs-panel marginbottom10 background-gray">
-    <% form_tag({:action => 'projects'}, {:id => 'project-search-form', :method => 'get'}) do %>
-      <div class="table">
-        <div class="project-search text-top">
-          <span class="note"><%= message('projects_role.criteria.name') -%></span><br/>
-          <% selected_name = @query_result.query.names.to_a.first if @query_result.query.names && @query_result.query.names.size == 1 %>
-          <%= text_field_tag 'names', selected_name, :id => 'search-text' %>
-        </div>
-        <div class="project-search text-top">
-          <span class="note"><%= message('projects_role.criteria.key') -%></span><br/>
-          <% selected_key = @query_result.query.keys.to_a.first if @query_result.query.keys && @query_result.query.keys.size == 1 %>
-          <%= text_field_tag 'keys', selected_key, :id => 'search-key' %>
-        </div>
-        <div class="project-search text-top">
-          <span class="note"><%= message('type') -%></span><br/>
-          <% selected_qualifier = @query_result.query.qualifiers.to_a.first if @query_result.query.qualifiers && @query_result.query.qualifiers.size == 1 %>
-          <%= dropdown_tag 'qualifiers', options_for_select(@available_qualifiers, selected_qualifier), {
-                                           :width => '150px'
-                                       }, {:id => 'search-qualifier'} -%>
-        </div>
-        <div class="project-search">
-          <br/>
-          <%= submit_tag message('search_verb'), :id => 'submit-search', :onclick => 'submitSearch();' %>
-        </div>
-      </div>
-    <% end %>
-  </div>
-
-  <header class="page-header">
-    <div class="page-actions" id="project-roles-operations">
-      <div class="button-group">
-        <%= link_to message('projects_role.bulk_change'), {:action => :apply_template_form, :names => @components_names,
-                                                           :keys => @components_keys, :qualifiers => @components_qualifiers,
-                                                           :results_count => @query_result.paging.total},
-                    :id => 'apply-template-modal', :class => 'open-modal link-action button' %>
-      </div>
-    </div>
-  </header>
-
-  <table class="data width100" id="projects">
-    <thead>
-    <tr>
-      <th style="min-width: 10em">&nbsp;</th>
-      <th>
-        <%= message('projects_role.user') -%><br/>
-        <span class="small gray" style="font-size: 11px; font-weight: normal;"><%= message('projects_role.user.desc') -%></span>
-      </th>
-      <th>
-        <%= message('projects_role.admin') -%><br/>
-        <span class="small gray" style="font-size: 11px; font-weight: normal;"><%= message('projects_role.admin.desc') -%></span>
-      </th>
-      <th>
-        <%= message('projects_role.issueadmin') -%><br/>
-        <span class="small gray" style="font-size: 11px; font-weight: normal;"><%= message('projects_role.issueadmin.desc') -%></span>
-      </th>
-      <th>
-        <%= message('projects_role.codeviewer') -%><br/>
-        <span class="small gray" style="font-size: 11px; font-weight: normal;"><%= message('projects_role.codeviewer.desc') -%></span>
-      </th>
-      <th>&nbsp;</th>
-    </tr>
-    </thead>
-
-    <%= paginate_java(@query_result.paging, :colspan => 4, :id => 'project-roles-foot', :include_loading_icon => true) { |label, page_id|
-      link_to(label, params.merge({:pageIndex => page_id}))
-    }
-    %>
-
-    <tbody>
-    <% if @projects.empty? %>
-      <tr class="even">
-        <td colspan="5" align="left"><%= message('no_results') %></td>
-      </tr>
-    <% end
-
-       @projects.each do |project|
-    %>
-      <tr class="<%= cycle('even', 'odd') -%>">
-        <td valign="top"><b><%= h project.name %></b><br/>
-          <span class="small gray"><%= h project.key -%></span>
-        </td>
-        <% ['user', 'admin', 'issueadmin', 'codeviewer'].each do |permission| -%>
-          <td valign="top">
-            <%
-               users=Api::Utils.insensitive_sort(project.user_roles.select { |ur| ur.role==permission }.map { |ur| ur.user.name })
-               groups=Api::Utils.insensitive_sort(project.group_roles.select { |gr| gr.role==permission }.map { |gr| group_name(gr.group) })
-            %>
-            <span id="u-<%= permission -%>-<%= u project.kee -%>"><%= users.join(', ') %></span>
-            (<%= link_to_edit_roles_permission_form(message('select users'), permission, project.id, "select-u-#{permission}-#{u project.kee}") %>)<br/>
-            <span id="g-<%= permission -%>-<%= u project.kee -%>"><%= groups.join(', ') %></span>
-            (<%= link_to_edit_groups_permission_form(message('select groups'), permission, project.id, "select-g-#{permission}-#{u project.kee}") %>)<br/>
-          </td>
-        <% end %>
-        <td align="right">
-          <%= link_to message('projects_role.apply_template'), {:action => :apply_template_form, :components => [project.key], :names => project.name,
-                                                                :results_count => 1, :qualifiers => @components_qualifiers},
-                      :id => "apply-template-#{u project.kee}", :class => 'open-modal link-action' %>
-        </td>
-      </tr>
-    <%
-       end %>
-    </tbody>
-  </table>
-</div>
-
-
-<script>
-  function submitSearch () {
-    $j("#project-search-form").submit();
-  }
-
-  $j('#search-text').focus();
-</script>
diff --git a/server/sonar-web/src/test/json/permissions/project-permissions.json b/server/sonar-web/src/test/json/permissions/project-permissions.json
new file mode 100644 (file)
index 0000000..657f050
--- /dev/null
@@ -0,0 +1,55 @@
+{
+  "projects": [
+    {
+      "uuid": "10c394cc-c37c-4cf0-97b9-360165c47270",
+      "key": "my-project",
+      "name": "My Project",
+      "permissions": [
+        {
+          "key": "admin",
+          "usersCount": 1,
+          "groupsCount": 2
+        },
+        {
+          "key": "codeviewer",
+          "usersCount": 3,
+          "groupsCount": 4
+        }
+      ]
+    },
+    {
+      "uuid": "5d2408c9-b5c6-4426-8ee8-05be930a5f62",
+      "key": "another-project",
+      "name": "Another Project",
+      "permissions": [
+        {
+          "key": "admin",
+          "usersCount": 5,
+          "groupsCount": 6
+        },
+        {
+          "key": "codeviewer",
+          "usersCount": 7,
+          "groupsCount": 8
+        }
+      ]
+    }
+  ],
+  "permissions": [
+    {
+      "key": "admin",
+      "name": "Administer",
+      "description": "Ability to access project settings and perform administration tasks. (Users will also need \"Browse\" permission)"
+    },
+    {
+      "key": "codeviewer",
+      "name": "See Source Code",
+      "description": "Ability to view the project's source code. (Users will also need \"Browse\" permission)"
+    }
+  ],
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 25,
+    "total": 2
+  }
+}
index ee712c33f1845fa4689b093f1aadb5ab008cc1f6..520d2127a40746dcea872bba4bcb5e4ef96877f2 100644 (file)
@@ -31,7 +31,8 @@ define(['intern'], function (intern) {
       'test/medium/custom-measures.spec',
       'test/medium/quality-profiles.spec',
       'test/medium/source-viewer.spec',
-      'test/medium/global-permissions.spec'
+      'test/medium/global-permissions.spec',
+      'test/medium/project-permissions.spec'
     ],
 
     tunnel: tunnel,
diff --git a/server/sonar-web/test/medium/project-permissions.spec.js b/server/sonar-web/test/medium/project-permissions.spec.js
new file mode 100644 (file)
index 0000000..1e32733
--- /dev/null
@@ -0,0 +1,22 @@
+define(function (require) {
+  var bdd = require('intern!bdd');
+  require('../helpers/test-page');
+
+  bdd.describe('Project Permissions', function () {
+    bdd.it('should show permissions', function () {
+      return this.remote
+          .open()
+          .mockFromFile('/api/permissions/search_project_permissions', 'permissions/project-permissions.json')
+          .startApp('project-permissions')
+          .checkElementExist('#project-permissions-header')
+          .checkElementExist('#projects')
+          .checkElementCount('#projects > thead > tr > th', 3)
+          .checkElementCount('#projects > tbody > tr', 2)
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(1)', 'My Project')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(2)', '1')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(2)', '2')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(3)', '3')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(3)', '4');
+    });
+  });
+});