]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7050 SONAR-7051 SONAR-7052 add ability to generate, list and revoke user tokens
authorStas Vilchik <vilchiks@gmail.com>
Fri, 27 Nov 2015 10:30:34 +0000 (11:30 +0100)
committerStas Vilchik <vilchiks@gmail.com>
Fri, 27 Nov 2015 12:52:21 +0000 (13:52 +0100)
server/sonar-web/package.json
server/sonar-web/src/main/js/api/user-tokens.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/list-item-view.js
server/sonar-web/src/main/js/apps/users/list-view.js
server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs
server/sonar-web/src/main/js/apps/users/templates/users-list.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/templates/users-tokens.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/tokens-view.js [new file with mode: 0644]
server/sonar-web/src/main/less/init/tables.less

index a19052acaa5148d176f172d2555f940719be927f..213b3d97758a4af1d169f63a022a879a87d8c26a 100644 (file)
@@ -14,6 +14,7 @@
     "browserify-shim": "3.8.10",
     "chai": "3.3.0",
     "classnames": "^2.2.0",
+    "clipboard": "1.5.5",
     "d3": "3.5.6",
     "del": "2.0.2",
     "event-stream": "3.3.1",
diff --git a/server/sonar-web/src/main/js/api/user-tokens.js b/server/sonar-web/src/main/js/api/user-tokens.js
new file mode 100644 (file)
index 0000000..ca522f9
--- /dev/null
@@ -0,0 +1,39 @@
+import { getJSON, postJSON, post } from '../helpers/request.js';
+
+
+/**
+ * List tokens for given user login
+ * @param {string} login
+ * @returns {Promise}
+ */
+export function getTokens (login) {
+  let url = baseUrl + '/api/user_tokens/search';
+  let data = { login };
+  return getJSON(url, data).then(r => r.userTokens);
+}
+
+
+/**
+ * Generate a user token
+ * @param {string} userLogin
+ * @param {string} tokenName
+ * @returns {Promise}
+ */
+export function generateToken(userLogin, tokenName) {
+  let url = baseUrl + '/api/user_tokens/generate';
+  let data = { login: userLogin, name: tokenName };
+  return postJSON(url, data);
+}
+
+
+/**
+ * Revoke a user token
+ * @param {string} userLogin
+ * @param {string} tokenName
+ * @returns {Promise}
+ */
+export function revokeToken(userLogin, tokenName) {
+  let url = baseUrl + '/api/user_tokens/revoke';
+  let data = { login: userLogin, name: tokenName };
+  return post(url, data);
+}
index 316d04285766b93f21ce3cab24fea9882c5d817d..a92cc1b22856561d704eebe123a2e1e2e4112b3d 100644 (file)
@@ -4,11 +4,11 @@ import UpdateView from './update-view';
 import ChangePasswordView from './change-password-view';
 import DeactivateView from './deactivate-view';
 import GroupsView from './groups-view';
+import TokensView from './tokens-view';
 import Template from './templates/users-list-item.hbs';
 
 export default Marionette.ItemView.extend({
-  tagName: 'li',
-  className: 'panel panel-vertical',
+  tagName: 'tr',
   template: Template,
 
   events: {
@@ -17,7 +17,8 @@ export default Marionette.ItemView.extend({
     'click .js-user-update': 'onUpdateClick',
     'click .js-user-change-password': 'onChangePasswordClick',
     'click .js-user-deactivate': 'onDeactivateClick',
-    'click .js-user-groups': 'onGroupsClick'
+    'click .js-user-groups': 'onGroupsClick',
+    'click .js-user-tokens': 'onTokensClick'
   },
 
   initialize: function () {
@@ -64,6 +65,11 @@ export default Marionette.ItemView.extend({
     this.showGroups();
   },
 
+  onTokensClick: function (e) {
+    e.preventDefault();
+    this.showTokens();
+  },
+
   showMoreScm: function () {
     this.scmLimit = 10000;
     this.render();
@@ -96,6 +102,10 @@ export default Marionette.ItemView.extend({
     new GroupsView({ model: this.model }).render();
   },
 
+  showTokens: function () {
+    new TokensView({ model: this.model }).render();
+  },
+
   serializeData: function () {
     var scmAccounts = this.model.get('scmAccounts'),
         scmAccountsLimit = scmAccounts.length > this.scmLimit ? this.scmLimit - 1 : this.scmLimit,
index 22f699697e9466d153e1928bb8dc9dd553b92f70..09c9816c7f161f829870a8aecca3be1378c43270 100644 (file)
@@ -1,9 +1,13 @@
 import Marionette from 'backbone.marionette';
+
 import ListItemView from './list-item-view';
+import Template from './templates/users-list.hbs';
+
 
-export default Marionette.CollectionView.extend({
-  tagName: 'ul',
+export default Marionette.CompositeView.extend({
+  template: Template,
   childView: ListItemView,
+  childViewContainer: 'tbody',
 
   collectionEvents: {
     'request': 'showLoading',
index 9efab3f96672802f8ed6b239aaac274dda142e32..b410552e3427e05704d4e4df07826e2025cc97fe 100644 (file)
@@ -1,55 +1,51 @@
-<div class="pull-right big-spacer-left nowrap">
-  <a class="js-user-update icon-edit little-spacer-right" title="Update Details" data-toggle="tooltip" href="#"></a>
-  <a class="js-user-change-password icon-lock little-spacer-right" title="Change Password" data-toggle="tooltip"
-     href="#"></a>
-  <a class="js-user-deactivate icon-delete" title="Deactivate" data-toggle="tooltip" href="#"></a>
-</div>
-
 {{#ifShowAvatars}}
-  <div class="display-inline-block text-top big-spacer-right">
-    {{avatarHelper email 36}}
-  </div>
+  <td class="thin nowrap">
+    <div>{{avatarHelper email 36}}</div>
+  </td>
 {{/ifShowAvatars}}
 
-<div class="display-inline-block text-top width-30">
+<td>
   <div>
     <strong class="js-user-name">{{name}}</strong>
     <span class="js-user-login note little-spacer-left">{{login}}</span>
   </div>
   <div class="js-user-email little-spacer-top">{{email}}</div>
-</div>
+</td>
 
-<div class="display-inline-block text-top big-spacer-left width-25">
-  {{#notEmpty scmAccounts}}
-    <div class="pull-left spacer-right">
-      <strong>SCM</strong>
-    </div>
-    <ul class="overflow-hidden bordered-left">
-      {{#each firstScmAccounts}}
-        <li class="spacer-left little-spacer-bottom">{{this}}</li>
-      {{/each}}
-      {{#gt moreScmAccountsCount 0}}
-        <li class="spacer-left little-spacer-bottom">
-          <a class="js-user-more-scm" href="#">{{moreScmAccountsCount}} more</a>
-        </li>
-      {{/gt}}
-    </ul>
-  {{/notEmpty}}
-</div>
+<td>
+  <ul>
+    {{#each firstScmAccounts}}
+      <li class="little-spacer-bottom">{{this}}</li>
+    {{/each}}
+    {{#gt moreScmAccountsCount 0}}
+      <li class="little-spacer-bottom">
+        <a class="js-user-more-scm" href="#">{{moreScmAccountsCount}} more</a>
+      </li>
+    {{/gt}}
+  </ul>
+</td>
 
-<div class="display-inline-block text-top big-spacer-left width-25">
-  <div class="pull-left spacer-right">
-    <strong>Groups</strong>
-  </div>
-  <ul class="overflow-hidden bordered-left">
+<td>
+  <ul>
     {{#each firstGroups}}
-      <li class="spacer-left little-spacer-bottom">{{this}}</li>
+      <li class="little-spacer-bottom">{{this}}</li>
     {{/each}}
-    <li class="spacer-left little-spacer-bottom">
+    <li class="little-spacer-bottom">
       {{#gt moreGroupsCount 0}}
         <a class="js-user-more-groups spacer-right" href="#">{{moreGroupsCount}} more</a>
       {{/gt}}
       <a class="js-user-groups icon-bullet-list" title="Update Groups" data-toggle="tooltip" href="#"></a>
     </li>
   </ul>
-</div>
+</td>
+
+<td>
+  <a class="js-user-tokens icon-bullet-list" title="Update Tokens" data-toggle="tooltip" href="#"></a>
+</td>
+
+<td class="thin nowrap text-right">
+  <a class="js-user-update icon-edit little-spacer-right" title="Update Details" data-toggle="tooltip" href="#"></a>
+  <a class="js-user-change-password icon-lock little-spacer-right" title="Change Password" data-toggle="tooltip"
+     href="#"></a>
+  <a class="js-user-deactivate icon-delete" title="Deactivate" data-toggle="tooltip" href="#"></a>
+</td>
diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-list.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-list.hbs
new file mode 100644 (file)
index 0000000..1987624
--- /dev/null
@@ -0,0 +1,13 @@
+<table class="data zebra">
+  <thead>
+  <tr>
+    {{#ifShowAvatars}}<th>&nbsp;</th>{{/ifShowAvatars}}
+    <th class="nowrap">&nbsp;</th>
+    <th class="nowrap">SCM Accounts</th>
+    <th class="nowrap">Groups</th>
+    <th class="nowrap">Tokens</th>
+    <th class="nowrap">&nbsp;</th>
+  </tr>
+  </thead>
+  <tbody></tbody>
+</table>
diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-tokens.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-tokens.hbs
new file mode 100644 (file)
index 0000000..0d96f08
--- /dev/null
@@ -0,0 +1,85 @@
+<div class="modal-head">
+  <h2>Tokens</h2>
+</div>
+
+<div class="modal-body">
+  <div class="js-modal-messages"></div>
+
+  {{#notNull tokens}}
+    <table class="data zebra">
+      <thead>
+      <tr>
+        <th>Name</th>
+        <th class="text-right">Created</th>
+        <th>&nbsp;</th>
+      </tr>
+      </thead>
+      <tbody>
+      {{#each tokens}}
+        <tr>
+          <td>
+            {{name}}
+          </td>
+          <td class="thin nowrap text-right">
+            {{fromNow createdAt}}
+          </td>
+          <td class="thin nowrap text-right">
+            <div class="big-spacer-left">
+              <form id="revoke-token-form" data-token="{{name}}">
+                {{#if deleting}}
+                  <button class="button-red">Sure?</button>
+                {{else}}
+                  <button class="button-red">Revoke</button>
+                {{/if}}
+              </form>
+            </div>
+          </td>
+        </tr>
+      {{else}}
+        <tr>
+          <td colspan="3">
+            <span class="note">No tokens</span>
+          </td>
+        </tr>
+      {{/each}}
+      </tbody>
+    </table>
+  {{/notNull}}
+
+  <hr class="big-spacer-top big-spacer-bottom">
+
+  <p class="spacer-bottom">Generate tokens for bla bla bla</p>
+
+  {{#each errors}}
+    <div class="alert alert-danger">{{msg}}</div>
+  {{/each}}
+
+  <form id="generate-token-form">
+    <input type="text" required maxlength="30" placeholder="Enter Token Name">
+    <button>Generate</button>
+  </form>
+
+  {{#if newToken}}
+    <div class="panel panel-white big-spacer-top">
+      <div class="alert alert-warning">
+        Make sure you copy the token now. You won’t be able to see it again!
+      </div>
+
+      <table class="data">
+        <tr>
+
+          <td class="thin">
+            <button class="js-copy-to-clipboard" data-clipboard-text="{{newToken}}">Copy</button>
+          </td>
+          <td class="nowrap">
+            <code class="text-success">{{newToken}}</code>
+          </td>
+        </tr>
+      </table>
+    </div>
+  {{/if}}
+</div>
+
+<div class="modal-foot">
+  <a href="#" class="js-modal-close">Done</a>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/users/tokens-view.js b/server/sonar-web/src/main/js/apps/users/tokens-view.js
new file mode 100644 (file)
index 0000000..e986e84
--- /dev/null
@@ -0,0 +1,90 @@
+import $ from 'jquery';
+import _ from 'underscore';
+import Clipboard from 'clipboard';
+
+import Modal from '../../components/common/modals';
+import Template from './templates/users-tokens.hbs';
+import { getTokens, generateToken, revokeToken } from '../../api/user-tokens';
+
+
+export default Modal.extend({
+  template: Template,
+
+  events () {
+    return _.extend(Modal.prototype.events.apply(this, arguments), {
+      'submit #generate-token-form': 'onGenerateTokenFormSubmit',
+      'submit #revoke-token-form': 'onRevokeTokenFormSubmit'
+    });
+  },
+
+  initialize () {
+    Modal.prototype.initialize.apply(this, arguments);
+    this.tokens = null;
+    this.newToken = null;
+    this.errors = [];
+    this.requestTokens();
+  },
+
+  requestTokens () {
+    return getTokens(this.model.id).then(tokens => {
+      this.tokens = tokens;
+      this.render();
+    })
+  },
+
+  onGenerateTokenFormSubmit (e) {
+    e.preventDefault();
+    this.errors = [];
+    this.newToken = null;
+    let tokenName = this.$('#generate-token-form input').val();
+    generateToken(this.model.id, tokenName)
+        .then(response => {
+          this.newToken = response.token;
+          this.requestTokens();
+        })
+        .catch(e => {
+          e.response.json().then(response => {
+            this.errors = response.errors;
+            this.render();
+          });
+        });
+  },
+
+  onRevokeTokenFormSubmit(e) {
+    e.preventDefault();
+    let tokenName = $(e.currentTarget).data('token'),
+        token = _.findWhere(this.tokens, { name: `${tokenName}` });
+    if (token) {
+      if (token.deleting) {
+        revokeToken(this.model.id, tokenName).then(this.requestTokens.bind(this));
+      } else {
+        token.deleting = true;
+        this.render();
+      }
+    }
+  },
+
+  onRender () {
+    Modal.prototype.onRender.apply(this, arguments);
+    let copyButton = this.$('.js-copy-to-clipboard');
+    if (copyButton.length) {
+      let clipboard = new Clipboard(copyButton.get(0));
+      clipboard.on('success', () => {
+        copyButton.tooltip({ title: 'Copied!', placement: 'bottom', trigger: 'manual' }).tooltip('show');
+        setTimeout(() => copyButton.tooltip('hide'), 1000);
+      });
+    }
+    this.newToken = null;
+  },
+
+  serializeData() {
+    return _.extend(Modal.prototype.serializeData.apply(this, arguments), {
+      tokens: this.tokens,
+      newToken: this.newToken,
+      errors: this.errors
+    });
+  }
+
+});
+
+
index 4f63315ce920684b7f35476af1c911502b1d3a9c..e52bce463f2d6a114ac0971d95c4da8187c37653 100644 (file)
@@ -48,10 +48,6 @@ table.data td.small, table.data th.small {
   white-space: nowrap;
 }
 
-table.data th img, table.data td img {
-  vertical-align: sub;
-}
-
 table.data.zebra > tbody > tr:nth-child(odd) {
   background-color: #f5f5f5;
 }