]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7880 Put actions on permissions on the Projects Management page
authorStas Vilchik <vilchiks@gmail.com>
Mon, 11 Jul 2016 15:06:07 +0000 (17:06 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Wed, 13 Jul 2016 16:12:50 +0000 (18:12 +0200)
server/sonar-web/src/main/js/api/permissions.js
server/sonar-web/src/main/js/apps/permissions/project/templates/ApplyTemplateTemplate.hbs
server/sonar-web/src/main/js/apps/permissions/project/views/ApplyTemplateView.js
server/sonar-web/src/main/js/apps/projects/header.js
server/sonar-web/src/main/js/apps/projects/main.js
server/sonar-web/src/main/js/apps/projects/projects.js
server/sonar-web/src/main/js/apps/projects/templates/BulkApplyTemplateTemplate.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/views/BulkApplyTemplateView.js [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/urls.js
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 80a77e565392f73f8d79f744bc97974c95bcc744..e7ea01ed4b7cf3e2d9a7288b6360b1933a0f52ee 100644 (file)
@@ -114,9 +114,9 @@ export function applyTemplateToProject (data) {
   return post(url, data);
 }
 
-export function bulkApplyTemplateToProject (options) {
-  const url = window.baseUrl + '/api/permissions/bulk_apply_template';
-  return request(_.extend({ type: 'POST', url }, options));
+export function bulkApplyTemplate (data) {
+  const url = '/api/permissions/bulk_apply_template';
+  return post(url, data);
 }
 
 export function addProjectCreatorToTemplate (templateName, permission) {
index c7307670cbe4ecc7845f347c0761179d6f5411a9..82c157e5d535d6f846838dbe0667f81fc9fbf509 100644 (file)
@@ -5,26 +5,37 @@
 
   <div class="modal-body">
     <div class="js-modal-messages"></div>
-    {{#notNull permissionTemplates}}
-      <div class="modal-field">
-        <label for="project-permissions-template">
-          Template<em class="mandatory">*</em>
-        </label>
-        <select id="project-permissions-template">
-          {{#each permissionTemplates}}
-            <option value="{{id}}">{{name}}</option>
-          {{/each}}
-        </select>
+
+    {{#if done}}
+      <div class="alert alert-success">
+        {{t 'projects_role.apply_template.success'}}
       </div>
-    {{else}}
-      <i class="spinner"></i>
-    {{/notNull}}
+    {{/if}}
+
+    {{#unless done}}
+      {{#notNull permissionTemplates}}
+        <div class="modal-field">
+          <label for="project-permissions-template">
+            Template<em class="mandatory">*</em>
+          </label>
+          <select id="project-permissions-template">
+            {{#each permissionTemplates}}
+              <option value="{{id}}">{{name}}</option>
+            {{/each}}
+          </select>
+        </div>
+      {{else}}
+        <i class="spinner"></i>
+      {{/notNull}}
+    {{/unless}}
   </div>
 
   <div class="modal-foot">
-    {{#notNull permissionTemplates}}
-      <button id="project-permissions-apply-template">Apply</button>
-    {{/notNull}}
-    <a href="#" class="js-modal-close">Cancel</a>
+    {{#unless done}}
+      {{#notNull permissionTemplates}}
+        <button id="project-permissions-apply-template">Apply</button>
+      {{/notNull}}
+    {{/unless}}
+    <a href="#" class="js-modal-close">Close</a>
   </div>
 </form>
index 6aa2537c3f64b03e895100e41d4fa9af6cd62f56..14817206db76650ca4be6df186d7fed7829fd757 100644 (file)
@@ -29,6 +29,7 @@ export default ModalForm.extend({
 
   initialize () {
     this.loadPermissionTemplates();
+    this.done = false;
   },
 
   loadPermissionTemplates () {
@@ -56,7 +57,8 @@ export default ModalForm.extend({
       templateId: permissionTemplate
     }).then(() => {
       this.trigger('done');
-      this.destroy();
+      this.done = true;
+      this.render();
     }).catch(function (e) {
       e.response.json().then(r => {
         this.showErrors(r.errors, r.warnings);
@@ -68,7 +70,8 @@ export default ModalForm.extend({
   serializeData () {
     return {
       permissionTemplates: this.permissionTemplates,
-      project: this.options.project
+      project: this.options.project,
+      done: this.done
     };
   }
 });
index eb11aceb0c2466d9ae50fd25589a3b9c460dd8b9..1d4200e3aa59b540fea9bf115d7f4cff83e0a43e 100644 (file)
  */
 import React from 'react';
 import CreateView from './create-view';
+import BulkApplyTemplateView from './views/BulkApplyTemplateView';
 
-export default React.createClass({
-  propTypes: {
+export default class Header extends React.Component {
+  static propTypes = {
     hasProvisionPermission: React.PropTypes.bool.isRequired
-  },
+  };
 
-  createProject() {
+  createProject () {
     new CreateView({
       refresh: this.props.refresh
     }).render();
-  },
+  }
+
+  bulkApplyTemplate () {
+    new BulkApplyTemplateView({
+      total: this.props.total,
+      selection: this.props.selection,
+      query: this.props.query,
+      qualifier: this.props.qualifier
+    }).render();
+  }
 
-  renderCreateButton() {
+  renderCreateButton () {
     if (!this.props.hasProvisionPermission) {
       return null;
     }
-    return <button onClick={this.createProject}>Create Project</button>;
-  },
+    return (
+        <li>
+          <button onClick={this.createProject.bind(this)}>
+            Create Project
+          </button>
+        </li>
+    );
+  }
+
+  renderBulkApplyTemplateButton () {
+    return (
+        <li>
+          <button onClick={this.bulkApplyTemplate.bind(this)}>
+            Bulk Apply Permission Template
+          </button>
+        </li>
+    );
+  }
 
-  render() {
+  render () {
     return (
         <header className="page-header">
           <h1 className="page-title">Projects Management</h1>
-          <div className="page-actions">{this.renderCreateButton()}</div>
+          <div className="page-actions">
+            <ul className="list-inline">
+              {this.renderCreateButton()}
+              {this.renderBulkApplyTemplateButton()}
+            </ul>
+          </div>
           <p className="page-description">Use this page to delete multiple projects at once, or to provision projects
             if you would like to configure them before the first analysis. Note that once a project is provisioned, you
             have access to perform all project configurations on it.</p>
         </header>
     );
   }
-});
+}
index adfc332c8d1749bae32b74ee7adf8495f58f1993..900cfba250253917da09accd44982ab114339ad6 100644 (file)
@@ -23,7 +23,12 @@ import Header from './header';
 import Search from './search';
 import Projects from './projects';
 import { PAGE_SIZE, TYPE } from './constants';
-import { getComponents, getProvisioned, getGhosts, deleteComponents } from '../../api/components';
+import {
+    getComponents,
+    getProvisioned,
+    getGhosts,
+    deleteComponents
+} from '../../api/components';
 import ListFooter from '../../components/controls/ListFooter';
 
 export default React.createClass({
@@ -77,7 +82,7 @@ export default React.createClass({
         break;
       default:
 
-      // should never happen
+        // should never happen
     }
   },
 
@@ -120,7 +125,8 @@ export default React.createClass({
   },
 
   loadMore() {
-    this.setState({ ready: false, page: this.state.page + 1 }, this.requestProjects);
+    this.setState({ ready: false, page: this.state.page + 1 },
+        this.requestProjects);
   },
 
   onSearch(query) {
@@ -187,6 +193,10 @@ export default React.createClass({
         <div className="page">
           <Header
               hasProvisionPermission={this.props.hasProvisionPermission}
+              selection={this.state.selection}
+              total={this.state.total}
+              query={this.state.query}
+              qualifier={this.state.qualifiers}
               refresh={this.requestProjects}/>
 
           <Search {...this.props} {...this.state}
index a6c0949f6424728863a046663b5fbd0b8a49b6be..140471de31727a04785174d150cfa81952673378 100644 (file)
  */
 import classNames from 'classnames';
 import React from 'react';
-import { getComponentUrl } from '../../helpers/urls';
+import {
+    getComponentUrl,
+    getComponentPermissionsUrl
+} from '../../helpers/urls';
+import ApplyTemplateView from '../permissions/project/views/ApplyTemplateView';
 import Checkbox from '../../components/controls/Checkbox';
 import QualifierIcon from '../../components/shared/qualifier-icon';
+import { translate } from '../../helpers/l10n';
 
-export default React.createClass({
-  propTypes: {
+export default class Projects extends React.Component {
+  static propTypes = {
     projects: React.PropTypes.array.isRequired,
     selection: React.PropTypes.array.isRequired,
     refresh: React.PropTypes.func.isRequired
-  },
+  };
 
-  onProjectCheck(project, checked) {
+  componentWillMount () {
+    this.renderProject = this.renderProject.bind(this);
+  }
+
+  onProjectCheck (project, checked) {
     if (checked) {
       this.props.onProjectSelected(project);
     } else {
       this.props.onProjectDeselected(project);
     }
-  },
+  }
 
-  isProjectSelected(project) {
+  onApplyTemplateClick (project, e) {
+    e.preventDefault();
+    e.target.blur();
+    new ApplyTemplateView({ project }).render();
+  }
+
+  isProjectSelected (project) {
     return this.props.selection.indexOf(project.id) !== -1;
-  },
+  }
+
+  renderProject (project) {
+    const permissionsUrl = getComponentPermissionsUrl(project.key);
 
-  renderProject(project) {
     return (
         <tr key={project.id}>
           <td className="thin">
@@ -50,25 +67,51 @@ export default React.createClass({
                 checked={this.isProjectSelected(project)}
                 onCheck={this.onProjectCheck.bind(this, project)}/>
           </td>
-          <td className="thin">
-            <QualifierIcon qualifier={project.qualifier}/>
-          </td>
           <td className="nowrap">
-            <a href={getComponentUrl(project.key)}>{project.name}</a>
+            <a className="link-with-icon" href={getComponentUrl(project.key)}>
+              <QualifierIcon qualifier={project.qualifier}/>
+              {' '}
+              <span>{project.name}</span>
+            </a>
           </td>
           <td className="nowrap">
             <span className="note">{project.key}</span>
           </td>
+          <td className="thin nowrap">
+            <div className="dropdown">
+              <button className="dropdown-toggle" data-toggle="dropdown">
+                {translate('actions')}
+                {' '}
+                <i className="icon-dropdown"/>
+              </button>
+              <ul className="dropdown-menu dropdown-menu-right">
+                <li>
+                  <a href={permissionsUrl}>
+                    {translate('edit_permissions')}
+                  </a>
+                </li>
+                <li>
+                  <a href={permissionsUrl}
+                     onClick={this.onApplyTemplateClick.bind(this, project)}>
+                    {translate('projects_role.apply_template')}
+                  </a>
+                </li>
+              </ul>
+            </div>
+          </td>
         </tr>
     );
-  },
+  }
+
+  render () {
+    const className = classNames('data', 'zebra',
+        { 'new-loading': !this.props.ready }
+    );
 
-  render() {
-    const className = classNames('data', 'zebra', { 'new-loading': !this.props.ready });
     return (
         <table className={className}>
           <tbody>{this.props.projects.map(this.renderProject)}</tbody>
         </table>
     );
   }
-});
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/templates/BulkApplyTemplateTemplate.hbs b/server/sonar-web/src/main/js/apps/projects/templates/BulkApplyTemplateTemplate.hbs
new file mode 100644 (file)
index 0000000..b4933eb
--- /dev/null
@@ -0,0 +1,68 @@
+<form class="js-form" autocomplete="off">
+  <div class="modal-head">
+    <h2>Bulk Apply Permission Template</h2>
+  </div>
+
+  <div class="modal-body">
+    <div class="js-modal-messages"></div>
+
+    {{#if done}}
+      <div class="alert alert-success">
+        {{t 'projects_role.apply_template.success'}}
+      </div>
+    {{/if}}
+
+    {{#unless done}}
+      {{#notNull permissionTemplates}}
+        <div class="modal-field">
+          <label for="project-permissions-template">
+            Template<em class="mandatory">*</em>
+          </label>
+          <select id="project-permissions-template">
+            {{#each permissionTemplates}}
+              <option value="{{id}}">{{name}}</option>
+            {{/each}}
+          </select>
+        </div>
+      {{else}}
+        <i class="spinner"></i>
+      {{/notNull}}
+
+
+      <div class="modal-field">
+        <label>Apply To</label>
+        <ul style="padding-top: 4px;">
+          {{#if selectionTotal}}
+            <li>
+              <input value="selected" id="apply-to-selected" name="apply-to"
+                     type="radio" checked>
+              <label
+                  for="apply-to-selected"
+                  style="float: none; left: 0; display: inline; padding: 0;">
+                Only Selected ({{selectionTotal}})
+              </label>
+            </li>
+          {{/if}}
+          <li>
+            <input value="all" id="apply-to-all" name="apply-to" type="radio"
+                   {{#unless selectionTotal}}checked{{/unless}}>
+            <label
+                for="apply-to-all"
+                style="float: none; left: 0; display: inline; padding: 0;">
+              All ({{total}})
+            </label>
+          </li>
+        </ul>
+      </div>
+    {{/unless}}
+  </div>
+
+  <div class="modal-foot">
+    {{#unless done}}
+      {{#notNull permissionTemplates}}
+        <button class="js-apply">Apply</button>
+      {{/notNull}}
+    {{/unless}}
+    <a href="#" class="js-modal-close">Close</a>
+  </div>
+</form>
diff --git a/server/sonar-web/src/main/js/apps/projects/views/BulkApplyTemplateView.js b/server/sonar-web/src/main/js/apps/projects/views/BulkApplyTemplateView.js
new file mode 100644 (file)
index 0000000..d2fc1ab
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import ModalForm from '../../../components/common/modal-form';
+import {
+    applyTemplateToProject,
+    bulkApplyTemplate,
+    getPermissionTemplates
+} from '../../../api/permissions';
+import Template from '../templates/BulkApplyTemplateTemplate.hbs';
+
+export default ModalForm.extend({
+  template: Template,
+
+  initialize () {
+    this.loadPermissionTemplates();
+    this.done = false;
+  },
+
+  loadPermissionTemplates () {
+    return getPermissionTemplates().then(r => {
+      this.permissionTemplates = r.permissionTemplates;
+      this.render();
+    });
+  },
+
+  onRender () {
+    ModalForm.prototype.onRender.apply(this, arguments);
+    this.$('#project-permissions-template').select2({
+      width: '250px',
+      minimumResultsForSearch: 20
+    });
+  },
+
+  bulkApplyToAll (permissionTemplate) {
+    const data = { templateId: permissionTemplate };
+
+    if (this.options.query) {
+      data.q = this.options.query;
+    }
+
+    if (this.options.qualifier) {
+      data.qualifier = this.options.qualifier;
+    }
+
+    return bulkApplyTemplate(data);
+  },
+
+  bulkApplyToSelected(permissionTemplate) {
+    const { selection } = this.options;
+    let lastRequest = Promise.resolve();
+
+    selection.forEach(projectId => {
+      const data = { templateId: permissionTemplate, projectId };
+      lastRequest = lastRequest.then(() => applyTemplateToProject(data));
+    });
+
+    return lastRequest;
+  },
+
+  onFormSubmit () {
+    ModalForm.prototype.onFormSubmit.apply(this, arguments);
+    const permissionTemplate = this.$('#project-permissions-template').val();
+    const applyTo = this.$('[name="apply-to"]:checked').val();
+    this.disableForm();
+
+    const request = applyTo === 'all' ?
+        this.bulkApplyToAll(permissionTemplate) :
+        this.bulkApplyToSelected(permissionTemplate);
+
+    request.then(() => {
+      this.trigger('done');
+      this.done = true;
+      this.render();
+    }).catch(function (e) {
+      e.response.json().then(r => {
+        this.showErrors(r.errors, r.warnings);
+        this.enableForm();
+      });
+    });
+  },
+
+  serializeData () {
+    return {
+      permissionTemplates: this.permissionTemplates,
+      selection: this.options.selection,
+      selectionTotal: this.options.selection.length,
+      total: this.options.total,
+      done: this.done
+    };
+  }
+});
index 17c424ab9e7f7d3e267cc0e514c09fd1e3d3faf4..ef25fe9a9525d33c0ecf0d2f857f3cac230696af 100644 (file)
@@ -84,6 +84,15 @@ export function getComponentDashboardManagementUrl (componentKey) {
   return window.baseUrl + '/dashboards?resource=' + encodeURIComponent(componentKey);
 }
 
+/**
+ * Generate URL for a component's permissions page
+ * @param {string} componentKey
+ * @returns {string}
+ */
+export function getComponentPermissionsUrl (componentKey) {
+  return window.baseUrl + '/project_roles?id=' + encodeURIComponent(componentKey);
+}
+
 /**
  * Generate URL for a quality profile
  * @param {string} key
index c8257a3b8c4c0bcd1ef959944ada5d65ff63079b..824ffab50786a9590cdd4f60b3d4700de7577a82 100644 (file)
@@ -207,6 +207,7 @@ added_since_previous_version_detailed=Added since previous version ({0})
 added_since_version=Added since version {0}
 all_violations=All violations
 all_issues=All issues
+apply_template
 are_you_sure=Are you sure?
 assigned_to=Assigned to
 bulk_change=Bulk Change
@@ -221,6 +222,7 @@ default_error_message=The request cannot be processed. Try again later.
 default_severity=Default severity
 default_sort_on=Default sort on
 disable_treemap=Disable treemap
+edit_permissions=Edit Permissions
 enable_treemap=Enable treemap
 equals=Equals
 false_positive=False positive
@@ -2695,6 +2697,7 @@ projects_role.scan.desc=Ability to get all settings required to perform an analy
 projects_role.bulk_change=Bulk Change
 projects_role.apply_template=Apply Permission Template
 projects_role.apply_template_to_xxx=Apply Permission Template To "{0}"
+projects_role.apply_template.success=Permission template was successfully applied.
 projects_role.no_projects=There are currently no results to apply the permission template to.