aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-11-27 11:30:34 +0100
committerStas Vilchik <vilchiks@gmail.com>2015-11-27 13:52:21 +0100
commitdae5bf58a4d228047caadfd5f2fe2c149cb266c8 (patch)
treefcc9a1a2bc5445ab688f286d68cffd445b0b9d89
parentd32e10df85f49769060b5485cfb3d117e14bec2c (diff)
downloadsonarqube-dae5bf58a4d228047caadfd5f2fe2c149cb266c8.tar.gz
sonarqube-dae5bf58a4d228047caadfd5f2fe2c149cb266c8.zip
SONAR-7050 SONAR-7051 SONAR-7052 add ability to generate, list and revoke user tokens
-rw-r--r--server/sonar-web/package.json1
-rw-r--r--server/sonar-web/src/main/js/api/user-tokens.js39
-rw-r--r--server/sonar-web/src/main/js/apps/users/list-item-view.js16
-rw-r--r--server/sonar-web/src/main/js/apps/users/list-view.js8
-rw-r--r--server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs70
-rw-r--r--server/sonar-web/src/main/js/apps/users/templates/users-list.hbs13
-rw-r--r--server/sonar-web/src/main/js/apps/users/templates/users-tokens.hbs85
-rw-r--r--server/sonar-web/src/main/js/apps/users/tokens-view.js90
-rw-r--r--server/sonar-web/src/main/less/init/tables.less4
9 files changed, 280 insertions, 46 deletions
diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json
index a19052acaa5..213b3d97758 100644
--- a/server/sonar-web/package.json
+++ b/server/sonar-web/package.json
@@ -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
index 00000000000..ca522f914b4
--- /dev/null
+++ b/server/sonar-web/src/main/js/api/user-tokens.js
@@ -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);
+}
diff --git a/server/sonar-web/src/main/js/apps/users/list-item-view.js b/server/sonar-web/src/main/js/apps/users/list-item-view.js
index 316d0428576..a92cc1b2285 100644
--- a/server/sonar-web/src/main/js/apps/users/list-item-view.js
+++ b/server/sonar-web/src/main/js/apps/users/list-item-view.js
@@ -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,
diff --git a/server/sonar-web/src/main/js/apps/users/list-view.js b/server/sonar-web/src/main/js/apps/users/list-view.js
index 22f699697e9..09c9816c7f1 100644
--- a/server/sonar-web/src/main/js/apps/users/list-view.js
+++ b/server/sonar-web/src/main/js/apps/users/list-view.js
@@ -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',
diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs
index 9efab3f9667..b410552e342 100644
--- a/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs
+++ b/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs
@@ -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
index 00000000000..1987624e130
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/users/templates/users-list.hbs
@@ -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
index 00000000000..0d96f081f01
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/users/templates/users-tokens.hbs
@@ -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
index 00000000000..e986e84e7ac
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/users/tokens-view.js
@@ -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
+ });
+ }
+
+});
+
+
diff --git a/server/sonar-web/src/main/less/init/tables.less b/server/sonar-web/src/main/less/init/tables.less
index 4f63315ce92..e52bce463f2 100644
--- a/server/sonar-web/src/main/less/init/tables.less
+++ b/server/sonar-web/src/main/less/init/tables.less
@@ -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;
}