diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2015-06-08 12:20:03 +0200 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2015-06-08 17:00:05 +0200 |
commit | 42c8a155eccbabdab673d8ae496ed3c615d34147 (patch) | |
tree | 437928ed4c175fa8bb73071e27c25b8f97cb060c /server/sonar-web/src | |
parent | 4a2247c24efee48de53ca07302b6810ab7205621 (diff) | |
download | sonarqube-42c8a155eccbabdab673d8ae496ed3c615d34147.tar.gz sonarqube-42c8a155eccbabdab673d8ae496ed3c615d34147.zip |
SONAR-6624 refactor custom metrics page
Diffstat (limited to 'server/sonar-web/src')
35 files changed, 1060 insertions, 283 deletions
diff --git a/server/sonar-web/src/main/js/apps/metrics/app.js b/server/sonar-web/src/main/js/apps/metrics/app.js new file mode 100644 index 00000000000..4792cd7464e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/app.js @@ -0,0 +1,61 @@ +define([ + './layout', + './metrics', + './header-view', + './list-view', + './list-footer-view' +], function (Layout, Metrics, HeaderView, ListView, ListFooterView) { + + var $ = jQuery, + App = new Marionette.Application(), + init = function (options) { + // Layout + this.layout = new Layout({ el: options.el }); + this.layout.render(); + + // Collection + this.metrics = new Metrics(); + + // Header View + this.headerView = new HeaderView({ + collection: this.metrics, + domains: this.domains, + types: this.types + }); + this.layout.headerRegion.show(this.headerView); + + // List View + this.listView = new ListView({ + collection: this.metrics, + domains: this.domains, + types: this.types + }); + this.layout.listRegion.show(this.listView); + + // List Footer View + this.listFooterView = new ListFooterView({ collection: this.metrics }); + this.layout.listFooterRegion.show(this.listFooterView); + + // Go! + this.metrics.fetch(); + }, + requestDomains = function () { + return $.get(baseUrl + '/api/metrics/domains').done(function (r) { + App.domains = r.domains; + }); + }, + requestTypes = function () { + return $.get(baseUrl + '/api/metrics/types').done(function (r) { + App.types = r.types; + }); + }; + + App.on('start', function (options) { + $.when(window.requestMessages(), requestDomains(), requestTypes()).done(function () { + init.call(App, options); + }); + }); + + return App; + +}); diff --git a/server/sonar-web/src/main/js/apps/metrics/create-view.js b/server/sonar-web/src/main/js/apps/metrics/create-view.js new file mode 100644 index 00000000000..0db9fa70f11 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/create-view.js @@ -0,0 +1,33 @@ +define([ + './metric', + './form-view' +], function (Metric, FormView) { + + return FormView.extend({ + + sendRequest: function () { + var that = this, + metric = new Metric({ + key: this.$('#create-metric-key').val(), + name: this.$('#create-metric-name').val(), + description: this.$('#create-metric-description').val(), + domain: this.$('#create-metric-domain').val(), + type: this.$('#create-metric-type').val() + }); + this.disableForm(); + return metric.save(null, { + statusCode: { + // do not show global error + 400: null + } + }).done(function () { + that.collection.refresh(); + that.close(); + }).fail(function (jqXHR) { + that.enableForm(); + that.showErrors([{ msg: jqXHR.responseJSON.err_msg }]); + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/metrics/delete-view.js b/server/sonar-web/src/main/js/apps/metrics/delete-view.js new file mode 100644 index 00000000000..0acc3c055a9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/delete-view.js @@ -0,0 +1,32 @@ +define([ + 'components/common/modal-form', + './templates' +], function (ModalForm) { + + return ModalForm.extend({ + template: Templates['metrics-delete'], + + onFormSubmit: function (e) { + this._super(e); + 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.refresh(); + that.close(); + }).fail(function (jqXHR) { + that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/metrics/form-view.js b/server/sonar-web/src/main/js/apps/metrics/form-view.js new file mode 100644 index 00000000000..77b5f2a54a5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/form-view.js @@ -0,0 +1,57 @@ +define([ + 'components/common/modal-form', + './templates' +], function (ModalForm) { + + var $ = jQuery; + + return ModalForm.extend({ + template: Templates['metrics-form'], + + onRender: function () { + var that = this; + this._super(); + this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); + this.$('#create-metric-domain').select2({ + width: '250px', + createSearchChoice: function (term) { + return { id: term, text: '+' + term }; + }, + createSearchChoicePosition: 'top', + initSelection: function (element, callback) { + var value = $(element).val(); + callback({ id: value, text: value }); + }, + query: function (options) { + var items = that.options.domains.filter(function (d) { + return d.toLowerCase().indexOf(options.term.toLowerCase()) !== -1; + }), + results = items.map(function (item) { + return { id: item, text: item }; + }); + options.callback({ results: results, more: false }); + } + }).select2('val', this.model && this.model.get('domain')); + this.$('#create-metric-type').select2({ width: '250px' }); + }, + + onClose: function () { + this._super(); + this.$('[data-toggle="tooltip"]').tooltip('destroy'); + }, + + onFormSubmit: function (e) { + this._super(e); + this.sendRequest(); + }, + + serializeData: function () { + return _.extend(this._super(), { + domains: this.options.domains, + types: this.options.types + }); + } + + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/metrics/header-view.js b/server/sonar-web/src/main/js/apps/metrics/header-view.js new file mode 100644 index 00000000000..aed1d449218 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/header-view.js @@ -0,0 +1,27 @@ +define([ + './create-view', + './templates' +], function (CreateView) { + + return Marionette.ItemView.extend({ + template: Templates['metrics-header'], + + events: { + 'click #metrics-create': 'onCreateClick' + }, + + onCreateClick: function (e) { + e.preventDefault(); + this.createMetric(); + }, + + createMetric: function () { + new CreateView({ + collection: this.collection, + domains: this.options.domains, + types: this.options.types + }).render(); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/metrics/layout.js b/server/sonar-web/src/main/js/apps/metrics/layout.js new file mode 100644 index 00000000000..812212a42fa --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/layout.js @@ -0,0 +1,15 @@ +define([ + './templates' +], function () { + + return Marionette.Layout.extend({ + template: Templates['metrics-layout'], + + regions: { + headerRegion: '#metrics-header', + listRegion: '#metrics-list', + listFooterRegion: '#metrics-list-footer' + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/metrics/list-footer-view.js b/server/sonar-web/src/main/js/apps/metrics/list-footer-view.js new file mode 100644 index 00000000000..932dfd6d35f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/list-footer-view.js @@ -0,0 +1,34 @@ +define([ + './templates' +], function () { + + return Marionette.ItemView.extend({ + template: Templates['metrics-list-footer'], + + collectionEvents: { + 'all': 'render' + }, + + events: { + 'click #metrics-fetch-more': 'onMoreClick' + }, + + onMoreClick: function (e) { + e.preventDefault(); + this.fetchMore(); + }, + + fetchMore: function () { + this.collection.fetchMore(); + }, + + serializeData: function () { + return _.extend(this._super(), { + total: this.collection.total, + count: this.collection.length, + more: this.collection.hasMore() + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/metrics/list-item-view.js b/server/sonar-web/src/main/js/apps/metrics/list-item-view.js new file mode 100644 index 00000000000..224ab4c700e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/list-item-view.js @@ -0,0 +1,50 @@ +define([ + './update-view', + './delete-view', + './templates' +], function (UpdateView, DeleteView) { + + return Marionette.ItemView.extend({ + tagName: 'li', + className: 'panel panel-vertical', + template: Templates['metrics-list-item'], + + events: { + 'click .js-metric-update': 'onUpdateClick', + 'click .js-metric-delete': 'onDeleteClick' + }, + + onRender: function () { + this.$el.attr('data-id', this.model.id); + this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); + }, + + onClose: function () { + this.$('[data-toggle="tooltip"]').tooltip('destroy'); + }, + + onUpdateClick: function (e) { + e.preventDefault(); + this.updateMetric(); + }, + + onDeleteClick: function (e) { + e.preventDefault(); + this.deleteMetric(); + }, + + updateMetric: function () { + new UpdateView({ + model: this.model, + collection: this.model.collection, + types: this.options.types, + domains: this.options.domains + }).render(); + }, + + deleteMetric: function () { + new DeleteView({ model: this.model }).render(); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/metrics/list-view.js b/server/sonar-web/src/main/js/apps/metrics/list-view.js new file mode 100644 index 00000000000..27060bbe7d4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/list-view.js @@ -0,0 +1,18 @@ +define([ + './list-item-view', + './templates' +], function (ListItemView) { + + return Marionette.CollectionView.extend({ + tagName: 'ul', + itemView: ListItemView, + + itemViewOptions: function () { + return { + types: this.options.types, + domains: this.options.domains + }; + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/metrics/metric.js b/server/sonar-web/src/main/js/apps/metrics/metric.js new file mode 100644 index 00000000000..cb160c882d0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/metric.js @@ -0,0 +1,37 @@ +define(function () { + + return Backbone.Model.extend({ + idAttribute: 'id', + + urlRoot: function () { + return baseUrl + '/api/metrics'; + }, + + sync: function (method, model, options) { + var opts = options || {}; + if (method === 'create') { + _.defaults(opts, { + url: this.urlRoot() + '/create', + type: 'POST', + data: _.pick(model.toJSON(), 'key', 'name', 'description', 'domain', 'type') + }); + } + if (method === 'update') { + _.defaults(opts, { + url: this.urlRoot() + '/update', + type: 'POST', + data: _.pick(model.toJSON(), 'id', 'key', 'name', 'description', 'domain', 'type') + }); + } + if (method === 'delete') { + _.defaults(opts, { + url: this.urlRoot() + '/delete', + type: 'POST', + data: { ids: this.id } + }); + } + return Backbone.ajax(opts); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/metrics/metrics.js b/server/sonar-web/src/main/js/apps/metrics/metrics.js new file mode 100644 index 00000000000..393ebe3c2b1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/metrics.js @@ -0,0 +1,41 @@ +define([ + './metric' +], function (Metric) { + + return Backbone.Collection.extend({ + model: Metric, + + url: function () { + return baseUrl + '/api/metrics/search'; + }, + + parse: function (r) { + this.total = r.total; + this.p = r.p; + this.ps = r.ps; + return r.metrics; + }, + + fetch: function (options) { + var opts = _.defaults(options || {}, { data: {} }); + this.q = opts.data.q; + opts.data.isCustom = true; + return this._super(opts); + }, + + 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/js/apps/metrics/templates/metrics-delete.hbs b/server/sonar-web/src/main/js/apps/metrics/templates/metrics-delete.hbs new file mode 100644 index 00000000000..73cddffcbf0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/templates/metrics-delete.hbs @@ -0,0 +1,13 @@ +<form id="delete-metric-form"> + <div class="modal-head"> + <h2>Delete Metric</h2> + </div> + <div class="modal-body"> + <div class="js-modal-messages"></div> + Are you sure you want to delete metric "{{name}}"? + </div> + <div class="modal-foot"> + <button id="delete-metric-submit" class="button-red">Delete</button> + <a href="#" class="js-modal-close" id="delete-metric-cancel">Cancel</a> + </div> +</form> diff --git a/server/sonar-web/src/main/js/apps/metrics/templates/metrics-form.hbs b/server/sonar-web/src/main/js/apps/metrics/templates/metrics-form.hbs new file mode 100644 index 00000000000..43c533f8416 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/templates/metrics-form.hbs @@ -0,0 +1,36 @@ +<form id="create-metric-form" autocomplete="off"> + <div class="modal-head"> + <h2>{{#if id}}Update{{else}}Create{{/if}} Metric</h2> + </div> + <div class="modal-body"> + <div class="js-modal-messages"></div> + <div class="modal-field"> + <label for="create-metric-key">Key<em class="mandatory">*</em></label> + <input id="create-metric-key" name="key" type="text" maxlength="200" required value="{{key}}"> + </div> + <div class="modal-field"> + <label for="create-metric-name">Name<em class="mandatory">*</em></label> + <input id="create-metric-name" name="name" type="text" maxlength="200" required value="{{name}}"> + </div> + <div class="modal-field"> + <label for="create-metric-description">Description</label> + <textarea id="create-metric-description" name="description">{{description}}</textarea> + </div> + <div class="modal-field"> + <label for="create-metric-domain">Domain</label> + <input id="create-metric-domain" name="domain" type="text" maxlength="200" value="{{domain}}"> + </div> + <div class="modal-field"> + <label for="create-metric-type">Type<em class="mandatory">*</em></label> + <select id="create-metric-type" name="type"> + {{#each types}} + <option value="{{key}}" {{#eq key ../type.key}}selected{{/eq}}>{{name}}</option> + {{/each}} + </select> + </div> + </div> + <div class="modal-foot"> + <button id="create-metric-submit">{{#if id}}Update{{else}}Create{{/if}}</button> + <a href="#" class="js-modal-close" id="create-metric-cancel">Cancel</a> + </div> +</form> diff --git a/server/sonar-web/src/main/js/apps/metrics/templates/metrics-header.hbs b/server/sonar-web/src/main/js/apps/metrics/templates/metrics-header.hbs new file mode 100644 index 00000000000..05ef5f04fad --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/templates/metrics-header.hbs @@ -0,0 +1,10 @@ +<header class="page-header"> + <h1 class="page-title">Custom Metrics</h1> + <div class="page-actions"> + <div class="button-group"> + <button id="metrics-create">Create Metric</button> + </div> + </div> + <p class="page-description">These metrics are available for all projects. Manual measures can be set at project level + via the configuration interface.</p> +</header> diff --git a/server/sonar-web/src/main/js/apps/metrics/templates/metrics-layout.hbs b/server/sonar-web/src/main/js/apps/metrics/templates/metrics-layout.hbs new file mode 100644 index 00000000000..e1f1fd2b20f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/templates/metrics-layout.hbs @@ -0,0 +1,5 @@ +<div class="page"> + <div id="metrics-header"></div> + <div id="metrics-list"></div> + <div id="metrics-list-footer"></div> +</div> diff --git a/server/sonar-web/src/main/js/apps/metrics/templates/metrics-list-footer.hbs b/server/sonar-web/src/main/js/apps/metrics/templates/metrics-list-footer.hbs new file mode 100644 index 00000000000..c389b85f818 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/templates/metrics-list-footer.hbs @@ -0,0 +1,6 @@ +<footer class="spacer-top note text-center"> + {{count}}/{{total}} shown + {{#if more}} + <a id="metrics-fetch-more" class="spacer-left" href="#">show more</a> + {{/if}} +</footer> diff --git a/server/sonar-web/src/main/js/apps/metrics/templates/metrics-list-item.hbs b/server/sonar-web/src/main/js/apps/metrics/templates/metrics-list-item.hbs new file mode 100644 index 00000000000..d4a59f17d7b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/templates/metrics-list-item.hbs @@ -0,0 +1,23 @@ +<div class="pull-right big-spacer-left nowrap"> + <a class="js-metric-update icon-edit" title="Update" data-toggle="tooltip" href="#"></a> + <a class="js-metric-delete icon-delete" title="Delete" data-toggle="tooltip" href="#"></a> +</div> + +<div class="display-inline-block text-top width-30"> + <div> + <strong class="js-metric-name">{{name}}</strong> + <span class="js-metric-key note little-spacer-left">{{key}}</span> + </div> +</div> + +<div class="display-inline-block text-top width-20"> + <span class="js-metric-domain">{{domain}}</span> +</div> + +<div class="display-inline-block text-top width-20"> + <span class="js-metric-type">{{type.name}}</span> +</div> + +<div class="display-inline-block text-top width-20"> + <span class="js-metric-description">{{description}}</span> +</div> diff --git a/server/sonar-web/src/main/js/apps/metrics/update-view.js b/server/sonar-web/src/main/js/apps/metrics/update-view.js new file mode 100644 index 00000000000..c4edec81a8d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/metrics/update-view.js @@ -0,0 +1,32 @@ +define([ + './form-view' +], function (FormView) { + + return FormView.extend({ + + sendRequest: function () { + var that = this; + this.model.set({ + key: this.$('#create-metric-key').val(), + name: this.$('#create-metric-name').val(), + description: this.$('#create-metric-description').val(), + domain: this.$('#create-metric-domain').val(), + type: this.$('#create-metric-type').val() + }); + 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/nav/templates/nav-settings-navbar.hbs b/server/sonar-web/src/main/js/apps/nav/templates/nav-settings-navbar.hbs index 7ecdc88a5f4..750d44830be 100644 --- a/server/sonar-web/src/main/js/apps/nav/templates/nav-settings-navbar.hbs +++ b/server/sonar-web/src/main/js/apps/nav/templates/nav-settings-navbar.hbs @@ -15,7 +15,7 @@ <a href="{{link '/settings/index'}}">{{t 'settings.page'}}</a> </li> <li> - <a href="{{link '/metrics/index'}}">{{t 'manual_metrics.page'}}</a> + <a href="{{link '/metrics/index'}}">Custom Metrics</a> </li> <li> <a href="{{link '/admin_dashboards/index'}}">{{t 'default_dashboards.page'}}</a> diff --git a/server/sonar-web/src/main/less/components/modals.less b/server/sonar-web/src/main/less/components/modals.less index 49863dbcfcc..8bf28d7d870 100644 --- a/server/sonar-web/src/main/less/components/modals.less +++ b/server/sonar-web/src/main/less/components/modals.less @@ -145,7 +145,8 @@ ul.modal-head-metadata li { .modal-field input[type=text], .modal-field input[type=email], .modal-field input[type=password], -.modal-field textarea { +.modal-field textarea, +.modal-field select { width: 250px; } diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/metrics_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/metrics_controller.rb index 0cd475f4485..f2e2a7d5a00 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/metrics_controller.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/metrics_controller.rb @@ -24,133 +24,7 @@ class MetricsController < ApplicationController SECTION=Navigation::SECTION_CONFIGURATION def index - prepare_metrics_and_domains - if params['id'] - @metric=Metric.find(params['id'].to_i) - params['domain']=@metric.domain(false) - else - @metric=Metric.new - end - render :action => 'index' - end - - def create_form - prepare_metrics_and_domains - - if params['id'] - @metric=Metric.find(params['id'].to_i) - params['domain']=@metric.domain(false) - else - @metric=Metric.new - end - render :partial => 'metrics/create_form' - end - - def edit_form - prepare_metrics_and_domains - - @metric=Metric.find(params['id'].to_i) - params['domain']=@metric.domain(false) - - render :partial => 'metrics/edit_form' - end - - def reactivate_form - render :partial => 'metrics/reactivate_form' - end - - def save_from_web - short_name = params[:metric][:short_name] - metric_name = short_name.downcase.gsub(/\s/, '_')[0..59] - - @errors = [] - if params[:id] - metric = Metric.find(params[:id].to_i) - else - metric = Metric.first(:conditions => ["name = ?", metric_name]) - if metric - @reactivate_metric = metric - else - metric = Metric.new - end - end - - metric.attributes=params[:metric] - if metric.short_name(false) - metric.name = metric.short_name(false).downcase.gsub(/\s/, '_')[0..59] unless params[:id] - end - unless params[:newdomain].blank? - metric.domain = params[:newdomain] - end - metric.direction = 0 - metric.user_managed = true - metric.qualitative = false - metric.enabled = true unless @reactivate_metric - - begin - new_rec = metric.new_record? - metric.save! - unless @reactivate_metric - Metric.clear_cache - if new_rec - flash[:notice] = 'Successfully created.' - else - flash[:notice] = 'Successfully updated.' - end - end - rescue - # If the key (name in db) is invalid, override error message to not display 'Key is invalid' but 'Name is invalid' - if metric.errors[:name] - metric.errors.clear - metric.errors.add_to_base('Name is invalid. Only alphanumerical characters and space characters are allowed.') - end - - @errors << metric.errors.full_messages.join("<br/>\n") - end - - if @reactivate_metric - prepare_metrics_and_domains - render :partial => 'metrics/reactivate_form', :status => 400 - elsif !@errors.empty? - @metric = metric - @domains = metric.domain - render :partial => 'metrics/create_form', :status => 400 - else - render :text => 'ok', :status => 200 - end - end - - def reactivate - begin - metric = Metric.find(params[:id].to_i) - metric.enabled = true - metric.save! - Metric.clear_cache - flash[:notice] = 'Successfully reactivated.' - rescue - flash[:error] = metric.errors.full_messages.join("<br/>\n") - end - redirect_to :action => 'index', :domain => metric.domain(false) - end - - def delete_from_web - metric = Metric.by_id(params[:id].to_i) if params[:id] && params[:id].size > 0 - if metric - Metric.delete_with_manual_measures(params[:id].to_i) - flash[:notice] = 'Successfully deleted.' - Metric.clear_cache - else - flash[:error] = 'Sorry there was a problem with deleting' - end - redirect_to :action => 'index' - end - - private - - def prepare_metrics_and_domains - @metrics = Metric.all.select { |metric| metric.user_managed? } - @domains = Api::Utils.insensitive_sort(Metric.all.map { |metric| metric.domain(false) }.compact.uniq) end end diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/_create_form.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/_create_form.html.erb deleted file mode 100644 index 638a2cc25c7..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/_create_form.html.erb +++ /dev/null @@ -1,35 +0,0 @@ -<% form_for :metric, @metric, :url => { :action => 'save_from_web', :id => @metric.id }, :html => { :id =>'metric_create_form', :method => 'post'} do |f| %> - <fieldset> - <div class="modal-head"> - <h2>Create Manual Metric</h2> - </div> - <div class="modal-body"> - <% if @errors %> - <p class="error"><%= @errors -%></p> - <% end %> - - <div class="modal-field"> - <label for="user[login]">Name <em class="mandatory">*</em></label><%= f.text_field :short_name %><br/> - </div> - <div class="modal-field"> - <label for="user[login]"> Description</label><%= f.text_area :description, :size => 40, :cols => 50, :rows => 5 %><br/> - </div> - <div class="modal-field"> - <label for="user[login]">Domain</label><%= f.select( :domain, @domains, :include_blank => true) %> - <span class="desc">or</span> <input id="newdomain" name="newdomain" size="15" type="text"><br/> - </div> - <div class="modal-field"> - <label for="user[login]">Type</label><%= f.select( :val_type, Metric.value_type_names.invert) %><br/> - </div> - </div> - - <div class="modal-foot"> - <%= submit_tag message('create') %> - <%= link_to message('cancel'), { :controller => 'metrics', :action => 'index', :id => nil}, { :class => 'action' } %> - </div> - </fieldset> -<% end %> - -<script> - $j("#metric_create_form").modalForm(); -</script> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/_edit_form.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/_edit_form.html.erb deleted file mode 100644 index 9410f8e64cd..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/_edit_form.html.erb +++ /dev/null @@ -1,35 +0,0 @@ -<% form_for :metric, @metric, :url => { :action => 'save_from_web', :id => @metric.id }, :html => { :id =>'metric_edit_form', :method => 'post'} do |f| %> - <fieldset> - <div class="modal-head"> - <h2>Edit Manual Metric: <%= @metric.key -%></h2> - </div> - <div class="modal-body"> - <% if @errors %> - <p class="error"><%= @errors -%></p> - <% end %> - - <div class="modal-field"> - <label for="user[login]">Name <em class="mandatory">*</em></label><%= f.text_field :short_name %><br/> - </div> - <div class="modal-field"> - <label for="user[login]"> Description</label><%= f.text_area :description, :size => 40, :cols => 50, :rows => 5 %><br/> - </div> - <div class="modal-field"> - <label for="user[login]">Domain</label><%= f.select( :domain, @domains, :include_blank => true) %> - <span class="desc">or</span> <input id="newdomain" name="newdomain" size="15" type="text"><br/> - </div> - <div class="modal-field"> - <label for="user[login]">Type</label><%= f.select( :val_type, Metric.value_type_names.invert) %><br/> - </div> - </div> - - <div class="modal-foot"> - <%= submit_tag 'Save' %> - <%= link_to message('cancel'), { :controller => 'metrics', :action => 'index', :id => nil}, { :class => 'action' } %> - </div> - </fieldset> -<% end %> - -<script> - $j("#metric_edit_form").modalForm(); -</script> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/_reactivate_form.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/_reactivate_form.html.erb deleted file mode 100644 index ff413c2137e..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/_reactivate_form.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -<% form_for :metric, @reactivate_metric, :url => { :action => 'reactivate', :id => @reactivate_metric.id }, :html => { :id =>'metric_reactivate_form', :method => 'post'} do |f| %> - <fieldset> - <div class="modal-head"> - <h2>Reactivate Manual Metric: <%= @reactivate_metric.key -%></h2> - </div> - <div class="modal-body"> - <p class="error"> - A metric named "<%= @reactivate_metric.short_name(false) -%>" already exists in the database but is deactivated.<br/> - <br/> - Do you really want to reactivate this metric? - </p> - </div> - <div class="modal-foot"> - <%= submit_tag 'Reactivate' %> - <%= link_to message('cancel'), { :controller => 'metrics', :action => 'index', :id => nil}, { :class => 'action' } %> - </div> - </fieldset> -<% end %> - -<script> - $j("#metric_reactivate_form").modalForm(); -</script> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/index.html.erb index ad42427d840..5a130df09d7 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/index.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/index.html.erb @@ -1,63 +1,6 @@ -<div class="page"> - <header class="page-header"> - <h1 class="page-title"><%= message('manual_metrics.page') -%></h1> - <% if profiles_administrator? %> - <div class="page-actions"> - <a id="create-link-metric" - href="<%= ApplicationController.root_context -%>/metrics/create_form" - class="open-modal"><%= message('manual_metrics.add_manual_metric') -%></a> - </div> - <% end %> - <p class="page-description"><%= message('manual_metrics.page.description') -%> </p> - </header> - - <table width="100%"> - <tr> - <td valign="top"> - <table class="sortable data width100" id="metrics"> - <thead> - <tr> - <th class="text-left"><a>Key</a></th> - <th class="text-left sortfirstasc"><a>Name</a></th> - <th class="text-left"><a>Description</a></th> - <th class="text-left"><a>Domain</a></th> - <th class="text-left"><a>Type</a></th> - <th class="text-left nosort"><a>Operations</a></th> - </tr> - </thead> - <tbody> - <% if @metrics.empty? %> - <tr class="even"> - <td colspan="6"><%= message('no_results') -%></td> - </tr> - <% end %> - <% @metrics.each do |metric| %> - <tr> - <td class="text-left" nowrap id="metric_key_<%= metric.key -%>"><span class="note"><%= metric.key -%></span> - </td> - <td class="text-left" nowrap id="metric_name_<%= metric.key -%>"><%= h metric.short_name -%></td> - <td class="text-left" id="metric_desc_<%= metric.key -%>"><%= h metric.description -%></td> - <td class="text-left" id="metric_domain_<%= metric.key -%>"><%= h metric.domain -%></td> - <td class="text-left" id="metric_type_name<%= metric.key -%>"><%= h metric.value_type_name -%></td> - <td class="text-right thin nowrap"> - <% if is_admin? %> - <a id="edit_<%= metric.key.parameterize -%>" href="<%= ApplicationController.root_context -%>/metrics/edit_form/<%= metric.id -%>" id="edit_<%= h(metric.short_name) -%>" class="open-modal link-action">Edit</a> - <%= link_to_action message('delete'), "#{ApplicationController.root_context}/metrics/delete_from_web/#{metric.id}", - :class => 'link-action link-red', - :id => "delete_#{h(metric.short_name)}", - :confirm_button => message('delete'), - :confirm_title => message('manual_metrics.delete_manual_metric'), - :confirm_msg => message('manual_metrics.delete_manual_metric_message', :params => [h(metric.key)]), - :confirm_msg_params => [metric.id] - -%> - <% end %> - </td> - </tr> - <% end %> - </tbody> - </table> - <script>jQuery('#metrics').sortable();</script> - </td> - </tr> - </table> -</div> +<div id="metrics"></div> +<script> + require(['apps/metrics/app'], function (App) { + App.start({ el: '#metrics' }); + }); +</script> diff --git a/server/sonar-web/src/test/js/metrics-spec.js b/server/sonar-web/src/test/js/metrics-spec.js new file mode 100644 index 00000000000..3556ed5cc09 --- /dev/null +++ b/server/sonar-web/src/test/js/metrics-spec.js @@ -0,0 +1,276 @@ +/* globals casper: false */ +var lib = require('../lib'), + testName = lib.testName('Metrics'); + +lib.initMessages(); +lib.changeWorkingDirectory('metrics-spec'); +lib.configureCasper(); + +casper.test.begin(testName('List'), 9, function (test) { + casper + .start(lib.buildUrl('metrics'), function () { + lib.setDefaultViewport(); + lib.mockRequestFromFile('/api/metrics/domains', 'domains.json'); + lib.mockRequestFromFile('/api/metrics/types', 'types.json'); + lib.mockRequestFromFile('/api/metrics/search', 'search.json'); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/metrics/app'], function (App) { + App.start({ el: '#metrics' }); + }); + }); + }) + + .then(function () { + casper.waitForSelector('#metrics-list li'); + }) + + .then(function () { + test.assertElementCount('#metrics-list li[data-id]', 3); + test.assertSelectorContains('#metrics-list .js-metric-name', 'Business value'); + test.assertSelectorContains('#metrics-list .js-metric-key', 'business_value'); + test.assertSelectorContains('#metrics-list .js-metric-domain', 'Complexity'); + test.assertSelectorContains('#metrics-list .js-metric-type', 'Percent'); + test.assertSelectorContains('#metrics-list .js-metric-description', 'Description of Business value'); + test.assertElementCount('#metrics-list .js-metric-update', 3); + test.assertElementCount('#metrics-list .js-metric-delete', 3); + test.assertSelectorContains('#metrics-list-footer', '3/3'); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Show More'), 4, function (test) { + casper + .start(lib.buildUrl('metrics'), function () { + lib.setDefaultViewport(); + lib.mockRequestFromFile('/api/metrics/domains', 'domains.json'); + lib.mockRequestFromFile('/api/metrics/types', 'types.json'); + this.searchMock = lib.mockRequestFromFile('/api/metrics/search', 'search-big-1.json'); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/metrics/app'], function (App) { + App.start({ el: '#metrics' }); + }); + }); + }) + + .then(function () { + casper.waitForSelector('#metrics-list li'); + }) + + .then(function () { + test.assertElementCount('#metrics-list li[data-id]', 2); + test.assertSelectorContains('#metrics-list-footer', '2/3'); + lib.clearRequestMock(this.searchMock); + this.searchMock = lib.mockRequestFromFile('/api/metrics/search', 'search-big-2.json', { data: { p: '2' } }); + casper.click('#metrics-fetch-more'); + casper.waitForSelectorTextChange('#metrics-list-footer'); + }) + + .then(function () { + test.assertElementCount('#metrics-list li[data-id]', 3); + test.assertSelectorContains('#metrics-list-footer', '3/3'); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Create'), 4, function (test) { + casper + .start(lib.buildUrl('metrics'), function () { + lib.setDefaultViewport(); + lib.mockRequestFromFile('/api/metrics/domains', 'domains.json'); + lib.mockRequestFromFile('/api/metrics/types', 'types.json'); + this.searchMock = lib.mockRequestFromFile('/api/metrics/search', 'search.json'); + this.createMock = lib.mockRequestFromFile('/api/metrics/create', 'error.json', { status: 400 }); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/metrics/app'], function (App) { + App.start({ el: '#metrics' }); + }); + jQuery.ajaxSetup({ dataType: 'json' }); + }); + }) + + .then(function () { + casper.waitForSelector('#metrics-list li'); + }) + + .then(function () { + test.assertElementCount('#metrics-list li[data-id]', 3); + casper.click('#metrics-create'); + casper.waitForSelector('#create-metric-form'); + }) + + .then(function () { + casper.click('#create-metric-submit'); + casper.waitForSelector('.alert.alert-danger'); + }) + + .then(function () { + lib.clearRequestMock(this.searchMock); + lib.mockRequestFromFile('/api/metrics/search', 'search-created.json'); + lib.clearRequestMock(this.createMock); + lib.mockRequest('/api/metrics/create', '{}', + { data: { key: 'new_metric', name: 'New Metric', domain: 'Domain for New Metric', type: 'RATING' } }); + casper.evaluate(function () { + jQuery('#create-metric-key').val('new_metric'); + jQuery('#create-metric-name').val('New Metric'); + jQuery('#create-metric-domain').val('Domain for New Metric'); + jQuery('#create-metric-type').val('RATING'); + }); + casper.click('#create-metric-submit'); + casper.waitForSelectorTextChange('#metrics-list-footer'); + }) + + .then(function () { + test.assertElementCount('#metrics-list li[data-id]', 4); + test.assertSelectorContains('#metrics-list .js-metric-key', 'new_metric'); + test.assertSelectorContains('#metrics-list .js-metric-name', 'New Metric'); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Update'), 4, function (test) { + casper + .start(lib.buildUrl('metrics'), function () { + lib.setDefaultViewport(); + lib.mockRequestFromFile('/api/metrics/domains', 'domains.json'); + lib.mockRequestFromFile('/api/metrics/types', 'types.json'); + this.searchMock = lib.mockRequestFromFile('/api/metrics/search', 'search.json'); + this.updateMock = lib.mockRequestFromFile('/api/metrics/update', 'error.json', { status: 400 }); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/metrics/app'], function (App) { + App.start({ el: '#metrics' }); + }); + jQuery.ajaxSetup({ dataType: 'json' }); + }); + }) + + .then(function () { + casper.waitForSelector('#metrics-list li'); + }) + + .then(function () { + casper.click('[data-id="3"] .js-metric-update'); + casper.waitForSelector('#create-metric-form'); + }) + + .then(function () { + casper.click('#create-metric-submit'); + casper.waitForSelector('.alert.alert-danger'); + }) + + .then(function () { + lib.clearRequestMock(this.searchMock); + lib.mockRequestFromFile('/api/metrics/search', 'search-updated.json'); + lib.clearRequestMock(this.createMock); + lib.mockRequest('/api/metrics/update', '{}', + { data: { id: '3', key: 'updated_key', name: 'Updated Name', domain: 'Random Domain', type: 'WORK_DUR' } }); + casper.evaluate(function () { + jQuery('#create-metric-key').val('updated_key'); + jQuery('#create-metric-name').val('Updated Name'); + jQuery('#create-metric-domain').val('Random Domain'); + jQuery('#create-metric-type').val('WORK_DUR'); + }); + casper.click('#create-metric-submit'); + casper.waitForSelectorTextChange('#metrics-list [data-id="3"] .js-metric-name'); + }) + + .then(function () { + test.assertSelectorContains('#metrics-list [data-id="3"] .js-metric-key', 'updated_key'); + test.assertSelectorContains('#metrics-list [data-id="3"] .js-metric-name', 'Updated Name'); + test.assertSelectorContains('#metrics-list [data-id="3"] .js-metric-domain', 'Random Domain'); + test.assertSelectorContains('#metrics-list [data-id="3"] .js-metric-type', 'Duration'); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Delete'), 1, function (test) { + casper + .start(lib.buildUrl('metrics'), function () { + lib.setDefaultViewport(); + lib.mockRequestFromFile('/api/metrics/domains', 'domains.json'); + lib.mockRequestFromFile('/api/metrics/types', 'types.json'); + this.searchMock = lib.mockRequestFromFile('/api/metrics/search', 'search.json'); + this.updateMock = lib.mockRequestFromFile('/api/metrics/delete', 'error.json', { status: 400 }); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/metrics/app'], function (App) { + App.start({ el: '#metrics' }); + }); + jQuery.ajaxSetup({ dataType: 'json' }); + }); + }) + + .then(function () { + casper.waitForSelector('#metrics-list li'); + }) + + .then(function () { + casper.click('[data-id="3"] .js-metric-delete'); + casper.waitForSelector('#delete-metric-form'); + }) + + .then(function () { + casper.click('#delete-metric-submit'); + casper.waitForSelector('.alert.alert-danger'); + }) + + .then(function () { + lib.clearRequestMock(this.updateMock); + lib.mockRequest('/api/metrics/delete', '{}', { data: { ids: '3'} }); + casper.click('#delete-metric-submit'); + casper.waitWhileSelector('[data-id="3"]'); + }) + + .then(function () { + test.assert(true); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + diff --git a/server/sonar-web/src/test/json/metrics-spec/domains.json b/server/sonar-web/src/test/json/metrics-spec/domains.json new file mode 100644 index 00000000000..80713fcf818 --- /dev/null +++ b/server/sonar-web/src/test/json/metrics-spec/domains.json @@ -0,0 +1,16 @@ +{ + "domains": [ + "Tests (Overall)", + "Complexity", + "Issues", + "SCM", + "Tests (Integration)", + "Duplication", + "Technical Debt", + "General", + "Management", + "Tests", + "Documentation", + "Size" + ] +} diff --git a/server/sonar-web/src/test/json/metrics-spec/error.json b/server/sonar-web/src/test/json/metrics-spec/error.json new file mode 100644 index 00000000000..dc1b261128c --- /dev/null +++ b/server/sonar-web/src/test/json/metrics-spec/error.json @@ -0,0 +1,7 @@ +{ + "errors": [ + { + "msg": "Some error message" + } + ] +} diff --git a/server/sonar-web/src/test/json/metrics-spec/search-big-1.json b/server/sonar-web/src/test/json/metrics-spec/search-big-1.json new file mode 100644 index 00000000000..cedb98c247a --- /dev/null +++ b/server/sonar-web/src/test/json/metrics-spec/search-big-1.json @@ -0,0 +1,28 @@ +{ + "metrics": [ + { + "id": "1", + "key": "burned_budget", + "name": "Burned budget", + "domain": "Management", + "type": { + "key": "FLOAT", + "name": "Float" + } + }, + { + "id": "2", + "key": "business_value", + "name": "Business value", + "domain": "Complexity", + "type": { + "key": "PERCENT", + "name": "Percent" + }, + "description": "Description of Business value" + } + ], + "total": 3, + "p": 1, + "ps": 2 +} diff --git a/server/sonar-web/src/test/json/metrics-spec/search-big-2.json b/server/sonar-web/src/test/json/metrics-spec/search-big-2.json new file mode 100644 index 00000000000..432d6c007bb --- /dev/null +++ b/server/sonar-web/src/test/json/metrics-spec/search-big-2.json @@ -0,0 +1,17 @@ +{ + "metrics": [ + { + "id": "3", + "key": "team_size", + "name": "Team size", + "domain": "Management", + "type": { + "key": "INT", + "name": "Integer" + } + } + ], + "total": 3, + "p": 2, + "ps": 2 +} diff --git a/server/sonar-web/src/test/json/metrics-spec/search-created.json b/server/sonar-web/src/test/json/metrics-spec/search-created.json new file mode 100644 index 00000000000..879b52af9d0 --- /dev/null +++ b/server/sonar-web/src/test/json/metrics-spec/search-created.json @@ -0,0 +1,48 @@ +{ + "metrics": [ + { + "id": "1", + "key": "burned_budget", + "name": "Burned budget", + "domain": "Management", + "type": { + "key": "FLOAT", + "name": "Float" + } + }, + { + "id": "2", + "key": "business_value", + "name": "Business value", + "domain": "Complexity", + "type": { + "key": "PERCENT", + "name": "Percent" + }, + "description": "Description of Business value" + }, + { + "id": "3", + "key": "team_size", + "name": "Team size", + "domain": "Management", + "type": { + "key": "INT", + "name": "Integer" + } + }, + { + "id": "4", + "key": "new_metric", + "name": "New Metric", + "domain": "Domain for New Metric", + "type": { + "key": "RATING", + "name": "Rating" + } + } + ], + "total": 4, + "p": 1, + "ps": 100 +} diff --git a/server/sonar-web/src/test/json/metrics-spec/search-updated.json b/server/sonar-web/src/test/json/metrics-spec/search-updated.json new file mode 100644 index 00000000000..639f5835a63 --- /dev/null +++ b/server/sonar-web/src/test/json/metrics-spec/search-updated.json @@ -0,0 +1,38 @@ +{ + "metrics": [ + { + "id": "1", + "key": "burned_budget", + "name": "Burned budget", + "domain": "Management", + "type": { + "key": "FLOAT", + "name": "Float" + } + }, + { + "id": "2", + "key": "business_value", + "name": "Business value", + "domain": "Complexity", + "type": { + "key": "PERCENT", + "name": "Percent" + }, + "description": "Description of Business value" + }, + { + "id": "3", + "key": "updated_key", + "name": "Updated Name", + "domain": "Random Domain", + "type": { + "key": "WORK_DUR", + "name": "Duration" + } + } + ], + "total": 3, + "p": 1, + "ps": 100 +} diff --git a/server/sonar-web/src/test/json/metrics-spec/search.json b/server/sonar-web/src/test/json/metrics-spec/search.json new file mode 100644 index 00000000000..fb7c53c35eb --- /dev/null +++ b/server/sonar-web/src/test/json/metrics-spec/search.json @@ -0,0 +1,38 @@ +{ + "metrics": [ + { + "id": "1", + "key": "burned_budget", + "name": "Burned budget", + "domain": "Management", + "type": { + "key": "FLOAT", + "name": "Float" + } + }, + { + "id": "2", + "key": "business_value", + "name": "Business value", + "domain": "Complexity", + "type": { + "key": "PERCENT", + "name": "Percent" + }, + "description": "Description of Business value" + }, + { + "id": "3", + "key": "team_size", + "name": "Team size", + "domain": "Management", + "type": { + "key": "INT", + "name": "Integer" + } + } + ], + "total": 3, + "p": 1, + "ps": 100 +} diff --git a/server/sonar-web/src/test/json/metrics-spec/types.json b/server/sonar-web/src/test/json/metrics-spec/types.json new file mode 100644 index 00000000000..fcfb12493e2 --- /dev/null +++ b/server/sonar-web/src/test/json/metrics-spec/types.json @@ -0,0 +1,48 @@ +{ + "types": [ + { + "key": "INT", + "name": "Integer" + }, + { + "key": "FLOAT", + "name": "Float" + }, + { + "key": "PERCENT", + "name": "Percent" + }, + { + "key": "BOOL", + "name": "Yes/No" + }, + { + "key": "STRING", + "name": "String" + }, + { + "key": "MILLISEC", + "name": "Milliseconds" + }, + { + "key": "DATA", + "name": "Data" + }, + { + "key": "LEVEL", + "name": "Level" + }, + { + "key": "DISTRIB", + "name": "Distribution" + }, + { + "key": "RATING", + "name": "Rating" + }, + { + "key": "WORK_DUR", + "name": "Work Duration" + } + ] +} diff --git a/server/sonar-web/src/test/views/metrics.jade b/server/sonar-web/src/test/views/metrics.jade new file mode 100644 index 00000000000..123e3d8855c --- /dev/null +++ b/server/sonar-web/src/test/views/metrics.jade @@ -0,0 +1,5 @@ +extends layouts/main + +block body + #content + #metrics |