diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2015-05-20 10:54:59 +0200 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2015-05-22 15:53:42 +0200 |
commit | e009561a5e3210480c7bef0ee20cebbc83d531bc (patch) | |
tree | f12b4c8f21213d882cd3ca1427cc7fc48b2df4b6 /server | |
parent | 06cafc9839cb231a8f71e1d1c41da43ac1dc0139 (diff) | |
download | sonarqube-e009561a5e3210480c7bef0ee20cebbc83d531bc.tar.gz sonarqube-e009561a5e3210480c7bef0ee20cebbc83d531bc.zip |
SONAR-6565 refactor users page
Diffstat (limited to 'server')
45 files changed, 1341 insertions, 162 deletions
diff --git a/server/sonar-web/Gruntfile.coffee b/server/sonar-web/Gruntfile.coffee index b9431a16328..82a5708b8f9 100644 --- a/server/sonar-web/Gruntfile.coffee +++ b/server/sonar-web/Gruntfile.coffee @@ -158,6 +158,10 @@ module.exports = (grunt) -> name: 'apps/markdown/app' out: '<%= ASSETS_PATH %>/js/apps/markdown/app.js' + users: options: + name: 'apps/users/app' + out: '<%= ASSETS_PATH %>/js/apps/users/app.js' + parallel: build: @@ -178,6 +182,7 @@ module.exports = (grunt) -> 'requirejs:nav' 'requirejs:issueFilterWidget' 'requirejs:markdown' + 'requirejs:users' ] casper: options: grunt: true @@ -197,6 +202,7 @@ module.exports = (grunt) -> 'casper:treemap' 'casper:ui' 'casper:workspace' + 'casper:users' ] @@ -254,6 +260,9 @@ module.exports = (grunt) -> '<%= BUILD_PATH %>/js/apps/markdown/templates.js': [ '<%= SOURCE_PATH %>/js/apps/markdown/templates/**/*.hbs' ] + '<%= BUILD_PATH %>/js/apps/users/templates.js': [ + '<%= SOURCE_PATH %>/js/apps/users/templates/**/*.hbs' + ] clean: @@ -299,15 +308,21 @@ module.exports = (grunt) -> port: expressPort testCoverageLight: options: + concise: false verbose: true + 'no-colors': false src: ['src/test/js/**/*<%= grunt.option("spec") %>*.js'] single: options: + concise: false verbose: true + 'no-colors': false src: ['src/test/js/<%= grunt.option("spec") %>-spec.js'] testfile: options: + concise: false verbose: true + 'no-colors': false src: ['<%= grunt.option("file") %>'] apiDocumentation: @@ -340,6 +355,8 @@ module.exports = (grunt) -> src: ['src/test/js/ui*.js'] workspace: src: ['src/test/js/workspace*.js'] + users: + src: ['src/test/js/users*.js'] uglify: build: @@ -402,7 +419,7 @@ module.exports = (grunt) -> tasks: ['copy:js', 'concat:build', 'copy:assets-all-js'] handlebars: - files: '<%= SOURCE_PATH %>/hbs/**/*.hbs' + files: '<%= SOURCE_PATH %>/**/*.hbs' tasks: ['handlebars:build', 'copy:assets-all-js'] diff --git a/server/sonar-web/src/main/js/apps/users/app.js b/server/sonar-web/src/main/js/apps/users/app.js new file mode 100644 index 00000000000..9fa9df3759a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/app.js @@ -0,0 +1,47 @@ +define([ + './layout', + './users', + './header-view', + './search-view', + './list-view', + './list-footer-view' +], function (Layout, Users, HeaderView, SearchView, ListView, ListFooterView) { + + var App = new Marionette.Application(), + init = function (options) { + // Layout + this.layout = new Layout({ el: options.el }); + this.layout.render(); + + // Collection + this.users = new Users(); + + // Header View + this.headerView = new HeaderView({ collection: this.users }); + this.layout.headerRegion.show(this.headerView); + + // Search View + this.searchView = new SearchView({ collection: this.users }); + this.layout.searchRegion.show(this.searchView); + + // List View + this.listView = new ListView({ collection: this.users }); + this.layout.listRegion.show(this.listView); + + // List Footer View + this.listFooterView = new ListFooterView({ collection: this.users }); + this.layout.listFooterRegion.show(this.listFooterView); + + // Go! + this.users.fetch(); + }; + + App.on('start', function (options) { + window.requestMessages().done(function () { + init.call(App, options); + }); + }); + + return App; + +}); diff --git a/server/sonar-web/src/main/js/apps/users/change-password-view.js b/server/sonar-web/src/main/js/apps/users/change-password-view.js new file mode 100644 index 00000000000..cd2a892fc2b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/change-password-view.js @@ -0,0 +1,37 @@ +define([ + 'components/common/modal-form', + './templates' +], function (ModalForm) { + + return ModalForm.extend({ + template: Templates['users-change-password'], + + onFormSubmit: function () { + ModalForm.prototype.onFormSubmit.apply(this, arguments); + this.sendRequest(); + }, + + sendRequest: function () { + var that = this, + password = this.$('#change-user-password-password').val(), + confirmation = this.$('#change-user-password-password-confirmation').val(); + if (password !== confirmation) { + that.showErrors([{ msg: 'New password and its confirmation do not match' }]); + return; + } + this.disableForm(); + return this.model.changePassword(password, { + statusCode: { + // do not show global error + 400: null + } + }).done(function () { + that.close(); + }).fail(function (jqXHR) { + that.enableForm(); + that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/users/create-view.js b/server/sonar-web/src/main/js/apps/users/create-view.js new file mode 100644 index 00000000000..026f8095056 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/create-view.js @@ -0,0 +1,33 @@ +define([ + './user', + './form-view' +], function (User, FormView) { + + return FormView.extend({ + + sendRequest: function () { + var that = this, + user = new User({ + login: this.$('#create-user-login').val(), + name: this.$('#create-user-name').val(), + email: this.$('#create-user-email').val(), + password: this.$('#create-user-password').val(), + scmAccounts: this.getScmAccounts() + }); + this.disableForm(); + return user.save(null, { + statusCode: { + // do not show global error + 400: null + } + }).done(function () { + that.collection.refresh(); + that.close(); + }).fail(function (jqXHR) { + that.enableForm(); + that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/users/deactivate-view.js b/server/sonar-web/src/main/js/apps/users/deactivate-view.js new file mode 100644 index 00000000000..cf4c4654984 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/deactivate-view.js @@ -0,0 +1,32 @@ +define([ + 'components/common/modal-form', + './templates' +], function (ModalForm) { + + return ModalForm.extend({ + template: Templates['users-deactivate'], + + onFormSubmit: function () { + ModalForm.prototype.onFormSubmit.apply(this, arguments); + this.sendRequest(); + }, + + sendRequest: function () { + var that = this, + collection = this.model.collection; + return this.model.destroy({ + wait: true, + statusCode: { + // do not show global error + 400: null + } + }).done(function () { + collection.total--; + that.close(); + }).fail(function (jqXHR) { + that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/users/form-view.js b/server/sonar-web/src/main/js/apps/users/form-view.js new file mode 100644 index 00000000000..2cf2e1ac3f1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/form-view.js @@ -0,0 +1,52 @@ +define([ + 'components/common/modal-form', + './templates' +], function (ModalForm) { + + var $ = jQuery; + + return ModalForm.extend({ + template: Templates['users-form'], + + events: function () { + return _.extend(ModalForm.prototype.events.apply(this, arguments), { + 'click #create-user-add-scm-account': 'onAddScmAccountClick' + }); + }, + + onRender: function () { + ModalForm.prototype.onRender.apply(this, arguments); + this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); + }, + + onClose: function () { + ModalForm.prototype.onClose.apply(this, arguments); + this.$('[data-toggle="tooltip"]').tooltip('destroy'); + }, + + onFormSubmit: function () { + ModalForm.prototype.onFormSubmit.apply(this, arguments); + this.sendRequest(); + }, + + onAddScmAccountClick: function (e) { + e.preventDefault(); + this.addScmAccount(); + }, + + getScmAccounts: function () { + var scmAccounts = this.$('[name="scmAccounts"]').map(function () { + return $(this).val(); + }).toArray(); + return scmAccounts.filter(function (value) { + return !!value; + }); + }, + + addScmAccount: function () { + var fields = this.$('[name="scmAccounts"]'); + fields.first().clone().val('').insertAfter(fields.last()); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/users/header-view.js b/server/sonar-web/src/main/js/apps/users/header-view.js new file mode 100644 index 00000000000..c8b76193b4c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/header-view.js @@ -0,0 +1,25 @@ +define([ + './create-view', + './templates' +], function (CreateView) { + + return Marionette.ItemView.extend({ + template: Templates['users-header'], + + events: { + 'click #users-create': 'onCreateClick' + }, + + onCreateClick: function (e) { + e.preventDefault(); + this.createUser(); + }, + + createUser: function () { + new CreateView({ + collection: this.collection + }).render(); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/users/layout.js b/server/sonar-web/src/main/js/apps/users/layout.js new file mode 100644 index 00000000000..d2b625162e0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/layout.js @@ -0,0 +1,16 @@ +define([ + './templates' +], function () { + + return Marionette.Layout.extend({ + template: Templates['users-layout'], + + regions: { + headerRegion: '#users-header', + searchRegion: '#users-search', + listRegion: '#users-list', + listFooterRegion: '#users-list-footer' + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/users/list-footer-view.js b/server/sonar-web/src/main/js/apps/users/list-footer-view.js new file mode 100644 index 00000000000..968ad0990d5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/list-footer-view.js @@ -0,0 +1,34 @@ +define([ + './templates' +], function () { + + return Marionette.ItemView.extend({ + template: Templates['users-list-footer'], + + collectionEvents: { + 'all': 'render' + }, + + events: { + 'click #users-fetch-more': 'onMoreClick' + }, + + onMoreClick: function (e) { + e.preventDefault(); + this.fetchMore(); + }, + + fetchMore: function () { + this.collection.fetchMore(); + }, + + serializeData: function () { + return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), { + total: this.collection.total, + count: this.collection.length, + more: this.collection.hasMore() + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/users/list-item-view.js b/server/sonar-web/src/main/js/apps/users/list-item-view.js new file mode 100644 index 00000000000..0a3e62ace44 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/list-item-view.js @@ -0,0 +1,86 @@ +define([ + './update-view', + './change-password-view', + './deactivate-view', + './templates' +], function (UpdateView, ChangePasswordView, DeactivateView) { + + return Marionette.ItemView.extend({ + tagName: 'li', + className: 'panel panel-vertical', + template: Templates['users-list-item'], + + events: { + 'click .js-user-more-scm': 'onMoreScmClick', + 'click .js-user-update': 'onUpdateClick', + 'click .js-user-change-password': 'onChangePasswordClick', + 'click .js-user-deactivate': 'onDeactivateClick' + }, + + initialize: function () { + this.scmLimit = 3; + }, + + onRender: function () { + this.$el.attr('data-login', this.model.id); + this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); + }, + + onClose: function () { + this.$('[data-toggle="tooltip"]').tooltip('destroy'); + }, + + onMoreScmClick: function (e) { + e.preventDefault(); + this.showMoreScm(); + }, + + onUpdateClick: function (e) { + e.preventDefault(); + this.updateUser(); + }, + + onChangePasswordClick: function (e) { + e.preventDefault(); + this.changePassword(); + }, + + onDeactivateClick: function (e) { + e.preventDefault(); + this.deactivateUser(); + }, + + showMoreScm: function () { + this.scmLimit = 10000; + this.render(); + }, + + updateUser: function () { + new UpdateView({ + model: this.model, + collection: this.model.collection + }).render(); + }, + + changePassword: function () { + new ChangePasswordView({ + model: this.model, + collection: this.model.collection + }).render(); + }, + + deactivateUser: function () { + new DeactivateView({ model: this.model }).render(); + }, + + serializeData: function () { + var scmAccounts = this.model.get('scmAccounts'), + scmAccountsLimit = scmAccounts.length > this.scmLimit ? this.scmLimit - 1 : this.scmLimit; + return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), { + firstScmAccounts: _.first(scmAccounts, scmAccountsLimit), + moreScmAccountsCount: scmAccounts.length - scmAccountsLimit + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/users/list-view.js b/server/sonar-web/src/main/js/apps/users/list-view.js new file mode 100644 index 00000000000..138c36b7619 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/list-view.js @@ -0,0 +1,11 @@ +define([ + './list-item-view', + './templates' +], function (ListItemView) { + + return Marionette.CollectionView.extend({ + tagName: 'ul', + itemView: ListItemView + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/users/search-view.js b/server/sonar-web/src/main/js/apps/users/search-view.js new file mode 100644 index 00000000000..5129bf5a253 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/search-view.js @@ -0,0 +1,49 @@ +define([ + './templates' +], function () { + + return Marionette.ItemView.extend({ + template: Templates['users-search'], + + events: { + 'submit #users-search-form': 'onFormSubmit', + 'search #users-search-query': 'debouncedOnKeyUp', + 'keyup #users-search-query': 'debouncedOnKeyUp' + }, + + initialize: function () { + this._bufferedValue = null; + this.debouncedOnKeyUp = _.debounce(this.onKeyUp, 400); + }, + + onRender: function () { + this.delegateEvents(); + }, + + onFormSubmit: function (e) { + e.preventDefault(); + this.debouncedOnKeyUp(); + }, + + onKeyUp: function () { + var q = this.getQuery(); + if (q === this._bufferedValue) { + return; + } + this._bufferedValue = this.getQuery(); + if (this.searchRequest != null) { + this.searchRequest.abort(); + } + this.searchRequest = this.search(q); + }, + + getQuery: function () { + return this.$('#users-search-query').val(); + }, + + search: function (q) { + return this.collection.fetch({ reset: true, data: { q: q } }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-change-password.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-change-password.hbs new file mode 100644 index 00000000000..22684806543 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/templates/users-change-password.hbs @@ -0,0 +1,24 @@ +<form id="change-user-password-form" autocomplete="off"> + <div class="modal-head"> + <h2>Change Password</h2> + </div> + <div class="modal-body"> + <div class="js-modal-messages"></div> + <div class="modal-field"> + <label for="change-user-password-password">New Password<em class="mandatory">*</em></label> + {{! keep this fake field to hack browser autofill }} + <input id="change-user-password-password-fake" name="password-fake" type="password" class="hidden"> + <input id="change-user-password-password" name="password" type="password" size="50" maxlength="50" required> + </div> + <div class="modal-field"> + <label for="change-user-password-password-confirmation">Confirm Password<em class="mandatory">*</em></label> + {{! keep this fake field to hack browser autofill }} + <input id="change-user-password-password-confirmation-fake" name="password-confirmation-fake" type="password" class="hidden"> + <input id="change-user-password-password-confirmation" name="password-confirmation" type="password" size="50" maxlength="50" required> + </div> + </div> + <div class="modal-foot"> + <button id="change-user-password-submit">Change</button> + <a href="#" class="js-modal-close" id="change-user-password-cancel">Cancel</a> + </div> +</form> diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-deactivate.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-deactivate.hbs new file mode 100644 index 00000000000..4d92cfd6e72 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/templates/users-deactivate.hbs @@ -0,0 +1,13 @@ +<form id="deactivate-user-form" autocomplete="off"> + <div class="modal-head"> + <h2>Deactivate User</h2> + </div> + <div class="modal-body"> + <div class="js-modal-messages"></div> + Are you sure you want to deactivate "{{name}} ({{login}})"? + </div> + <div class="modal-foot"> + <button id="deactivate-user-submit">Deactivate</button> + <a href="#" class="js-modal-close" id="deactivate-user-cancel">Cancel</a> + </div> +</form> diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-form.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-form.hbs new file mode 100644 index 00000000000..f2637ce8108 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/templates/users-form.hbs @@ -0,0 +1,53 @@ +<form id="create-user-form" autocomplete="off"> + <div class="modal-head"> + <h2>{{#if login}}Update{{else}}Create{{/if}} User</h2> + </div> + <div class="modal-body"> + <div class="js-modal-messages"></div> + {{#unless login}} + <div class="modal-field"> + <label for="create-user-login">Login<em class="mandatory">*</em></label> + {{! keep this fake field to hack browser autofill }} + <input id="create-user-login-fake" name="login-fake" type="text" class="hidden"> + <input id="create-user-login" name="login" type="text" size="50" minlength="2" maxlength="255" required + value="{{login}}"> + <p class="note">Minimum 2 characters</p> + </div> + {{/unless}} + <div class="modal-field"> + <label for="create-user-name">Name<em class="mandatory">*</em></label> + {{! keep this fake field to hack browser autofill }} + <input id="create-user-name-fake" name="name-fake" type="text" class="hidden"> + <input id="create-user-name" name="name" type="text" size="50" maxlength="200" required value="{{name}}"> + </div> + <div class="modal-field"> + <label for="create-user-email">Email</label> + {{! keep this fake field to hack browser autofill }} + <input id="create-user-email-fake" name="email-fake" type="email" class="hidden"> + <input id="create-user-email" name="email" type="email" size="50" maxlength="100" value="{{email}}"> + </div> + {{#unless login}} + <div class="modal-field"> + <label for="create-user-password">Password<em class="mandatory">*</em></label> + {{! keep this fake field to hack browser autofill }} + <input id="create-user-password-fake" name="password-fake" type="password" class="hidden"> + <input id="create-user-password" name="password" type="password" size="50" maxlength="50" required> + </div> + {{/unless}} + <div class="modal-field"> + <label>SCM Accounts</label> + {{#each scmAccounts}} + <input name="scmAccounts" type="text" size="50" maxlength="255" value="{{this}}"> + {{else}} + <input name="scmAccounts" type="text" size="50" maxlength="255"> + {{/each}} + <a id="create-user-add-scm-account" class="icon-plus" href="#" title="Add another SCM account" + data-toggle="tooltip"></a> + <p class="note">Note that login and email are automatically considered as SCM accounts</p> + </div> + </div> + <div class="modal-foot"> + <button id="create-user-submit">{{#if login}}Update{{else}}Create{{/if}}</button> + <a href="#" class="js-modal-close" id="create-user-cancel">Cancel</a> + </div> +</form> diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-header.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-header.hbs new file mode 100644 index 00000000000..e3560039288 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/templates/users-header.hbs @@ -0,0 +1,9 @@ +<header class="page-header"> + <h1 class="page-title">{{t 'users.page'}}</h1> + <div class="page-actions"> + <div class="button-group"> + <button id="users-create">Create User</button> + </div> + </div> + <p class="page-description">{{t 'users.page.description'}}</p> +</header> diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-layout.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-layout.hbs new file mode 100644 index 00000000000..e89130ed737 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/templates/users-layout.hbs @@ -0,0 +1,6 @@ +<div class="page"> + <div id="users-header"></div> + <div id="users-search"></div> + <div id="users-list"></div> + <div id="users-list-footer"></div> +</div> diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-list-footer.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-list-footer.hbs new file mode 100644 index 00000000000..3cf34d7be8f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/templates/users-list-footer.hbs @@ -0,0 +1,6 @@ +<footer class="spacer-top note text-center"> + {{count}}/{{total}} shown + {{#if more}} + <a id="users-fetch-more" class="spacer-left" href="#">show more</a> + {{/if}} +</footer> diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs new file mode 100644 index 00000000000..ce816033e05 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs @@ -0,0 +1,46 @@ +<div class="pull-right big-spacer-left nowrap"> + <a class="js-user-update icon-edit little-spacer-right" title="Update Details" data-toggle="tooltip" href="#"></a> + <a class="js-user-change-password icon-lock little-spacer-right" title="Change Password" data-toggle="tooltip" + href="#"></a> + <a class="js-user-deactivate icon-delete" title="Deactivate" data-toggle="tooltip" href="#"></a> +</div> + +<div class="display-inline-block text-top width-30"> + <div> + <strong class="js-user-name">{{name}}</strong> + <span class="js-user-login note little-spacer-left">{{login}}</span> + </div> + <div class="js-user-email little-spacer-top">{{email}}</div> +</div> + +<div class="display-inline-block text-top big-spacer-left width-25"> + {{#notEmpty scmAccounts}} + <div class="pull-left spacer-right"> + <strong>SCM</strong> + </div> + <ul class="overflow-hidden bordered-left"> + {{#each firstScmAccounts}} + <li class="spacer-left little-spacer-bottom">{{this}}</li> + {{/each}} + {{#gt moreScmAccountsCount 0}} + <li class="spacer-left little-spacer-bottom"> + <a class="js-user-more-scm" href="#">{{moreScmAccountsCount}} more</a> + </li> + {{/gt}} + </ul> + {{/notEmpty}} +</div> + +<div class="display-inline-block text-top big-spacer-left width-25"> + <div class="pull-left spacer-right"> + <strong>Groups</strong> + </div> + <ul class="overflow-hidden bordered-left"> + <li class="spacer-left little-spacer-bottom">sonar-users ?</li> + <li class="spacer-left little-spacer-bottom">sonar-administrators ?</li> + <li class="spacer-left little-spacer-bottom"> + <a class="js-user-more-groups" href="#">? more</a> + <a class="icon-bullet-list spacer-left" title="Update Groups" data-toggle="tooltip" href="#"></a> + </li> + </ul> +</div> diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-search.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-search.hbs new file mode 100644 index 00000000000..38805050b80 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/templates/users-search.hbs @@ -0,0 +1,6 @@ +<div class="panel panel-vertical bordered-bottom spacer-bottom"> + <form id="users-search-form" class="search-box"> + <button id="users-search-submit" class="search-box-submit button-clean"><i class="icon-search"></i></button> + <input id="users-search-query" class="search-box-input" type="search" name="q" placeholder="Search" maxlength="100"> + </form> +</div> diff --git a/server/sonar-web/src/main/js/apps/users/update-view.js b/server/sonar-web/src/main/js/apps/users/update-view.js new file mode 100644 index 00000000000..81497a3a75d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/update-view.js @@ -0,0 +1,30 @@ +define([ + './form-view' +], function (FormView) { + + return FormView.extend({ + + sendRequest: function () { + var that = this; + this.model.set({ + name: this.$('#create-user-name').val(), + email: this.$('#create-user-email').val(), + scmAccounts: this.getScmAccounts() + }); + this.disableForm(); + return this.model.save(null, { + statusCode: { + // do not show global error + 400: null + } + }).done(function () { + that.collection.refresh(); + that.close(); + }).fail(function (jqXHR) { + that.enableForm(); + that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/users/user.js b/server/sonar-web/src/main/js/apps/users/user.js new file mode 100644 index 00000000000..119a27fbfd5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/user.js @@ -0,0 +1,71 @@ +define(function () { + + return Backbone.Model.extend({ + idAttribute: 'login', + + urlRoot: function () { + return baseUrl + '/api/users'; + }, + + defaults: function () { + return { + groupsCount: 0, + scmAccounts: [] + }; + }, + + toQuery: function () { + var q = this.toJSON(); + _.each(q, function (value, key) { + if (_.isArray(value)) { + q[key] = value.join(','); + } + }); + return q; + }, + + isNew: function() { + // server never sends a password + return this.has('password'); + }, + + sync: function (method, model, options) { + var opts = options || {}; + if (method === 'create') { + _.defaults(opts, { + url: this.urlRoot() + '/create', + type: 'POST', + data: _.pick(model.toQuery(), 'login', 'name', 'email', 'password', 'scmAccounts') + }); + } + if (method === 'update') { + _.defaults(opts, { + url: this.urlRoot() + '/update', + type: 'POST', + data: _.pick(model.toQuery(), 'login', 'name', 'email', 'scmAccounts') + }); + } + if (method === 'delete') { + _.defaults(opts, { + url: this.urlRoot() + '/deactivate', + type: 'POST', + data: { login: this.id } + }); + } + return Backbone.ajax(opts); + }, + + changePassword: function (password, options) { + var opts = _.defaults(options || {}, { + url: this.urlRoot() + '/change_password', + type: 'POST', + data: { + login: this.id, + password: password + } + }); + return Backbone.ajax(opts); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/users/users.js b/server/sonar-web/src/main/js/apps/users/users.js new file mode 100644 index 00000000000..adf80b669da --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/users.js @@ -0,0 +1,40 @@ +define([ + './user' +], function (User) { + + return Backbone.Collection.extend({ + model: User, + + url: function () { + return baseUrl + '/api/users/search'; + }, + + parse: function (r) { + this.total = +r.total; + this.p = +r.p; + this.ps = +r.ps; + return r.users; + }, + + fetch: function (options) { + var d = (options && options.data) || {}; + this.q = d.q; + return Backbone.Collection.prototype.fetch.apply(this, arguments); + }, + + fetchMore: function () { + var p = this.p + 1; + return this.fetch({ add: true, remove: false, data: { p: p, ps: this.ps, q: this.q } }); + }, + + refresh: function () { + return this.fetch({ reset: true, data: { q: this.q } }); + }, + + hasMore: function () { + return this.total > this.p * this.ps; + } + + }); + +}); diff --git a/server/sonar-web/src/main/less/components.less b/server/sonar-web/src/main/less/components.less index 58c47a924de..929acdc7e74 100644 --- a/server/sonar-web/src/main/less/components.less +++ b/server/sonar-web/src/main/less/components.less @@ -42,3 +42,4 @@ @import "components/badges"; @import "components/columns"; @import "components/workspace"; +@import "components/search"; diff --git a/server/sonar-web/src/main/less/components/modals.less b/server/sonar-web/src/main/less/components/modals.less index 98fd4a4af1e..49863dbcfcc 100644 --- a/server/sonar-web/src/main/less/components/modals.less +++ b/server/sonar-web/src/main/less/components/modals.less @@ -143,6 +143,7 @@ ul.modal-head-metadata li { } .modal-field input[type=text], +.modal-field input[type=email], .modal-field input[type=password], .modal-field textarea { width: 250px; diff --git a/server/sonar-web/src/main/less/components/search.less b/server/sonar-web/src/main/less/components/search.less new file mode 100644 index 00000000000..48be2ff2905 --- /dev/null +++ b/server/sonar-web/src/main/less/components/search.less @@ -0,0 +1,24 @@ +@import (reference) "../variables"; +@import (reference) "../mixins"; + +.search-box { + position: relative; + font-size: 0; +} + +.search-box-input { + vertical-align: middle; + width: 250px; + border: none !important; + font-size: @baseFontSize; +} + +.search-box-submit { + display: inline-block; + vertical-align: middle; + + .icon-search:before { + color: @secondFontColor; + font-size: @iconSmallFontSize; + } +} diff --git a/server/sonar-web/src/main/less/init/icons.less b/server/sonar-web/src/main/less/init/icons.less index fe27ce965a9..e94718f0f4f 100644 --- a/server/sonar-web/src/main/less/init/icons.less +++ b/server/sonar-web/src/main/less/init/icons.less @@ -310,6 +310,7 @@ a[class^="icon-"], a[class*=" icon-"] { } .icon-bullet-list:before { content: "\f03a"; + font-size: @iconSmallFontSize; } .icon-settings:before { content: "\f015"; @@ -586,6 +587,10 @@ a[class^="icon-"], a[class*=" icon-"] { content: "\f0b0"; font-size: @iconFontSize; } +.icon-lock:before { + content: "\f023"; + font-size: @iconFontSize; +} .icon-issues { display: inline-block; .square(60px); diff --git a/server/sonar-web/src/main/less/init/misc.less b/server/sonar-web/src/main/less/init/misc.less index b766304822a..3cc8526e54d 100644 --- a/server/sonar-web/src/main/less/init/misc.less +++ b/server/sonar-web/src/main/less/init/misc.less @@ -43,6 +43,11 @@ .spacer-bottom { margin-bottom: 8px; } .spacer-top { margin-top: 8px; } +.big-spacer-left { margin-left: 16px; } +.big-spacer-right { margin-right: 16px; } +.big-spacer-bottom { margin-bottom: 16px; } +.big-spacer-top { margin-top: 16px; } + .little-spacer-left { margin-left: 4px; } .little-spacer-right { margin-right: 4px; } .little-spacer-bottom { margin-bottom: 4px; } @@ -62,12 +67,16 @@ td.spacer-top { padding-top: 8px; } .bordered-bottom { border-bottom: 1px solid @barBorderColor; } .bordered-top { border-top: 1px solid @barBorderColor; } +.overflow-hidden { overflow: hidden; } + .width-100 { width: 100%; } .width-80 { width: 80%; } .width-60 { width: 60%; } .width-55 { width: 55%; } .width-50 { width: 50%; } .width-40 { width: 40%; } +.width-30 { width: 30%; } +.width-25 { width: 25%; } .width-20 { width: 20%; } .width-15 { width: 15%; } diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/users_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/users_controller.rb index 779a828b329..ac643f9c0d4 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/users_controller.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/users_controller.rb @@ -54,98 +54,15 @@ class UsersController < ApplicationController end def index - init_users_list - if params[:id] - @user = User.find(params[:id]) - else - @user = User.new - end - end - def create_form - @user = User.new - render :partial => 'users/create_form' end def new render :action => 'new', :layout => 'nonav' end - def edit_form - call_backend do - @user = Internal.users_api.getByLogin(params[:id]) - render :partial => 'users/edit_form', :status => 200 - end - end - - def change_password_form - @user = User.find(params[:id]) - render :partial => 'users/change_password_form', :status => 200 - end - - def update_password - user = User.find(params[:id]) - @user = user - if params[:user][:password].blank? - @errors = message('my_profile.password.empty') - render :partial => 'users/change_password_form', :status => 400 - elsif user.update_attributes(:password => params[:user][:password], :password_confirmation => params[:user][:password_confirmation]) - flash[:notice] = 'Password was successfully updated.' - render :text => 'ok', :status => 200 - else - @errors = user.errors.full_messages.join("<br/>\n") - render :partial => 'users/change_password_form', :status => 400 - end - end - - def update - call_backend do - Internal.users_api.update(params[:user]) - flash[:notice] = 'User was successfully updated.' - render :text => 'ok', :status => 200 - end - end - - def delete - begin - user = User.find(params[:id]) - Api.users.deactivate(user.login) - flash[:notice] = 'User is deleted.' - rescue NativeException => exception - if exception.cause.java_kind_of? Java::OrgSonarServerExceptions::ServerException - error = exception.cause - flash[:error] = (error.getMessage ? error.getMessage : Api::Utils.message(error.l10nKey, :params => error.l10nParams.to_a)) - else - flash[:error] = 'Error when deleting this user.' - end - end - - redirect_to(:action => 'index', :id => nil) - end - - def select_group - @user = User.find(params[:id]) - render :partial => 'users/select_group' - end - - def set_groups - @user = User.find(params[:id]) - - if @user.set_groups(params[:groups]) - flash[:notice] = 'User is updated.' - end - - redirect_to(:action => 'index') - end - - def to_index(errors, id) - if !errors.empty? - flash[:error] = errors.full_messages.join("<br/>\n") - end - - redirect_to(:action => 'index', :id => id) - end + private def prepare_user user = User.new(params[:user]) @@ -155,11 +72,4 @@ class UsersController < ApplicationController user end - - private - - def init_users_list - @users = User.find(:all, :conditions => ["active=?", true], :include => 'groups') - end - end diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/users/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/users/index.html.erb index 59ac4c82243..e3026f52084 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/users/index.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/users/index.html.erb @@ -1,63 +1,6 @@ -<% content_for :script do %> - <script>require(['components/common/select-list']);</script> -<% end %> - -<div class="page"> - <header class="page-header"> - <h1 class="page-title"><%= message('users.page') -%></h1> - <% if profiles_administrator? %> - <div class="page-actions"> - <a id="create-link-user" href="<%= ApplicationController.root_context -%>/users/create_form" class="open-modal"> - Add new user - </a> - </div> - <% end %> - <p class="page-description"><%= message('users.page.description') -%> </p> - </header> - - <table width="100%"> - <tr> - <td valign="top"> - <table class="data width100 sortable" id="users"> - <thead> - <tr> - <th class="text-left"><a>Login</a></th> - <th class="text-left sortfirstasc"><a>Name</a></th> - <th class="text-left"><a>Email</a></th> - <th class="text-left nosort"><a>Groups</a></th> - <th class="text-right nosort" nowrap><a>Operations</a></th> - </tr> - </thead> - <tbody> - <% @users.each do |user| %> - <tr id="user-<%= user.login.parameterize -%>"> - <td class="text-left" valign="top"><%= h user.login -%></td> - <td class="text-left" valign="top"><%= h user.name -%></td> - <td class="text-left" valign="top"><%= h user.email -%></td> - <td class="text-left" valign="top"> - <%= h user.groups.sort.map(&:name).join(', ') %> - (<%= link_to "select", {:action => 'select_group', :id => user.id}, {:id => "select-#{user.login.parameterize}", :class => 'open-modal link-action'} %>) - </td> - <td class="text-right" valign="top"> - <a id="edit-<%= user.login -%>" class="open-modal link-action" href="<%= ApplicationController.root_context -%>/users/edit_form/<%= u user.login -%>">Edit</a> - - <%= link_to 'Change password', {:id => user.id, :action => 'change_password_form'}, {:id => "change-password-#{user.login.parameterize}", :class => 'open-modal link-action'} -%> - - <%= link_to_action message('delete'), "#{ApplicationController.root_context}/users/delete/#{user.id}", - :class => 'link-action link-red', - :id => "delete-#{user.login}", - :confirm_button => message('delete'), - :confirm_title => 'Delete user: '+user.login, - :confirm_msg => 'Warning : are you sure to delete the user "' + user.name+'"?', - :confirm_msg_params => [user.name] - -%> - </td> - </tr> - <% end %> - </tbody> - </table> - <script>jQuery('#users').sortable();</script> - </td> - </tr> - </table> -</div> +<div id="users"></div> +<script> + require(['apps/users/app'], function (App) { + App.start({ el: '#users' }); + }); +</script> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb b/server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb index 6f17f4f61b0..2a60362d112 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb @@ -1,10 +1,4 @@ ActionController::Routing::Routes.draw do |map| - map.connect 'users/select_group', :controller => 'users', :action => 'select_group' - map.connect 'users/set_groups', :controller => 'users', :action => 'set_groups' - map.connect 'users/create_form', :controller => 'users', :action => 'create_form' - map.connect 'users/edit_form', :controller => 'users', :action => 'edit_form' - map.resources :users - map.namespace :api do |api| api.resources :events, :only => [:index, :show, :create, :destroy] api.resources :user_properties, :only => [:index, :show, :create, :destroy], :requirements => { :id => /.*/ } diff --git a/server/sonar-web/src/main/webapp/fonts/sonar-5.2.eot b/server/sonar-web/src/main/webapp/fonts/sonar-5.2.eot Binary files differindex 2b2f832e8c8..fbc4d06cfa3 100755 --- a/server/sonar-web/src/main/webapp/fonts/sonar-5.2.eot +++ b/server/sonar-web/src/main/webapp/fonts/sonar-5.2.eot diff --git a/server/sonar-web/src/main/webapp/fonts/sonar-5.2.svg b/server/sonar-web/src/main/webapp/fonts/sonar-5.2.svg index 972c3f3ee0c..1d648b7b456 100755 --- a/server/sonar-web/src/main/webapp/fonts/sonar-5.2.svg +++ b/server/sonar-web/src/main/webapp/fonts/sonar-5.2.svg @@ -40,6 +40,7 @@ <glyph unicode="" d="M73.143 82.286h585.143v438.857h-237.714q-22.857 0-38.857 16t-16 38.857v237.714h-292.571v-731.429zM438.857 594.286h214.857q-5.714 16.571-12.571 23.429l-178.857 178.857q-6.857 6.857-23.429 12.571v-214.857zM731.429 576v-512q0-22.857-16-38.857t-38.857-16h-621.714q-22.857 0-38.857 16t-16 38.857v768q0 22.857 16 38.857t38.857 16h365.714q22.857 0 50.286-11.429t43.429-27.429l178.286-178.286q16-16 27.429-43.429t11.429-50.286z" horiz-adv-x="731" /> <glyph unicode="" d="M512 649.143v-256q0-8-5.143-13.143t-13.143-5.143h-182.857q-8 0-13.143 5.143t-5.143 13.143v36.571q0 8 5.143 13.143t13.143 5.143h128v201.143q0 8 5.143 13.143t13.143 5.143h36.571q8 0 13.143-5.143t5.143-13.143zM749.714 448q0 84.571-41.714 156t-113.143 113.143-156 41.714-156-41.714-113.143-113.143-41.714-156 41.714-156 113.143-113.143 156-41.714 156 41.714 113.143 113.143 41.714 156zM877.714 448q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" /> <glyph unicode="" d="M863.429 356.571q0-2.857-0.571-4-36.571-153.143-153.143-248.286t-273.143-95.143q-83.429 0-161.429 31.429t-139.143 89.714l-73.714-73.714q-10.857-10.857-25.714-10.857t-25.714 10.857-10.857 25.714v256q0 14.857 10.857 25.714t25.714 10.857h256q14.857 0 25.714-10.857t10.857-25.714-10.857-25.714l-78.286-78.286q40.571-37.714 92-58.286t106.857-20.571q76.571 0 142.857 37.143t106.286 102.286q6.286 9.714 30.286 66.857 4.571 13.143 17.143 13.143h109.714q7.429 0 12.857-5.429t5.429-12.857zM877.714 813.714v-256q0-14.857-10.857-25.714t-25.714-10.857h-256q-14.857 0-25.714 10.857t-10.857 25.714 10.857 25.714l78.857 78.857q-84.571 78.286-199.429 78.286-76.571 0-142.857-37.143t-106.286-102.286q-6.286-9.714-30.286-66.857-4.571-13.143-17.143-13.143h-113.714q-7.429 0-12.857 5.429t-5.429 12.857v4q37.143 153.143 154.286 248.286t274.286 95.143q83.429 0 162.286-31.714t140-89.429l74.286 73.714q10.857 10.857 25.714 10.857t25.714-10.857 10.857-25.714z" /> +<glyph unicode="" d="M182.857 512h292.571v109.714q0 60.571-42.857 103.429t-103.429 42.857-103.429-42.857-42.857-103.429v-109.714zM658.286 457.143v-329.143q0-22.857-16-38.857t-38.857-16h-548.571q-22.857 0-38.857 16t-16 38.857v329.143q0 22.857 16 38.857t38.857 16h18.286v109.714q0 105.143 75.429 180.571t180.571 75.429 180.571-75.429 75.429-180.571v-109.714h18.286q22.857 0 38.857-16t16-38.857z" horiz-adv-x="658" /> <glyph unicode="" d="M256 704q0 30.286-21.429 51.714t-51.714 21.429-51.714-21.429-21.429-51.714 21.429-51.714 51.714-21.429 51.714 21.429 21.429 51.714zM865.714 374.857q0-30.286-21.143-51.429l-280.571-281.143q-22.286-21.143-52-21.143-30.286 0-51.429 21.143l-408.571 409.143q-21.714 21.143-36.857 57.714t-15.143 66.857v237.714q0 29.714 21.714 51.429t51.429 21.714h237.714q30.286 0 66.857-15.143t58.286-36.857l408.571-408q21.143-22.286 21.143-52zM1085.143 374.857q0-30.286-21.143-51.429l-280.571-281.143q-22.286-21.143-52-21.143-20.571 0-33.714 8t-30.286 25.714l268.571 268.571q21.143 21.143 21.143 51.429 0 29.714-21.143 52l-408.571 408q-21.714 21.714-58.286 36.857t-66.857 15.143h128q30.286 0 66.857-15.143t58.286-36.857l408.571-408q21.143-22.286 21.143-52z" horiz-adv-x="1097" /> <glyph unicode="" d="M1024 192v-73.143q0-14.857-10.857-25.714t-25.714-10.857h-950.857q-14.857 0-25.714 10.857t-10.857 25.714v73.143q0 14.857 10.857 25.714t25.714 10.857h950.857q14.857 0 25.714-10.857t10.857-25.714zM1024 411.429v-73.143q0-14.857-10.857-25.714t-25.714-10.857h-950.857q-14.857 0-25.714 10.857t-10.857 25.714v73.143q0 14.857 10.857 25.714t25.714 10.857h950.857q14.857 0 25.714-10.857t10.857-25.714zM1024 630.857v-73.143q0-14.857-10.857-25.714t-25.714-10.857h-950.857q-14.857 0-25.714 10.857t-10.857 25.714v73.143q0 14.857 10.857 25.714t25.714 10.857h950.857q14.857 0 25.714-10.857t10.857-25.714zM1024 850.286v-73.143q0-14.857-10.857-25.714t-25.714-10.857h-950.857q-14.857 0-25.714 10.857t-10.857 25.714v73.143q0 14.857 10.857 25.714t25.714 10.857h950.857q14.857 0 25.714-10.857t10.857-25.714z" /> <glyph unicode="" d="M146.286 210.286v-109.714q0-7.429-5.429-12.857t-12.857-5.429h-109.714q-7.429 0-12.857 5.429t-5.429 12.857v109.714q0 7.429 5.429 12.857t12.857 5.429h109.714q7.429 0 12.857-5.429t5.429-12.857zM146.286 429.714v-109.714q0-7.429-5.429-12.857t-12.857-5.429h-109.714q-7.429 0-12.857 5.429t-5.429 12.857v109.714q0 7.429 5.429 12.857t12.857 5.429h109.714q7.429 0 12.857-5.429t5.429-12.857zM146.286 649.143v-109.714q0-7.429-5.429-12.857t-12.857-5.429h-109.714q-7.429 0-12.857 5.429t-5.429 12.857v109.714q0 7.429 5.429 12.857t12.857 5.429h109.714q7.429 0 12.857-5.429t5.429-12.857zM1024 210.286v-109.714q0-7.429-5.429-12.857t-12.857-5.429h-768q-7.429 0-12.857 5.429t-5.429 12.857v109.714q0 7.429 5.429 12.857t12.857 5.429h768q7.429 0 12.857-5.429t5.429-12.857zM146.286 868.571v-109.714q0-7.429-5.429-12.857t-12.857-5.429h-109.714q-7.429 0-12.857 5.429t-5.429 12.857v109.714q0 7.429 5.429 12.857t12.857 5.429h109.714q7.429 0 12.857-5.429t5.429-12.857zM1024 429.714v-109.714q0-7.429-5.429-12.857t-12.857-5.429h-768q-7.429 0-12.857 5.429t-5.429 12.857v109.714q0 7.429 5.429 12.857t12.857 5.429h768q7.429 0 12.857-5.429t5.429-12.857zM1024 649.143v-109.714q0-7.429-5.429-12.857t-12.857-5.429h-768q-7.429 0-12.857 5.429t-5.429 12.857v109.714q0 7.429 5.429 12.857t12.857 5.429h768q7.429 0 12.857-5.429t5.429-12.857zM1024 868.571v-109.714q0-7.429-5.429-12.857t-12.857-5.429h-768q-7.429 0-12.857 5.429t-5.429 12.857v109.714q0 7.429 5.429 12.857t12.857 5.429h768q7.429 0 12.857-5.429t5.429-12.857z" /> @@ -62,6 +63,7 @@ <glyph unicode="" d="M846.857 360q26.286-14.857 34-44.286t-7.143-55.714l-36.571-62.857q-14.857-26.286-44.286-34t-55.714 7.143l-152 87.429v-175.429q0-29.714-21.714-51.429t-51.429-21.714h-73.143q-29.714 0-51.429 21.714t-21.714 51.429v175.429l-152-87.429q-26.286-14.857-55.714-7.143t-44.286 34l-36.571 62.857q-14.857 26.286-7.143 55.714t34 44.286l152 88-152 88q-26.286 14.857-34 44.286t7.143 55.714l36.571 62.857q14.857 26.286 44.286 34t55.714-7.143l152-87.429v175.429q0 29.714 21.714 51.429t51.429 21.714h73.143q29.714 0 51.429-21.714t21.714-51.429v-175.429l152 87.429q26.286 14.857 55.714 7.143t44.286-34l36.571-62.857q14.857-26.286 7.143-55.714t-34-44.286l-152-88z" horiz-adv-x="951" /> <glyph unicode="" d="M73.143 9.143h164.571v164.571h-164.571v-164.571zM274.286 9.143h182.857v164.571h-182.857v-164.571zM73.143 210.286h164.571v182.857h-164.571v-182.857zM274.286 210.286h182.857v182.857h-182.857v-182.857zM73.143 429.714h164.571v164.571h-164.571v-164.571zM493.714 9.143h182.857v164.571h-182.857v-164.571zM274.286 429.714h182.857v164.571h-182.857v-164.571zM713.143 9.143h164.571v164.571h-164.571v-164.571zM493.714 210.286h182.857v182.857h-182.857v-182.857zM292.571 704v164.571q0 7.429-5.429 12.857t-12.857 5.429h-36.571q-7.429 0-12.857-5.429t-5.429-12.857v-164.571q0-7.429 5.429-12.857t12.857-5.429h36.571q7.429 0 12.857 5.429t5.429 12.857zM713.143 210.286h164.571v182.857h-164.571v-182.857zM493.714 429.714h182.857v164.571h-182.857v-164.571zM713.143 429.714h164.571v164.571h-164.571v-164.571zM731.429 704v164.571q0 7.429-5.429 12.857t-12.857 5.429h-36.571q-7.429 0-12.857-5.429t-5.429-12.857v-164.571q0-7.429 5.429-12.857t12.857-5.429h36.571q7.429 0 12.857 5.429t5.429 12.857zM950.857 740.571v-731.429q0-29.714-21.714-51.429t-51.429-21.714h-804.571q-29.714 0-51.429 21.714t-21.714 51.429v731.429q0 29.714 21.714 51.429t51.429 21.714h73.143v54.857q0 37.714 26.857 64.571t64.571 26.857h36.571q37.714 0 64.571-26.857t26.857-64.571v-54.857h219.429v54.857q0 37.714 26.857 64.571t64.571 26.857h36.571q37.714 0 64.571-26.857t26.857-64.571v-54.857h73.143q29.714 0 51.429-21.714t21.714-51.429z" horiz-adv-x="951" /> <glyph unicode="" d="M1024 448q0-99.429-68.571-183.714t-186.286-133.143-257.143-48.857q-40 0-82.857 4.571-113.143-100-262.857-138.286-28-8-65.143-12.571-9.714-1.143-17.429 5.143t-10 16.571v0.571q-1.714 2.286-0.286 6.857t1.143 5.714 2.571 5.429l3.429 5.143t4 4.857 4.571 5.143q4 4.571 17.714 19.714t19.714 21.714 17.714 22.571 18.571 29.143 15.429 33.714 14.857 43.429q-89.714 50.857-141.429 125.714t-51.714 160.571q0 74.286 40.571 142t109.143 116.857 163.429 78 198.857 28.857q139.429 0 257.143-48.857t186.286-133.143 68.571-183.714z" /> +<glyph unicode="" d="M365.714 438.857v-292.571h-146.286v292.571h146.286zM585.143 731.428v-585.143h-146.286v585.143h146.286zM1170.286 73.143v-73.143h-1170.286v877.714h73.143v-804.571h1097.143zM804.571 585.143v-438.857h-146.286v438.857h146.286zM1024 804.571v-658.286h-146.286v658.286h146.286z" horiz-adv-x="1170" /> <glyph unicode="" d="M512 448q0 60.571-42.857 103.429t-103.429 42.857-103.429-42.857-42.857-103.429 42.857-103.429 103.429-42.857 103.429 42.857 42.857 103.429zM950.857 155.429q0 29.714-21.714 51.429t-51.429 21.714-51.429-21.714-21.714-51.429q0-30.286 21.429-51.714t51.714-21.429 51.714 21.429 21.429 51.714zM950.857 740.571q0 29.714-21.714 51.429t-51.429 21.714-51.429-21.714-21.714-51.429q0-30.286 21.429-51.714t51.714-21.429 51.714 21.429 21.429 51.714zM731.429 500v-105.714q0-5.714-4-11.143t-9.143-6l-88.571-13.714q-6.286-20-18.286-43.429 19.429-27.429 51.429-65.714 4-5.714 4-11.429 0-6.857-4-10.857-13.143-17.143-47.143-51.143t-44.857-34q-6.286 0-12 4l-65.714 51.429q-21.143-10.857-44-17.714-6.286-61.714-13.143-88.571-4-13.714-17.143-13.714h-106.286q-6.286 0-11.429 4.286t-5.714 10l-13.143 87.429q-19.429 5.714-42.857 17.714l-67.429-50.857q-4-4-11.429-4-6.286 0-12 4.571-82.286 76-82.286 91.429 0 5.143 4 10.857 5.714 8 23.429 30.286t26.857 34.857q-13.143 25.143-20 46.857l-86.857 13.714q-5.714 0.571-9.714 5.429t-4 11.143v105.714q0 5.714 4 11.143t9.143 6l88.571 13.714q6.286 20 18.286 43.429-19.429 27.429-51.429 65.714-4 6.286-4 11.429 0 6.857 4 11.429 12.571 17.143 46.857 50.857t45.143 33.714q6.286 0 12-4l65.714-51.429q19.429 10.286 44 18.286 6.286 61.714 13.143 88 4 13.714 17.143 13.714h106.286q6.286 0 11.429-4.286t5.714-10l13.143-87.429q19.429-5.714 42.857-17.714l67.429 50.857q4.571 4 11.429 4 6.286 0 12-4.571 82.286-76 82.286-91.429 0-5.143-4-10.857-6.857-9.143-24-30.857t-25.714-34.286q13.143-27.429 19.429-46.857l86.857-13.143q5.714-1.143 9.714-6t4-11.143zM1097.143 195.429v-80q0-9.143-85.143-17.714-6.857-15.429-17.143-29.714 29.143-64.571 29.143-78.857 0-2.286-2.286-4-69.714-40.571-70.857-40.571-4.571 0-26.286 26.857t-29.714 38.857q-11.429-1.143-17.143-1.143t-17.143 1.143q-8-12-29.714-38.857t-26.286-26.857q-1.143 0-70.857 40.571-2.286 1.714-2.286 4 0 14.286 29.143 78.857-10.286 14.286-17.143 29.714-85.143 8.571-85.143 17.714v80q0 9.143 85.143 17.714 7.429 16.571 17.143 29.714-29.143 64.571-29.143 78.857 0 2.286 2.286 4 2.286 1.143 20 11.429t33.714 19.429 17.143 9.143q4.571 0 26.286-26.571t29.714-38.571q11.429 1.143 17.143 1.143t17.143-1.143q29.143 40.571 52.571 64l3.429 1.143q2.286 0 70.857-40 2.286-1.714 2.286-4 0-14.286-29.143-78.857 9.714-13.143 17.143-29.714 85.143-8.571 85.143-17.714zM1097.143 780.571v-80q0-9.143-85.143-17.714-6.857-15.429-17.143-29.714 29.143-64.571 29.143-78.857 0-2.286-2.286-4-69.714-40.571-70.857-40.571-4.571 0-26.286 26.857t-29.714 38.857q-11.429-1.143-17.143-1.143t-17.143 1.143q-8-12-29.714-38.857t-26.286-26.857q-1.143 0-70.857 40.571-2.286 1.714-2.286 4 0 14.286 29.143 78.857-10.286 14.286-17.143 29.714-85.143 8.571-85.143 17.714v80q0 9.143 85.143 17.714 7.429 16.571 17.143 29.714-29.143 64.571-29.143 78.857 0 2.286 2.286 4 2.286 1.143 20 11.429t33.714 19.429 17.143 9.143q4.571 0 26.286-26.571t29.714-38.571q11.429 1.143 17.143 1.143t17.143-1.143q29.143 40.571 52.571 64l3.429 1.143q2.286 0 70.857-40 2.286-1.714 2.286-4 0-14.286-29.143-78.857 9.714-13.143 17.143-29.714 85.143-8.571 85.143-17.714z" horiz-adv-x="1097" /> <glyph unicode="" d="M804.571 429.714v-182.857q0-68-48.286-116.286t-116.286-48.286h-475.429q-68 0-116.286 48.286t-48.286 116.286v475.429q0 68 48.286 116.286t116.286 48.286h402.286q8 0 13.143-5.143t5.143-13.143v-36.571q0-8-5.143-13.143t-13.143-5.143h-402.286q-37.714 0-64.571-26.857t-26.857-64.571v-475.429q0-37.714 26.857-64.571t64.571-26.857h475.429q37.714 0 64.571 26.857t26.857 64.571v182.857q0 8 5.143 13.143t13.143 5.143h36.571q8 0 13.143-5.143t5.143-13.143zM1024 923.429v-292.571q0-14.857-10.857-25.714t-25.714-10.857-25.714 10.857l-100.571 100.571-372.571-372.571q-5.714-5.714-13.143-5.714t-13.143 5.714l-65.143 65.143q-5.714 5.714-5.714 13.143t5.714 13.143l372.571 372.571-100.571 100.571q-10.857 10.857-10.857 25.714t10.857 25.714 25.714 10.857h292.571q14.857 0 25.714-10.857t10.857-25.714z" /> <glyph unicode="" d="M261.714 455.429q-42.286 92.571-42.286 212h-146.286v-54.857q0-44.571 54-92.571t134.571-64.571zM877.714 612.571v54.857h-146.286q0-119.429-42.286-212 80.571 16.571 134.571 64.571t54 92.571zM950.857 685.714v-73.143q0-40.571-23.714-81.714t-64-74.286-98.857-55.714-123.143-25.429q-24-30.857-54.286-54.286-21.714-19.429-30-41.429t-8.286-51.143q0-30.857 17.429-52t55.714-21.143q42.857 0 76.286-26t33.429-65.429v-36.571q0-8-5.143-13.143t-13.143-5.143h-475.429q-8 0-13.143 5.143t-5.143 13.143v36.571q0 39.429 33.429 65.429t76.286 26q38.286 0 55.714 21.143t17.429 52q0 29.143-8.286 51.143t-30 41.429q-30.286 23.429-54.286 54.286-64.571 2.857-123.143 25.429t-98.857 55.714-64 74.286-23.714 81.714v73.143q0 22.857 16 38.857t38.857 16h164.571v54.857q0 37.714 26.857 64.571t64.571 26.857h329.143q37.714 0 64.571-26.857t26.857-64.571v-54.857h164.571q22.857 0 38.857-16t16-38.857z" horiz-adv-x="951" /> diff --git a/server/sonar-web/src/main/webapp/fonts/sonar-5.2.ttf b/server/sonar-web/src/main/webapp/fonts/sonar-5.2.ttf Binary files differindex d4e65cacf9d..fa26fd2b826 100755 --- a/server/sonar-web/src/main/webapp/fonts/sonar-5.2.ttf +++ b/server/sonar-web/src/main/webapp/fonts/sonar-5.2.ttf diff --git a/server/sonar-web/src/main/webapp/fonts/sonar-5.2.woff b/server/sonar-web/src/main/webapp/fonts/sonar-5.2.woff Binary files differindex ae53de1e82b..1e9e0d823fc 100755 --- a/server/sonar-web/src/main/webapp/fonts/sonar-5.2.woff +++ b/server/sonar-web/src/main/webapp/fonts/sonar-5.2.woff diff --git a/server/sonar-web/src/test/js/users-spec.js b/server/sonar-web/src/test/js/users-spec.js new file mode 100644 index 00000000000..1b963016acd --- /dev/null +++ b/server/sonar-web/src/test/js/users-spec.js @@ -0,0 +1,406 @@ +/* globals casper: false */ +var lib = require('../lib'), + testName = lib.testName('Users'); + +lib.initMessages(); +lib.changeWorkingDirectory('users-spec'); +lib.configureCasper(); + +casper.test.begin(testName('List'), 11, function (test) { + casper + .start(lib.buildUrl('users'), function () { + lib.setDefaultViewport(); + lib.mockRequestFromFile('/api/users/search', 'search.json'); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/users/app'], function (App) { + App.start({ el: '#users' }); + }); + }); + }) + + .then(function () { + casper.waitForText('Bob'); + }) + + .then(function () { + test.assertExists('#users-list ul'); + test.assertElementCount('#users-list li[data-login]', 3); + test.assertSelectorContains('#users-list .js-user-login', 'smith'); + test.assertSelectorContains('#users-list .js-user-name', 'Bob'); + test.assertSelectorContains('#users-list .js-user-email', 'bob@example.com'); + test.assertElementCount('#users-list .js-user-update', 3); + test.assertElementCount('#users-list .js-user-change-password', 3); + test.assertElementCount('#users-list .js-user-deactivate', 3); + test.assertSelectorContains('#users-list-footer', '3/3'); + }) + + .then(function () { + test.assertSelectorDoesntContain('[data-login="ryan"]', 'another@example.com'); + casper.click('[data-login="ryan"] .js-user-more-scm'); + test.assertSelectorContains('[data-login="ryan"]', 'another@example.com'); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Search'), 4, function (test) { + casper + .start(lib.buildUrl('users'), function () { + lib.setDefaultViewport(); + this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search.json'); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/users/app'], function (App) { + App.start({ el: '#users' }); + }); + }); + }) + + .then(function () { + casper.waitForText('Bob'); + }) + + .then(function () { + test.assertElementCount('#users-list li[data-login]', 3); + lib.clearRequestMock(this.searchMock); + this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search-filtered.json', { data: { q: 'ryan' } }); + casper.evaluate(function () { + jQuery('#users-search-query').val('ryan'); + }); + casper.click('#users-search-submit'); + casper.waitForSelectorTextChange('#users-list-footer'); + }) + + .then(function () { + test.assertElementCount('#users-list li[data-login]', 1); + lib.clearRequestMock(this.searchMock); + this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search.json'); + casper.evaluate(function () { + jQuery('#users-search-query').val(''); + }); + casper.click('#users-search-submit'); + casper.waitForSelectorTextChange('#users-list-footer'); + }) + + .then(function () { + test.assertElementCount('#users-list li[data-login]', 3); + test.assert(casper.evaluate(function () { + return jQuery('#users-search-query').val() === ''; + })); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Show More'), 4, function (test) { + casper + .start(lib.buildUrl('users'), function () { + lib.setDefaultViewport(); + this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search-big-1.json'); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/users/app'], function (App) { + App.start({ el: '#users' }); + }); + }); + }) + + .then(function () { + casper.waitForText('Bob'); + }) + + .then(function () { + test.assertElementCount('#users-list li[data-login]', 2); + test.assertSelectorContains('#users-list-footer', '2/3'); + lib.clearRequestMock(this.searchMock); + this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search-big-2.json', { data: { p: '2' } }); + casper.click('#users-fetch-more'); + casper.waitForSelectorTextChange('#users-list-footer'); + }) + + .then(function () { + test.assertElementCount('#users-list li[data-login]', 3); + test.assertSelectorContains('#users-list-footer', '3/3'); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Create'), 5, function (test) { + casper + .start(lib.buildUrl('users'), function () { + lib.setDefaultViewport(); + this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search.json'); + this.createMock = lib.mockRequestFromFile('/api/users/create', 'error.json', { status: 400 }); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/users/app'], function (App) { + App.start({ el: '#users' }); + }); + jQuery.ajaxSetup({ dataType: 'json' }); + }); + }) + + .then(function () { + casper.waitForText('Bob'); + }) + + .then(function () { + test.assertElementCount('#users-list li[data-login]', 3); + casper.click('#users-create'); + casper.waitForSelector('#create-user-form'); + }) + + .then(function () { + casper.click('#create-user-submit'); + casper.waitForSelector('.alert.alert-danger'); + }) + + .then(function () { + lib.clearRequestMock(this.searchMock); + lib.mockRequestFromFile('/api/users/search', 'search-created.json'); + lib.clearRequestMock(this.createMock); + lib.mockRequest('/api/users/create', '{}', + { data: { login: 'login', name: 'name', email: 'email@example.com', scmAccounts: 'scm1,scm2' } }); + casper.click('#create-user-add-scm-account'); + casper.click('#create-user-add-scm-account'); + casper.evaluate(function () { + jQuery('#create-user-login').val('login'); + jQuery('#create-user-name').val('name'); + jQuery('#create-user-email').val('email@example.com'); + jQuery('[name="scmAccounts"]').first().val('scm1'); + jQuery('[name="scmAccounts"]').last().val('scm2'); + }); + casper.click('#create-user-submit'); + casper.waitForSelectorTextChange('#users-list-footer'); + }) + + .then(function () { + test.assertElementCount('#users-list li[data-login]', 4); + test.assertSelectorContains('#users-list .js-user-login', 'login'); + test.assertSelectorContains('#users-list .js-user-name', 'name'); + test.assertSelectorContains('#users-list .js-user-email', 'email@example.com'); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Update'), 3, function (test) { + casper + .start(lib.buildUrl('users'), function () { + lib.setDefaultViewport(); + this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search.json'); + this.updateMock = lib.mockRequestFromFile('/api/users/update', 'error.json', { status: 400 }); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/users/app'], function (App) { + App.start({ el: '#users' }); + }); + jQuery.ajaxSetup({ dataType: 'json' }); + }); + }) + + .then(function () { + casper.waitForText('Bob'); + }) + + .then(function () { + casper.click('[data-login="smith"] .js-user-update'); + casper.waitForSelector('#create-user-form'); + }) + + .then(function () { + casper.click('#create-user-submit'); + casper.waitForSelector('.alert.alert-danger'); + }) + + .then(function () { + lib.clearRequestMock(this.searchMock); + lib.mockRequestFromFile('/api/users/search', 'search-updated.json'); + lib.clearRequestMock(this.updateMock); + lib.mockRequest('/api/users/update', '{}', + { data: { login: 'smith', name: 'Mike', email: 'mike@example.com', scmAccounts: 'scm5,scm6' } }); + casper.click('#create-user-add-scm-account'); + casper.evaluate(function () { + jQuery('#create-user-name').val('Mike'); + jQuery('#create-user-email').val('mike@example.com'); + jQuery('[name="scmAccounts"]').first().val('scm5'); + jQuery('[name="scmAccounts"]').last().val('scm6'); + }); + casper.click('#create-user-submit'); + casper.waitForText('Mike'); + }) + + .then(function () { + test.assertSelectorContains('[data-login="smith"] .js-user-login', 'smith'); + test.assertSelectorContains('[data-login="smith"] .js-user-name', 'Mike'); + test.assertSelectorContains('[data-login="smith"] .js-user-email', 'mike@example.com'); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Change Password'), 1, function (test) { + casper + .start(lib.buildUrl('users'), function () { + lib.setDefaultViewport(); + this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search.json'); + this.updateMock = lib.mockRequestFromFile('/api/users/change_password', 'error.json', { status: 400 }); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/users/app'], function (App) { + App.start({ el: '#users' }); + }); + jQuery.ajaxSetup({ dataType: 'json' }); + }); + }) + + .then(function () { + casper.waitForText('Bob'); + }) + + .then(function () { + casper.click('[data-login="smith"] .js-user-change-password'); + casper.waitForSelector('#change-user-password-form'); + }) + + .then(function () { + casper.click('#change-user-password-submit'); + casper.waitForSelector('.alert.alert-danger'); + }) + + .then(function () { + casper.click('#change-user-password-cancel'); + casper.waitWhileSelector('#change-user-password-form'); + }) + + .then(function () { + casper.click('[data-login="smith"] .js-user-change-password'); + casper.waitForSelector('#change-user-password-form'); + }) + + .then(function () { + lib.clearRequestMock(this.updateMock); + lib.mockRequest('/api/users/change_password', '{}', { data: { login: 'smith', password: 'secret' } }); + casper.evaluate(function () { + jQuery('#change-user-password-password').val('secret'); + jQuery('#change-user-password-password-confirmation').val('another'); + }); + casper.click('#change-user-password-submit'); + casper.waitForSelector('.alert.alert-danger'); + }) + + .then(function () { + casper.evaluate(function () { + jQuery('#change-user-password-password').val('secret'); + jQuery('#change-user-password-password-confirmation').val('secret'); + }); + casper.click('#change-user-password-submit'); + casper.waitWhileSelector('#change-user-password-form'); + }) + + .then(function () { + test.assert(true); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Deactivate'), 1, function (test) { + casper + .start(lib.buildUrl('users'), function () { + lib.setDefaultViewport(); + this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search.json'); + this.updateMock = lib.mockRequestFromFile('/api/users/deactivate', 'error.json', { status: 400 }); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/users/app'], function (App) { + App.start({ el: '#users' }); + }); + jQuery.ajaxSetup({ dataType: 'json' }); + }); + }) + + .then(function () { + casper.waitForText('Bob'); + }) + + .then(function () { + casper.click('[data-login="smith"] .js-user-deactivate'); + casper.waitForSelector('#deactivate-user-form'); + }) + + .then(function () { + casper.click('#deactivate-user-submit'); + casper.waitForSelector('.alert.alert-danger'); + }) + + .then(function () { + lib.clearRequestMock(this.updateMock); + lib.mockRequest('/api/users/deactivate', '{}', { data: { login: 'smith'} }); + casper.click('#deactivate-user-submit'); + casper.waitWhileSelector('[data-login="smith"]'); + }) + + .then(function () { + test.assert(true); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); diff --git a/server/sonar-web/src/test/json/users-spec/error.json b/server/sonar-web/src/test/json/users-spec/error.json new file mode 100644 index 00000000000..dc1b261128c --- /dev/null +++ b/server/sonar-web/src/test/json/users-spec/error.json @@ -0,0 +1,7 @@ +{ + "errors": [ + { + "msg": "Some error message" + } + ] +} diff --git a/server/sonar-web/src/test/json/users-spec/search-big-1.json b/server/sonar-web/src/test/json/users-spec/search-big-1.json new file mode 100644 index 00000000000..5e3f9811177 --- /dev/null +++ b/server/sonar-web/src/test/json/users-spec/search-big-1.json @@ -0,0 +1,19 @@ +{ + "total": 3, + "p": 1, + "ps": 2, + "users": [ + { + "login": "admin", + "name": "Administrator", + "email": "admin@example.com", + "scmAccounts": [] + }, + { + "login": "smith", + "name": "Bob", + "email": "bob@example.com", + "scmAccounts": [] + } + ] +} diff --git a/server/sonar-web/src/test/json/users-spec/search-big-2.json b/server/sonar-web/src/test/json/users-spec/search-big-2.json new file mode 100644 index 00000000000..605d9c1d62f --- /dev/null +++ b/server/sonar-web/src/test/json/users-spec/search-big-2.json @@ -0,0 +1,13 @@ +{ + "total": 3, + "p": 2, + "ps": 2, + "users": [ + { + "login": "ryan", + "name": "John", + "email": "john@example.com", + "scmAccounts": [] + } + ] +} diff --git a/server/sonar-web/src/test/json/users-spec/search-created.json b/server/sonar-web/src/test/json/users-spec/search-created.json new file mode 100644 index 00000000000..a4f1282ba5c --- /dev/null +++ b/server/sonar-web/src/test/json/users-spec/search-created.json @@ -0,0 +1,34 @@ +{ + "total": 4, + "p": 1, + "ps": 50, + "users": [ + { + "login": "admin", + "name": "Administrator", + "email": "admin@example.com", + "scmAccounts": [] + }, + { + "login": "login", + "name": "name", + "email": "email@example.com", + "scmAccounts": [ + "scm1", + "scm2" + ] + }, + { + "login": "smith", + "name": "Bob", + "email": "bob@example.com", + "scmAccounts": [] + }, + { + "login": "ryan", + "name": "John", + "email": "john@example.com", + "scmAccounts": [] + } + ] +} diff --git a/server/sonar-web/src/test/json/users-spec/search-filtered.json b/server/sonar-web/src/test/json/users-spec/search-filtered.json new file mode 100644 index 00000000000..ac74e4b63a3 --- /dev/null +++ b/server/sonar-web/src/test/json/users-spec/search-filtered.json @@ -0,0 +1,13 @@ +{ + "total": 1, + "p": 1, + "ps": 50, + "users": [ + { + "login": "ryan", + "name": "John", + "email": "john@example.com", + "scmAccounts": [] + } + ] +} diff --git a/server/sonar-web/src/test/json/users-spec/search-updated.json b/server/sonar-web/src/test/json/users-spec/search-updated.json new file mode 100644 index 00000000000..294f7ed51ce --- /dev/null +++ b/server/sonar-web/src/test/json/users-spec/search-updated.json @@ -0,0 +1,25 @@ +{ + "total": 3, + "p": 1, + "ps": 50, + "users": [ + { + "login": "admin", + "name": "Administrator", + "email": "admin@example.com", + "scmAccounts": [] + }, + { + "login": "smith", + "name": "Mike", + "email": "mike@example.com", + "scmAccounts": [] + }, + { + "login": "ryan", + "name": "John", + "email": "john@example.com", + "scmAccounts": [] + } + ] +} diff --git a/server/sonar-web/src/test/json/users-spec/search.json b/server/sonar-web/src/test/json/users-spec/search.json new file mode 100644 index 00000000000..abe8f1dfb34 --- /dev/null +++ b/server/sonar-web/src/test/json/users-spec/search.json @@ -0,0 +1,25 @@ +{ + "total": 3, + "p": 1, + "ps": 50, + "users": [ + { + "login": "admin", + "name": "Administrator", + "email": "admin@example.com", + "scmAccounts": [] + }, + { + "login": "smith", + "name": "Bob", + "email": "bob@example.com", + "scmAccounts": ["smith@example.com"] + }, + { + "login": "ryan", + "name": "John", + "email": "john@example.com", + "scmAccounts": ["ryan@example.com", "ryan", "john", "another@example.com"] + } + ] +} diff --git a/server/sonar-web/src/test/server-coverage.js b/server/sonar-web/src/test/server-coverage.js index a6a46527f57..3fd4400cea3 100644 --- a/server/sonar-web/src/test/server-coverage.js +++ b/server/sonar-web/src/test/server-coverage.js @@ -24,7 +24,7 @@ var express = require('express'), url = require('url'), JS_RE = /\.js$/, THIRD_PARTY_RE = /\/third-party\//, - TEMPLATES_RE = /\/templates\//; + TEMPLATES_RE = /\/templates.js/; var app = express(); diff --git a/server/sonar-web/src/test/views/users.jade b/server/sonar-web/src/test/views/users.jade new file mode 100644 index 00000000000..d469a627868 --- /dev/null +++ b/server/sonar-web/src/test/views/users.jade @@ -0,0 +1,5 @@ +extends layouts/main + +block body + #content + #users |