"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",
--- /dev/null
+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);
+}
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: {
'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 () {
this.showGroups();
},
+ onTokensClick: function (e) {
+ e.preventDefault();
+ this.showTokens();
+ },
+
showMoreScm: function () {
this.scmLimit = 10000;
this.render();
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,
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',
-<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+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
+ });
+ }
+
+});
+
+
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;
}