PageRenderTime 44ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/reviewboard/static/rb/js/accountPrefsPage/views/apiTokensView.js

http://github.com/reviewboard/reviewboard
JavaScript | 605 lines | 348 code | 90 blank | 167 comment | 17 complexity | cdcb25782edc83d9e8355e4e0df6a592 MD5 | raw file
Possible License(s): GPL-2.0
  1. (function() {
  2. var APITokenItem,
  3. APITokenItemCollection,
  4. APITokenItemView,
  5. PolicyEditorView,
  6. SiteAPITokensView,
  7. POLICY_READ_WRITE = 'rw',
  8. POLICY_READ_ONLY = 'ro',
  9. POLICY_CUSTOM = 'custom',
  10. POLICY_LABELS = {};
  11. POLICY_LABELS[POLICY_READ_WRITE] = gettext('Full access');
  12. POLICY_LABELS[POLICY_READ_ONLY] = gettext('Read-only');
  13. POLICY_LABELS[POLICY_CUSTOM] = gettext('Custom');
  14. /*
  15. * Represents an API token in the list.
  16. *
  17. * This provides actions for editing the policy type for the token and
  18. * removing the token.
  19. */
  20. APITokenItem = RB.Config.ResourceListItem.extend({
  21. defaults: _.defaults({
  22. policyType: POLICY_READ_WRITE,
  23. localSiteName: null,
  24. showRemove: true
  25. }, RB.Config.ResourceListItem.prototype.defaults),
  26. syncAttrs: ['id', 'note', 'policy', 'tokenValue'],
  27. /*
  28. * Initializes the item.
  29. *
  30. * This computes the type of policy used, for display, and builds the
  31. * policy actions menu.
  32. */
  33. initialize: function(options) {
  34. var policy,
  35. policyType;
  36. _super(this).initialize.call(this, options);
  37. this.on('change:policyType', this._onPolicyTypeChanged, this);
  38. policy = this.get('policy') || {};
  39. policyType = this._guessPolicyType(policy);
  40. this._policyMenuAction = {
  41. id: 'policy',
  42. label: POLICY_LABELS[policyType],
  43. children: [
  44. this._makePolicyAction(POLICY_READ_WRITE),
  45. this._makePolicyAction(POLICY_READ_ONLY),
  46. this._makePolicyAction(POLICY_CUSTOM, {
  47. id: 'policy-custom',
  48. dispatchOnClick: true
  49. })
  50. ]
  51. };
  52. this.actions.unshift(this._policyMenuAction);
  53. this.set('policyType', policyType);
  54. },
  55. /*
  56. * Creates an APIToken resource for the given attributes.
  57. */
  58. createResource: function(attrs) {
  59. return new RB.APIToken(_.defaults({
  60. userName: RB.UserSession.instance.get('username'),
  61. localSitePrefix: this.collection.localSitePrefix
  62. }, attrs));
  63. },
  64. /*
  65. * Sets the provided note on the token and saves it.
  66. */
  67. saveNote: function(note, options, context) {
  68. this._saveAttribute('note', note, options, context);
  69. },
  70. /*
  71. * Sets the provided policy on the token and saves it.
  72. */
  73. savePolicy: function(policy, options, context) {
  74. this._saveAttribute('policy', policy, options, context);
  75. },
  76. /*
  77. * Sets an attribute on the token and saves it.
  78. *
  79. * This is a helper function that will set an attribute on the token
  80. * and save it, but only after the token is ready.
  81. */
  82. _saveAttribute: function(attr, value, options, context) {
  83. this.resource.ready({
  84. ready: function() {
  85. this.resource.set(attr, value);
  86. this.resource.save(options, context);
  87. }
  88. }, this);
  89. },
  90. /*
  91. * Guesses the policy type for a given policy definition.
  92. *
  93. * This compares the policy against the built-in versions that
  94. * RB.APIToken provides. If one of them matches, the appropriate
  95. * policy type will be returned. Otherwise, this assumes it's a
  96. * custom policy.
  97. */
  98. _guessPolicyType: function(policy) {
  99. if (_.isEqual(policy, RB.APIToken.defaultPolicies.readOnly)) {
  100. return POLICY_READ_ONLY;
  101. } else if (_.isEqual(policy, RB.APIToken.defaultPolicies.readWrite)) {
  102. return POLICY_READ_WRITE;
  103. } else {
  104. return POLICY_CUSTOM;
  105. }
  106. },
  107. /*
  108. * Creates and returns an action for the policy menu.
  109. *
  110. * This takes a policy type and any options to include with the
  111. * action definition. It will then return a suitable action,
  112. * for display in the policy menu.
  113. */
  114. _makePolicyAction: function(policyType, options) {
  115. return _.defaults({
  116. label: POLICY_LABELS[policyType],
  117. type: 'radio',
  118. name: 'policy-type',
  119. propName: 'policyType',
  120. radioValue: policyType
  121. }, options);
  122. },
  123. /*
  124. * Handler for when the policy type changes.
  125. *
  126. * This will set the policy menu's label to that of the selected
  127. * policy and rebuild the menu.
  128. *
  129. * Then, if not using a custom policy, the built-in policy definition
  130. * matching the selected policy will be saved to the server.
  131. */
  132. _onPolicyTypeChanged: function() {
  133. var policyType = this.get('policyType'),
  134. newPolicy;
  135. this._policyMenuAction.label = POLICY_LABELS[policyType];
  136. this.trigger('actionsChanged');
  137. if (policyType === POLICY_READ_ONLY) {
  138. newPolicy = RB.APIToken.defaultPolicies.readOnly;
  139. } else if (policyType === POLICY_READ_WRITE) {
  140. newPolicy = RB.APIToken.defaultPolicies.readWrite;
  141. } else {
  142. return;
  143. }
  144. if (!_.isEqual(newPolicy, this.get('policy'))) {
  145. this.savePolicy(newPolicy);
  146. }
  147. }
  148. });
  149. /*
  150. * A collection of APITokenItems.
  151. *
  152. * This works like a standard Backbone.Collection, but can also have
  153. * a LocalSite URL prefix attached to it, for use in API calls in
  154. * APITokenItem.
  155. */
  156. APITokenItemCollection = Backbone.Collection.extend({
  157. model: APITokenItem,
  158. initialize: function(models, options) {
  159. this.localSitePrefix = options.localSitePrefix;
  160. }
  161. });
  162. /*
  163. * Renders an APITokenItem to the page, and handles actions.
  164. *
  165. * This will display the information on the given token. Specifically,
  166. * the token value, the note, and the actions.
  167. *
  168. * This also handles deleting the token when the Remove action is clicked,
  169. * and displaying the policy editor when choosing a custom policy.
  170. */
  171. APITokenItemView = Djblets.Config.ListItemView.extend({
  172. EMPTY_NOTE_PLACEHOLDER: gettext('Click to describe this token'),
  173. template: _.template([
  174. '<div class="config-api-token-value"><%- tokenValue %></div>',
  175. '<span class="config-api-token-note"></span>'
  176. ].join('')),
  177. actionHandlers: {
  178. 'delete': '_onRemoveClicked',
  179. 'policy-custom': '_onCustomPolicyClicked'
  180. },
  181. /*
  182. * Initializes the view.
  183. */
  184. initialize: function(options) {
  185. _super(this).initialize.call(this, options);
  186. this._$note = null;
  187. this.listenTo(this.model.resource, 'change:note', this._updateNote);
  188. },
  189. /*
  190. * Renders the view.
  191. */
  192. render: function() {
  193. _super(this).render.call(this);
  194. this._$note = this.$('.config-api-token-note')
  195. .inlineEditor({
  196. editIconClass: 'rb-icon rb-icon-edit'
  197. })
  198. .on({
  199. beginEdit: _.bind(function() {
  200. this._$note.inlineEditor('setValue',
  201. this.model.get('note'));
  202. }, this),
  203. complete: _.bind(function(e, value) {
  204. this.model.saveNote(value);
  205. }, this)
  206. });
  207. this._updateNote();
  208. return this;
  209. },
  210. /*
  211. * Updates the displayed note.
  212. *
  213. * If no note is set, then a placeholder will be shown, informing the
  214. * user that they can edit the note. Otherwise, their note contents
  215. * will be shown.
  216. */
  217. _updateNote: function() {
  218. var note = this.model.resource.get('note');
  219. if (note) {
  220. this._$note
  221. .removeClass('empty')
  222. .text(note);
  223. } else {
  224. this._$note
  225. .addClass('empty')
  226. .text(this.EMPTY_NOTE_PLACEHOLDER);
  227. }
  228. },
  229. /*
  230. * Handler for when the "Custom" policy action is clicked.
  231. *
  232. * This displays the policy editor, allowing the user to edit a
  233. * custom policy for the token.
  234. *
  235. * The previously selected policy type is passed along to the editor,
  236. * so that the editor can revert to it if the user cancels.
  237. */
  238. _onCustomPolicyClicked: function() {
  239. var view = new PolicyEditorView({
  240. model: this.model,
  241. prevPolicyType: this.model.previous('policyType')
  242. });
  243. view.render();
  244. return false;
  245. },
  246. /*
  247. * Handler for when the Remove action is clicked.
  248. *
  249. * This will prompt for confirmation before removing the token from
  250. * the server.
  251. */
  252. _onRemoveClicked: function() {
  253. $('<p/>')
  254. .html(gettext('This will prevent clients using this token when authenticating.'))
  255. .modalBox({
  256. title: gettext('Are you sure you want to remove this token?'),
  257. buttons: [
  258. $('<input type="button"/>')
  259. .val(gettext('Cancel')),
  260. $('<input type="button" class="danger" />')
  261. .val(gettext('Remove'))
  262. .click(_.bind(function() {
  263. this.model.resource.destroy();
  264. }, this))
  265. ]
  266. });
  267. }
  268. });
  269. /*
  270. * Provides an editor for constructing or modifying a custom policy definition.
  271. *
  272. * This renders as a modalBox with a CodeMirror editor inside of it. The
  273. * editor is set to allow easy editing of a JSON payload, complete with
  274. * lintian checking. Only valid policy payloads can be saved to the server.
  275. */
  276. PolicyEditorView = Backbone.View.extend({
  277. id: 'custom_policy_editor',
  278. template: _.template([
  279. '<p><%= instructions %></p>',
  280. '<textarea/>'
  281. ].join('')),
  282. /*
  283. * Initializes the editor.
  284. */
  285. initialize: function(options) {
  286. this.prevPolicyType = options.prevPolicyType;
  287. this._codeMirror = null;
  288. this._$policy = null;
  289. this._$saveButtons = null;
  290. },
  291. /*
  292. * Renders the editor.
  293. *
  294. * The CodeMirror editor will be set up and configured, and then the
  295. * view will be placed inside a modalBox.
  296. */
  297. render: function() {
  298. var policy = this.model.get('policy');
  299. if (_.isEmpty(this.model.get('policy'))) {
  300. policy = RB.APIToken.defaultPolicies.custom;
  301. }
  302. this.$el.html(this.template({
  303. instructions: interpolate(
  304. gettext('You can limit access to the API through a custom policy. See the <a href="%s" target="_blank">documentation</a> on how to write policies.'),
  305. [MANUAL_URL + 'webapi/2.0/api-token-policy/'])
  306. }));
  307. this._$policy = this.$('textarea')
  308. .val(JSON.stringify(policy, null, ' '));
  309. this.$el.modalBox({
  310. title: gettext('Custom Token Access Policy'),
  311. buttons: [
  312. $('<input type="button"/>')
  313. .val(gettext('Cancel'))
  314. .click(_.bind(this.cancel, this)),
  315. $('<input type="button" class="save-button"/>')
  316. .val(gettext('Save and continue editing'))
  317. .click(_.bind(function() {
  318. this.save();
  319. return false;
  320. }, this)),
  321. $('<input type="button" class="btn primary save-button"/>')
  322. .val(gettext('Save'))
  323. .click(_.bind(function() {
  324. this.save(true);
  325. return false;
  326. }, this))
  327. ]
  328. });
  329. this._$saveButtons = this.$el.modalBox('buttons').find('.save-button');
  330. this._codeMirror = CodeMirror.fromTextArea(this._$policy[0], {
  331. mode: 'application/json',
  332. lineNumbers: true,
  333. lineWrapping: true,
  334. matchBrackets: true,
  335. lint: {
  336. onUpdateLinting: _.bind(this._onUpdateLinting, this)
  337. },
  338. gutters: ['CodeMirror-lint-markers']
  339. });
  340. this._codeMirror.focus();
  341. },
  342. /*
  343. * Removes the policy editor from the page.
  344. */
  345. remove: function() {
  346. this.$el.modalBox('destroy');
  347. },
  348. /*
  349. * Cancels the editor.
  350. *
  351. * The previously-selected policy type will be set on the model.
  352. */
  353. cancel: function() {
  354. this.model.set('policyType', this.prevPolicyType);
  355. },
  356. /*
  357. * Saves the editor.
  358. *
  359. * The policy will be saved to the server for immediate use.
  360. */
  361. save: function(closeOnSave) {
  362. var policyStr = this._codeMirror.getValue().strip(),
  363. policy;
  364. try {
  365. policy = JSON.parse(policyStr);
  366. } catch (e) {
  367. alert(interpolate(
  368. gettext('There is a syntax error in your policy: %s'),
  369. [e]));
  370. return false;
  371. }
  372. this.model.savePolicy(policy, {
  373. success: function() {
  374. if (closeOnSave) {
  375. this.remove();
  376. }
  377. },
  378. error: function(model, xhr) {
  379. if (xhr.errorPayload.err.code === 105 &&
  380. xhr.errorPayload.fields.policy) {
  381. alert(xhr.errorPayload.fields.policy);
  382. } else {
  383. alert(xhr.errorPayload.err.msg);
  384. }
  385. }
  386. }, this);
  387. return false;
  388. },
  389. /*
  390. * Handler for when lintian checking has run.
  391. *
  392. * This will disable the save buttons if there are any lintian errors.
  393. */
  394. _onUpdateLinting: function(annotationsNotSorted) {
  395. this._$saveButtons.prop('disabled', annotationsNotSorted.length > 0);
  396. }
  397. });
  398. /*
  399. * Renders and manages a list of global or per-LocalSite API tokens.
  400. *
  401. * This will display all provided API tokens in a list, optionally labeled
  402. * by Local Site name. These can be removed or edited, or new tokens generated
  403. * through a "Generate a new API token" link.
  404. */
  405. SiteAPITokensView = Backbone.View.extend({
  406. className: 'config-site-api-tokens',
  407. template: _.template([
  408. '<% if (name) { %>',
  409. ' <h3><%- name %></h3>',
  410. '<% } %>',
  411. '<div class="api-tokens box-recessed">',
  412. ' <div class="generate-api-token config-forms-list-item">',
  413. ' <a href="#"><%- generateText %></a>',
  414. ' </div>',
  415. '</div>'
  416. ].join('')),
  417. events: {
  418. 'click .generate-api-token': '_onGenerateClicked'
  419. },
  420. /*
  421. * Initializes the view.
  422. *
  423. * This will construct the collection of tokens and construct
  424. * a list for the ListView.
  425. */
  426. initialize: function(options) {
  427. this.localSiteName = options.localSiteName;
  428. this.localSitePrefix = options.localSitePrefix;
  429. this.collection = new APITokenItemCollection(options.apiTokens, {
  430. localSitePrefix: this.localSitePrefix
  431. });
  432. this.apiTokensList = new Djblets.Config.List({}, {
  433. collection: this.collection
  434. });
  435. this._listView = null;
  436. },
  437. /*
  438. * Renders the view.
  439. *
  440. * This will render the list of API token items, along with a link
  441. * for generating new tokens.
  442. */
  443. render: function() {
  444. this._listView = new Djblets.Config.ListView({
  445. ItemView: APITokenItemView,
  446. animateItems: true,
  447. model: this.apiTokensList
  448. });
  449. this.$el.html(this.template({
  450. name: this.localSiteName,
  451. generateText: gettext('Generate a new API token')
  452. }));
  453. this._listView.render().$el.prependTo(this.$('.api-tokens'));
  454. return this;
  455. },
  456. /*
  457. * Handler for when the "Generate a new API token" link is clicked.
  458. *
  459. * This creates a new API token on the server and displays it in the list.
  460. */
  461. _onGenerateClicked: function() {
  462. var apiToken = new RB.APIToken({
  463. localSitePrefix: this.localSitePrefix,
  464. userName: RB.UserSession.instance.get('username')
  465. });
  466. apiToken.save({
  467. success: function() {
  468. this.collection.add({
  469. resource: apiToken
  470. });
  471. }
  472. }, this);
  473. return false;
  474. }
  475. });
  476. /*
  477. * Renders and manages a page of API tokens.
  478. *
  479. * This will take the provided tokens and group them into SiteAPITokensView
  480. * instances, one per Local Site and one for the global tokens.
  481. */
  482. RB.APITokensView = Backbone.View.extend({
  483. template: _.template([
  484. '<div class="api-tokens-list" />'
  485. ].join('')),
  486. /*
  487. * Initializes the view.
  488. */
  489. initialize: function(options) {
  490. this.apiTokens = options.apiTokens;
  491. this._$listsContainer = null;
  492. this._apiTokenViews = [];
  493. },
  494. /*
  495. * Renders the view.
  496. *
  497. * This will set up the elements and the list of SiteAPITokensViews.
  498. */
  499. render: function() {
  500. this.$el.html(this.template());
  501. this._$listsContainer = this.$('.api-tokens-list');
  502. _.each(this.apiTokens, function(info, localSiteName) {
  503. var view = new SiteAPITokensView({
  504. localSiteName: localSiteName,
  505. localSitePrefix: info.localSitePrefix,
  506. apiTokens: info.tokens
  507. });
  508. view.$el.appendTo(this._$listsContainer);
  509. view.render();
  510. this._apiTokenViews.push(view);
  511. }, this);
  512. return this;
  513. }
  514. });
  515. })();