PageRenderTime 27ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/jira-project/jira-components/jira-webapp/src/main/webapp/includes/jira/admin/application/app-role.js

https://bitbucket.org/ahmed_bilal_360factors/jira7-core
JavaScript | 1137 lines | 904 code | 90 blank | 143 comment | 84 complexity | 2f755893678a1f71947e991fd58e28b7 MD5 | raw file
Possible License(s): Apache-2.0
  1. define("jira/admin/application/grouppicker",
  2. [
  3. 'jira/util/formatter',
  4. "jquery",
  5. "underscore",
  6. "backbone",
  7. "jira/ajs/select/single-select",
  8. "jira/ajs/list/group-descriptor",
  9. "jira/ajs/list/item-descriptor",
  10. "wrm/context-path"
  11. ],
  12. function(
  13. formatter,
  14. $,
  15. _,
  16. Backbone,
  17. SingleSelect,
  18. GroupDescriptor,
  19. ItemDescriptor,
  20. wrmContextPath
  21. ) {
  22. "use strict";
  23. var templates = JIRA.Templates.Admin.ApplicationAccess;
  24. var Marionette = Backbone.Marionette;
  25. var contextPath = wrmContextPath();
  26. /**
  27. * A `Marionette.ItemView` for a group selector.
  28. *
  29. * Its a `SingleSelect` that makes calls to the group picker resource to find the groups.
  30. *
  31. * @extends Marionette.ItemView
  32. */
  33. return Marionette.ItemView.extend({
  34. initialize: function () {
  35. this._exclude = [];
  36. },
  37. onShow: function () {
  38. this.select = new SingleSelect({
  39. element: this.ui.groups,
  40. itemAttrDisplayed: "label",
  41. showDropdownButton: true,
  42. ajaxOptions: {
  43. data: function (query) {
  44. return {
  45. query: query
  46. };
  47. },
  48. url: contextPath + "/rest/api/2/groups/picker",
  49. query: true, // keep going back to the server for each keystroke
  50. formatResponse: this._format.bind(this)
  51. }
  52. });
  53. this.select.showErrorMessage = $.noop;
  54. },
  55. tagName: "div",
  56. template: templates.groupSingleSelect,
  57. ui: {
  58. groups: ".ss-group-picker"
  59. },
  60. events: {
  61. "selected .ss-group-picker": "_changed"
  62. },
  63. _changed: function () {
  64. this.options.bus.trigger("selectGroup", this.ui.groups.val()[0]);
  65. this.select.clear();
  66. },
  67. excludeGroups: function () {
  68. this._exclude = _.union(this._exclude, _.toArray(arguments));
  69. },
  70. includeGroups: function () {
  71. this._exclude = _.difference(this._exclude, _.toArray(arguments));
  72. },
  73. _format: function (data) {
  74. var descriptorArgs = {weight: 0};
  75. var exclude = this._exclude;
  76. var total = data.total;
  77. var items = [];
  78. _.each(data.groups, function (group) {
  79. if (_.contains(exclude, group.name)) {
  80. total = total - 1;
  81. } else {
  82. items.push(new ItemDescriptor({
  83. value: group.name, // value of item added to select
  84. label: group.name, // title of lozenge
  85. html: group.html,
  86. highlighted: true
  87. }));
  88. }
  89. });
  90. if (items.length < total) {
  91. descriptorArgs.label = formatter.I18n.getText("application.access.configuration.groups.partial", items.length, total);
  92. }
  93. var groupDescriptor = new GroupDescriptor(descriptorArgs);
  94. _.each(items, function (item) {
  95. groupDescriptor.addItem(item);
  96. });
  97. return [groupDescriptor];
  98. },
  99. serializeData: function(){
  100. return {
  101. id: _.uniqueId()
  102. };
  103. },
  104. showLoading: function () {
  105. var instance = this;
  106. if (this.select && !this._loading) {
  107. this._loading = $(templates.loading());
  108. this._timeout = window.setTimeout(function () {
  109. instance.ui.groups.before(instance._loading);
  110. }, 100);
  111. }
  112. return this;
  113. },
  114. hideLoading: function () {
  115. if (this._loading) {
  116. window.clearTimeout(this._timeout);
  117. this._timeout = null;
  118. this._loading.remove();
  119. delete this._loading;
  120. }
  121. return this;
  122. }
  123. });
  124. });
  125. define("jira/admin/application/approleeditor",
  126. [
  127. "jquery",
  128. "underscore",
  129. "backbone",
  130. "jira/skate",
  131. "jira/admin/application/grouppicker",
  132. "wrm/data",
  133. "aui/inline-dialog2",
  134. "jira/admin/application/application-role-labels"
  135. ],
  136. function($, _, Backbone, skate, GroupPicker, wrmData) {
  137. "use strict";
  138. var templates = JIRA.Templates.Admin.ApplicationAccess;
  139. var upgradeJIRAUrl = wrmData.claim("com.atlassian.jira.web.action.admin.application-access:upgrade-jira-url");
  140. var reduceUserCountUrl = wrmData.claim("com.atlassian.jira.web.action.admin.application-access:reduce-user-count-url");
  141. var managingGroupsUrl = wrmData.claim("com.atlassian.jira.web.action.admin.application-access:managing-groups-url");
  142. var Marionette = Backbone.Marionette;
  143. var Bus = function () {
  144. return _.extend(this, Backbone.Events);
  145. };
  146. /**
  147. * Represents a group in a ApplicationRole.
  148. */
  149. var ApplicationRoleGroupModel = Backbone.Model.extend({
  150. defaults: {
  151. name: null,
  152. isDefault: false,
  153. canRemove: true,
  154. canToggle: true
  155. },
  156. idAttribute: "name",
  157. disable: function () {
  158. this.set({
  159. canRemove: false,
  160. canToggle: false
  161. });
  162. },
  163. enable: function () {
  164. this.set({
  165. canToggle: true,
  166. canRemove: true
  167. });
  168. }
  169. });
  170. /**
  171. * Represents the set of groups in a ApplicationRole.
  172. */
  173. var ApplicationRoleGroupCollection = Backbone.Collection.extend({
  174. model: ApplicationRoleGroupModel
  175. });
  176. /**
  177. * Represents a ApplicationRole.
  178. */
  179. var ApplicationRoleModel = Backbone.Model.extend({
  180. defaults: {
  181. name: null,
  182. groups: null
  183. }
  184. });
  185. /**
  186. * View to render when no groups exist for a ApplicationRole.
  187. */
  188. var ApplicationRoleEditorEmptyRowView = Marionette.ItemView.extend({
  189. template: templates.roleEditorTableEmpty,
  190. tagName: "tr"
  191. });
  192. /**
  193. * A controller that deals with showing warning dialogs.
  194. */
  195. var WarningDialogController = Marionette.Controller.extend({
  196. initialize: function(options) {
  197. this.bus = options.bus;
  198. this.model = options.model;
  199. this.listenTo(this.bus, "showDefaultGroupWarning", this._showDefaultGroupWarning);
  200. this.listenTo(this.bus, "showAddGroupWarning", this._showAddGroupWarning);
  201. },
  202. _getApplicationRoleId: function () {
  203. return this.model.collection.applicationRoleModel.get("key") + "-" + this.model.cid;
  204. },
  205. getDefaultGroupWarningId: function () {
  206. return "group-reuse-" + this._getApplicationRoleId() + "-default-warning";
  207. },
  208. getAddGroupWarningId: function() {
  209. return "group-reuse-" + this._getApplicationRoleId() + "-add-warning";
  210. },
  211. _showAddGroupWarning: function(groupName) {
  212. if (this.model.get("name") === groupName) {
  213. this._showWarningDialog(this.getAddGroupWarningId());
  214. }
  215. },
  216. _showDefaultGroupWarning: function(groupName) {
  217. if (this.model.get("name") === groupName) {
  218. this._showWarningDialog(this.getDefaultGroupWarningId());
  219. }
  220. },
  221. _showWarningDialog: function(warningDialogId) {
  222. var warningDialog = $("#" + warningDialogId);
  223. var warningDialogEl = warningDialog[0];
  224. // We need to force skate to initialize the component, as skate lazy loads components
  225. skate.init(warningDialogEl);
  226. warningDialog.one("aui-layer-hide", function(e) {
  227. e.preventDefault();
  228. warningDialog.remove();
  229. });
  230. // TODO: Once AUI-4155 fix is deployed, check if we can remove this setTimeout
  231. setTimeout(function(){
  232. warningDialogEl.open = true;
  233. }, 0);
  234. }
  235. });
  236. /**
  237. * View to render for each group in a ApplicationRole.
  238. */
  239. var ApplicationRoleEditorRowView = Marionette.ItemView.extend({
  240. template: templates.roleEditorRow,
  241. tagName: "tr",
  242. events: {
  243. "click .application-role-remove": "_remove",
  244. "change .application-role-default-input": "_toggleDefault"
  245. },
  246. modelEvents: {
  247. "change:isDefault change:canToggle change:canRemove change:userCount": "render"
  248. },
  249. initialize: function (options) {
  250. this.bus = options.bus;
  251. this.warningController = new WarningDialogController({ bus: this.bus, model: this.model });
  252. },
  253. onClose: function() {
  254. this.warningController.stopListening();
  255. },
  256. _remove: function (e) {
  257. e.preventDefault();
  258. this.bus.trigger("removeGroup", this.model.id);
  259. },
  260. _toggleDefault: function (e) {
  261. var dialog = document.getElementById(this.warningController.getDefaultGroupWarningId());
  262. if (dialog) {
  263. dialog.open = false;
  264. }
  265. e.preventDefault();
  266. this.bus.trigger("toggleDefault", this.model.id);
  267. },
  268. _getAppsContainingGroup: function(appRoles, groupName) {
  269. return _.pairs(appRoles).filter(function (e) {
  270. return _.contains(e[1], groupName);
  271. }).map(function (e) {
  272. return e[0];
  273. });
  274. },
  275. serializeData: function serializeGroupData() {
  276. var applicationRoleModel = this.model.collection.applicationRoleModel;
  277. var reusedGroups = applicationRoleModel.get("defaultGroupsExistingInAnyOtherRoles");
  278. var appsAndDefaultRoles = applicationRoleModel.get("appsAndDefaultRoles");
  279. var groupName = this.model.get("name");
  280. var appsReusingThisGroup = this._getAppsContainingGroup(reusedGroups, groupName);
  281. var appsWhereGroupIsDefault = this._getAppsContainingGroup(appsAndDefaultRoles, groupName);
  282. return _.extend(this.model.toJSON(), {
  283. appsReusingGroup : appsReusingThisGroup,
  284. appsWhereGroupIsDefault: appsWhereGroupIsDefault,
  285. roleKey: this.model.collection.applicationRoleModel.get("key"),
  286. roleName: this.model.collection.applicationRoleModel.get("name"),
  287. defaultGroupWarningId : this.warningController.getDefaultGroupWarningId(),
  288. addGroupWarningId : this.warningController.getAddGroupWarningId(),
  289. managingGroupsUrl : managingGroupsUrl
  290. });
  291. }
  292. });
  293. /**
  294. * The view of the groups in a ApplicationRole.
  295. */
  296. var ApplicationRoleEditorTableView = Marionette.CompositeView.extend({
  297. template: templates.roleEditorTable,
  298. itemView: ApplicationRoleEditorRowView,
  299. itemViewContainer: "tbody",
  300. emptyView: ApplicationRoleEditorEmptyRowView,
  301. modelEvents: {
  302. "change:userCount change:numberOfSeats change:remainingSeats change:hasUnlimitedSeats change:defaultGroupsExistingInAnyOtherRoles change:selectedByDefault": "render"
  303. },
  304. collectionEvents: {
  305. "change:isDefault": "render"
  306. },
  307. initialize: function (options) {
  308. this.bus = options.bus;
  309. this.itemViewOptions = {bus: this.bus};
  310. },
  311. _getDialogId: function() {
  312. return 'user-count-details-' + this.model.get("key");
  313. },
  314. serializeData: function () {
  315. var hasDefaultGroup = this.collection.some(function(group){
  316. return group.get("isDefault");
  317. });
  318. return _.extend({
  319. hasDefaultGroup: hasDefaultGroup,
  320. upgradeJIRAUrl: upgradeJIRAUrl,
  321. reduceUserCountUrl: reduceUserCountUrl,
  322. dialogId: this._getDialogId()
  323. }, this.model.toJSON());
  324. }
  325. });
  326. /**
  327. * The view of an editor for a ApplicationRole. It is the composition of a `ApplicationRoleEditorTableView` and a group
  328. * selector.
  329. */
  330. var ApplicationRoleEditorView = Marionette.Layout.extend({
  331. template: templates.roleEditor,
  332. regions: {
  333. table: ".application-role-editor-container",
  334. groupSelector: ".application-role-selector-container"
  335. },
  336. events: {
  337. "submit .application-role-editor-form": "_submit"
  338. },
  339. onShow: function () {
  340. this.table.show(new ApplicationRoleEditorTableView(_.pick(this.options, "model", "collection", "bus")));
  341. var groupPicker = this.groupPicker = new GroupPicker(_.pick(this.options, "bus"));
  342. groupPicker.excludeGroups.apply(groupPicker, this.options.collection.pluck("name"));
  343. this.groupSelector.show(groupPicker);
  344. this.listenTo(this.options.bus, "removeGroup", function (groupName) {
  345. groupPicker.includeGroups(groupName);
  346. });
  347. this.listenTo(this.options.bus, "selectGroup", function (groupName) {
  348. groupPicker.excludeGroups(groupName);
  349. this.options.bus.trigger("addGroup", groupName);
  350. });
  351. },
  352. _submit: function (e) {
  353. e.preventDefault();
  354. },
  355. showLoading: function () {
  356. this.groupPicker && this.groupPicker.showLoading();
  357. return this;
  358. },
  359. hideLoading: function () {
  360. this.groupPicker && this.groupPicker.hideLoading();
  361. return this;
  362. }
  363. });
  364. /**
  365. * An object that can be used to render and edit a single ApplicationRole.
  366. *
  367. * @param {function} options.setRole Called by the editor to save the current state of the role. It must be of the
  368. * form `function(groups: Array, defaultGroups: Array)`.
  369. * @param {Object} options.role The current state of the role. For example,
  370. * `{name: "name", groups: ["a"], defaultGroups: ["a"]}`.
  371. */
  372. var ApplicationRoleEditor = function (options) {
  373. this._put = options.setRole || $.noop;
  374. this.IO = options.IO;
  375. var roleModel = this._toModel(options.data || {});
  376. this._groups = roleModel.get("groups");
  377. this._active = 0;
  378. this.bus = options.bus || new Bus();
  379. this.view = new ApplicationRoleEditorView({
  380. model: roleModel,
  381. collection: this._groups,
  382. bus: this.bus
  383. });
  384. this.listenTo(this.bus, "removeGroup", this._removeGroup);
  385. this.listenTo(this.bus, "toggleDefault", this._toggleDefault);
  386. this.listenTo(this.bus, "addGroup", this._addGroup);
  387. _.each(this._getGroupNames(), this._loadUserCountForGroup, this);
  388. };
  389. _.extend(ApplicationRoleEditor.prototype, Backbone.Events, {
  390. _toggleDefault: function (defaultKey) {
  391. var groupNames = this._getGroupNames();
  392. var defaultGroupNames = this._getDefaultGroupNames();
  393. var group = this._groups.get(defaultKey);
  394. if (group) {
  395. var wasDefault = group.get("isDefault");
  396. if (wasDefault) {
  397. defaultGroupNames = _.without(defaultGroupNames, defaultKey);
  398. if (defaultGroupNames.length === 1) {
  399. //We only have one default which means we need to make sure it cannot be removed or edited.
  400. this._groups.get(defaultGroupNames[0]).disable();
  401. }
  402. } else {
  403. defaultGroupNames.push(defaultKey);
  404. if(defaultGroupNames.length > 1) {
  405. //We now have more than one default. This means that other defaults can be removed and edited.
  406. _.each(defaultGroupNames, function (groupName) {
  407. this._groups.get(groupName).enable();
  408. }, this);
  409. } else {
  410. //This is now the only default. Make sure that it cannot be edited or removed.
  411. group.disable();
  412. }
  413. }
  414. group.set("isDefault", !wasDefault);
  415. this._setRole(groupNames, defaultGroupNames).done(function(){
  416. if (!wasDefault && this._canShowDefaultGroupWarning(defaultKey)) {
  417. this.bus.trigger("showDefaultGroupWarning", defaultKey);
  418. }
  419. }.bind(this));
  420. }
  421. },
  422. _canShowDefaultGroupWarning: function(groupName) {
  423. var defaultGroupsInOtherRoles = this._groups.applicationRoleModel.get("defaultGroupsExistingInAnyOtherRoles");
  424. return _.contains(_.flatten(_.values(defaultGroupsInOtherRoles)), groupName);
  425. },
  426. _removeGroup: function (groupName) {
  427. var existingGroup = this._groups.get(groupName);
  428. var groupNames = this._getGroupNames();
  429. var defaultGroupNames = this._getDefaultGroupNames();
  430. if (existingGroup && existingGroup.get("canRemove")) {
  431. var remainingGroups = _.without(groupNames, groupName);
  432. var remainingDefaultGroups = _.without(defaultGroupNames, groupName);
  433. this._groups.remove(existingGroup);
  434. if (remainingDefaultGroups.length === 1) {
  435. //If only one default is left; disable all interaction (i.e. no remove, no toggle default).
  436. this._groups.get(remainingDefaultGroups[0]).disable();
  437. } else if (remainingGroups.length === 1) {
  438. //If its the last group then we need to make sure it can't be removed. The group can only be
  439. //non-default and removable at this point. As it is the last group it cannot be removed but it
  440. //can be made default.
  441. this._groups.get(remainingGroups[0]).set("canRemove", false);
  442. }
  443. this._setRole(remainingGroups, remainingDefaultGroups);
  444. }
  445. },
  446. _addGroup: function (groupName) {
  447. var existingGroup = this._groups.get(groupName);
  448. if (!existingGroup) {
  449. var groupNames = this._getGroupNames();
  450. groupNames.push(groupName);
  451. //We need to check if a non-default && non-removable group can be made removable. The only time
  452. //such a group exists is when it is the only group. In all other cases we want to leave the state
  453. //of current group(s) alone.
  454. if (this._groups.size() === 1) {
  455. var onlyGroup = this._groups.at(0);
  456. if (!onlyGroup.get("canRemove") && !onlyGroup.get("isDefault")) {
  457. onlyGroup.set("canRemove", true);
  458. }
  459. }
  460. this._groups.add({
  461. name: groupName,
  462. isDefault: false,
  463. //The group can only be removed if there are more than group currently.
  464. canRemove: groupNames.length > 1,
  465. canToggle: true});
  466. this._loadUserCountForGroup(groupName);
  467. this._setRole(groupNames, this._getDefaultGroupNames()).done(function() {
  468. if (this._getApplicationsWhereTheRoleIsDefault(groupName,
  469. this._groups.applicationRoleModel.get("appsAndDefaultRoles")).length > 0) {
  470. this.bus.trigger("showAddGroupWarning", groupName);
  471. }
  472. }.bind(this));
  473. }
  474. },
  475. /** Load the user count for a group, and update the model */
  476. _loadUserCountForGroup: function(groupName) {
  477. this.IO.getGroupDetails(groupName).pipe(function (groupResponse) {
  478. var group = this._groups.get(groupResponse.name);
  479. if (group) {
  480. group.set("userCount", groupResponse.users.size);
  481. }
  482. }.bind(this));
  483. },
  484. _getApplicationsWhereTheRoleIsDefault: function(groupName, roles) {
  485. return _.pairs(roles).filter(function (e) {
  486. return _.contains(e[1], groupName);
  487. }).map(function (e) {
  488. return e[0];
  489. });
  490. },
  491. _setRole: function(groupNames, defaultGroupNames) {
  492. if (this._active === 0) {
  493. this.view.showLoading();
  494. }
  495. this._active++;
  496. return this._put(groupNames || [], defaultGroupNames || []).always(function (roleModelUpdateResult) {
  497. this._active = Math.max(0, this._active - 1);
  498. if (this._active === 0) {
  499. this.view.hideLoading();
  500. }
  501. if (roleModelUpdateResult) {
  502. this.view.model.set({
  503. remainingSeats: roleModelUpdateResult.remainingSeats,
  504. numberOfSeats: roleModelUpdateResult.numberOfSeats,
  505. userCount: roleModelUpdateResult.userCount,
  506. hasUnlimitedSeats: roleModelUpdateResult.hasUnlimitedSeats
  507. });
  508. }
  509. }.bind(this));
  510. },
  511. _getDefaultGroupNames: function () {
  512. return _.map(this._groups.where({isDefault: true}), function (item) {
  513. return item.get("name");
  514. });
  515. },
  516. _getGroupNames: function () {
  517. return this._groups.pluck("name");
  518. },
  519. /**
  520. * Converts the raw role data into a `ApplicationRoleModel`.
  521. *
  522. * @param {Object} data the raw data. For example, {name: "name", groups: ["a"], defaultGroups: ["a"]}.
  523. * @returns {ApplicationRoleModel}
  524. */
  525. _toModel: function (data) {
  526. var collection = new ApplicationRoleGroupCollection();
  527. var groups = data.groups || [];
  528. var defaultGroups = _.intersection(groups, data.defaultGroups || []);
  529. collection.add(_.map(groups, function (groupName) {
  530. var isDefault = _.contains(defaultGroups, groupName);
  531. return new ApplicationRoleGroupModel({
  532. isDefault: isDefault,
  533. name: groupName,
  534. /*
  535. * A group can be:
  536. * 1. Always made default not matter what.
  537. * 2. Always made non-default provided there is at least one other default.
  538. */
  539. canToggle: !isDefault || defaultGroups.length > 1,
  540. /**
  541. * A group can always be removed provided:
  542. * 1. There is more than one group; and
  543. * 2. Its not a default or its not the last default.
  544. */
  545. canRemove: groups.length > 1 && (!isDefault || defaultGroups.length > 1)
  546. });
  547. }));
  548. var applicationRoleModel = new ApplicationRoleModel({
  549. key: data.key,
  550. name: data.name || "",
  551. groups: collection,
  552. defined: data.defined,
  553. selectedByDefault: data.selectedByDefault,
  554. userCount: data.userCount,
  555. userCountDescription: data.userCountDescription,
  556. remainingSeats: data.remainingSeats,
  557. numberOfSeats: data.numberOfSeats,
  558. hasUnlimitedSeats: data.hasUnlimitedSeats,
  559. defaultGroupsExistingInAnyOtherRoles: data.defaultGroupsExistingInAnyOtherRoles || {},
  560. appsAndDefaultRoles: data.appsAndDefaultRoles || {}
  561. });
  562. collection.applicationRoleModel = applicationRoleModel;
  563. return applicationRoleModel;
  564. }
  565. });
  566. return ApplicationRoleEditor;
  567. });
  568. define("jira/admin/application/approleseditor",
  569. [
  570. 'jira/util/formatter',
  571. 'jira/util/logger',
  572. "jira/jquery/deferred",
  573. "jquery",
  574. "underscore",
  575. "backbone",
  576. "jira/ajs/ajax/smart-ajax/web-sudo",
  577. "jira/dialog/error-dialog",
  578. "jira/admin/application/approleeditor",
  579. "jira/admin/application/defaults",
  580. 'wrm/context-path'
  581. ],
  582. function(
  583. formatter,
  584. logger,
  585. Deferred,
  586. $,
  587. _,
  588. Backbone,
  589. WebSudo,
  590. ErrorDialog,
  591. RoleEditor,
  592. ApplicationDefaults,
  593. wrmContextPath) {
  594. "use strict";
  595. var templates = JIRA.Templates.Admin.ApplicationAccess;
  596. var Marionette = Backbone.Marionette;
  597. var contextPath = wrmContextPath();
  598. /**
  599. * Generic error handler.
  600. */
  601. var errorHandler = function (xhr, reason) {
  602. //Aborts are fine, lets not report them.
  603. if (reason !== "abort") {
  604. //We just display a reload error on bad request. This means that the groups are out of sync
  605. //with what is currently available on the server. A reload of the page will fix this. We don't use
  606. //the error message from the REST request because it may not relate the the action that user is performing
  607. //(e.g. it may report that group1 is invalid even though the user is adding group3).
  608. if (!xhr || xhr.status === 400) {
  609. new ErrorDialog({
  610. message: formatter.I18n.getText("application.access.configuration.out.of.date"),
  611. mode: "warning"
  612. }).show();
  613. } else {
  614. ErrorDialog.openErrorDialogForXHR(xhr);
  615. }
  616. }
  617. };
  618. /**
  619. * Compares the passed two strings in a case-insensitive manner.
  620. *
  621. * @param {String} a left-hand side of the comparison.
  622. * @param {String} b right-hand side of the comparison.
  623. * @returns {number} Returns < 0 when a < b, > 0 when a > b or 0 when a == b.
  624. */
  625. var localeCompare = function (a, b) {
  626. a = a || "";
  627. b = b || "";
  628. return a.localeCompare(b);
  629. };
  630. var RolesIO = function (options) {
  631. this._queued = [];
  632. this._requests = {};
  633. this._defaultFail = options.defaultFail || null;
  634. this._current = null;
  635. this._aborted = false;
  636. this._websudo = false;
  637. };
  638. /**
  639. * An abstraction for all the IO needed to the server.
  640. */
  641. _.extend(RolesIO.prototype, {
  642. busy: function () {
  643. return this._current != null || this._queued.length > 0;
  644. },
  645. sudoVisible: function () {
  646. return this._websudo;
  647. },
  648. /**
  649. * Return all the roles on the server.
  650. *
  651. * @returns {jQuery.Deferred}
  652. */
  653. getRoles: function () {
  654. var instance = this;
  655. return this._wrap(this._ajax({
  656. url: this._makeAllUrl(),
  657. dataType: "json"
  658. }).pipe(function (data, statusText, xhr) {
  659. _.each(data, function (item) {
  660. instance._sortGroups(item);
  661. });
  662. return {
  663. applicationRoles: data,
  664. versionHash: xhr.getResponseHeader("ETag")
  665. };
  666. }));
  667. },
  668. /**
  669. * Get group details
  670. * @param groupName the group name
  671. * @returns {jquery.Deferred}
  672. */
  673. getGroupDetails: function (groupName) {
  674. return this._wrap(this._ajax({
  675. url: this._makeGroupUrl(groupName),
  676. dataType: "json"
  677. }));
  678. },
  679. /**
  680. * Update the passed role on the server.
  681. *
  682. * @param roleKey the id of the role to update.
  683. * @param groups the groups to store for the role.
  684. * @param defaultGroups the default groups for the role.
  685. * @param selectedByDefault a default role on user creation.
  686. * @param versionHash version of data received from server in last update.
  687. *
  688. * @returns {jQuery.Deferred}
  689. */
  690. putRole: function (roleKey, groups, defaultGroups, selectedByDefault, versionHash) {
  691. var data = {
  692. groups: _.toArray(groups),
  693. defaultGroups: _.toArray(defaultGroups),
  694. selectedByDefault: selectedByDefault
  695. };
  696. return this._wrap(this._ajaxForPut(roleKey, {
  697. url: this._makeRoleUrl(roleKey),
  698. dataType: "json",
  699. type: "PUT",
  700. contentType: "application/json",
  701. headers: {"If-Match": versionHash},
  702. data: JSON.stringify(data)
  703. })).pipe(this._sortGroups);
  704. },
  705. putRoles: function(collection) {
  706. var instance = this;
  707. return this._wrap(this._ajaxForPut("all", {
  708. url: this._makeAllUrl(),
  709. dataType: "json",
  710. type: "PUT",
  711. contentType: "application/json",
  712. headers: {"If-Match": collection.versionHash},
  713. data: JSON.stringify(collection)
  714. })).pipe(function (data, statusText, xhr) {
  715. _.each(data, function (item) {
  716. instance._sortGroups(item);
  717. });
  718. return {
  719. applicationRoles: data,
  720. versionHash: xhr.getResponseHeader("ETag")
  721. };
  722. });
  723. },
  724. _sortGroups: function (data) {
  725. if (data && data.groups) {
  726. data.groups.sort(localeCompare);
  727. }
  728. return data;
  729. },
  730. _makeGroupUrl: function (groupName) {
  731. return contextPath + "/rest/api/2/group?groupname=" + encodeURIComponent(groupName);
  732. },
  733. /**
  734. * Create a URL for the passed ROLE.
  735. *
  736. * @returns {string} the URL for the passed ROLE.
  737. * @private
  738. */
  739. _makeRoleUrl: function (roleKey) {
  740. return contextPath + "/rest/api/2/applicationrole/" + roleKey;
  741. },
  742. /**
  743. * Create a URL to GET all roles.
  744. *
  745. * @returns {string} the URL for all roles.
  746. * @private
  747. */
  748. _makeAllUrl: function () {
  749. return contextPath + "/rest/api/2/applicationrole";
  750. },
  751. abort: function () {
  752. this._current && this._current.result.reject(null, "abort");
  753. _.each(this._requests, function (item) {
  754. item.result.reject(null, "abort");
  755. });
  756. this._current = null;
  757. this._queued = [];
  758. this._requests = {};
  759. this._aborted = true;
  760. },
  761. _activate: function (request) {
  762. this._current = request;
  763. var ajax = this._ajax(request.options, {
  764. beforeShow: function () {
  765. that._websudo = true;
  766. }
  767. });
  768. var that = this;
  769. ajax.fail(function () {
  770. //Reject current request.
  771. request.result.rejectWith.call(request.result, this, _.toArray(arguments));
  772. //Abort all others.
  773. that._current = null;
  774. that.abort();
  775. });
  776. ajax.done(function () {
  777. request.result.resolveWith.call(request.result, this, _.toArray(arguments));
  778. that._dequeue();
  779. });
  780. ajax.always(function () {
  781. that._websudo = false;
  782. });
  783. },
  784. _dequeue: function () {
  785. this._current = null;
  786. var next = this._queued.shift();
  787. if (next) {
  788. var request = this._requests[next];
  789. delete this._requests[next];
  790. this._activate(request);
  791. } else {
  792. logger.trace("role.put.finished");
  793. }
  794. },
  795. _ajaxForPut: function (roleKey, options) {
  796. if (this._aborted) {
  797. return Deferred().reject(null, "abort");
  798. }
  799. var newRequest = {
  800. roleKey: roleKey,
  801. result: Deferred(),
  802. options: options
  803. };
  804. if (this._current == null) {
  805. this._activate(newRequest);
  806. } else {
  807. var oldRequest = this._requests[roleKey];
  808. if (oldRequest) {
  809. oldRequest.result.reject(null, "abort");
  810. } else {
  811. this._queued.push(roleKey);
  812. }
  813. this._requests[roleKey] = newRequest;
  814. }
  815. return newRequest.result.promise();
  816. },
  817. _ajax: function (options, dialog) {
  818. return WebSudo.makeWebSudoRequest(options, dialog || {});
  819. },
  820. _wrap: function (def) {
  821. if (def && this._defaultFail) {
  822. def.fail(this._defaultFail);
  823. }
  824. return def;
  825. }
  826. });
  827. var ApplicationRoleModel = Backbone.Model.extend({
  828. defaults: {
  829. name: null,
  830. groups: null,
  831. defaultGroups: null,
  832. selectedByDefault: false
  833. },
  834. idAttribute: "key",
  835. update: function(data) {
  836. data = _.extend(this.toJSON(), data);
  837. return this.collection.IO.putRole(data.key, data.groups, data.defaultGroups, data.selectedByDefault,
  838. this.collection.versionHash).done(function(result) {
  839. _.pairs(result).forEach(function(pair) {
  840. this.set.apply(this, pair);
  841. }, this);
  842. }.bind(this));
  843. },
  844. defaultGroupsExistingInAnyOtherRoles: function () {
  845. var that = this;
  846. var otherApplications = this.collection.models.filter(function (model) {
  847. return model !== that;
  848. });
  849. var groupsByApplication = _.object(otherApplications.map(function (e) {
  850. return e.get("name");
  851. }), otherApplications.map(function (e) {
  852. return _.intersection(that.get("defaultGroups"), e.get("groups"));
  853. }));
  854. return _.object(_.pairs(groupsByApplication).filter(function (value) {
  855. return !_.isEmpty(value[1]);
  856. }));
  857. },
  858. /**
  859. * @returns {Object} mapping applications to array of default groups, for example
  860. * `Object { "JIRA Software" : { "jira-administrators", "jira-users" } }`
  861. */
  862. appsAndDefaultRoles: function () {
  863. var that = this;
  864. var otherApplications = this.collection.models.filter(function (model) {
  865. return model !== that;
  866. });
  867. var groupsByApplication = _.object(otherApplications.map(function (e) {
  868. return e.get("name");
  869. }), otherApplications.map(function (e) {
  870. return e.get("defaultGroups");
  871. }));
  872. return groupsByApplication;
  873. }
  874. });
  875. var ApplicationRoleCollection = Backbone.Collection.extend({
  876. model: ApplicationRoleModel,
  877. versionHash: null,
  878. initialize: function(models, options) {
  879. this.IO = options.IO;
  880. this._fetched = new Deferred();
  881. this.IO.getRoles().done(function (data) {
  882. this.versionHash = data.versionHash;
  883. this.reset(data.applicationRoles);
  884. }.bind(this)).fail(function() {
  885. this._fetched.reject();
  886. }.bind(this));
  887. this.once("reset", function() {
  888. this._fetched.resolve();
  889. });
  890. },
  891. parse: function(response) {
  892. return response.applicationRoles;
  893. },
  894. whenFetched: function() {
  895. return this._fetched.promise();
  896. },
  897. reload: function() {
  898. return this.IO.getRoles().done(function (data) {
  899. this.set(data.applicationRoles);
  900. this.versionHash = data.versionHash;
  901. }.bind(this));
  902. },
  903. updateDefaults: function() {
  904. return this.IO.putRoles(this).done(function (data) {
  905. this.versionHash = data.versionHash;
  906. }.bind(this));
  907. }
  908. });
  909. /**
  910. * View to render when there are no ApplicationRoles.
  911. */
  912. var ApplicationRolesEditorEmptyView = Marionette.ItemView.extend({
  913. tagName: "div",
  914. template: templates.noRoles
  915. });
  916. /**
  917. * View to render to view and edit all ApplicationRoles.
  918. */
  919. var ApplicationRolesEditorView = Marionette.CollectionView.extend({
  920. itemView: RoleEditor,
  921. tagName: "div",
  922. emptyView: ApplicationRolesEditorEmptyView,
  923. initialize: function(){
  924. this.updateEditors = this.options.updateEditors || $.noop;
  925. },
  926. collectionEvents: {
  927. "change" : "updateEditors"
  928. },
  929. buildItemView: function (model, ItemView) {
  930. if (ItemView === this.itemView) {
  931. return this.options.buildRoleEditor(model);
  932. } else {
  933. //It's the empty view. Just do the default.
  934. return Marionette.CollectionView.prototype.buildItemView.apply(this, arguments);
  935. }
  936. }
  937. });
  938. /**
  939. * View to render while waiting for REST response from server.
  940. */
  941. var ApplicationRoleEditorLoadingView = Marionette.ItemView.extend({
  942. template: templates.roleEditorEmpty,
  943. onShow: function () {
  944. var view = this;
  945. this.timeout = window.setTimeout(function () {
  946. view.ui.icon.css({visibility: "visible"});
  947. }, 250);
  948. },
  949. onClose: function () {
  950. window.clearTimeout(this.timeout);
  951. },
  952. ui: {
  953. icon: ".icon"
  954. }
  955. });
  956. /**
  957. * An object that can be used to render and edit all ApplicationRoles
  958. *
  959. * @param {Element|jQuery} options.el The element to render the editor into.
  960. */
  961. return function (options) {
  962. if (!options.el) {
  963. return;
  964. }
  965. var $el = $(options.el);
  966. if (!$el.length){
  967. return;
  968. }
  969. var region = new Marionette.Region({
  970. el: $el
  971. });
  972. $el.addClass("loading");
  973. region.show(new ApplicationRoleEditorLoadingView());
  974. var IO = options.IO || new RolesIO({
  975. defaultFail: errorHandler
  976. });
  977. var collection = new ApplicationRoleCollection([], { IO: IO });
  978. var applicationDefaults = new ApplicationDefaults(collection);
  979. collection.whenFetched().then(function() {
  980. var view = new ApplicationRolesEditorView({
  981. collection: collection,
  982. buildRoleEditor: function (model) {
  983. var id = model.id;
  984. return new RoleEditor({
  985. data: _.extend(model.toJSON(), {
  986. defaultGroupsExistingInAnyOtherRoles: model.defaultGroupsExistingInAnyOtherRoles(),
  987. appsAndDefaultRoles: model.appsAndDefaultRoles()
  988. }),
  989. IO: IO,
  990. setRole: function (groups, defaultGroups) {
  991. var roleModel = collection.get({id: model.get("key")});
  992. if (roleModel) {
  993. var updateDfd = roleModel.update({
  994. id: id,
  995. groups: groups,
  996. defaultGroups: defaultGroups
  997. });
  998. return updateDfd.pipe(function (roleModelUpdateResult) {
  999. collection.reload();
  1000. return roleModelUpdateResult;
  1001. });
  1002. } else {
  1003. return new Deferred().reject();
  1004. }
  1005. }
  1006. }).view;
  1007. },
  1008. updateEditors: function() {
  1009. // we need to get snapshot for all applications and update information about seats
  1010. region.currentView.children.each(function (roleEditor) {
  1011. var roleKey = roleEditor.model.get("key");
  1012. var roleData = collection.get(roleKey).toJSON();
  1013. var roleModel = collection.get(roleKey);
  1014. if (roleData) {
  1015. roleEditor.model.set({
  1016. userCount: roleData.userCount,
  1017. numberOfSeats: roleData.numberOfSeats,
  1018. remainingSeats: roleData.remainingSeats,
  1019. defaultGroupsExistingInAnyOtherRoles: roleModel.defaultGroupsExistingInAnyOtherRoles(),
  1020. appsAndDefaultRoles: roleModel.appsAndDefaultRoles(),
  1021. selectedByDefault: roleData.selectedByDefault
  1022. });
  1023. }
  1024. });
  1025. logger.trace("role.editors.updated");
  1026. }
  1027. });
  1028. region.show(view);
  1029. }).always(function (){
  1030. $el.removeClass("loading");
  1031. });
  1032. var ouload = window.onbeforeunload;
  1033. window.onbeforeunload = function () {
  1034. var result = ouload && ouload.call(window);
  1035. if (!result) {
  1036. if (IO.busy()) {
  1037. if (!IO.sudoVisible()) {
  1038. result = formatter.I18n.getText("application.access.configuration.active.ajax");
  1039. } else {
  1040. IO.abort();
  1041. }
  1042. }
  1043. }
  1044. return result || void 0;
  1045. };
  1046. };
  1047. });