aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/permission-templates
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-09-14 13:45:40 +0200
committerTeryk Bellahsene <teryk.bellahsene@sonarsource.com>2015-09-28 16:35:02 +0200
commit24b80b59a96988e27c6aae6234d7d00c22ebc262 (patch)
tree8dd2a34ce8ed3add3256b391c5113c5f122b1a6d /server/sonar-web/src/main/js/apps/permission-templates
parent2634b3b9ed7c5601a886fa7ba8e3ad0cdc76c100 (diff)
downloadsonarqube-24b80b59a96988e27c6aae6234d7d00c22ebc262.tar.gz
sonarqube-24b80b59a96988e27c6aae6234d7d00c22ebc262.zip
SONAR-6801 rewrite the permission templates page
Diffstat (limited to 'server/sonar-web/src/main/js/apps/permission-templates')
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/app.jsx23
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/create-view.jsx26
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/delete-view.jsx28
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/form-view.js25
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/groups-view.js52
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/header.jsx23
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/main.jsx76
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/permission-template-defaults.jsx24
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/permission-template-set-defaults.jsx47
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/permission-template.jsx116
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/permission-templates.jsx28
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/permissions-header.jsx27
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-delete.hbs13
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-form.hbs28
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-groups.hbs10
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-users.hbs10
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/update-view.jsx27
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/users-view.js49
18 files changed, 632 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/app.jsx b/server/sonar-web/src/main/js/apps/permission-templates/app.jsx
new file mode 100644
index 00000000000..44ff6954266
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/app.jsx
@@ -0,0 +1,23 @@
+import $ from 'jquery';
+import React from 'react';
+import Main from './main';
+
+let topQualifiers = [];
+
+export default {
+ start(options) {
+ $.when(
+ window.requestMessages(),
+ this.requestTopQualifiers()
+ ).then(() => {
+ var el = document.querySelector(options.el);
+ React.render(<Main topQualifiers={topQualifiers}/>, el);
+ });
+ },
+
+ requestTopQualifiers() {
+ return $.get(baseUrl + '/api/navigation/global').done(r => {
+ topQualifiers = r.qualifiers;
+ });
+ }
+};
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/create-view.jsx b/server/sonar-web/src/main/js/apps/permission-templates/create-view.jsx
new file mode 100644
index 00000000000..733e970069f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/create-view.jsx
@@ -0,0 +1,26 @@
+import FormView from './form-view';
+import {createPermissionTemplate} from '../../api/permissions';
+
+export default FormView.extend({
+ sendRequest: function () {
+ var that = this;
+ this.disableForm();
+ return createPermissionTemplate({
+ data: {
+ name: this.$('#permission-template-name').val(),
+ description: this.$('#permission-template-description').val(),
+ projectKeyPattern: this.$('#permission-template-project-key-pattern').val()
+ },
+ statusCode: {
+ // do not show global error
+ 400: null
+ }
+ }).done(function () {
+ that.options.refresh();
+ that.destroy();
+ }).fail(function (jqXHR) {
+ that.enableForm();
+ that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
+ });
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/delete-view.jsx b/server/sonar-web/src/main/js/apps/permission-templates/delete-view.jsx
new file mode 100644
index 00000000000..60b81fb701c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/delete-view.jsx
@@ -0,0 +1,28 @@
+import ModalForm from '../../components/common/modal-form';
+import {deletePermissionTemplate} from '../../api/permissions';
+import './templates';
+
+export default ModalForm.extend({
+ template: Templates['permission-templates-delete'],
+
+ onFormSubmit: function () {
+ ModalForm.prototype.onFormSubmit.apply(this, arguments);
+ this.sendRequest();
+ },
+
+ sendRequest: function () {
+ var that = this;
+ return deletePermissionTemplate({
+ data: { templateId: this.model.id },
+ statusCode: {
+ // do not show global error
+ 400: null
+ }
+ }).done(function () {
+ that.options.refresh();
+ that.destroy();
+ }).fail(function (jqXHR) {
+ that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
+ });
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/form-view.js b/server/sonar-web/src/main/js/apps/permission-templates/form-view.js
new file mode 100644
index 00000000000..71d0802a790
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/form-view.js
@@ -0,0 +1,25 @@
+import ModalForm from '../../components/common/modal-form';
+import './templates';
+
+export default ModalForm.extend({
+ template: Templates['permission-templates-form'],
+
+ onRender: function () {
+ this._super();
+ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' });
+ this.$('#create-custom-measure-metric').select2({
+ width: '250px',
+ minimumResultsForSearch: 20
+ });
+ },
+
+ onDestroy: function () {
+ this._super();
+ this.$('[data-toggle="tooltip"]').tooltip('destroy');
+ },
+
+ onFormSubmit: function () {
+ ModalForm.prototype.onFormSubmit.apply(this, arguments);
+ this.sendRequest();
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/groups-view.js b/server/sonar-web/src/main/js/apps/permission-templates/groups-view.js
new file mode 100644
index 00000000000..21831f45c31
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/groups-view.js
@@ -0,0 +1,52 @@
+import Modal from '../../components/common/modals';
+import '../../components/common/select-list';
+import './templates';
+
+function getSearchUrl (permission, permissionTemplate) {
+ return baseUrl + '/api/permissions/template_groups?ps=100&permission=' + permission +
+ '&templateId=' + permissionTemplate.id;
+}
+
+export default Modal.extend({
+ template: Templates['permission-templates-groups'],
+
+ onRender: function () {
+ this._super();
+ new window.SelectList({
+ el: this.$('#permission-templates-groups'),
+ width: '100%',
+ readOnly: false,
+ focusSearch: false,
+ format: function (item) {
+ return item.name;
+ },
+ queryParam: 'q',
+ searchUrl: getSearchUrl(this.options.permission, this.options.permissionTemplate),
+ selectUrl: baseUrl + '/api/permissions/add_group_to_template',
+ deselectUrl: baseUrl + '/api/permissions/remove_group_from_template',
+ extra: {
+ permission: this.options.permission,
+ templateId: this.options.permissionTemplate.id
+ },
+ selectParameter: 'groupName',
+ selectParameterValue: 'name',
+ parse: function (r) {
+ this.more = false;
+ return r.groups;
+ }
+ });
+ },
+
+ onDestroy: function () {
+ if (this.options.refresh) {
+ this.options.refresh();
+ }
+ this._super();
+ },
+
+ serializeData: function () {
+ return _.extend(Modal.prototype.serializeData.apply(this, arguments), {
+ permissionTemplateName: this.options.permissionTemplate.name
+ });
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/header.jsx b/server/sonar-web/src/main/js/apps/permission-templates/header.jsx
new file mode 100644
index 00000000000..0325d4bf6cb
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/header.jsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import CreateView from './create-view';
+
+export default React.createClass({
+ onCreate(e) {
+ e.preventDefault();
+ new CreateView({
+ refresh: this.props.refresh
+ }).render();
+ },
+
+ render() {
+ return (
+ <header id="project-permissions-header" className="page-header">
+ <h1 className="page-title">{window.t('permission_templates.page')}</h1>
+ <div className="page-actions">
+ <button onClick={this.onCreate}>Create</button>
+ </div>
+ <p className="page-description">{window.t('roles.page.description')}</p>
+ </header>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/main.jsx b/server/sonar-web/src/main/js/apps/permission-templates/main.jsx
new file mode 100644
index 00000000000..1a0abfc8ead
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/main.jsx
@@ -0,0 +1,76 @@
+import _ from 'underscore';
+import React from 'react';
+import Header from './header';
+import PermissionTemplates from './permission-templates';
+import {getPermissionTemplates} from '../../api/permissions';
+
+const PERMISSIONS_ORDER = ['user', 'codeviewer', 'issueadmin', 'admin'];
+
+export default React.createClass({
+ propTypes: {
+ topQualifiers: React.PropTypes.array.isRequired
+ },
+
+ getInitialState() {
+ return { permissions: [], permissionTemplates: [] };
+ },
+
+ componentDidMount() {
+ this.requestPermissions();
+ },
+
+ sortPermissions(permissions) {
+ return _.sortBy(permissions, p => PERMISSIONS_ORDER.indexOf(p.key));
+ },
+
+ mergePermissionsToTemplates(permissionTemplates, basePermissions) {
+ return permissionTemplates.map(permissionTemplate => {
+ // it's important to keep the order of the permission template's permissions
+ // the same as the order of base permissions
+ let permissions = basePermissions.map(basePermission => {
+ let projectPermission = _.findWhere(permissionTemplate.permissions, { key: basePermission.key });
+ return _.extend({ usersCount: 0, groupsCount: 0 }, basePermission, projectPermission);
+ });
+ return _.extend({}, permissionTemplate, { permissions: permissions });
+ });
+ },
+
+ mergeDefaultsToTemplates(permissionTemplates, defaultTemplates = []) {
+ return permissionTemplates.map(permissionTemplate => {
+ let defaultFor = [];
+ defaultTemplates.forEach(defaultTemplate => {
+ if (defaultTemplate.templateId === permissionTemplate.id) {
+ defaultFor.push(defaultTemplate.qualifier);
+ }
+ });
+ return _.extend({}, permissionTemplate, { defaultFor });
+ });
+ },
+
+ requestPermissions() {
+ getPermissionTemplates().done(r => {
+ let permissions = this.sortPermissions(r.permissions);
+ let permissionTemplates = this.mergePermissionsToTemplates(r.permissionTemplates, permissions);
+ let permissionTemplatesWithDefaults = this.mergeDefaultsToTemplates(permissionTemplates, r.defaultTemplates);
+ this.setState({
+ permissionTemplates: permissionTemplatesWithDefaults,
+ permissions: permissions
+ });
+ });
+ },
+
+ render() {
+ return (
+ <div className="page">
+ <Header
+ refresh={this.requestPermissions}/>
+
+ <PermissionTemplates
+ permissionTemplates={this.state.permissionTemplates}
+ permissions={this.state.permissions}
+ topQualifiers={this.props.topQualifiers}
+ refresh={this.requestPermissions}/>
+ </div>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/permission-template-defaults.jsx b/server/sonar-web/src/main/js/apps/permission-templates/permission-template-defaults.jsx
new file mode 100644
index 00000000000..2cfb51b4123
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/permission-template-defaults.jsx
@@ -0,0 +1,24 @@
+import _ from 'underscore';
+import React from 'react';
+import QualifierIcon from '../../components/shared/qualifier-icon';
+
+export default React.createClass({
+ propTypes: {
+ permissionTemplate: React.PropTypes.object.isRequired
+ },
+
+ render() {
+ if (_.size(this.props.permissionTemplate.defaultFor) === 0) {
+ return null;
+ }
+ let defaults = this.props.permissionTemplate.defaultFor.map(qualifier => {
+ return <li key={qualifier}><QualifierIcon qualifier={qualifier}/>&nbsp;{window.t('qualifier', qualifier)}</li>;
+ });
+ return (
+ <ul className="list-inline nowrap spacer-bottom">
+ <li>Default for</li>
+ {defaults}
+ </ul>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/permission-template-set-defaults.jsx b/server/sonar-web/src/main/js/apps/permission-templates/permission-template-set-defaults.jsx
new file mode 100644
index 00000000000..6bda07a46b2
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/permission-template-set-defaults.jsx
@@ -0,0 +1,47 @@
+import _ from 'underscore';
+import React from 'react';
+import {setDefaultPermissionTemplate} from '../../api/permissions';
+import QualifierIcon from '../../components/shared/qualifier-icon';
+
+export default React.createClass({
+ propTypes: {
+ permissionTemplate: React.PropTypes.object.isRequired,
+ topQualifiers: React.PropTypes.array.isRequired,
+ refresh: React.PropTypes.func.isRequired
+ },
+
+ getAvailableQualifiers() {
+ return _.difference(this.props.topQualifiers, this.props.permissionTemplate.defaultFor);
+ },
+
+ setDefault(qualifier, e) {
+ e.preventDefault();
+ setDefaultPermissionTemplate(this.props.permissionTemplate.id, qualifier).done(() => this.props.refresh());
+ },
+
+ render() {
+ let availableQualifiers = this.getAvailableQualifiers();
+ if (availableQualifiers.length === 0) {
+ return null;
+ }
+
+ let qualifiers = availableQualifiers.map(qualifier => {
+ return (
+ <li key={qualifier}>
+ <a onClick={this.setDefault.bind(this, qualifier)} href="#">
+ Set Default for <QualifierIcon qualifier={qualifier}/> {window.t('qualifier', qualifier)}
+ </a>
+ </li>
+ );
+ });
+
+ return (
+ <span className="dropdown little-spacer-right">
+ <button className="dropdown-toggle" data-toggle="dropdown">
+ Set Default <i className="icon-dropdown"></i>
+ </button>
+ <ul className="dropdown-menu">{qualifiers}</ul>
+ </span>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/permission-template.jsx b/server/sonar-web/src/main/js/apps/permission-templates/permission-template.jsx
new file mode 100644
index 00000000000..71645b34d75
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/permission-template.jsx
@@ -0,0 +1,116 @@
+import _ from 'underscore';
+import React from 'react';
+import Defaults from './permission-template-defaults';
+import SetDefaults from './permission-template-set-defaults';
+import UsersView from './users-view';
+import GroupsView from './groups-view';
+import UpdateView from './update-view';
+import DeleteView from './delete-view';
+
+export default React.createClass({
+ propTypes: {
+ permissionTemplate: React.PropTypes.object.isRequired,
+ topQualifiers: React.PropTypes.array.isRequired,
+ refresh: React.PropTypes.func.isRequired
+ },
+
+ showGroups(permission, e) {
+ e.preventDefault();
+ new GroupsView({
+ permission: permission,
+ permissionTemplate: this.props.permissionTemplate,
+ refresh: this.props.refresh
+ }).render();
+ },
+
+ showUsers(permission, e) {
+ e.preventDefault();
+ new UsersView({
+ permission: permission,
+ permissionTemplate: this.props.permissionTemplate,
+ refresh: this.props.refresh
+ }).render();
+ },
+
+ onUpdate(e) {
+ e.preventDefault();
+ new UpdateView({
+ model: new Backbone.Model(this.props.permissionTemplate),
+ refresh: this.props.refresh
+ }).render();
+ },
+
+ onDelete(e) {
+ e.preventDefault();
+ new DeleteView({
+ model: new Backbone.Model(this.props.permissionTemplate),
+ refresh: this.props.refresh
+ }).render();
+ },
+
+ renderAssociation() {
+ let projectKeyPattern = this.props.permissionTemplate.projectKeyPattern;
+ if (!projectKeyPattern) {
+ return null;
+ }
+ return <div className="spacer-bottom">Project Key Pattern: <code>{projectKeyPattern}</code></div>;
+ },
+
+ renderDeleteButton() {
+ if (_.size(this.props.permissionTemplate.defaultFor) > 0) {
+ return null;
+ }
+ return <button onClick={this.onDelete} className="button-red">Delete</button>;
+ },
+
+ render() {
+ let permissions = this.props.permissionTemplate.permissions.map(p => {
+ return (
+ <td key={p.key}>
+ <table>
+ <tr>
+ <td className="spacer-right">Users</td>
+ <td className="spacer-left bordered-left">{p.usersCount}</td>
+ <td className="spacer-left">
+ <a onClick={this.showUsers.bind(this, p.key)} className="icon-bullet-list" title="Update Users"
+ data-toggle="tooltip" href="#"></a>
+ </td>
+ </tr>
+ <tr>
+ <td className="spacer-right">Groups</td>
+ <td className="spacer-left bordered-left">{p.groupsCount}</td>
+ <td className="spacer-left">
+ <a onClick={this.showGroups.bind(this, p.key)} className="icon-bullet-list" title="Update Users"
+ data-toggle="tooltip" href="#"></a>
+ </td>
+ </tr>
+ </table>
+ </td>
+ );
+ });
+ return (
+ <tr>
+ <td>
+ <strong>{this.props.permissionTemplate.name}</strong>
+ <p className="note little-spacer-top">{this.props.permissionTemplate.description}</p>
+ </td>
+ {permissions}
+ <td className="thin text-right">
+ {this.renderAssociation()}
+ <Defaults permissionTemplate={this.props.permissionTemplate}/>
+ <div className="nowrap">
+ <SetDefaults
+ permissionTemplate={this.props.permissionTemplate}
+ topQualifiers={this.props.topQualifiers}
+ refresh={this.props.refresh}/>
+
+ <div className="button-group">
+ <button onClick={this.onUpdate}>Update</button>
+ {this.renderDeleteButton()}
+ </div>
+ </div>
+ </td>
+ </tr>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/permission-templates.jsx b/server/sonar-web/src/main/js/apps/permission-templates/permission-templates.jsx
new file mode 100644
index 00000000000..a86379e256d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/permission-templates.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import PermissionsHeader from './permissions-header';
+import PermissionTemplate from './permission-template';
+
+export default React.createClass({
+ propTypes:{
+ permissionTemplates: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ permissions: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ topQualifiers: React.PropTypes.array.isRequired,
+ refresh: React.PropTypes.func.isRequired
+ },
+
+ render() {
+ let permissionTemplates = this.props.permissionTemplates.map(p => {
+ return <PermissionTemplate
+ key={p.id}
+ permissionTemplate={p}
+ topQualifiers={this.props.topQualifiers}
+ refresh={this.props.refresh}/>;
+ });
+ return (
+ <table id="permission-templates" className="data zebra">
+ <PermissionsHeader permissions={this.props.permissions}/>
+ <tbody>{permissionTemplates}</tbody>
+ </table>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/permissions-header.jsx b/server/sonar-web/src/main/js/apps/permission-templates/permissions-header.jsx
new file mode 100644
index 00000000000..7768f9ec414
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/permissions-header.jsx
@@ -0,0 +1,27 @@
+import React from 'react';
+
+export default React.createClass({
+ propTypes: {
+ permissions: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
+ },
+
+ render() {
+ let cellWidth = (80 / this.props.permissions.length) + '%';
+ let cells = this.props.permissions.map(p => {
+ return (
+ <th key={p.key} style={{ width: cellWidth }}>
+ {p.name}<br/><span className="small">{p.description}</span>
+ </th>
+ );
+ });
+ return (
+ <thead>
+ <tr>
+ <th style={{ width: '20%' }}>&nbsp;</th>
+ {cells}
+ <th className="thin">&nbsp;</th>
+ </tr>
+ </thead>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-delete.hbs b/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-delete.hbs
new file mode 100644
index 00000000000..178214f7ba0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-delete.hbs
@@ -0,0 +1,13 @@
+<form id="delete-permission-template-form">
+ <div class="modal-head">
+ <h2>Delete Permission Template</h2>
+ </div>
+ <div class="modal-body">
+ <div class="js-modal-messages"></div>
+ Are you sure you want to delete permission template "{{name}}"?
+ </div>
+ <div class="modal-foot">
+ <button id="delete-permission-template-submit" class="button-red">Delete</button>
+ <a href="#" class="js-modal-close" id="delete-permission-template-cancel">Cancel</a>
+ </div>
+</form>
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-form.hbs b/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-form.hbs
new file mode 100644
index 00000000000..3478f988bdd
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-form.hbs
@@ -0,0 +1,28 @@
+<form id="permission-template-form" autocomplete="off">
+ <div class="modal-head">
+ <h2>{{#if id}}Update{{else}}Create{{/if}} Permission Template</h2>
+ </div>
+ <div class="modal-body">
+ <div class="js-modal-messages"></div>
+
+ <div class="modal-field">
+ <label for="permission-template-name">Name<em class="mandatory">*</em></label>
+ <input id="permission-template-name" name="name" type="text" maxlength="256" required value="{{name}}">
+ </div>
+
+ <div class="modal-field">
+ <label for="permission-template-description">Description</label>
+ <textarea id="permission-template-description" name="description" maxlength="4000" rows="5">{{description}}</textarea>
+ </div>
+
+ <div class="modal-field">
+ <label for="permission-template-project-key-pattern">Project Key Pattern</label>
+ <input id="permission-template-project-key-pattern" name="keyPattern" type="text" maxlength="500"
+ value="{{projectKeyPattern}}">
+ </div>
+ </div>
+ <div class="modal-foot">
+ <button id="permission-template-submit">{{#if id}}Update{{else}}Create{{/if}}</button>
+ <a href="#" class="js-modal-close" id="permission-template-cancel">Cancel</a>
+ </div>
+</form>
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-groups.hbs b/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-groups.hbs
new file mode 100644
index 00000000000..3301ece4ca5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-groups.hbs
@@ -0,0 +1,10 @@
+<div class="modal-head">
+ <h2>Update Groups of "{{permissionTemplateName}}"</h2>
+</div>
+<div class="modal-body">
+ <div class="js-modal-messages"></div>
+ <div id="permission-templates-groups"></div>
+</div>
+<div class="modal-foot">
+ <a href="#" class="js-modal-close">Done</a>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-users.hbs b/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-users.hbs
new file mode 100644
index 00000000000..d51f89a5e34
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-users.hbs
@@ -0,0 +1,10 @@
+<div class="modal-head">
+ <h2>Update Users of "{{permissionTemplateName}}"</h2>
+</div>
+<div class="modal-body">
+ <div class="js-modal-messages"></div>
+ <div id="permission-templates-users"></div>
+</div>
+<div class="modal-foot">
+ <a href="#" class="js-modal-close">Done</a>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/update-view.jsx b/server/sonar-web/src/main/js/apps/permission-templates/update-view.jsx
new file mode 100644
index 00000000000..38e546b820d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/update-view.jsx
@@ -0,0 +1,27 @@
+import FormView from './form-view';
+import {updatePermissionTemplate} from '../../api/permissions';
+
+export default FormView.extend({
+ sendRequest: function () {
+ var that = this;
+ this.disableForm();
+ return updatePermissionTemplate({
+ data: {
+ id: this.model.id,
+ name: this.$('#permission-template-name').val(),
+ description: this.$('#permission-template-description').val(),
+ projectKeyPattern: this.$('#permission-template-project-key-pattern').val()
+ },
+ statusCode: {
+ // do not show global error
+ 400: null
+ }
+ }).done(function () {
+ that.options.refresh();
+ that.destroy();
+ }).fail(function (jqXHR) {
+ that.enableForm();
+ that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
+ });
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/users-view.js b/server/sonar-web/src/main/js/apps/permission-templates/users-view.js
new file mode 100644
index 00000000000..31275f25b6f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/permission-templates/users-view.js
@@ -0,0 +1,49 @@
+import Modal from '../../components/common/modals';
+import '../../components/common/select-list';
+import './templates';
+
+export default Modal.extend({
+ template: Templates['permission-templates-users'],
+
+ onRender: function () {
+ this._super();
+ var searchUrl = baseUrl + '/api/permissions/template_users?ps=100&permission=' + this.options.permission +
+ '&templateId=' + this.options.permissionTemplate.id;
+ new window.SelectList({
+ el: this.$('#permission-templates-users'),
+ width: '100%',
+ readOnly: false,
+ focusSearch: false,
+ format: function (item) {
+ return item.name + '<br><span class="note">' + item.login + '</span>';
+ },
+ queryParam: 'q',
+ searchUrl: searchUrl,
+ selectUrl: baseUrl + '/api/permissions/add_user_to_template',
+ deselectUrl: baseUrl + '/api/permissions/remove_user_from_template',
+ extra: {
+ permission: this.options.permission,
+ templateId: this.options.permissionTemplate.id
+ },
+ selectParameter: 'login',
+ selectParameterValue: 'login',
+ parse: function (r) {
+ this.more = false;
+ return r.users;
+ }
+ });
+ },
+
+ onDestroy: function () {
+ if (this.options.refresh) {
+ this.options.refresh();
+ }
+ this._super();
+ },
+
+ serializeData: function () {
+ return _.extend(Modal.prototype.serializeData.apply(this, arguments), {
+ permissionTemplateName: this.options.permissionTemplate.name
+ });
+ }
+});