]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6802 SONAR-6803 add an ability to apply a permission template to projects
authorStas Vilchik <vilchiks@gmail.com>
Mon, 14 Sep 2015 12:53:09 +0000 (14:53 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Mon, 14 Sep 2015 13:00:24 +0000 (15:00 +0200)
server/sonar-web/src/main/js/api/permissions.jsx
server/sonar-web/src/main/js/apps/project-permissions/app.jsx
server/sonar-web/src/main/js/apps/project-permissions/apply-template-view.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-permissions/main.jsx
server/sonar-web/src/main/js/apps/project-permissions/permissions-header.jsx
server/sonar-web/src/main/js/apps/project-permissions/permissions.jsx
server/sonar-web/src/main/js/apps/project-permissions/project.jsx
server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-apply-template.hbs [new file with mode: 0644]
server/sonar-web/src/test/json/permissions/permission-templates.json [new file with mode: 0644]
server/sonar-web/src/test/json/permissions/project-permissions-changed.json [new file with mode: 0644]
server/sonar-web/test/medium/project-permissions.spec.js

index fb565c22c32bf9f075a7787bbd324bb9c1807cc9..3f4fc520ba79429604d781748741cc22e1fe4b57 100644 (file)
@@ -90,3 +90,9 @@ export function revokeFromGroup(permission, group, project) {
   }
   return _request({ type: 'POST', url: url, data: data });
 }
+
+
+export function applyTemplateToProject(options) {
+  let url = _url('/api/permissions/apply_template');
+  return _request(_.extend({ type: 'POST', url: url }, options));
+}
index 28dc73b7f428ac2d9b3e9572b23c8bad83fdae05..85dc51eaf02cacdbf98c8ef5785f83b5c68cc9ea 100644 (file)
@@ -1,13 +1,23 @@
+import $ from 'jquery';
 import React from 'react';
 import Main from './main';
 
-const $ = jQuery;
+let permissionTemplates = [];
 
 export default {
   start(options) {
-    window.requestMessages().done(() => {
-      var el = document.querySelector(options.el);
-      React.render(<Main/>, el);
+    $.when(
+        window.requestMessages(),
+        this.requestPermissionTemplates()
+    ).then(() => {
+          var el = document.querySelector(options.el);
+          React.render(<Main permissionTemplates={permissionTemplates}/>, el);
+        });
+  },
+
+  requestPermissionTemplates() {
+    return $.get(baseUrl + '/api/permissions/search_templates').done(r => {
+      permissionTemplates = r.permissionTemplates;
     });
   }
 };
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/apply-template-view.jsx b/server/sonar-web/src/main/js/apps/project-permissions/apply-template-view.jsx
new file mode 100644 (file)
index 0000000..9bcd05d
--- /dev/null
@@ -0,0 +1,50 @@
+import $ from 'jquery';
+import ModalForm from '../../components/common/modal-form';
+import {applyTemplateToProject} from '../../api/permissions';
+import './templates';
+
+export default ModalForm.extend({
+  template: Templates['project-permissions-apply-template'],
+
+  onRender: function () {
+    ModalForm.prototype.onRender.apply(this, arguments);
+    this.$('#project-permissions-template').select2({
+      width: '250px',
+      minimumResultsForSearch: 20
+    });
+  },
+
+  onFormSubmit: function () {
+    ModalForm.prototype.onFormSubmit.apply(this, arguments);
+    var that = this;
+    this.disableForm();
+
+    var projects = this.options.project ? [this.options.project] : this.options.projects,
+        permissionTemplate = this.$('#project-permissions-template').val(),
+        looper = $.Deferred().resolve();
+
+    projects.forEach(function (project) {
+      looper = looper.then(function () {
+        return applyTemplateToProject({
+          data: { projectId: project.id, templateId: permissionTemplate }
+        });
+      });
+    });
+
+    looper.done(function () {
+      that.options.refresh();
+      that.destroy();
+    }).fail(function (jqXHR) {
+      that.enableForm();
+      that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
+    });
+  },
+
+  serializeData: function () {
+    return _.extend(ModalForm.prototype.serializeData.apply(this, arguments), {
+      permissionTemplates: this.options.permissionTemplates,
+      project: this.options.project,
+      projectsCount: _.size(this.options.projects)
+    });
+  }
+});
index 7862f60f0b4dd8debb8bf7eaab2e9e823c37cbd3..479b50fde4657727a36f90167cc59c9693b61ff6 100644 (file)
@@ -1,14 +1,18 @@
+import $ from 'jquery';
 import _ from 'underscore';
 import React from 'react';
 import Permissions from './permissions';
 import PermissionsFooter from './permissions-footer';
 import Search from './search';
-
-let $ = jQuery;
+import ApplyTemplateView from './apply-template-view';
 
 const PERMISSIONS_ORDER = ['user', 'codeviewer', 'issueadmin', 'admin'];
 
 export default React.createClass({
+  propTypes: {
+    permissionTemplates: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
+  },
+
   getInitialState() {
     return { permissions: [], projects: [], total: 0 };
   },
@@ -59,11 +63,23 @@ export default React.createClass({
     this.requestPermissions(1, query);
   },
 
+  bulkApplyTemplate(e) {
+    e.preventDefault();
+    new ApplyTemplateView({
+      projects: this.state.projects,
+      permissionTemplates: this.props.permissionTemplates,
+      refresh: this.requestPermissions
+    }).render();
+  },
+
   render() {
     return (
         <div className="page">
           <header id="project-permissions-header" className="page-header">
             <h1 className="page-title">{window.t('roles.page')}</h1>
+            <div className="page-actions">
+              <button onClick={this.bulkApplyTemplate} className="js-bulk-apply-template">Bulk Apply Template</button>
+            </div>
             <p className="page-description">{window.t('roles.page.description2')}</p>
           </header>
 
@@ -73,6 +89,7 @@ export default React.createClass({
           <Permissions
               projects={this.state.projects}
               permissions={this.state.permissions}
+              permissionTemplates={this.props.permissionTemplates}
               refresh={this.requestPermissions}/>
 
           <PermissionsFooter
index a68cca21fc27c7a2ea7d99587c47196678af2b7b..7768f9ec4140132238cb9d67ee803f0b0c670e84 100644 (file)
@@ -19,6 +19,7 @@ export default React.createClass({
         <tr>
           <th style={{ width: '20%' }}>&nbsp;</th>
           {cells}
+          <th className="thin">&nbsp;</th>
         </tr>
         </thead>
     );
index 4ae679f345cce6b8b88113092d7f8682f0166157..26da7da40d67043392465bee6a8c151caf745a3a 100644 (file)
@@ -6,12 +6,17 @@ 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() {
     let projects = this.props.projects.map(p => {
-      return <Project key={p.id} project={p} refresh={this.props.refresh}/>
+      return <Project
+          key={p.id}
+          project={p}
+          permissionTemplates={this.props.permissionTemplates}
+          refresh={this.props.refresh}/>;
     });
     return (
         <table id="projects" className="data zebra">
index b0da14dcb182df5377554011c390586258537009..b8f2d66ad31620245893eee3020d5c6df61fd359 100644 (file)
@@ -1,11 +1,13 @@
 import React from 'react';
 import UsersView from './users-view';
 import GroupsView from './groups-view';
+import ApplyTemplateView from './apply-template-view';
 import {getProjectUrl} from '../../helpers/Url';
 
 export default React.createClass({
   propTypes: {
     project: React.PropTypes.object.isRequired,
+    permissionTemplates: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
     refresh: React.PropTypes.func.isRequired
   },
 
@@ -29,6 +31,15 @@ export default React.createClass({
     }).render();
   },
 
+  applyTemplate(e) {
+    e.preventDefault();
+    new ApplyTemplateView({
+      permissionTemplates: this.props.permissionTemplates,
+      project: this.props.project,
+      refresh: this.props.refresh
+    }).render();
+  },
+
   render() {
     let permissions = this.props.project.permissions.map(p => {
       return (
@@ -62,6 +73,9 @@ export default React.createClass({
             </strong>
           </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/templates/project-permissions-apply-template.hbs b/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-apply-template.hbs
new file mode 100644 (file)
index 0000000..25f4943
--- /dev/null
@@ -0,0 +1,25 @@
+<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/test/json/permissions/permission-templates.json b/server/sonar-web/src/test/json/permissions/permission-templates.json
new file mode 100644 (file)
index 0000000..b34f812
--- /dev/null
@@ -0,0 +1,89 @@
+{
+  "permissionTemplates": [
+    {
+      "id": "default_template",
+      "name": "Default template",
+      "description": "This permission template will be used as default when no other permission configuration is available",
+      "projectKeyPattern": "abc.*",
+      "createdAt": "2015-08-07T15:30:59+0200",
+      "updatedAt": "2015-09-14T12:56:21+0200",
+      "permissions": [
+        {
+          "key": "admin",
+          "usersCount": 0,
+          "groupsCount": 1
+        },
+        {
+          "key": "codeviewer",
+          "usersCount": 0,
+          "groupsCount": 1
+        },
+        {
+          "key": "issueadmin",
+          "usersCount": 0,
+          "groupsCount": 1
+        },
+        {
+          "key": "user",
+          "usersCount": 0,
+          "groupsCount": 1
+        }
+      ]
+    },
+    {
+      "id": "test_20150820_133222",
+      "name": "test",
+      "description": "",
+      "projectKeyPattern": "Javascript*",
+      "createdAt": "2015-08-20T13:32:22+0200",
+      "updatedAt": "2015-09-14T12:07:26+0200",
+      "permissions": [
+        {
+          "key": "user",
+          "usersCount": 1,
+          "groupsCount": 0
+        }
+      ]
+    },
+    {
+      "id": "AU_Lp7-59jciAH96Rd8H",
+      "name": "test2",
+      "description": "Default permission template",
+      "projectKeyPattern": "",
+      "createdAt": "2015-09-14T13:40:12+0200",
+      "updatedAt": "2015-09-14T13:41:23+0200"
+    }
+  ],
+  "defaultTemplates": [
+    {
+      "templateId": "test_20150820_133222",
+      "qualifier": "VW"
+    },
+    {
+      "templateId": "AU_Lp7-59jciAH96Rd8H",
+      "qualifier": "TRK"
+    }
+  ],
+  "permissions": [
+    {
+      "key": "user",
+      "name": "Browse",
+      "description": "Access a project, browse its measures, and create/edit issues for it."
+    },
+    {
+      "key": "admin",
+      "name": "Administer",
+      "description": "Access project settings and perform administration tasks. (Users will also need \"Browse\" permission)"
+    },
+    {
+      "key": "issueadmin",
+      "name": "Administer Issues",
+      "description": "Perform advanced editing on issues: marking an issue False Positive / Won't Fix, and changing an Issue's severity. (Users will also need \"Browse\" permission)"
+    },
+    {
+      "key": "codeviewer",
+      "name": "See Source Code",
+      "description": "View the project's source code. (Users will also need \"Browse\" permission)"
+    }
+  ]
+}
diff --git a/server/sonar-web/src/test/json/permissions/project-permissions-changed.json b/server/sonar-web/src/test/json/permissions/project-permissions-changed.json
new file mode 100644 (file)
index 0000000..39ad975
--- /dev/null
@@ -0,0 +1,55 @@
+{
+  "projects": [
+    {
+      "id": "10c394cc-c37c-4cf0-97b9-360165c47270",
+      "key": "my-project",
+      "name": "My Project",
+      "permissions": [
+        {
+          "key": "admin",
+          "usersCount": 11,
+          "groupsCount": 12
+        },
+        {
+          "key": "codeviewer",
+          "usersCount": 13,
+          "groupsCount": 14
+        }
+      ]
+    },
+    {
+      "id": "5d2408c9-b5c6-4426-8ee8-05be930a5f62",
+      "key": "another-project",
+      "name": "Another Project",
+      "permissions": [
+        {
+          "key": "admin",
+          "usersCount": 15,
+          "groupsCount": 16
+        },
+        {
+          "key": "codeviewer",
+          "usersCount": 17,
+          "groupsCount": 18
+        }
+      ]
+    }
+  ],
+  "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 f5f24fcec571e75d0299c35b227fc38472e706bf..f26b9d1d53b058f0faad154c30309acd807ac69f 100644 (file)
@@ -7,10 +7,11 @@ define(function (require) {
       return this.remote
           .open()
           .mockFromFile('/api/permissions/search_project_permissions', 'permissions/project-permissions.json')
+          .mockFromFile('/api/permissions/search_templates', 'permissions/permission-templates.json')
           .startApp('project-permissions')
           .checkElementExist('#project-permissions-header')
           .checkElementExist('#projects')
-          .checkElementCount('#projects > thead > tr > th', 3)
+          .checkElementCount('#projects > thead > tr > th', 4)
           .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)', '3')
@@ -18,5 +19,49 @@ define(function (require) {
           .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(3)', '1')
           .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(3)', '2');
     });
+
+    bdd.it('should apply a permission template', function () {
+      return this.remote
+          .open()
+          .mockFromFile('/api/permissions/search_project_permissions', 'permissions/project-permissions.json')
+          .mockFromFile('/api/permissions/search_templates', 'permissions/permission-templates.json')
+          .startApp('project-permissions')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(1)', 'My Project')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(2)', '3')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(2)', '4')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(3)', '1')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(3)', '2')
+          .clearMocks()
+          .mockFromFile('/api/permissions/search_project_permissions', 'permissions/project-permissions-changed.json')
+          .mockFromString('/api/permissions/apply_template', '{}')
+          .clickElement('#projects > tbody > tr:first-child .js-apply-template')
+          .clickElement('#project-permissions-apply-template')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(2)', '13')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(2)', '14')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(3)', '11')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(3)', '12');
+    });
+
+    bdd.it('should bulk apply a permission template', function () {
+      return this.remote
+          .open()
+          .mockFromFile('/api/permissions/search_project_permissions', 'permissions/project-permissions.json')
+          .mockFromFile('/api/permissions/search_templates', 'permissions/permission-templates.json')
+          .startApp('project-permissions')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(1)', 'My Project')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(2)', '3')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(2)', '4')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(3)', '1')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(3)', '2')
+          .clearMocks()
+          .mockFromFile('/api/permissions/search_project_permissions', 'permissions/project-permissions-changed.json')
+          .mockFromString('/api/permissions/apply_template', '{}')
+          .clickElement('.js-bulk-apply-template')
+          .clickElement('#project-permissions-apply-template')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(2)', '13')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(2)', '14')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(3)', '11')
+          .checkElementInclude('#projects > tbody > tr:first-child td:nth-child(3)', '12');
+    });
   });
 });