From 7a65a44016c32c5656e7aa5f0e174e670b6849b0 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Thu, 20 Aug 2015 15:39:03 +0200 Subject: SONAR-6797 rewrite project permissions page --- server/sonar-web/Gruntfile.coffee | 4 + .../src/main/js/apps/nav/settings/settings-nav.jsx | 1 + .../src/main/js/apps/project-permissions/app.jsx | 13 +++ .../js/apps/project-permissions/groups-view.js | 43 +++++++ .../src/main/js/apps/project-permissions/main.jsx | 80 +++++++++++++ .../project-permissions/permissions-footer.jsx | 20 ++++ .../project-permissions/permissions-header.jsx | 26 +++++ .../js/apps/project-permissions/permissions.jsx | 23 ++++ .../main/js/apps/project-permissions/project.jsx | 66 +++++++++++ .../main/js/apps/project-permissions/search.jsx | 34 ++++++ .../templates/project-permissions-groups.hbs | 10 ++ .../templates/project-permissions-users.hbs | 10 ++ .../main/js/apps/project-permissions/users-view.js | 43 +++++++ server/sonar-web/src/main/js/helpers/Url.jsx | 6 + .../WEB-INF/app/controllers/roles_controller.rb | 17 --- .../app/views/permission_templates/index.html.erb | 7 +- .../WEB-INF/app/views/roles/projects.html.erb | 130 +-------------------- .../test/json/permissions/project-permissions.json | 55 +++++++++ server/sonar-web/test/intern.js | 3 +- .../test/medium/project-permissions.spec.js | 22 ++++ 20 files changed, 465 insertions(+), 148 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/project-permissions/app.jsx create mode 100644 server/sonar-web/src/main/js/apps/project-permissions/groups-view.js create mode 100644 server/sonar-web/src/main/js/apps/project-permissions/main.jsx create mode 100644 server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.jsx create mode 100644 server/sonar-web/src/main/js/apps/project-permissions/permissions-header.jsx create mode 100644 server/sonar-web/src/main/js/apps/project-permissions/permissions.jsx create mode 100644 server/sonar-web/src/main/js/apps/project-permissions/project.jsx create mode 100644 server/sonar-web/src/main/js/apps/project-permissions/search.jsx create mode 100644 server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-groups.hbs create mode 100644 server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-users.hbs create mode 100644 server/sonar-web/src/main/js/apps/project-permissions/users-view.js create mode 100644 server/sonar-web/src/main/js/helpers/Url.jsx create mode 100644 server/sonar-web/src/test/json/permissions/project-permissions.json create mode 100644 server/sonar-web/test/medium/project-permissions.spec.js (limited to 'server/sonar-web') 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'))} 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(
, 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 ( +
+
+

{window.t('roles.page')}

+

{window.t('roles.page.description2')}

+
+ + + + + + +
+ ); + } +}); 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 = show more; + return ( +
+ {this.props.count}/{this.props.total} shown + {hasMore ? loadMoreLink : null} +
+ ); + } +}); 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 ( + + {p.name}
{p.description} + + ); + }); + return ( + + +   + {cells} + + + ); + } +}); 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 + }); + return ( + + + {projects} +
+ ); + } +}); 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 ( + + + + + + + + + + + + +
Users{p.usersCount} + +
Groups{p.groupsCount} + +
+ + ); + }); + return ( + + + + {this.props.project.name} + + + {permissions} + + ); + } +}); 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 ( +
+
+ + +
+
+ ); + } +}); 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 @@ + + + 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 @@ + + + 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 + '
' + item.login + ''; + }, + 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=] 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 @@
- - <%= render :partial => 'roles/tabs', :locals => {:selected_tab => 'Permission templates'} %> -
- 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 %> - +<% content_for :extra_script do %> + <% end %> - -
- - - <%= render :partial => 'roles/tabs', :locals => {:selected_tab => 'Projects'} %> - -
- <% form_tag({:action => 'projects'}, {:id => 'project-search-form', :method => 'get'}) do %> -
- - - - -
- <% end %> -
- - - -
- - - - - - - - - - - - <%= 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})) - } - %> - - - <% if @projects.empty? %> - - - - <% end - - @projects.each do |project| - %> - - - <% ['user', 'admin', 'issueadmin', 'codeviewer'].each do |permission| -%> - - <% end %> - - - <% - end %> - -
  - <%= message('projects_role.user') -%>
- <%= message('projects_role.user.desc') -%> -
- <%= message('projects_role.admin') -%>
- <%= message('projects_role.admin.desc') -%> -
- <%= message('projects_role.issueadmin') -%>
- <%= message('projects_role.issueadmin.desc') -%> -
- <%= message('projects_role.codeviewer') -%>
- <%= message('projects_role.codeviewer.desc') -%> -
 
<%= message('no_results') %>
<%= h project.name %>
- <%= h project.key -%> -
- <% - 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) }) - %> - <%= users.join(', ') %> - (<%= link_to_edit_roles_permission_form(message('select users'), permission, project.id, "select-u-#{permission}-#{u project.kee}") %>)
- <%= groups.join(', ') %> - (<%= link_to_edit_groups_permission_form(message('select groups'), permission, project.id, "select-g-#{permission}-#{u project.kee}") %>)
-
- <%= 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' %> -
-
- - - 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'); + }); + }); +}); -- cgit v1.2.3