]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6624 refactor custom metrics page
authorStas Vilchik <vilchiks@gmail.com>
Mon, 8 Jun 2015 10:20:03 +0000 (12:20 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Mon, 8 Jun 2015 15:00:05 +0000 (17:00 +0200)
36 files changed:
server/sonar-web/Gruntfile.coffee
server/sonar-web/src/main/js/apps/metrics/app.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/create-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/delete-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/form-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/header-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/layout.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/list-footer-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/list-item-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/list-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/metric.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/metrics.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/templates/metrics-delete.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/templates/metrics-form.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/templates/metrics-header.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/templates/metrics-layout.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/templates/metrics-list-footer.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/templates/metrics-list-item.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/metrics/update-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/nav/templates/nav-settings-navbar.hbs
server/sonar-web/src/main/less/components/modals.less
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/metrics_controller.rb
server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/_create_form.html.erb [deleted file]
server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/_edit_form.html.erb [deleted file]
server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/_reactivate_form.html.erb [deleted file]
server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/index.html.erb
server/sonar-web/src/test/js/metrics-spec.js [new file with mode: 0644]
server/sonar-web/src/test/json/metrics-spec/domains.json [new file with mode: 0644]
server/sonar-web/src/test/json/metrics-spec/error.json [new file with mode: 0644]
server/sonar-web/src/test/json/metrics-spec/search-big-1.json [new file with mode: 0644]
server/sonar-web/src/test/json/metrics-spec/search-big-2.json [new file with mode: 0644]
server/sonar-web/src/test/json/metrics-spec/search-created.json [new file with mode: 0644]
server/sonar-web/src/test/json/metrics-spec/search-updated.json [new file with mode: 0644]
server/sonar-web/src/test/json/metrics-spec/search.json [new file with mode: 0644]
server/sonar-web/src/test/json/metrics-spec/types.json [new file with mode: 0644]
server/sonar-web/src/test/views/metrics.jade [new file with mode: 0644]

index 7375822b38e6c2920fa7dacd992d5521046ffa06..be7a3ae2ab3f208ad212daa7833847db74dfdd06 100644 (file)
@@ -127,6 +127,7 @@ module.exports = (grunt) ->
           'build-app:groups'
           'build-app:markdown'
           'build-app:measures'
+          'build-app:metrics'
           'build-app:nav'
           'build-app:provisioning'
           'build-app:quality-gates'
@@ -161,6 +162,7 @@ module.exports = (grunt) ->
           'casper:groups'
           'casper:provisioning'
           'casper:computation'
+          'casper:metrics'
         ]
 
 
@@ -227,6 +229,9 @@ module.exports = (grunt) ->
           '<%= BUILD_PATH %>/js/apps/computation/templates.js': [
             '<%= SOURCE_PATH %>/js/apps/computation/templates/**/*.hbs'
           ]
+          '<%= BUILD_PATH %>/js/apps/metrics/templates.js': [
+            '<%= SOURCE_PATH %>/js/apps/metrics/templates/**/*.hbs'
+          ]
 
 
     clean:
@@ -327,6 +332,8 @@ module.exports = (grunt) ->
         src: ['src/test/js/computation*.js']
       groups:
         src: ['src/test/js/groups-spec.js']
+      metrics:
+        src: ['src/test/js/metrics-spec.js']
 
     uglify:
       build:
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 (file)
index 0000000..4792cd7
--- /dev/null
@@ -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 (file)
index 0000000..0db9fa7
--- /dev/null
@@ -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 (file)
index 0000000..0acc3c0
--- /dev/null
@@ -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 (file)
index 0000000..77b5f2a
--- /dev/null
@@ -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 (file)
index 0000000..aed1d44
--- /dev/null
@@ -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 (file)
index 0000000..812212a
--- /dev/null
@@ -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 (file)
index 0000000..932dfd6
--- /dev/null
@@ -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 (file)
index 0000000..224ab4c
--- /dev/null
@@ -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 (file)
index 0000000..27060bb
--- /dev/null
@@ -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 (file)
index 0000000..cb160c8
--- /dev/null
@@ -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 (file)
index 0000000..393ebe3
--- /dev/null
@@ -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 (file)
index 0000000..73cddff
--- /dev/null
@@ -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 (file)
index 0000000..43c533f
--- /dev/null
@@ -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 (file)
index 0000000..05ef5f0
--- /dev/null
@@ -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 (file)
index 0000000..e1f1fd2
--- /dev/null
@@ -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 (file)
index 0000000..c389b85
--- /dev/null
@@ -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 (file)
index 0000000..d4a59f1
--- /dev/null
@@ -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 (file)
index 0000000..c4edec8
--- /dev/null
@@ -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);
+      });
+    }
+  });
+
+});
index 7ecdc88a5f437a80c5a9716db5da0dc669b4a8fc..750d44830be824efe6f7763f8b13de5025c3335f 100644 (file)
@@ -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>
index 49863dbcfcc76131217c4564e966db96d4ff3d9b..8bf28d7d87059bec397a9999cfd327e545840e63 100644 (file)
@@ -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;
 }
 
index 0cd475f44855bc8146d28eecd82be81b74b29cb9..f2e2a7d5a005bcc29f658883fa06eec04a34ab68 100644 (file)
@@ -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 (file)
index 638a2cc..0000000
+++ /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 (file)
index 9410f8e..0000000
+++ /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 (file)
index ff413c2..0000000
+++ /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>
index ad42427d8403d584cd586a713d0f26c236559cec..5a130df09d76ce604f9c85763c89b561f348e60d 100644 (file)
@@ -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>&nbsp;
-                  <%= 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 (file)
index 0000000..3556ed5
--- /dev/null
@@ -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 (file)
index 0000000..80713fc
--- /dev/null
@@ -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 (file)
index 0000000..dc1b261
--- /dev/null
@@ -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 (file)
index 0000000..cedb98c
--- /dev/null
@@ -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 (file)
index 0000000..432d6c0
--- /dev/null
@@ -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 (file)
index 0000000..879b52a
--- /dev/null
@@ -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 (file)
index 0000000..639f583
--- /dev/null
@@ -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 (file)
index 0000000..fb7c53c
--- /dev/null
@@ -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 (file)
index 0000000..fcfb124
--- /dev/null
@@ -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 (file)
index 0000000..123e3d8
--- /dev/null
@@ -0,0 +1,5 @@
+extends layouts/main
+
+block body
+  #content
+    #metrics