aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-08-20 15:39:03 +0200
committerStas Vilchik <vilchiks@gmail.com>2015-08-24 09:22:49 +0200
commit7a65a44016c32c5656e7aa5f0e174e670b6849b0 (patch)
treeea8840f4bc19efd99d78a917e797bc1c2902e98e /server/sonar-web
parentc007b1899708a5cb4dce68087ed33a0c36551877 (diff)
downloadsonarqube-7a65a44016c32c5656e7aa5f0e174e670b6849b0.tar.gz
sonarqube-7a65a44016c32c5656e7aa5f0e174e670b6849b0.zip
SONAR-6797 rewrite project permissions page
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/Gruntfile.coffee4
-rw-r--r--server/sonar-web/src/main/js/apps/nav/settings/settings-nav.jsx1
-rw-r--r--server/sonar-web/src/main/js/apps/project-permissions/app.jsx13
-rw-r--r--server/sonar-web/src/main/js/apps/project-permissions/groups-view.js43
-rw-r--r--server/sonar-web/src/main/js/apps/project-permissions/main.jsx80
-rw-r--r--server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.jsx20
-rw-r--r--server/sonar-web/src/main/js/apps/project-permissions/permissions-header.jsx26
-rw-r--r--server/sonar-web/src/main/js/apps/project-permissions/permissions.jsx23
-rw-r--r--server/sonar-web/src/main/js/apps/project-permissions/project.jsx66
-rw-r--r--server/sonar-web/src/main/js/apps/project-permissions/search.jsx34
-rw-r--r--server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-groups.hbs10
-rw-r--r--server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-users.hbs10
-rw-r--r--server/sonar-web/src/main/js/apps/project-permissions/users-view.js43
-rw-r--r--server/sonar-web/src/main/js/helpers/Url.jsx6
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/roles_controller.rb17
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/permission_templates/index.html.erb7
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/roles/projects.html.erb130
-rw-r--r--server/sonar-web/src/test/json/permissions/project-permissions.json55
-rw-r--r--server/sonar-web/test/intern.js3
-rw-r--r--server/sonar-web/test/medium/project-permissions.spec.js22
20 files changed, 465 insertions, 148 deletions
diff --git a/server/sonar-web/Gruntfile.coffee b/server/sonar-web/Gruntfile.coffee
index 050967793ab..240adc7e642 100644
--- a/server/sonar-web/Gruntfile.coffee
+++ b/server/sonar-web/Gruntfile.coffee
@@ -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:
diff --git a/server/sonar-web/src/main/js/apps/nav/settings/settings-nav.jsx b/server/sonar-web/src/main/js/apps/nav/settings/settings-nav.jsx
index 20f1ce5a08d..760034e7384 100644
--- a/server/sonar-web/src/main/js/apps/nav/settings/settings-nav.jsx
+++ b/server/sonar-web/src/main/js/apps/nav/settings/settings-nav.jsx
@@ -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
index 00000000000..28dc73b7f42
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-permissions/app.jsx
@@ -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
index 00000000000..ba98ca7e525
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-permissions/groups-view.js
@@ -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
index 00000000000..96b77f7cb72
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-permissions/main.jsx
@@ -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
index 00000000000..f4913a753c2
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.jsx
@@ -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
index 00000000000..a68cca21fc2
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-permissions/permissions-header.jsx
@@ -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
index 00000000000..4638c209651
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-permissions/permissions.jsx
@@ -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
index 00000000000..31c03a1572f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-permissions/project.jsx
@@ -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
index 00000000000..3506d78834b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-permissions/search.jsx
@@ -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
index 00000000000..c5f551e3682
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-groups.hbs
@@ -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
index 00000000000..acfd4eaf75d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-users.hbs
@@ -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
index 00000000000..6a715835ba7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-permissions/users-view.js
@@ -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
index 00000000000..e88ddb55808
--- /dev/null
+++ b/server/sonar-web/src/main/js/helpers/Url.jsx
@@ -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)}`;
+}
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/roles_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/roles_controller.rb
index 01abb33a423..5cdcf358245 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/roles_controller.rb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/roles_controller.rb
@@ -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>]
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/permission_templates/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/permission_templates/index.html.erb
index 8f7b4840298..965847fcb40 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/permission_templates/index.html.erb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/permission_templates/index.html.erb
@@ -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>
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/roles/projects.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/roles/projects.html.erb
index b383932b5a5..db45ae6982e 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/roles/projects.html.erb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/roles/projects.html.erb
@@ -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
index 00000000000..657f05042ef
--- /dev/null
+++ b/server/sonar-web/src/test/json/permissions/project-permissions.json
@@ -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
+ }
+}
diff --git a/server/sonar-web/test/intern.js b/server/sonar-web/test/intern.js
index ee712c33f18..520d2127a40 100644
--- a/server/sonar-web/test/intern.js
+++ b/server/sonar-web/test/intern.js
@@ -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
index 00000000000..1e32733fbf3
--- /dev/null
+++ b/server/sonar-web/test/medium/project-permissions.spec.js
@@ -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');
+ });
+ });
+});