aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-server/src/main/coffee/quality-gate
diff options
context:
space:
mode:
Diffstat (limited to 'sonar-server/src/main/coffee/quality-gate')
-rw-r--r--sonar-server/src/main/coffee/quality-gate/app.coffee142
-rw-r--r--sonar-server/src/main/coffee/quality-gate/collections/conditions.coffee11
-rw-r--r--sonar-server/src/main/coffee/quality-gate/collections/quality-gates.coffee30
-rw-r--r--sonar-server/src/main/coffee/quality-gate/layout.coffee35
-rw-r--r--sonar-server/src/main/coffee/quality-gate/models/condition.coffee42
-rw-r--r--sonar-server/src/main/coffee/quality-gate/models/quality-gate.coffee10
-rw-r--r--sonar-server/src/main/coffee/quality-gate/router.coffee42
-rw-r--r--sonar-server/src/main/coffee/quality-gate/views/quality-gate-actions-view.coffee27
-rw-r--r--sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-condition-view.coffee123
-rw-r--r--sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-conditions-empty-view.coffee11
-rw-r--r--sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-conditions-view.coffee86
-rw-r--r--sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-header-view.coffee90
-rw-r--r--sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-projects-view.coffee38
-rw-r--r--sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-view.coffee50
-rw-r--r--sonar-server/src/main/coffee/quality-gate/views/quality-gate-edit-view.coffee86
-rw-r--r--sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-empty-view.coffee12
-rw-r--r--sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-item-view.coffee27
-rw-r--r--sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-view.coffee25
18 files changed, 887 insertions, 0 deletions
diff --git a/sonar-server/src/main/coffee/quality-gate/app.coffee b/sonar-server/src/main/coffee/quality-gate/app.coffee
new file mode 100644
index 00000000000..db18db98711
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/app.coffee
@@ -0,0 +1,142 @@
+requirejs.config
+ baseUrl: "#{baseUrl}/js"
+
+ paths:
+ 'jquery': 'third-party/jquery'
+ 'backbone': 'third-party/backbone'
+ 'backbone.marionette': 'third-party/backbone.marionette'
+ 'handlebars': 'third-party/handlebars'
+ 'moment': 'third-party/moment'
+ 'select-list': 'common/select-list'
+
+ shim:
+ 'backbone.marionette':
+ deps: ['backbone']
+ exports: 'Marionette'
+ 'backbone':
+ exports: 'Backbone'
+ 'handlebars':
+ exports: 'Handlebars'
+ 'moment':
+ exports: 'moment'
+ 'select-list':
+ exports: 'SelectList'
+
+
+requirejs [
+ 'backbone', 'backbone.marionette', 'handlebars',
+ 'quality-gate/collections/quality-gates',
+ 'quality-gate/views/quality-gate-sidebar-list-view',
+ 'quality-gate/views/quality-gate-actions-view',
+ 'quality-gate/views/quality-gate-edit-view',
+ 'quality-gate/router',
+ 'quality-gate/layout',
+ 'common/handlebars-extensions'
+], (
+ Backbone, Marionette, Handlebars,
+ QualityGates,
+ QualityGateSidebarListItemView,
+ QualityGateActionsView,
+ QualityGateEditView,
+ QualityGateRouter,
+ QualityGateLayout
+) ->
+
+ # Create a generic error handler for ajax requests
+ jQuery.ajaxSetup
+ error: (jqXHR) ->
+ text = jqXHR.responseText
+ errorBox = jQuery('.modal-error')
+ if jqXHR.responseJSON?.errors?
+ text = _.pluck(jqXHR.responseJSON.errors, 'msg').join '. '
+ if errorBox.length > 0
+ errorBox.show().text text
+ else
+ alert text
+
+
+ # Add html class to mark the page as navigator page
+ jQuery('html').addClass('navigator-page quality-gates-page');
+
+
+ # Create a Quality Gate Application
+ App = new Marionette.Application
+
+
+ App.qualityGates = new QualityGates
+
+
+ App.openFirstQualityGate = ->
+ if @qualityGates.length > 0
+ @router.navigate "show/#{@qualityGates.models[0].get('id')}", trigger: true
+ else
+ App.layout.detailsRegion.reset()
+
+
+ App.deleteQualityGate = (id) ->
+ App.qualityGates.remove id
+ App.openFirstQualityGate()
+
+
+ App.unsetDefaults = (id) ->
+ App.qualityGates.each (gate) ->
+ gate.set('default', false) unless gate.id == id
+
+
+ # Construct layout
+ App.addInitializer ->
+ @layout = new QualityGateLayout app: @
+ jQuery('body').append @layout.render().el
+
+
+ # Construct actions bar
+ App.addInitializer ->
+ @codingRulesHeaderView = new QualityGateActionsView
+ app: @
+ @layout.actionsRegion.show @codingRulesHeaderView
+
+
+ # Construct sidebar
+ App.addInitializer ->
+ @qualityGateSidebarListView = new QualityGateSidebarListItemView
+ collection: @qualityGates
+ app: @
+ @layout.resultsRegion.show @qualityGateSidebarListView
+
+
+ # Construct edit view
+ App.addInitializer ->
+ @qualityGateEditView = new QualityGateEditView app: @
+ @qualityGateEditView.render()
+
+
+ # Start router
+ App.addInitializer ->
+ @router = new QualityGateRouter app: @
+ Backbone.history.start()
+
+
+ # Open first quality gate when come to the page
+ App.addInitializer ->
+ initial = Backbone.history.fragment == ''
+ App.openFirstQualityGate() if initial
+
+
+ # Call app, Load metrics and the list of quality gates before start the application
+ appXHR = jQuery.ajax
+ url: "#{baseUrl}/api/qualitygates/app"
+ .done (r) =>
+ App.canEdit = r.edit
+ App.periods = r.periods
+ App.metrics = r.metrics
+ window.messages = r.messages
+
+ qualityGatesXHR = App.qualityGates.fetch()
+
+ jQuery.when(qualityGatesXHR, appXHR)
+ .done ->
+ # Remove the initial spinner
+ jQuery('#quality-gate-page-loader').remove()
+
+ # Start the application
+ App.start()
diff --git a/sonar-server/src/main/coffee/quality-gate/collections/conditions.coffee b/sonar-server/src/main/coffee/quality-gate/collections/conditions.coffee
new file mode 100644
index 00000000000..8899fc14405
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/collections/conditions.coffee
@@ -0,0 +1,11 @@
+define [
+ 'backbone',
+ 'quality-gate/models/condition'
+], (
+ Backbone,
+ Condition
+) ->
+
+ class Conditions extends Backbone.Collection
+ model: Condition
+ comparator: 'metric'
diff --git a/sonar-server/src/main/coffee/quality-gate/collections/quality-gates.coffee b/sonar-server/src/main/coffee/quality-gate/collections/quality-gates.coffee
new file mode 100644
index 00000000000..1d1c8542481
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/collections/quality-gates.coffee
@@ -0,0 +1,30 @@
+define [
+ 'backbone',
+ 'quality-gate/models/quality-gate'
+], (
+ Backbone,
+ QualityGate
+) ->
+
+ class QualityGates extends Backbone.Collection
+ model: QualityGate
+
+
+ url: ->
+ "#{baseUrl}/api/qualitygates/list"
+
+
+ # {
+ # "qualitygates": [
+ # { "id": 42, "name": "QG 1" },
+ # { "id": 43, "name": "QG 2" },
+ # { "id": 44, "name": "QG 3" }
+ # ],
+ # "default": 42
+ # }
+ parse: (r) ->
+ r.qualitygates.map (gate) ->
+ _.extend gate, default: gate.id == r.default
+
+
+ comparator: (item) -> item.get('name').toLowerCase()
diff --git a/sonar-server/src/main/coffee/quality-gate/layout.coffee b/sonar-server/src/main/coffee/quality-gate/layout.coffee
new file mode 100644
index 00000000000..ff7f6f54bbd
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/layout.coffee
@@ -0,0 +1,35 @@
+define [
+ 'backbone.marionette',
+ 'templates/quality-gates'
+], (
+ Marionette,
+ Templates
+) ->
+
+ class AppLayout extends Marionette.Layout
+ className: 'navigator quality-gates-navigator'
+ template: Templates['quality-gates-layout']
+
+
+ regions:
+ headerRegion: '.navigator-header'
+ actionsRegion: '.navigator-actions'
+ resultsRegion: '.navigator-results'
+ detailsRegion: '.navigator-details'
+
+
+ initialize: (options) ->
+ @listenTo options.app.qualityGates, 'all', @updateLayout
+
+
+ updateLayout: ->
+ empty = @options.app.qualityGates.length == 0
+ @$(@headerRegion.el).toggle !empty
+ @$(@detailsRegion.el).toggle !empty
+
+
+ onRender: ->
+ @updateLayout()
+
+ # Adjust details region height
+ @$(@detailsRegion.el).css 'bottom', jQuery('#footer').outerHeight()
diff --git a/sonar-server/src/main/coffee/quality-gate/models/condition.coffee b/sonar-server/src/main/coffee/quality-gate/models/condition.coffee
new file mode 100644
index 00000000000..1fe2c634a41
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/models/condition.coffee
@@ -0,0 +1,42 @@
+define [
+ 'backbone'
+], (
+ Backbone
+) ->
+
+ class Condition extends Backbone.Model
+
+ url: ->
+ "#{baseUrl}/api/qualitygates/create_condition"
+
+
+ save: ->
+ method = unless @isNew() then 'update' else 'create'
+ data =
+ metric: @get('metric').key
+ op: @get('op')
+ warning: @get('warning')
+ error: @get('error')
+
+ unless @get('period') == '0'
+ data.period = @get('period')
+
+ unless @isNew()
+ data.id = @id
+ else
+ data.gateId = @get('gateId')
+
+ jQuery.ajax({
+ url: "#{baseUrl}/api/qualitygates/#{method}_condition"
+ type: 'POST'
+ data: data
+ }).done (r) =>
+ @set 'id', r.id
+
+
+ delete: ->
+ jQuery.ajax
+ url: "#{baseUrl}/api/qualitygates/delete_condition"
+ type: 'POST'
+ data: id: @id
+
diff --git a/sonar-server/src/main/coffee/quality-gate/models/quality-gate.coffee b/sonar-server/src/main/coffee/quality-gate/models/quality-gate.coffee
new file mode 100644
index 00000000000..5acffc066e7
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/models/quality-gate.coffee
@@ -0,0 +1,10 @@
+define [
+ 'backbone'
+], (
+ Backbone
+) ->
+
+ class QualityGate extends Backbone.Model
+
+ url: ->
+ "#{baseUrl}/api/qualitygates/show?id=#{@get('id')}"
diff --git a/sonar-server/src/main/coffee/quality-gate/router.coffee b/sonar-server/src/main/coffee/quality-gate/router.coffee
new file mode 100644
index 00000000000..f21161a97bf
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/router.coffee
@@ -0,0 +1,42 @@
+define [
+ 'backbone',
+ 'quality-gate/models/quality-gate',
+ 'quality-gate/views/quality-gate-detail-view',
+ 'quality-gate/views/quality-gate-detail-header-view',
+], (
+ Backbone,
+ QualityGate,
+ QualityGateDetailView,
+ QualityGateDetailHeaderView
+) ->
+
+ class QualityGateRouter extends Backbone.Router
+
+ routes:
+ 'show/:id': 'show'
+
+
+ initialize: (options) ->
+ @app = options.app
+
+
+ show: (id) ->
+ qualityGate = @app.qualityGates.get id
+ if qualityGate
+ @app.qualityGateSidebarListView.highlight id
+
+ qualityGateDetailHeaderView = new QualityGateDetailHeaderView
+ app: @app
+ model: qualityGate
+ @app.layout.headerRegion.show qualityGateDetailHeaderView
+
+ qualityGateDetailView = new QualityGateDetailView
+ app: @app
+ model: qualityGate
+ @app.layout.detailsRegion.show qualityGateDetailView
+ qualityGateDetailView.$el.hide()
+
+ qualityGateDetailHeaderView.showSpinner()
+ qualityGate.fetch().done ->
+ qualityGateDetailView.$el.show()
+ qualityGateDetailHeaderView.hideSpinner()
diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-actions-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-actions-view.coffee
new file mode 100644
index 00000000000..6025a3c5b3c
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-actions-view.coffee
@@ -0,0 +1,27 @@
+define [
+ 'backbone.marionette',
+ 'templates/quality-gates'
+ 'quality-gate/models/quality-gate'
+], (
+ Marionette,
+ Templates
+ QualityGate
+) ->
+
+ class QualityGateActionsView extends Marionette.ItemView
+ template: Templates['quality-gate-actions']
+
+
+ events:
+ 'click #quality-gate-add': 'add'
+
+
+ add: ->
+ qualityGate = new QualityGate()
+ @options.app.qualityGateEditView.method = 'create'
+ @options.app.qualityGateEditView.model = qualityGate
+ @options.app.qualityGateEditView.show()
+
+
+ serializeData: ->
+ _.extend super, canEdit: @options.app.canEdit
diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-condition-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-condition-view.coffee
new file mode 100644
index 00000000000..e928b169b25
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-condition-view.coffee
@@ -0,0 +1,123 @@
+define [
+ 'backbone.marionette',
+ 'templates/quality-gates'
+], (
+ Marionette,
+ Templates
+) ->
+
+ class QualityGateDetailConditionView extends Marionette.ItemView
+ tagName: 'tr'
+ template: Templates['quality-gate-detail-condition']
+ spinner: '<i class="spinner"></i>'
+
+
+ modelEvents:
+ 'change:id': 'render'
+
+
+ ui:
+ periodSelect: '[name=period]'
+ operatorSelect: '[name=operator]'
+ warningInput: '[name=warning]'
+ errorInput: '[name=error]'
+ actionsBox: '.quality-gate-condition-actions'
+ updateButton: '.update-condition'
+
+
+ events:
+ 'click @ui.updateButton': 'saveCondition'
+ 'click .delete-condition': 'deleteCondition'
+ 'click .add-condition': 'saveCondition'
+ 'click .cancel-add-condition': 'cancelAddCondition'
+ 'keyup :input': 'enableUpdate'
+ 'change :input': 'enableUpdate'
+
+
+ initialize: ->
+ @populateMetric()
+
+
+ populateMetric: ->
+ metricKey = @model.get('metric')
+ metric = _.findWhere @options.app.metrics, key: metricKey
+ if metric?
+ switch metric.type
+ when 'WORK_DUR' then metric.placeholder = '1d 7h 59min'
+ when 'RATING' then metric.placeholder = 'A'
+ @model.set { metric: metric }, { silent: true }
+ @model.set { isDiffMetric: metric.key.indexOf('new_') == 0 }, { silent: true }
+
+
+ onRender: ->
+ @ui.periodSelect.val @model.get('period') || '0'
+ @ui.operatorSelect.val @model.get('op')
+ @ui.warningInput.val @model.get('warning')
+ @ui.errorInput.val @model.get('error')
+
+ @ui.periodSelect.select2
+ allowClear: false
+ minimumResultsForSearch: 999
+ width: '200px'
+
+ @ui.operatorSelect.select2
+ allowClear: false
+ minimumResultsForSearch: 999
+ width: '150px'
+
+ @ui.periodSelect.select2('open') if @model.isNew()
+
+
+ showSpinner: ->
+ jQuery(@spinner).prependTo @ui.actionsBox
+ @ui.actionsBox.find(':not(.spinner)').hide()
+
+
+ hideSpinner: ->
+ @ui.actionsBox.find('.spinner').remove()
+ @ui.actionsBox.find(':not(.spinner)').show()
+
+
+ saveCondition: ->
+ @showSpinner()
+ @model.set
+ period: @ui.periodSelect.val()
+ op: @ui.operatorSelect.val()
+ warning: @ui.warningInput.val()
+ error: @ui.errorInput.val()
+ @model.save()
+ .always =>
+ @ui.updateButton.prop 'disabled', true
+ @hideSpinner()
+ .done =>
+ @options.collectionView.updateConditions()
+
+
+ deleteCondition: ->
+ if confirm t('quality_gates.delete_condition.confirm.message')
+ @showSpinner()
+ @model.delete().done =>
+ @options.collectionView.collection.remove @model
+ @options.collectionView.updateConditions()
+ @close()
+
+
+ cancelAddCondition: ->
+ @close()
+
+
+ enableUpdate: ->
+ @ui.updateButton.prop 'disabled', false
+
+
+ serializeData: ->
+ period = _.findWhere(@options.app.periods, key: this.model.get('period'))
+ data = _.extend super,
+ canEdit: @options.app.canEdit
+ periods: @options.app.periods
+ periodText: period?.text
+ unless @options.app.canEdit
+ _.extend data,
+ warning: jQuery('<input>').data('type', @model.get('metric').type).val(@model.get('warning')).originalVal()
+ error: jQuery('<input>').data('type', @model.get('metric').type).val(@model.get('error')).originalVal()
+ data
diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-conditions-empty-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-conditions-empty-view.coffee
new file mode 100644
index 00000000000..9c0362f4e6a
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-conditions-empty-view.coffee
@@ -0,0 +1,11 @@
+define [
+ 'backbone.marionette',
+ 'templates/quality-gates'
+], (
+ Marionette,
+ Templates
+) ->
+
+ class QualityGateDetailConditionsView extends Marionette.ItemView
+ tagName: 'tr'
+ template: Templates['quality-gate-detail-conditions-empty']
diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-conditions-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-conditions-view.coffee
new file mode 100644
index 00000000000..7b76a55ae61
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-conditions-view.coffee
@@ -0,0 +1,86 @@
+define [
+ 'backbone.marionette',
+ 'templates/quality-gates'
+ 'quality-gate/models/condition',
+ 'quality-gate/views/quality-gate-detail-condition-view',
+ 'quality-gate/views/quality-gate-detail-conditions-empty-view'
+], (
+ Marionette,
+ Templates
+ Condition,
+ QualityGateDetailConditionView,
+ QualityGateDetailConditionsEmptyView,
+) ->
+
+ class QualityGateDetailConditionsView extends Marionette.CompositeView
+ template: Templates['quality-gate-detail-conditions']
+ itemView: QualityGateDetailConditionView
+ emptyView: QualityGateDetailConditionsEmptyView
+ itemViewContainer: '.quality-gate-conditions tbody'
+
+
+ ui:
+ metricSelect: '#quality-gate-new-condition-metric'
+ introductionShowMore: '.quality-gate-introduction-show-more'
+ introductionMore: '.quality-gate-introduction-more'
+
+
+ events:
+ 'click @ui.introductionShowMore': 'showMoreIntroduction'
+ 'change @ui.metricSelect': 'addCondition'
+
+
+ itemViewOptions: ->
+ app: @options.app
+ collectionView: @
+
+
+ appendHtml: (compositeView, itemView) ->
+ if (compositeView.isBuffering)
+ compositeView.elBuffer.appendChild itemView.el
+ compositeView._bufferedChildren.push itemView
+ else
+ container = @getItemViewContainer compositeView
+ container.prepend itemView.el
+
+
+ onRender: ->
+ @ui.introductionMore.hide()
+ @ui.metricSelect.select2
+ allowClear: false,
+ width: '250px',
+ placeholder: t('alerts.select_metric')
+
+
+ groupedMetrics: ->
+ metrics = @options.app.metrics
+ metrics = _.groupBy metrics, 'domain'
+ metrics = _.map metrics, (metrics, domain) ->
+ domain: domain, metrics: _.sortBy metrics, 'short_name'
+ _.sortBy metrics, 'domain'
+
+
+ serializeData: ->
+ _.extend super,
+ canEdit: @options.app.canEdit
+ metricGroups: @groupedMetrics()
+
+
+ showMoreIntroduction: ->
+ @ui.introductionShowMore.hide()
+ @ui.introductionMore.show()
+
+
+ addCondition: ->
+ metric = @ui.metricSelect.val()
+ @ui.metricSelect.select2('val', '')
+ condition = new Condition
+ metric: metric
+ gateId: @options.gateId
+ @collection.unshift condition
+
+
+ updateConditions: ->
+ conditions = @collection.map (item) -> _.extend item.toJSON(),
+ metric: item.get('metric').key
+ @options.qualityGate.set { conditions: conditions }, { silent: true }
diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-header-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-header-view.coffee
new file mode 100644
index 00000000000..acf593f149f
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-header-view.coffee
@@ -0,0 +1,90 @@
+define [
+ 'backbone.marionette',
+ 'templates/quality-gates',
+ 'quality-gate/models/quality-gate'
+], (
+ Marionette,
+ Templates
+ QualityGate
+) ->
+
+ class QualityGateDetailHeaderView extends Marionette.ItemView
+ template: Templates['quality-gate-detail-header']
+ spinner: '<i class="spinner"></i>'
+
+
+ modelEvents:
+ 'change': 'render'
+
+
+ events:
+ 'click #quality-gate-rename': 'renameQualityGate'
+ 'click #quality-gate-copy': 'copyQualityGate'
+ 'click #quality-gate-delete': 'deleteQualityGate'
+ 'click #quality-gate-set-as-default': 'setAsDefault'
+ 'click #quality-gate-unset-as-default': 'unsetAsDefault'
+
+
+ renameQualityGate: ->
+ @options.app.qualityGateEditView.method = 'rename'
+ @options.app.qualityGateEditView.model = @model
+ @options.app.qualityGateEditView.show()
+
+
+ copyQualityGate: ->
+ copiedModel = new QualityGate @model.toJSON()
+ copiedModel.set 'default', false
+ @options.app.qualityGateEditView.method = 'copy'
+ @options.app.qualityGateEditView.model = copiedModel
+ @options.app.qualityGateEditView.show()
+
+
+ deleteQualityGate: ->
+ message = if @model.get 'default' then 'quality_gates.delete.confirm.default' else 'quality_gates.delete.confirm.message'
+ if confirm t(message).replace('{0}', @model.get 'name')
+ @showSpinner()
+ jQuery.ajax
+ type: 'POST'
+ url: "#{baseUrl}/api/qualitygates/destroy"
+ data: id: @model.id
+ .always =>
+ @hideSpinner()
+ .done =>
+ @options.app.deleteQualityGate @model.id
+
+
+ changeDefault: (set) ->
+ @showSpinner()
+ data = if set then { id: @model.id } else {}
+ method = if set then 'set_as_default' else 'unset_default'
+ jQuery.ajax
+ type: 'POST'
+ url: "#{baseUrl}/api/qualitygates/#{method}"
+ data: data
+ .always =>
+ @hideSpinner()
+ .done =>
+ @options.app.unsetDefaults @model.id
+ @model.set 'default', !@model.get('default')
+
+
+ setAsDefault: ->
+ @changeDefault true
+
+
+ unsetAsDefault: ->
+ @changeDefault false
+
+
+ showSpinner: ->
+ @$el.hide()
+ jQuery(@spinner).insertBefore @$el
+
+
+ hideSpinner: ->
+ @$el.prev().remove()
+ @$el.show()
+
+
+ serializeData: ->
+ _.extend super, canEdit: @options.app.canEdit
diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-projects-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-projects-view.coffee
new file mode 100644
index 00000000000..afa85c6bad7
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-projects-view.coffee
@@ -0,0 +1,38 @@
+define [
+ 'backbone.marionette',
+ 'templates/quality-gates'
+ 'select-list'
+], (
+ Marionette,
+ Templates
+) ->
+
+ class QualityGateDetailProjectsView extends Marionette.ItemView
+ template: Templates['quality-gate-detail-projects']
+
+
+ onRender: ->
+ unless @model.get('default')
+ new SelectList
+ el: @$('#select-list-projects')
+ width: '100%'
+ readOnly: !@options.app.canEdit
+ format: (item) -> item.name
+ searchUrl: "#{baseUrl}/api/qualitygates/search?gateId=#{@options.gateId}"
+ selectUrl: "#{baseUrl}/api/qualitygates/select"
+ deselectUrl: "#{baseUrl}/api/qualitygates/deselect"
+ extra:
+ gateId: @options.gateId
+ selectParameter: 'projectId'
+ selectParameterValue: 'id'
+ labels:
+ selected: t('quality_gates.projects.with')
+ deselected: t('quality_gates.projects.without')
+ all: t('quality_gates.projects.all')
+ noResults: t('quality_gates.projects.noResults')
+ tooltips:
+ select: t('quality_gates.projects.select_hint')
+ deselect: t('quality_gates.projects.deselect_hint')
+
+ serializeData: ->
+ _.extend super, canEdit: @options.app.canEdit
diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-view.coffee
new file mode 100644
index 00000000000..ff2ca9c70ea
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-view.coffee
@@ -0,0 +1,50 @@
+define [
+ 'backbone.marionette',
+ 'templates/quality-gates',
+ 'quality-gate/collections/conditions',
+ 'quality-gate/views/quality-gate-detail-header-view',
+ 'quality-gate/views/quality-gate-detail-conditions-view',
+ 'quality-gate/views/quality-gate-detail-projects-view'
+], (
+ Marionette,
+ Templates,
+ Conditions,
+ QualityGateDetailHeaderView,
+ QualityGateDetailConditionsView,
+ QualityGateDetailProjectsView
+) ->
+
+ class QualityGateDetailView extends Marionette.Layout
+ template: Templates['quality-gate-detail']
+
+
+ regions:
+ conditionsRegion: '#quality-gate-conditions'
+ projectsRegion: '#quality-gate-projects'
+
+
+ modelEvents:
+ 'change': 'render'
+
+
+ onRender: ->
+ @showConditions()
+ @showProjects()
+
+
+ showConditions: ->
+ conditions = new Conditions @model.get('conditions')
+ view = new QualityGateDetailConditionsView
+ app: @options.app
+ collection: conditions
+ gateId: @model.id
+ qualityGate: @model
+ @conditionsRegion.show view
+
+
+ showProjects: ->
+ view = new QualityGateDetailProjectsView
+ app: @options.app
+ model: @model
+ gateId: @model.id
+ @projectsRegion.show view
diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-edit-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-edit-view.coffee
new file mode 100644
index 00000000000..47e0bbd9101
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-edit-view.coffee
@@ -0,0 +1,86 @@
+define [
+ 'backbone.marionette',
+ 'templates/quality-gates'
+], (
+ Marionette,
+ Templates
+) ->
+
+ class QualityGateEditView extends Marionette.ItemView
+ className: 'modal'
+ template: Templates['quality-gate-edit']
+
+
+ ui:
+ nameInput: '#quality-gate-edit-name'
+
+
+ events:
+ 'submit form': 'onSubmit'
+ 'click #quality-gate-cancel-create': 'hide'
+
+
+ onRender: ->
+ @$el.dialog
+ dialogClass: 'no-close',
+ width: '600px',
+ draggable: false,
+ autoOpen: false,
+ modal: true,
+ minHeight: 50,
+ resizable: false,
+ title: null
+
+
+ show: ->
+ @render()
+ @$el.dialog 'open'
+ @ui.nameInput.focus()
+
+
+ hide: ->
+ @$el.dialog 'close'
+
+
+ saveRequest: (data) ->
+ jQuery.ajax
+ type: 'POST'
+ url: "#{baseUrl}/api/qualitygates/#{@method}"
+ data: data
+ .done => @hide()
+
+
+ onSubmit: (e) ->
+ e.preventDefault()
+ switch @method
+ when 'create' then @createQualityGate()
+ when 'copy' then @copyQualityGate()
+ when 'rename' then @saveQualityGate()
+ else
+
+
+ createQualityGate: ->
+ data = name: @ui.nameInput.val()
+ @saveRequest(data).done (r) =>
+ @model.set id: r.id, name: r.name
+ @options.app.qualityGates.add @model
+ @options.app.router.navigate "show/#{r.id}", trigger: true
+
+
+ saveQualityGate: ->
+ data = id: @model.id, name: @ui.nameInput.val()
+ @saveRequest(data).done (r) =>
+ @model.set name: r.name
+
+
+ copyQualityGate: ->
+ data = id: @model.id, name: @ui.nameInput.val()
+ @saveRequest(data).done (r) =>
+ @model.set id: r.id, name: r.name
+ @options.app.qualityGates.add @model
+ @options.app.router.navigate "show/#{r.id}", trigger: true
+
+
+ serializeData: ->
+ if @model
+ _.extend @model.toJSON(), method: @method
diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-empty-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-empty-view.coffee
new file mode 100644
index 00000000000..c828e4b72f2
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-empty-view.coffee
@@ -0,0 +1,12 @@
+define [
+ 'backbone.marionette',
+ 'templates/quality-gates'
+], (
+ Marionette,
+ Templates
+) ->
+
+ class QualityGateSidebarListEmptyView extends Marionette.ItemView
+ tagName: 'li'
+ className: 'empty'
+ template: Templates['quality-gate-sidebar-list-empty']
diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-item-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-item-view.coffee
new file mode 100644
index 00000000000..70cd6d15366
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-item-view.coffee
@@ -0,0 +1,27 @@
+define [
+ 'backbone.marionette',
+ 'templates/quality-gates'
+], (
+ Marionette,
+ Templates
+) ->
+
+ class QualityGateSidebarListItemView extends Marionette.ItemView
+ tagName: 'li'
+ template: Templates['quality-gate-sidebar-list-item']
+
+
+ modelEvents:
+ 'change': 'render'
+
+
+ events:
+ 'click': 'showQualityGate'
+
+
+ onRender: ->
+ @$el.toggleClass 'active', @options.highlighted
+
+
+ showQualityGate: ->
+ @options.app.router.navigate "show/#{@model.id}", trigger: true
diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-view.coffee
new file mode 100644
index 00000000000..43f92f74d27
--- /dev/null
+++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-view.coffee
@@ -0,0 +1,25 @@
+define [
+ 'backbone.marionette',
+ 'quality-gate/views/quality-gate-sidebar-list-item-view',
+ 'quality-gate/views/quality-gate-sidebar-list-empty-view'
+], (
+ Marionette,
+ QualityGateSidebarListItemView,
+ QualityGateSidebarListEmptyView,
+) ->
+
+ class QualityGateSidebarListView extends Marionette.CollectionView
+ tagName: 'ol'
+ className: 'navigator-results-list'
+ itemView: QualityGateSidebarListItemView
+ emptyView: QualityGateSidebarListEmptyView
+
+
+ itemViewOptions: (model) ->
+ app: @options.app
+ highlighted: model.get('id') == +@highlighted
+
+
+ highlight: (id) ->
+ @highlighted = id
+ @render()