aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-05-26 10:25:10 +0200
committerStas Vilchik <vilchiks@gmail.com>2015-05-26 10:25:10 +0200
commitb13f58aef4dc708dbad195aa46b403e1a7560aee (patch)
treea15c1008b65d8501d16715731d71c00ba7939040
parent15672574f33437c01932c234ce18a1085343ecf7 (diff)
parent7d1c87f025ee1271947ad53f8a7ab581194f5a82 (diff)
downloadsonarqube-b13f58aef4dc708dbad195aa46b403e1a7560aee.tar.gz
sonarqube-b13f58aef4dc708dbad195aa46b403e1a7560aee.zip
Merge pull request #327 from SonarSource/feature/server/provisioning
SONAR-6581 refactor provisioning page
-rw-r--r--server/sonar-web/Gruntfile.coffee11
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/app.js47
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/create-view.js31
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/delete-view.js32
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/form-view.js26
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/header-view.js25
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/layout.js16
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/list-footer-view.js34
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/list-item-view.js34
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/list-view.js11
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/project.js30
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/projects.js40
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/search-view.js49
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-delete.hbs13
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-form.hbs30
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-header.hbs9
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-layout.hbs6
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-list-footer.hbs6
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-list-item.hbs12
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-search.hbs6
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/provisioning_controller.rb50
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/provisioning/_create_form.html.erb33
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/provisioning/_delete_form.html.erb25
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/provisioning/index.html.erb52
-rw-r--r--server/sonar-web/src/test/js/provisioning-spec.js256
-rw-r--r--server/sonar-web/src/test/json/provisioning-spec/delete-error.json7
-rw-r--r--server/sonar-web/src/test/json/provisioning-spec/error.json4
-rw-r--r--server/sonar-web/src/test/json/provisioning-spec/search-big-1.json19
-rw-r--r--server/sonar-web/src/test/json/provisioning-spec/search-big-2.json13
-rw-r--r--server/sonar-web/src/test/json/provisioning-spec/search-created.json31
-rw-r--r--server/sonar-web/src/test/json/provisioning-spec/search-filtered.json13
-rw-r--r--server/sonar-web/src/test/json/provisioning-spec/search.json25
-rw-r--r--server/sonar-web/src/test/views/provisioning.jade5
33 files changed, 847 insertions, 154 deletions
diff --git a/server/sonar-web/Gruntfile.coffee b/server/sonar-web/Gruntfile.coffee
index 3f33fd9dc95..460c62b4802 100644
--- a/server/sonar-web/Gruntfile.coffee
+++ b/server/sonar-web/Gruntfile.coffee
@@ -163,6 +163,10 @@ module.exports = (grunt) ->
name: 'apps/users/app'
out: '<%= ASSETS_PATH %>/js/apps/users/app.js'
+ provisioning: options:
+ name: 'apps/provisioning/app'
+ out: '<%= ASSETS_PATH %>/js/apps/provisioning/app.js'
+
parallel:
build:
@@ -184,6 +188,7 @@ module.exports = (grunt) ->
'requirejs:issueFilterWidget'
'requirejs:markdown'
'requirejs:users'
+ 'requirejs:provisioning'
]
casper:
options: grunt: true
@@ -204,6 +209,7 @@ module.exports = (grunt) ->
'casper:ui'
'casper:workspace'
'casper:users'
+ 'casper:provisioning'
]
@@ -264,6 +270,9 @@ module.exports = (grunt) ->
'<%= BUILD_PATH %>/js/apps/users/templates.js': [
'<%= SOURCE_PATH %>/js/apps/users/templates/**/*.hbs'
]
+ '<%= BUILD_PATH %>/js/apps/provisioning/templates.js': [
+ '<%= SOURCE_PATH %>/js/apps/provisioning/templates/**/*.hbs'
+ ]
clean:
@@ -358,6 +367,8 @@ module.exports = (grunt) ->
src: ['src/test/js/workspace*.js']
users:
src: ['src/test/js/users*.js']
+ provisioning:
+ src: ['src/test/js/provisioning*.js']
uglify:
build:
diff --git a/server/sonar-web/src/main/js/apps/provisioning/app.js b/server/sonar-web/src/main/js/apps/provisioning/app.js
new file mode 100644
index 00000000000..aa754e5ba77
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/app.js
@@ -0,0 +1,47 @@
+define([
+ './layout',
+ './projects',
+ './header-view',
+ './search-view',
+ './list-view',
+ './list-footer-view'
+], function (Layout, Projects, HeaderView, SearchView, ListView, ListFooterView) {
+
+ var App = new Marionette.Application(),
+ init = function (options) {
+ // Layout
+ this.layout = new Layout({ el: options.el });
+ this.layout.render();
+
+ // Collection
+ this.projects = new Projects();
+
+ // Header View
+ this.headerView = new HeaderView({ collection: this.projects });
+ this.layout.headerRegion.show(this.headerView);
+
+ // Search View
+ this.searchView = new SearchView({ collection: this.projects });
+ this.layout.searchRegion.show(this.searchView);
+
+ // List View
+ this.listView = new ListView({ collection: this.projects });
+ this.layout.listRegion.show(this.listView);
+
+ // List Footer View
+ this.listFooterView = new ListFooterView({ collection: this.projects });
+ this.layout.listFooterRegion.show(this.listFooterView);
+
+ // Go!
+ this.projects.fetch();
+ };
+
+ App.on('start', function (options) {
+ window.requestMessages().done(function () {
+ init.call(App, options);
+ });
+ });
+
+ return App;
+
+});
diff --git a/server/sonar-web/src/main/js/apps/provisioning/create-view.js b/server/sonar-web/src/main/js/apps/provisioning/create-view.js
new file mode 100644
index 00000000000..2f7d3b3965c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/create-view.js
@@ -0,0 +1,31 @@
+define([
+ './project',
+ './form-view'
+], function (Project, FormView) {
+
+ return FormView.extend({
+
+ sendRequest: function () {
+ var that = this,
+ project = new Project({
+ name: this.$('#create-project-name').val(),
+ branch: this.$('#create-project-branch').val(),
+ key: this.$('#create-project-key').val()
+ });
+ this.disableForm();
+ return project.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/provisioning/delete-view.js b/server/sonar-web/src/main/js/apps/provisioning/delete-view.js
new file mode 100644
index 00000000000..2ff38de169f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/delete-view.js
@@ -0,0 +1,32 @@
+define([
+ 'components/common/modal-form',
+ './templates'
+], function (ModalForm) {
+
+ return ModalForm.extend({
+ template: Templates['provisioning-delete'],
+
+ onFormSubmit: function () {
+ ModalForm.prototype.onFormSubmit.apply(this, arguments);
+ this.sendRequest();
+ },
+
+ sendRequest: function () {
+ var that = this,
+ collection = this.model.collection;
+ return this.model.destroy({
+ wait: true,
+ statusCode: {
+ // do not show global error
+ 400: null
+ }
+ }).done(function () {
+ collection.refresh();
+ that.close();
+ }).fail(function (jqXHR) {
+ that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/provisioning/form-view.js b/server/sonar-web/src/main/js/apps/provisioning/form-view.js
new file mode 100644
index 00000000000..ed7fe702f2c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/form-view.js
@@ -0,0 +1,26 @@
+define([
+ 'components/common/modal-form',
+ './templates'
+], function (ModalForm) {
+
+ return ModalForm.extend({
+ template: Templates['provisioning-form'],
+
+ onRender: function () {
+ ModalForm.prototype.onRender.apply(this, arguments);
+ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' });
+ },
+
+ onClose: function () {
+ ModalForm.prototype.onClose.apply(this, arguments);
+ this.$('[data-toggle="tooltip"]').tooltip('destroy');
+ },
+
+ onFormSubmit: function () {
+ ModalForm.prototype.onFormSubmit.apply(this, arguments);
+ this.sendRequest();
+ }
+
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/provisioning/header-view.js b/server/sonar-web/src/main/js/apps/provisioning/header-view.js
new file mode 100644
index 00000000000..4be7c21196d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/header-view.js
@@ -0,0 +1,25 @@
+define([
+ './create-view',
+ './templates'
+], function (CreateView) {
+
+ return Marionette.ItemView.extend({
+ template: Templates['provisioning-header'],
+
+ events: {
+ 'click #provisioning-create': 'onCreateClick'
+ },
+
+ onCreateClick: function (e) {
+ e.preventDefault();
+ this.createProject();
+ },
+
+ createProject: function () {
+ new CreateView({
+ collection: this.collection
+ }).render();
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/provisioning/layout.js b/server/sonar-web/src/main/js/apps/provisioning/layout.js
new file mode 100644
index 00000000000..d0627a1e5b8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/layout.js
@@ -0,0 +1,16 @@
+define([
+ './templates'
+], function () {
+
+ return Marionette.Layout.extend({
+ template: Templates['provisioning-layout'],
+
+ regions: {
+ headerRegion: '#provisioning-header',
+ searchRegion: '#provisioning-search',
+ listRegion: '#provisioning-list',
+ listFooterRegion: '#provisioning-list-footer'
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/provisioning/list-footer-view.js b/server/sonar-web/src/main/js/apps/provisioning/list-footer-view.js
new file mode 100644
index 00000000000..6dc243e7b67
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/list-footer-view.js
@@ -0,0 +1,34 @@
+define([
+ './templates'
+], function () {
+
+ return Marionette.ItemView.extend({
+ template: Templates['provisioning-list-footer'],
+
+ collectionEvents: {
+ 'all': 'render'
+ },
+
+ events: {
+ 'click #provisioning-fetch-more': 'onMoreClick'
+ },
+
+ onMoreClick: function (e) {
+ e.preventDefault();
+ this.fetchMore();
+ },
+
+ fetchMore: function () {
+ this.collection.fetchMore();
+ },
+
+ serializeData: function () {
+ return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
+ total: this.collection.total,
+ count: this.collection.length,
+ more: this.collection.hasMore()
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/provisioning/list-item-view.js b/server/sonar-web/src/main/js/apps/provisioning/list-item-view.js
new file mode 100644
index 00000000000..8e565d54981
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/list-item-view.js
@@ -0,0 +1,34 @@
+define([
+ './delete-view',
+ './templates'
+], function (DeleteView) {
+
+ return Marionette.ItemView.extend({
+ tagName: 'li',
+ className: 'panel panel-vertical',
+ template: Templates['provisioning-list-item'],
+
+ events: {
+ 'click .js-project-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');
+ },
+
+ onDeleteClick: function (e) {
+ e.preventDefault();
+ this.deleteProject();
+ },
+
+ deleteProject: function () {
+ new DeleteView({ model: this.model }).render();
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/provisioning/list-view.js b/server/sonar-web/src/main/js/apps/provisioning/list-view.js
new file mode 100644
index 00000000000..138c36b7619
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/list-view.js
@@ -0,0 +1,11 @@
+define([
+ './list-item-view',
+ './templates'
+], function (ListItemView) {
+
+ return Marionette.CollectionView.extend({
+ tagName: 'ul',
+ itemView: ListItemView
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/provisioning/project.js b/server/sonar-web/src/main/js/apps/provisioning/project.js
new file mode 100644
index 00000000000..90963b7a1b5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/project.js
@@ -0,0 +1,30 @@
+define(function () {
+
+ return Backbone.Model.extend({
+ idAttribute: 'uuid',
+
+ urlRoot: function () {
+ return baseUrl + '/api/projects';
+ },
+
+ 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', 'branch')
+ });
+ }
+ if (method === 'delete') {
+ _.defaults(opts, {
+ url: this.urlRoot() + '/delete',
+ type: 'POST',
+ data: { uuids: this.id }
+ });
+ }
+ return Backbone.ajax(opts);
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/provisioning/projects.js b/server/sonar-web/src/main/js/apps/provisioning/projects.js
new file mode 100644
index 00000000000..44f1c5b4b5a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/projects.js
@@ -0,0 +1,40 @@
+define([
+ './project'
+], function (Project) {
+
+ return Backbone.Collection.extend({
+ model: Project,
+
+ url: function () {
+ return baseUrl + '/api/projects/provisioned';
+ },
+
+ parse: function (r) {
+ this.total = r.total;
+ this.p = r.p;
+ this.ps = r.ps;
+ return r.projects;
+ },
+
+ fetch: function (options) {
+ var d = (options && options.data) || {};
+ this.q = d.q;
+ return Backbone.Collection.prototype.fetch.apply(this, arguments);
+ },
+
+ fetchMore: function () {
+ var p = this.p + 1;
+ return this.fetch({ add: true, remove: false, data: { p: p, ps: this.ps, q: this.q } });
+ },
+
+ refresh: function () {
+ return this.fetch({ reset: true, data: { q: this.q } });
+ },
+
+ hasMore: function () {
+ return this.total > this.p * this.ps;
+ }
+
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/provisioning/search-view.js b/server/sonar-web/src/main/js/apps/provisioning/search-view.js
new file mode 100644
index 00000000000..519e3c44b2a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/search-view.js
@@ -0,0 +1,49 @@
+define([
+ './templates'
+], function () {
+
+ return Marionette.ItemView.extend({
+ template: Templates['provisioning-search'],
+
+ events: {
+ 'submit #provisioning-search-form': 'onFormSubmit',
+ 'search #provisioning-search-query': 'debouncedOnKeyUp',
+ 'keyup #provisioning-search-query': 'debouncedOnKeyUp'
+ },
+
+ initialize: function () {
+ this._bufferedValue = null;
+ this.debouncedOnKeyUp = _.debounce(this.onKeyUp, 400);
+ },
+
+ onRender: function () {
+ this.delegateEvents();
+ },
+
+ onFormSubmit: function (e) {
+ e.preventDefault();
+ this.debouncedOnKeyUp();
+ },
+
+ onKeyUp: function () {
+ var q = this.getQuery();
+ if (q === this._bufferedValue) {
+ return;
+ }
+ this._bufferedValue = this.getQuery();
+ if (this.searchRequest != null) {
+ this.searchRequest.abort();
+ }
+ this.searchRequest = this.search(q);
+ },
+
+ getQuery: function () {
+ return this.$('#provisioning-search-query').val();
+ },
+
+ search: function (q) {
+ return this.collection.fetch({ reset: true, data: { q: q } });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-delete.hbs b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-delete.hbs
new file mode 100644
index 00000000000..c1d2a469848
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-delete.hbs
@@ -0,0 +1,13 @@
+<form id="delete-project-form">
+ <div class="modal-head">
+ <h2>Delete Project</h2>
+ </div>
+ <div class="modal-body">
+ <div class="js-modal-messages"></div>
+ Are you sure you want to delete project "{{name}}"?
+ </div>
+ <div class="modal-foot">
+ <button id="delete-project-submit" class="button-red">Delete</button>
+ <a href="#" class="js-modal-close" id="delete-project-cancel">Cancel</a>
+ </div>
+</form>
diff --git a/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-form.hbs b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-form.hbs
new file mode 100644
index 00000000000..e931b66efd7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-form.hbs
@@ -0,0 +1,30 @@
+<form id="create-project-form" autocomplete="off">
+ <div class="modal-head">
+ <h2>Create Project</h2>
+ </div>
+ <div class="modal-body">
+ <div class="js-modal-messages"></div>
+ <div class="modal-field">
+ <label for="create-project-name">Name<em class="mandatory">*</em></label>
+ {{! keep this fake field to hack browser autofill }}
+ <input id="create-project-name-fake" name="name-fake" type="text" class="hidden">
+ <input id="create-project-name" name="name" type="text" size="50" maxlength="200" required>
+ </div>
+ <div class="modal-field">
+ <label for="create-project-branch">Branch</label>
+ {{! keep this fake field to hack browser autofill }}
+ <input id="create-project-branch-fake" name="branch-fake" type="text" class="hidden">
+ <input id="create-project-branch" name="branch" type="text" size="50" maxlength="200">
+ </div>
+ <div class="modal-field">
+ <label for="create-project-key">Key<em class="mandatory">*</em></label>
+ {{! keep this fake field to hack browser autofill }}
+ <input id="create-project-key-fake" name="key-fake" type="text" class="hidden">
+ <input id="create-project-key" name="key" type="text" size="50" maxlength="50" required>
+ </div>
+ </div>
+ <div class="modal-foot">
+ <button id="create-project-submit">Create</button>
+ <a href="#" class="js-modal-close" id="create-project-cancel">Cancel</a>
+ </div>
+</form>
diff --git a/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-header.hbs b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-header.hbs
new file mode 100644
index 00000000000..3daa8f061e3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-header.hbs
@@ -0,0 +1,9 @@
+<header class="page-header">
+ <h1 class="page-title">{{t 'provisioning.page'}}</h1>
+ <div class="page-actions">
+ <div class="button-group">
+ <button id="provisioning-create">Create Project</button>
+ </div>
+ </div>
+ <p class="page-description">{{t 'provisioning.page.description'}}</p>
+</header>
diff --git a/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-layout.hbs b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-layout.hbs
new file mode 100644
index 00000000000..0f07a62acc6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-layout.hbs
@@ -0,0 +1,6 @@
+<div class="page">
+ <div id="provisioning-header"></div>
+ <div id="provisioning-search"></div>
+ <div id="provisioning-list"></div>
+ <div id="provisioning-list-footer"></div>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-list-footer.hbs b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-list-footer.hbs
new file mode 100644
index 00000000000..8d00837153f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-list-footer.hbs
@@ -0,0 +1,6 @@
+<footer class="spacer-top note text-center">
+ {{count}}/{{total}} shown
+ {{#if more}}
+ <a id="provisioning-fetch-more" class="spacer-left" href="#">show more</a>
+ {{/if}}
+</footer>
diff --git a/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-list-item.hbs b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-list-item.hbs
new file mode 100644
index 00000000000..b751d622f8d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-list-item.hbs
@@ -0,0 +1,12 @@
+<div class="pull-right big-spacer-left nowrap">
+ <a class="js-project-delete icon-delete" title="Delete" data-toggle="tooltip" href="#"></a>
+</div>
+
+<div class="display-inline-block text-top width-30">
+ <strong class="js-project-name">{{name}}</strong>
+ <span class="js-project-key note little-spacer-left">{{key}}</span>
+</div>
+
+<div class="display-inline-block text-top width-30">
+ Created at {{dt creationDate}}
+</div>
diff --git a/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-search.hbs b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-search.hbs
new file mode 100644
index 00000000000..3911130a30e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-search.hbs
@@ -0,0 +1,6 @@
+<div class="panel panel-vertical bordered-bottom spacer-bottom">
+ <form id="provisioning-search-form" class="search-box">
+ <button id="provisioning-search-submit" class="search-box-submit button-clean"><i class="icon-search"></i></button>
+ <input id="provisioning-search-query" class="search-box-input" type="search" name="q" placeholder="Search" maxlength="100">
+ </form>
+</div>
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/provisioning_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/provisioning_controller.rb
index 1b91cc25eca..e0ea0fdaa1d 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/provisioning_controller.rb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/provisioning_controller.rb
@@ -20,61 +20,11 @@
class ProvisioningController < ApplicationController
before_filter :admin_required
- verify :method => :delete, :only => [:delete], :redirect_to => {:action => :index}
SECTION=Navigation::SECTION_CONFIGURATION
def index
access_denied unless has_role?("provisioning")
- params['qualifiers'] = 'TRK'
-
- @query_result = Api::Utils.insensitive_sort(
- Internal.component_api.findProvisionedProjects(params)
- ) { |p| p.key }
- end
-
- def create
- verify_post_request
- @id = params[:id]
- @key = params[:key]
- @name = params[:name]
- @branch = params[:branch]
-
- begin
- bad_request('provisioning.missing.key') if @key.blank?
- bad_request('provisioning.missing.name') if @name.blank?
-
- Internal.component_api.createComponent(@key, @branch, @name, nil)
-
- redirect_to :action => 'index'
- rescue Exception => e
- flash.now[:error]= Api::Utils.message(e.message)
- render :partial => 'create_form', :key => @key, :branch => @branch, :name => @name, :status => 400
- end
- end
-
- def create_form
- @id = params[:id]
- @key = params[:key]
- @name = params[:name]
- render :partial => 'create_form'
end
- def delete_form
- @id = params[:id]
- render :partial => 'delete_form'
- end
-
- def delete
- access_denied unless has_role?("provisioning")
-
- @id = params[:id].to_i
- project = Project.first(:conditions => {:id => @id})
- Java::OrgSonarServerUi::JRubyFacade.getInstance().deleteResourceTree(project.key)
- flash.now[:notice]= Api::Utils.message('resource_viewer.resource_deleted')
- redirect_to :action => 'index'
- end
-
- private
-
end
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/provisioning/_create_form.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/provisioning/_create_form.html.erb
deleted file mode 100644
index 5b2bd449e40..00000000000
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/provisioning/_create_form.html.erb
+++ /dev/null
@@ -1,33 +0,0 @@
-<form id="create-resource-form" method="post" action="<%= ApplicationController.root_context -%>/provisioning/create">
- <fieldset>
- <div class="modal-head">
- <h2><%= message('qualifiers.new.TRK') -%></h2>
- </div>
- <div class="modal-body">
- <% if flash.now[:error] %>
- <p class="error"><%= h flash.now[:error] -%></p>
- <% end %>
- <div class="modal-field">
- <label for="key"><%= h message('key') -%> <em class="mandatory">*</em></label>
- <input id="key" name="key" value="<%= h @key -%>" type="text" size="50" maxlength="400" autofocus="autofocus"/>
- </div>
- <div class="modal-field">
- <label for="branch"><%= h message('branch') -%></label>
- <input id="branch" name="branch" value="<%= h @branch -%>" type="text" size="50" maxlength="400" autofocus="autofocus"/>
- </div>
- <div class="modal-field">
- <label for="name"><%= h message('name') -%> <em class="mandatory">*</em></label>
- <input id="name" name="name" value="<%= h @name -%>" type="text" size="50" maxlength="256" value=""/>
- </div>
- </div>
- <div class="modal-foot">
- <input type="submit" value="<%= h message('qualifiers.create.TRK') -%>" id="save-submit"/>
- <a href="#" onclick="return closeModalWindow()" id="save-cancel"><%= h message('cancel') -%></a>
- </div>
- </fieldset>
-</form>
-
-<script>
- $j("#create-resource-form").modalForm();
-</script>
-
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/provisioning/_delete_form.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/provisioning/_delete_form.html.erb
deleted file mode 100644
index 477affddcae..00000000000
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/provisioning/_delete_form.html.erb
+++ /dev/null
@@ -1,25 +0,0 @@
-<form id="delete-project-form" method="DELETE" action="<%= ApplicationController.root_context -%>/provisioning/delete">
- <input type="hidden" name="id" value="<%= @id -%>">
- <input type="hidden" name="_method" value="delete">
- <fieldset>
- <div class="modal-head">
- <h2><%= message 'qualifiers.delete.TRK' -%></h2>
- </div>
- <div class="modal-body">
- <div class="info">
- <img src="<%= ApplicationController.root_context -%>/images/information.png" style="vertical-align: text-bottom"/>
- <%= message 'qualifiers.delete_confirm.TRK' -%>
- </div>
- </div>
- <div class="modal-foot">
- <input type="submit" value="<%= message 'qualifiers.delete.TRK' -%>" id="confirm-submit"/>
- <a href="#" onclick="return closeModalWindow()" id="confirm-cancel"><%= h message('cancel') -%></a>
- </div>
- </fieldset>
-</form>
-
-<script>
- $j("#delete-project-form").modalForm({success: function (data) {
- window.location = baseUrl + '/provisioning';
- }});
-</script>
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/provisioning/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/provisioning/index.html.erb
index 204e2774790..96e3b8965f8 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/provisioning/index.html.erb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/provisioning/index.html.erb
@@ -1,46 +1,6 @@
-<div class="page">
- <header class="page-header">
- <h1 class="page-title"><%= message('provisioning.page') -%></h1>
- <div class="page-actions">
- <%= link_to message('create'), {:action => :create_form}, :id => "create-link-provisioning", :class => 'open-modal' %>
- </div>
- <p class="page-description"><%= message('provisioning.page.description') -%></p>
- </header>
-
- <% if @query_result.empty? %>
- <br/>
- <%= message('provisioning.no_results') -%>
- <% else %>
-
- <table class="data" id="provisioned-resources">
- <thead>
- <tr>
- <th><%= message('key') -%></th>
- <th><%= message('name') -%></span></th>
- <th><%= message('created') -%></th>
- <th class="text-right"><%= message('operations') -%></th>
- </tr>
- </thead>
- <tbody>
- <% @query_result.each_with_index do |resource, index| %>
-
- <tr id="entry-<%= resource.key.parameterize -%>" class="<%= cycle 'even', 'odd' -%>">
- <td>
- <%= link_to h(resource.key), {:controller => 'dashboard', :action => 'index', :id => resource.id},
- :id => "view-#{resource.key.parameterize}" %>
- </td>
- <td><%= h resource.name -%></td>
- <td><%= format_datetime(resource.created_at) -%></td>
- <td class="text-right">
- <%= link_to message('delete'), {:action => :delete_form, :id => resource.id},
- {:id => "delete-#{resource.key.parameterize}", :class => 'open-modal link-action link-red'} -%>
- </td>
- </tr>
- <% end %>
- </tbody>
- </table>
-
- <% end %>
-
-</div>
-
+<div id="provisioning"></div>
+<script>
+ require(['apps/provisioning/app'], function (App) {
+ App.start({ el: '#provisioning' });
+ });
+</script>
diff --git a/server/sonar-web/src/test/js/provisioning-spec.js b/server/sonar-web/src/test/js/provisioning-spec.js
new file mode 100644
index 00000000000..28482a165d5
--- /dev/null
+++ b/server/sonar-web/src/test/js/provisioning-spec.js
@@ -0,0 +1,256 @@
+/* globals casper: false */
+var lib = require('../lib'),
+ testName = lib.testName('Provisioning');
+
+lib.initMessages();
+lib.changeWorkingDirectory('provisioning-spec');
+lib.configureCasper();
+
+casper.test.begin(testName('List'), 5, function (test) {
+ casper
+ .start(lib.buildUrl('provisioning'), function () {
+ lib.setDefaultViewport();
+ lib.mockRequestFromFile('/api/projects/provisioned', 'search.json');
+ })
+
+ .then(function () {
+ casper.evaluate(function () {
+ require(['apps/provisioning/app'], function (App) {
+ App.start({ el: '#provisioning' });
+ });
+ });
+ })
+
+ .then(function () {
+ casper.waitForSelector('#provisioning-list li');
+ })
+
+ .then(function () {
+ test.assertElementCount('#provisioning-list li[data-id]', 3);
+ test.assertSelectorContains('#provisioning-list .js-project-name', 'SonarQube');
+ test.assertSelectorContains('#provisioning-list .js-project-key', 'sonarqube');
+ test.assertElementCount('#provisioning-list .js-project-delete', 3);
+ test.assertSelectorContains('#provisioning-list-footer', '3/3');
+ })
+
+ .then(function () {
+ lib.sendCoverage();
+ })
+ .run(function () {
+ test.done();
+ });
+});
+
+
+casper.test.begin(testName('Search'), 4, function (test) {
+ casper
+ .start(lib.buildUrl('provisioning'), function () {
+ lib.setDefaultViewport();
+ this.searchMock = lib.mockRequestFromFile('/api/projects/provisioned', 'search.json');
+ })
+
+ .then(function () {
+ casper.evaluate(function () {
+ require(['apps/provisioning/app'], function (App) {
+ App.start({ el: '#provisioning' });
+ });
+ });
+ })
+
+ .then(function () {
+ casper.waitForSelector('#provisioning-list li');
+ })
+
+ .then(function () {
+ test.assertElementCount('#provisioning-list li[data-id]', 3);
+ lib.clearRequestMock(this.searchMock);
+ this.searchMock = lib.mockRequestFromFile('/api/projects/provisioned', 'search-filtered.json',
+ { data: { q: 'script' } });
+ casper.evaluate(function () {
+ jQuery('#provisioning-search-query').val('script');
+ });
+ casper.click('#provisioning-search-submit');
+ casper.waitForSelectorTextChange('#provisioning-list-footer');
+ })
+
+ .then(function () {
+ test.assertElementCount('#provisioning-list li[data-id]', 1);
+ lib.clearRequestMock(this.searchMock);
+ this.searchMock = lib.mockRequestFromFile('/api/projects/provisioned', 'search.json');
+ casper.evaluate(function () {
+ jQuery('#provisioning-search-query').val('');
+ });
+ casper.click('#provisioning-search-submit');
+ casper.waitForSelectorTextChange('#provisioning-list-footer');
+ })
+
+ .then(function () {
+ test.assertElementCount('#provisioning-list li[data-id]', 3);
+ test.assert(casper.evaluate(function () {
+ return jQuery('#provisioning-search-query').val() === '';
+ }));
+ })
+
+ .then(function () {
+ lib.sendCoverage();
+ })
+ .run(function () {
+ test.done();
+ });
+});
+
+
+casper.test.begin(testName('Show More'), 4, function (test) {
+ casper
+ .start(lib.buildUrl('provisioning'), function () {
+ lib.setDefaultViewport();
+ this.searchMock = lib.mockRequestFromFile('/api/projects/provisioned', 'search-big-1.json');
+ })
+
+ .then(function () {
+ casper.evaluate(function () {
+ require(['apps/provisioning/app'], function (App) {
+ App.start({ el: '#provisioning' });
+ });
+ });
+ })
+
+ .then(function () {
+ casper.waitForSelector('#provisioning-list li');
+ })
+
+ .then(function () {
+ test.assertElementCount('#provisioning-list li[data-id]', 2);
+ test.assertSelectorContains('#provisioning-list-footer', '2/3');
+ lib.clearRequestMock(this.searchMock);
+ this.searchMock = lib.mockRequestFromFile('/api/projects/provisioned', 'search-big-2.json', { data: { p: '2' } });
+ casper.click('#provisioning-fetch-more');
+ casper.waitForSelectorTextChange('#provisioning-list-footer');
+ })
+
+ .then(function () {
+ test.assertElementCount('#provisioning-list li[data-id]', 3);
+ test.assertSelectorContains('#provisioning-list-footer', '3/3');
+ })
+
+ .then(function () {
+ lib.sendCoverage();
+ })
+ .run(function () {
+ test.done();
+ });
+});
+
+
+casper.test.begin(testName('Create'), 4, function (test) {
+ casper
+ .start(lib.buildUrl('provisioning'), function () {
+ lib.setDefaultViewport();
+ this.searchMock = lib.mockRequestFromFile('/api/projects/provisioned', 'search.json');
+ this.createMock = lib.mockRequestFromFile('/api/projects/create', 'error.json', { status: 400 });
+ })
+
+ .then(function () {
+ casper.evaluate(function () {
+ require(['apps/provisioning/app'], function (App) {
+ App.start({ el: '#provisioning' });
+ });
+ jQuery.ajaxSetup({ dataType: 'json' });
+ });
+ })
+
+ .then(function () {
+ casper.waitForSelector('#provisioning-list li');
+ })
+
+ .then(function () {
+ test.assertElementCount('#provisioning-list li[data-id]', 3);
+ casper.click('#provisioning-create');
+ casper.waitForSelector('#create-project-form');
+ })
+
+ .then(function () {
+ casper.click('#create-project-submit');
+ casper.waitForSelector('.alert.alert-danger');
+ })
+
+ .then(function () {
+ lib.clearRequestMock(this.searchMock);
+ lib.mockRequestFromFile('/api/projects/provisioned', 'search-created.json');
+ lib.clearRequestMock(this.createMock);
+ lib.mockRequest('/api/projects/create', '{}',
+ { data: { name: 'name', branch: 'branch', key: 'key' } });
+ casper.evaluate(function () {
+ jQuery('#create-project-name').val('name');
+ jQuery('#create-project-branch').val('branch');
+ jQuery('#create-project-key').val('key');
+ });
+ casper.click('#create-project-submit');
+ casper.waitForSelectorTextChange('#provisioning-list-footer');
+ })
+
+ .then(function () {
+ test.assertElementCount('#provisioning-list li[data-id]', 4);
+ test.assertSelectorContains('#provisioning-list .js-project-name', 'name');
+ test.assertSelectorContains('#provisioning-list .js-project-key', 'key:branch');
+ })
+
+ .then(function () {
+ lib.sendCoverage();
+ })
+ .run(function () {
+ test.done();
+ });
+});
+
+
+casper.test.begin(testName('Delete'), 1, function (test) {
+ casper
+ .start(lib.buildUrl('provisioning'), function () {
+ lib.setDefaultViewport();
+ this.searchMock = lib.mockRequestFromFile('/api/projects/provisioned', 'search.json');
+ this.updateMock = lib.mockRequestFromFile('/api/projects/delete', 'delete-error.json', { status: 400 });
+ })
+
+ .then(function () {
+ casper.evaluate(function () {
+ require(['apps/provisioning/app'], function (App) {
+ App.start({ el: '#provisioning' });
+ });
+ jQuery.ajaxSetup({ dataType: 'json' });
+ });
+ })
+
+ .then(function () {
+ casper.waitForSelector('#provisioning-list li');
+ })
+
+ .then(function () {
+ casper.click('[data-id="id-javascript"] .js-project-delete');
+ casper.waitForSelector('#delete-project-form');
+ })
+
+ .then(function () {
+ casper.click('#delete-project-submit');
+ casper.waitForSelector('.alert.alert-danger');
+ })
+
+ .then(function () {
+ lib.clearRequestMock(this.updateMock);
+ lib.mockRequest('/api/projects/delete', '{}', { data: { uuids: 'id-javascript'} });
+ casper.click('#delete-project-submit');
+ casper.waitWhileSelector('[data-id="id-javascript"]');
+ })
+
+ .then(function () {
+ test.assert(true);
+ })
+
+ .then(function () {
+ lib.sendCoverage();
+ })
+ .run(function () {
+ test.done();
+ });
+});
+
diff --git a/server/sonar-web/src/test/json/provisioning-spec/delete-error.json b/server/sonar-web/src/test/json/provisioning-spec/delete-error.json
new file mode 100644
index 00000000000..dc1b261128c
--- /dev/null
+++ b/server/sonar-web/src/test/json/provisioning-spec/delete-error.json
@@ -0,0 +1,7 @@
+{
+ "errors": [
+ {
+ "msg": "Some error message"
+ }
+ ]
+}
diff --git a/server/sonar-web/src/test/json/provisioning-spec/error.json b/server/sonar-web/src/test/json/provisioning-spec/error.json
new file mode 100644
index 00000000000..9f5a49f4904
--- /dev/null
+++ b/server/sonar-web/src/test/json/provisioning-spec/error.json
@@ -0,0 +1,4 @@
+{
+ "err_code": 400,
+ "err_msg": "error message"
+}
diff --git a/server/sonar-web/src/test/json/provisioning-spec/search-big-1.json b/server/sonar-web/src/test/json/provisioning-spec/search-big-1.json
new file mode 100644
index 00000000000..bbe3a9bd79e
--- /dev/null
+++ b/server/sonar-web/src/test/json/provisioning-spec/search-big-1.json
@@ -0,0 +1,19 @@
+{
+ "projects": [
+ {
+ "uuid": "id-sonarqube",
+ "key": "sonarqube",
+ "name": "SonarQube",
+ "creationDate": "2015-05-25T16:49:16+0200"
+ },
+ {
+ "uuid": "id-javascript",
+ "key": "javascript",
+ "name": "JavaScript",
+ "creationDate": "2015-05-25T16:49:41+0200"
+ }
+ ],
+ "total": 3,
+ "p": 1,
+ "ps": 2
+}
diff --git a/server/sonar-web/src/test/json/provisioning-spec/search-big-2.json b/server/sonar-web/src/test/json/provisioning-spec/search-big-2.json
new file mode 100644
index 00000000000..ac0bb15c13f
--- /dev/null
+++ b/server/sonar-web/src/test/json/provisioning-spec/search-big-2.json
@@ -0,0 +1,13 @@
+{
+ "projects": [
+ {
+ "uuid": "id-sonarqube-release",
+ "key": "sonarqube:release",
+ "name": "SonarQube",
+ "creationDate": "2015-05-25T16:49:30+0200"
+ }
+ ],
+ "total": 3,
+ "p": 2,
+ "ps": 100
+}
diff --git a/server/sonar-web/src/test/json/provisioning-spec/search-created.json b/server/sonar-web/src/test/json/provisioning-spec/search-created.json
new file mode 100644
index 00000000000..c750b72665b
--- /dev/null
+++ b/server/sonar-web/src/test/json/provisioning-spec/search-created.json
@@ -0,0 +1,31 @@
+{
+ "projects": [
+ {
+ "uuid": "id-sonarqube",
+ "key": "sonarqube",
+ "name": "SonarQube",
+ "creationDate": "2015-05-25T16:49:16+0200"
+ },
+ {
+ "uuid": "id-javascript",
+ "key": "javascript",
+ "name": "JavaScript",
+ "creationDate": "2015-05-25T16:49:41+0200"
+ },
+ {
+ "uuid": "id-sonarqube-release",
+ "key": "sonarqube:release",
+ "name": "SonarQube",
+ "creationDate": "2015-05-25T16:49:30+0200"
+ },
+ {
+ "uuid": "id-key-branch",
+ "key": "key:branch",
+ "name": "name",
+ "creationDate": "2015-05-25T16:49:30+0200"
+ }
+ ],
+ "total": 4,
+ "p": 1,
+ "ps": 100
+}
diff --git a/server/sonar-web/src/test/json/provisioning-spec/search-filtered.json b/server/sonar-web/src/test/json/provisioning-spec/search-filtered.json
new file mode 100644
index 00000000000..85e5bca139e
--- /dev/null
+++ b/server/sonar-web/src/test/json/provisioning-spec/search-filtered.json
@@ -0,0 +1,13 @@
+{
+ "projects": [
+ {
+ "uuid": "id-javascript",
+ "key": "javascript",
+ "name": "JavaScript",
+ "creationDate": "2015-05-25T16:49:41+0200"
+ }
+ ],
+ "total": 1,
+ "p": 1,
+ "ps": 100
+}
diff --git a/server/sonar-web/src/test/json/provisioning-spec/search.json b/server/sonar-web/src/test/json/provisioning-spec/search.json
new file mode 100644
index 00000000000..9697960ec61
--- /dev/null
+++ b/server/sonar-web/src/test/json/provisioning-spec/search.json
@@ -0,0 +1,25 @@
+{
+ "projects": [
+ {
+ "uuid": "id-sonarqube",
+ "key": "sonarqube",
+ "name": "SonarQube",
+ "creationDate": "2015-05-25T16:49:16+0200"
+ },
+ {
+ "uuid": "id-javascript",
+ "key": "javascript",
+ "name": "JavaScript",
+ "creationDate": "2015-05-25T16:49:41+0200"
+ },
+ {
+ "uuid": "id-sonarqube-release",
+ "key": "sonarqube:release",
+ "name": "SonarQube",
+ "creationDate": "2015-05-25T16:49:30+0200"
+ }
+ ],
+ "total": 3,
+ "p": 1,
+ "ps": 100
+}
diff --git a/server/sonar-web/src/test/views/provisioning.jade b/server/sonar-web/src/test/views/provisioning.jade
new file mode 100644
index 00000000000..41016938b55
--- /dev/null
+++ b/server/sonar-web/src/test/views/provisioning.jade
@@ -0,0 +1,5 @@
+extends layouts/main
+
+block body
+ #content
+ #provisioning