From dae5bf58a4d228047caadfd5f2fe2c149cb266c8 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Fri, 27 Nov 2015 11:30:34 +0100 Subject: [PATCH] SONAR-7050 SONAR-7051 SONAR-7052 add ability to generate, list and revoke user tokens --- server/sonar-web/package.json | 1 + .../sonar-web/src/main/js/api/user-tokens.js | 39 ++++++++ .../src/main/js/apps/users/list-item-view.js | 16 +++- .../src/main/js/apps/users/list-view.js | 8 +- .../apps/users/templates/users-list-item.hbs | 70 +++++++-------- .../js/apps/users/templates/users-list.hbs | 13 +++ .../js/apps/users/templates/users-tokens.hbs | 85 ++++++++++++++++++ .../src/main/js/apps/users/tokens-view.js | 90 +++++++++++++++++++ .../sonar-web/src/main/less/init/tables.less | 4 - 9 files changed, 280 insertions(+), 46 deletions(-) create mode 100644 server/sonar-web/src/main/js/api/user-tokens.js create mode 100644 server/sonar-web/src/main/js/apps/users/templates/users-list.hbs create mode 100644 server/sonar-web/src/main/js/apps/users/templates/users-tokens.hbs create mode 100644 server/sonar-web/src/main/js/apps/users/tokens-view.js 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 @@ -
- - - -
- {{#ifShowAvatars}} -
- {{avatarHelper email 36}} -
+ +
{{avatarHelper email 36}}
+ {{/ifShowAvatars}} -
+
{{name}}
{{email}}
-
+ -
- {{#notEmpty scmAccounts}} -
- SCM -
- - {{/notEmpty}} -
+ + + -
-
- Groups -
-
+ + + + + + + + + + + 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 @@ + + + + {{#ifShowAvatars}}{{/ifShowAvatars}} + + + + + + + + +
  SCM AccountsGroupsTokens 
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 @@ + + + + + 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; } -- 2.39.5