aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-06-08 12:20:03 +0200
committerStas Vilchik <vilchiks@gmail.com>2015-06-08 17:00:05 +0200
commit42c8a155eccbabdab673d8ae496ed3c615d34147 (patch)
tree437928ed4c175fa8bb73071e27c25b8f97cb060c /server/sonar-web/src
parent4a2247c24efee48de53ca07302b6810ab7205621 (diff)
downloadsonarqube-42c8a155eccbabdab673d8ae496ed3c615d34147.tar.gz
sonarqube-42c8a155eccbabdab673d8ae496ed3c615d34147.zip
SONAR-6624 refactor custom metrics page
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/app.js61
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/create-view.js33
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/delete-view.js32
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/form-view.js57
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/header-view.js27
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/layout.js15
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/list-footer-view.js34
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/list-item-view.js50
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/list-view.js18
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/metric.js37
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/metrics.js41
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/templates/metrics-delete.hbs13
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/templates/metrics-form.hbs36
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/templates/metrics-header.hbs10
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/templates/metrics-layout.hbs5
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/templates/metrics-list-footer.hbs6
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/templates/metrics-list-item.hbs23
-rw-r--r--server/sonar-web/src/main/js/apps/metrics/update-view.js32
-rw-r--r--server/sonar-web/src/main/js/apps/nav/templates/nav-settings-navbar.hbs2
-rw-r--r--server/sonar-web/src/main/less/components/modals.less3
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/metrics_controller.rb126
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/_create_form.html.erb35
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/_edit_form.html.erb35
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/_reactivate_form.html.erb22
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/metrics/index.html.erb69
-rw-r--r--server/sonar-web/src/test/js/metrics-spec.js276
-rw-r--r--server/sonar-web/src/test/json/metrics-spec/domains.json16
-rw-r--r--server/sonar-web/src/test/json/metrics-spec/error.json7
-rw-r--r--server/sonar-web/src/test/json/metrics-spec/search-big-1.json28
-rw-r--r--server/sonar-web/src/test/json/metrics-spec/search-big-2.json17
-rw-r--r--server/sonar-web/src/test/json/metrics-spec/search-created.json48
-rw-r--r--server/sonar-web/src/test/json/metrics-spec/search-updated.json38
-rw-r--r--server/sonar-web/src/test/json/metrics-spec/search.json38
-rw-r--r--server/sonar-web/src/test/json/metrics-spec/types.json48
-rw-r--r--server/sonar-web/src/test/views/metrics.jade5
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>&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
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