PageRenderTime 55ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 1ms

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

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