From: Stas Vilchik Date: Wed, 20 May 2015 08:54:59 +0000 (+0200) Subject: SONAR-6565 refactor users page X-Git-Tag: 5.2-RC1~1863 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=e009561a5e3210480c7bef0ee20cebbc83d531bc;p=sonarqube.git SONAR-6565 refactor users page --- 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 @@ +
+ + + +
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 @@ +
+ + + +
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 @@ +
+ + + +
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 @@ + 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 @@ +
+
+ +
+ +
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 @@ + 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 @@ +
+ + + +
+ +
+
+ {{name}} + +
+
{{email}}
+
+ +
+ {{#notEmpty scmAccounts}} +
+ SCM +
+ + {{/notEmpty}} +
+ +
+
+ Groups +
+ +
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 @@ +
+ +
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("
\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("
\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 %> - -<% end %> - -
- - - - - - -
- - - - - - - - - - - - <% @users.each do |user| %> - - - - - - - - <% end %> - -
LoginNameEmailGroupsOperations
<%= h user.login -%><%= h user.name -%><%= h user.email -%> - <%= 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'} %>) - - Edit -   - <%= 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] - -%> -
- -
-
+
+ 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 index 2b2f832e8c8..fbc4d06cfa3 100755 Binary files a/server/sonar-web/src/main/webapp/fonts/sonar-5.2.eot and b/server/sonar-web/src/main/webapp/fonts/sonar-5.2.eot differ 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 @@ + @@ -62,6 +63,7 @@ + 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 index d4e65cacf9d..fa26fd2b826 100755 Binary files a/server/sonar-web/src/main/webapp/fonts/sonar-5.2.ttf and b/server/sonar-web/src/main/webapp/fonts/sonar-5.2.ttf differ 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 index ae53de1e82b..1e9e0d823fc 100755 Binary files a/server/sonar-web/src/main/webapp/fonts/sonar-5.2.woff and b/server/sonar-web/src/main/webapp/fonts/sonar-5.2.woff differ 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