diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2015-11-27 11:30:34 +0100 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2015-11-27 13:52:21 +0100 |
commit | dae5bf58a4d228047caadfd5f2fe2c149cb266c8 (patch) | |
tree | fcc9a1a2bc5445ab688f286d68cffd445b0b9d89 | |
parent | d32e10df85f49769060b5485cfb3d117e14bec2c (diff) | |
download | sonarqube-dae5bf58a4d228047caadfd5f2fe2c149cb266c8.tar.gz sonarqube-dae5bf58a4d228047caadfd5f2fe2c149cb266c8.zip |
SONAR-7050 SONAR-7051 SONAR-7052 add ability to generate, list and revoke user tokens
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> </th>{{/ifShowAvatars}} + <th class="nowrap"> </th> + <th class="nowrap">SCM Accounts</th> + <th class="nowrap">Groups</th> + <th class="nowrap">Tokens</th> + <th class="nowrap"> </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> </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; } |