]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6796 rewrite global permissions page
authorStas Vilchik <vilchiks@gmail.com>
Wed, 19 Aug 2015 12:39:31 +0000 (14:39 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Wed, 19 Aug 2015 13:10:38 +0000 (15:10 +0200)
20 files changed:
server/sonar-web/Gruntfile.coffee
server/sonar-web/src/main/js/apps/global-permissions/app.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/global-permissions/groups-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/global-permissions/main.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/global-permissions/permission-groups.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/global-permissions/permission-users-groups-mixin.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/global-permissions/permission-users.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/global-permissions/permission.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/global-permissions/permissions-list.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-groups.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-users.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/global-permissions/users-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/avatar.jsx [new file with mode: 0644]
server/sonar-web/src/main/webapp/WEB-INF/app/views/roles/global.html.erb
server/sonar-web/src/test/json/permissions/global-permissions.json [new file with mode: 0644]
server/sonar-web/src/test/json/permissions/groups.json [new file with mode: 0644]
server/sonar-web/src/test/json/permissions/users.json [new file with mode: 0644]
server/sonar-web/test/intern.js
server/sonar-web/test/medium/base.html
server/sonar-web/test/medium/global-permissions.spec.js [new file with mode: 0644]

index a74b6f861714f9a8be5cef238ef7752e5f4ee7f6..050967793ab9dd9bb24bd051cacd25bb5ef2720d 100644 (file)
@@ -137,6 +137,7 @@ module.exports = (grunt) ->
           'build-app:computation'
           'build-app:custom-measures'
           'build-app:drilldown'
+          'build-app:global-permissions'
           'build-app:groups'
           'build-app:issues'
           'build-app:maintenance'
@@ -237,6 +238,9 @@ module.exports = (grunt) ->
           '<%= BUILD_PATH %>/js/apps/custom-measures/templates.js': [
             '<%= SOURCE_PATH %>/js/apps/custom-measures/templates/**/*.hbs'
           ]
+          '<%= BUILD_PATH %>/js/apps/global-permissions/templates.js': [
+            '<%= SOURCE_PATH %>/js/apps/global-permissions/templates/**/*.hbs'
+          ]
 
 
     clean:
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/app.jsx b/server/sonar-web/src/main/js/apps/global-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/global-permissions/groups-view.js b/server/sonar-web/src/main/js/apps/global-permissions/groups-view.js
new file mode 100644 (file)
index 0000000..e579ae3
--- /dev/null
@@ -0,0 +1,42 @@
+define([
+  'components/common/modals',
+  'components/common/select-list',
+  './templates'
+], function (Modal) {
+
+  return Modal.extend({
+    template: Templates['global-permissions-groups'],
+
+    onRender: function () {
+      this._super();
+      new window.SelectList({
+        el: this.$('#global-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,
+        selectUrl: baseUrl + '/api/permissions/add_group',
+        deselectUrl: baseUrl + '/api/permissions/remove_group',
+        extra: {
+          permission: this.options.permission
+        },
+        selectParameter: 'groupId',
+        selectParameterValue: 'id',
+        parse: function (r) {
+          this.more = false;
+          return r.groups;
+        }
+      });
+    },
+
+    onDestroy: function () {
+      this.options.refresh();
+      this._super();
+    }
+  });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/main.jsx b/server/sonar-web/src/main/js/apps/global-permissions/main.jsx
new file mode 100644 (file)
index 0000000..fd20958
--- /dev/null
@@ -0,0 +1,33 @@
+import React from 'react';
+import PermissionsList from './permissions-list';
+
+let $ = jQuery;
+
+export default React.createClass({
+  getInitialState() {
+    return { permissions: [] };
+  },
+
+  componentDidMount() {
+    this.requestPermissions();
+  },
+
+  requestPermissions() {
+    const url = `${window.baseUrl}/api/permissions/search_global_permissions`;
+    $.get(url).done(r => {
+      this.setState({ permissions: r.globalPermissions });
+    });
+  },
+
+  render() {
+    return (
+        <div className="page">
+          <header id="global-permissions-header" className="page-header">
+            <h1 className="page-title">{window.t('global_permissions.page')}</h1>
+            <p className="page-description">{window.t('global_permissions.page.description')}</p>
+          </header>
+          <PermissionsList permissions={this.state.permissions}/>
+        </div>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/permission-groups.jsx b/server/sonar-web/src/main/js/apps/global-permissions/permission-groups.jsx
new file mode 100644 (file)
index 0000000..a79c9c1
--- /dev/null
@@ -0,0 +1,34 @@
+import React from 'react';
+import PermissionsUsersGroupsMixin from './permission-users-groups-mixin';
+import GroupsView from './groups-view';
+
+export default React.createClass({
+  mixins: [PermissionsUsersGroupsMixin],
+
+  renderUpdateLink() {
+    return (
+        <a onClick={this.updateGroups}
+           className="icon-bullet-list"
+           title="Update Groups"
+           data-toggle="tooltip"
+           href="#"></a>
+    );
+  },
+
+  renderItem(item) {
+    return item.name;
+  },
+
+  renderTitle() {
+    console.log(this.props);
+    return 'Groups';
+  },
+
+  updateGroups(e) {
+    e.preventDefault();
+    new GroupsView({
+      permission: this.props.permission.key,
+      refresh: this.props.refresh
+    }).render();
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/permission-users-groups-mixin.jsx b/server/sonar-web/src/main/js/apps/global-permissions/permission-users-groups-mixin.jsx
new file mode 100644 (file)
index 0000000..ec7f415
--- /dev/null
@@ -0,0 +1,55 @@
+import React from 'react';
+
+let $ = jQuery;
+
+export default {
+  propTypes: {
+    permission: React.PropTypes.object.isRequired,
+    max: React.PropTypes.number.isRequired,
+    items: React.PropTypes.array,
+    total: React.PropTypes.number,
+    refresh: React.PropTypes.func.isRequired
+  },
+
+  renderNotDisplayed() {
+    const notDisplayedCount = this.props.total - this.props.max;
+    return notDisplayedCount > 0 ? <span className="note spacer-right" href="#">and {notDisplayedCount} more</span> : null;
+  },
+
+  renderItems() {
+    const displayed = this.props.items.map(item => {
+      return <li key={item.name} className="spacer-left little-spacer-bottom">{this.renderItem(item)}</li>;
+    });
+    return (
+        <ul className="overflow-hidden bordered-left">
+          {displayed}
+          <li className="spacer-left little-spacer-bottom">
+            {this.renderNotDisplayed()}
+            {this.renderUpdateLink()}
+          </li>
+        </ul>
+    );
+  },
+
+  renderCount() {
+    return (
+        <ul className="overflow-hidden bordered-left">
+          <li className="spacer-left little-spacer-bottom">
+            <span className="spacer-right">{this.props.total}</span>
+            {this.renderUpdateLink()}
+          </li>
+        </ul>
+    );
+  },
+
+  render() {
+    return (
+        <li className="abs-width-400">
+          <div className="pull-left spacer-right">
+            <strong>{this.renderTitle()}</strong>
+          </div>
+          {this.props.items ? this.renderItems() : this.renderCount()}
+        </li>
+    );
+  }
+};
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/permission-users.jsx b/server/sonar-web/src/main/js/apps/global-permissions/permission-users.jsx
new file mode 100644 (file)
index 0000000..3d8fe5d
--- /dev/null
@@ -0,0 +1,34 @@
+import React from 'react';
+import PermissionsUsersGroupsMixin from './permission-users-groups-mixin';
+import Avatar from 'components/shared/avatar';
+import UsersView from './users-view';
+
+export default React.createClass({
+  mixins: [PermissionsUsersGroupsMixin],
+
+  renderUpdateLink() {
+    return (
+        <a onClick={this.updateUsers}
+           className="icon-bullet-list"
+           title="Update Users"
+           data-toggle="tooltip"
+           href="#"></a>
+    );
+  },
+
+  renderItem(item) {
+    return item.name;
+  },
+
+  renderTitle() {
+    return 'Users';
+  },
+
+  updateUsers(e) {
+    e.preventDefault();
+    new UsersView({
+      permission: this.props.permission.key,
+      refresh: this.props.refresh
+    }).render();
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/permission.jsx b/server/sonar-web/src/main/js/apps/global-permissions/permission.jsx
new file mode 100644 (file)
index 0000000..1e87875
--- /dev/null
@@ -0,0 +1,56 @@
+import React from 'react';
+import PermissionUsers from './permission-users';
+import PermissionGroups from './permission-groups';
+
+let $ = jQuery;
+
+// Maximum number of displayed groups
+const MAX_ITEMS = 3;
+
+export default React.createClass({
+  propTypes: {
+    permission: React.PropTypes.object.isRequired
+  },
+
+  getInitialState() {
+    return {};
+  },
+
+  componentDidMount() {
+    this.requestUsers();
+    this.requestGroups();
+  },
+
+  requestUsers() {
+    const url = `${window.baseUrl}/api/permissions/users`;
+    const data = { permission: this.props.permission.key, ps: MAX_ITEMS };
+    $.get(url, data).done(r => this.setState({ users: r.users, totalUsers: r.paging && r.paging.total }));
+  },
+
+  requestGroups() {
+    const url = `${window.baseUrl}/api/permissions/groups`;
+    const data = { permission: this.props.permission.key, ps: MAX_ITEMS };
+    $.get(url, data).done(r => this.setState({ groups: r.groups, totalGroups: r.paging && r.paging.total }));
+  },
+
+  render() {
+    return (
+        <li className="panel panel-vertical" data-id={this.props.permission.key}>
+          <h3>{this.props.permission.name}</h3>
+          <p className="spacer-top" dangerouslySetInnerHTML={{ __html: this.props.permission.description }}/>
+          <ul className="list-inline spacer-top">
+            <PermissionUsers permission={this.props.permission}
+                             max={MAX_ITEMS}
+                             items={this.state.users}
+                             total={this.state.totalUsers || this.props.permission.usersCount}
+                             refresh={this.requestUsers}/>
+            <PermissionGroups permission={this.props.permission}
+                              max={MAX_ITEMS}
+                              items={this.state.groups}
+                              total={this.state.totalGroups || this.props.permission.groupsCount}
+                              refresh={this.requestGroups}/>
+          </ul>
+        </li>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/permissions-list.jsx b/server/sonar-web/src/main/js/apps/global-permissions/permissions-list.jsx
new file mode 100644 (file)
index 0000000..432f020
--- /dev/null
@@ -0,0 +1,18 @@
+import React from 'react';
+import Permission from './permission';
+
+export default React.createClass({
+  propTypes:{
+    permissions: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
+  },
+
+  renderPermissions() {
+    return this.props.permissions.map(permission => {
+      return <Permission key={permission.key} permission={permission}/>
+    });
+  },
+
+  render() {
+    return <ul id="global-permissions-list">{this.renderPermissions()}</ul>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-groups.hbs b/server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-groups.hbs
new file mode 100644 (file)
index 0000000..67149ae
--- /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="global-permissions-groups"></div>
+</div>
+<div class="modal-foot">
+  <a href="#" class="js-modal-close" id="global-permissions-groups-done">Done</a>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-users.hbs b/server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-users.hbs
new file mode 100644 (file)
index 0000000..fd4c59a
--- /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="global-permissions-users"></div>
+</div>
+<div class="modal-foot">
+  <a href="#" class="js-modal-close" id="global-permissions-users-done">Done</a>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/users-view.js b/server/sonar-web/src/main/js/apps/global-permissions/users-view.js
new file mode 100644 (file)
index 0000000..b5660c6
--- /dev/null
@@ -0,0 +1,42 @@
+define([
+  'components/common/modals',
+  'components/common/select-list',
+  './templates'
+], function (Modal) {
+
+  return Modal.extend({
+    template: Templates['global-permissions-users'],
+
+    onRender: function () {
+      this._super();
+      new window.SelectList({
+        el: this.$('#global-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,
+        selectUrl: baseUrl + '/api/permissions/add_user',
+        deselectUrl: baseUrl + '/api/permissions/remove_user',
+        extra: {
+          permission: this.options.permission
+        },
+        selectParameter: 'login',
+        selectParameterValue: 'login',
+        parse: function (r) {
+          this.more = false;
+          return r.users;
+        }
+      });
+    },
+
+    onDestroy: function () {
+      this.options.refresh();
+      this._super();
+    }
+  });
+
+});
diff --git a/server/sonar-web/src/main/js/components/shared/avatar.jsx b/server/sonar-web/src/main/js/components/shared/avatar.jsx
new file mode 100644 (file)
index 0000000..2f596bf
--- /dev/null
@@ -0,0 +1,20 @@
+import React from 'react';
+
+export default React.createClass({
+  propTypes: {
+    email: React.PropTypes.string,
+    size: React.PropTypes.number.isRequired
+  },
+
+  render() {
+    const shouldShowAvatar = window.SS && window.SS.lf && window.SS.lf.enableGravatar;
+    if (!shouldShowAvatar) {
+      return null;
+    }
+    const emailHash = window.md5(this.props.email || '').trim();
+    const url = ('' + window.SS.lf.gravatarServerUrl)
+            .replace('{EMAIL_MD5}', emailHash)
+            .replace('{SIZE}', this.props.size * 2);
+    return <img className="rounded" src={url} width={this.props.size} height={this.props.size} alt={this.props.email}/>;
+  }
+});
index 947f288de71946dbed85304d0d21dde258628763..99a961b12b3c7bd91b8aa6e03d6294652c8de6ad 100644 (file)
@@ -1,51 +1,7 @@
-<% content_for :script do %>
-  <script>require(['components/common/select-list']);</script>
+<% content_for :extra_script do %>
+  <script>
+    require(['apps/global-permissions/app'], function (App) {
+      App.start({ el: '#content' });
+    });
+  </script>
 <% end %>
-
-<div class="page">
-  <header class="page-header">
-    <h1 class="page-title"><%= h message 'global_permissions.page' -%></h1>
-    <p class="page-description"><%= message('global_permissions.page.description') -%> </p>
-  </header>
-
-  <table class="data width100" id="global-permissions">
-    <thead>
-    <tr>
-      <th><%= h message('global_permissions.permission') -%></th>
-      <th width="30%"><%= h message('global_permissions.users') -%></th>
-      <th width="30%"><%= h message('global_permissions.groups') -%></th>
-    </tr>
-    </thead>
-    <tbody>
-    <%
-       permission_keys = Internal.permissions.globalPermissions()
-       key_to_name = permission_keys.inject({}) do |hash, key|
-         hash[key] = message("global_permissions.#{key}")
-         hash
-       end
-    %>
-
-    <%
-       # Note that sorting by names should be case insensitive but it's not the case. It's not a problem in this page.
-       key_to_name.sort { |a, b| a[1]<=>b[1] }.each do |elem|
-         permission_key = elem[0]
-         permission_name = elem[1]
-    %>
-      <tr class="<%= cycle('even', 'odd', :name => 'global_permission') -%>">
-        <td valign="top">
-          <b><%= h permission_name -%></b><br/>
-          <span class="small gray"><%= message("global_permissions.#{permission_key}.desc") -%></span>
-        </td>
-        <td valign="top" style="word-break:break-all;width:30%;">
-          <span id="users-<%= permission_key.parameterize -%>"><%= users(permission_key).map(&:name).join(', ') -%></span>
-          (<%= link_to_edit_roles_permission_form(message('select'), permission_key, nil, "select-users-#{permission_key}") -%>)<br/>
-        </td>
-        <td valign="top" style="word-break:break-all;width:30%;">
-          <span id="groups-<%= permission_key.parameterize -%>"><%= groups(permission_key).map { |g| group_name(g) }.join(', ') %></span>
-          (<%= link_to_edit_groups_permission_form(message('select'), permission_key, nil, "select-groups-#{permission_key}") -%>)<br/>
-        </td>
-      </tr>
-    <% end %>
-    </tbody>
-  </table>
-</div>
diff --git a/server/sonar-web/src/test/json/permissions/global-permissions.json b/server/sonar-web/src/test/json/permissions/global-permissions.json
new file mode 100644 (file)
index 0000000..2d1ee99
--- /dev/null
@@ -0,0 +1,46 @@
+{
+  "globalPermissions": [
+    {
+      "key": "admin",
+      "name": "Administer System",
+      "description": "Ability to perform all administration functions for the instance: global configuration and personalization of default dashboards.",
+      "usersCount": 1,
+      "groupsCount": 5
+    },
+    {
+      "key": "profileadmin",
+      "name": "Administer Quality Profiles and Gates",
+      "description": "Ability to perform any action on the quality profiles and gates.",
+      "usersCount": 1,
+      "groupsCount": 0
+    },
+    {
+      "key": "shareDashboard",
+      "name": "Share Dashboards And Filters",
+      "description": "Ability to share dashboards, issue filters and measure filters.",
+      "usersCount": 0,
+      "groupsCount": 1
+    },
+    {
+      "key": "scan",
+      "name": "Execute Analysis",
+      "description": "Ability to execute analyses, and to get all settings required to perform the analysis, even the secured ones like the scm account password, the jira account password, and so on.",
+      "usersCount": 0,
+      "groupsCount": 1
+    },
+    {
+      "key": "dryRunScan",
+      "name": "Execute Preview Analysis",
+      "description": "Ability to execute preview analysis (results are not pushed to the server). This permission does not include the ability to access secured settings such as the scm account password, the jira account password, and so on.<br/>This permission is <em>required</em> to execute preview analysis in Eclipse or via the Issues Report plugin.",
+      "usersCount": 0,
+      "groupsCount": 1
+    },
+    {
+      "key": "provisioning",
+      "name": "Provision Projects",
+      "description": "Ability to initialize project structure before first analysis.",
+      "usersCount": 0,
+      "groupsCount": 1
+    }
+  ]
+}
diff --git a/server/sonar-web/src/test/json/permissions/groups.json b/server/sonar-web/src/test/json/permissions/groups.json
new file mode 100644 (file)
index 0000000..89da5da
--- /dev/null
@@ -0,0 +1,27 @@
+{
+  "groups": [
+    {
+      "id": "3",
+      "name": "1",
+      "description": "",
+      "selected": true
+    },
+    {
+      "id": "4",
+      "name": "2",
+      "description": "",
+      "selected": true
+    },
+    {
+      "id": "5",
+      "name": "3",
+      "description": "",
+      "selected": true
+    }
+  ],
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 3,
+    "total": 5
+  }
+}
diff --git a/server/sonar-web/src/test/json/permissions/users.json b/server/sonar-web/src/test/json/permissions/users.json
new file mode 100644 (file)
index 0000000..9945a28
--- /dev/null
@@ -0,0 +1,14 @@
+{
+  "users": [
+    {
+      "login": "admin",
+      "name": "Administrator",
+      "selected": true
+    }
+  ],
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 3,
+    "total": 1
+  }
+}
index e17f766168116da6f573c9128e5a7d6839bae639..74b2a6bd9ef8f2eca16da5a573aea52ec976cdac 100644 (file)
@@ -29,7 +29,8 @@ define(['intern'], function (intern) {
       'test/medium/coding-rules.spec',
       'test/medium/custom-measures.spec',
       'test/medium/quality-profiles.spec',
-      'test/medium/source-viewer.spec'
+      'test/medium/source-viewer.spec',
+      'test/medium/global-permissions.spec'
     ],
 
     tunnel: tunnel,
index 69dd16cfef485ad84edfd41dfe59c29acc65b49e..736dc6d6b6f6aaa67bc937c829a850b8cc3ea303 100644 (file)
     updateCenterActive: true
   };
   </script>
-  <script>requirejs.config({ baseUrl: baseUrl + '../../build/js' });</script>
+  <script>
+    requirejs.config({
+      baseUrl: baseUrl + '../../build/js',
+      paths: {
+        'react': 'libs/third-party/react-with-addons'
+      }
+    });
+  </script>
 </head>
 <body>
 <div id="content"></div>
diff --git a/server/sonar-web/test/medium/global-permissions.spec.js b/server/sonar-web/test/medium/global-permissions.spec.js
new file mode 100644 (file)
index 0000000..9b50894
--- /dev/null
@@ -0,0 +1,22 @@
+define(function (require) {
+  var bdd = require('intern!bdd');
+  require('../helpers/test-page');
+
+  bdd.describe('Global Permissions', function () {
+    bdd.it('should show permissions', function () {
+      return this.remote
+          .open()
+          .mockFromFile('/api/permissions/search_global_permissions', 'permissions/global-permissions.json')
+          .mockFromFile('/api/permissions/users', 'permissions/users.json')
+          .mockFromFile('/api/permissions/groups', 'permissions/groups.json')
+          .startApp('global-permissions')
+          .checkElementExist('#global-permissions-header')
+          .checkElementExist('#global-permissions-list')
+          .checkElementCount('#global-permissions-list > li', 6)
+          .checkElementInclude('#global-permissions-list > li h3', 'Administer System')
+          .checkElementInclude('#global-permissions-list > li p', 'Ability to perform all administration')
+          .checkElementInclude('#global-permissions-list > li ul > li:first-child', 'Administrator')
+          .checkElementInclude('#global-permissions-list > li ul > li:last-child', '1')
+    });
+  });
+});