]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6581 refactor provisioning page
authorStas Vilchik <vilchiks@gmail.com>
Mon, 25 May 2015 14:47:13 +0000 (16:47 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Mon, 25 May 2015 15:04:47 +0000 (17:04 +0200)
33 files changed:
server/sonar-web/Gruntfile.coffee
server/sonar-web/src/main/js/apps/provisioning/app.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/create-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/delete-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/form-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/header-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/layout.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/list-footer-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/list-item-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/list-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/project.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/projects.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/search-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-delete.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-form.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-header.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-layout.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-list-footer.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-list-item.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-search.hbs [new file with mode: 0644]
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/provisioning_controller.rb
server/sonar-web/src/main/webapp/WEB-INF/app/views/provisioning/_create_form.html.erb [deleted file]
server/sonar-web/src/main/webapp/WEB-INF/app/views/provisioning/_delete_form.html.erb [deleted file]
server/sonar-web/src/main/webapp/WEB-INF/app/views/provisioning/index.html.erb
server/sonar-web/src/test/js/provisioning-spec.js [new file with mode: 0644]
server/sonar-web/src/test/json/provisioning-spec/delete-error.json [new file with mode: 0644]
server/sonar-web/src/test/json/provisioning-spec/error.json [new file with mode: 0644]
server/sonar-web/src/test/json/provisioning-spec/search-big-1.json [new file with mode: 0644]
server/sonar-web/src/test/json/provisioning-spec/search-big-2.json [new file with mode: 0644]
server/sonar-web/src/test/json/provisioning-spec/search-created.json [new file with mode: 0644]
server/sonar-web/src/test/json/provisioning-spec/search-filtered.json [new file with mode: 0644]
server/sonar-web/src/test/json/provisioning-spec/search.json [new file with mode: 0644]
server/sonar-web/src/test/views/provisioning.jade [new file with mode: 0644]

index 3f33fd9dc95c89b1853db0863d2a103cd4dc2dfc..460c62b480215a4e290bbcdeeebdde48e7b49569 100644 (file)
@@ -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 (file)
index 0000000..aa754e5
--- /dev/null
@@ -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 (file)
index 0000000..100da85
--- /dev/null
@@ -0,0 +1,32 @@
+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();
+        console.log(jqXHR.responseJSON);
+        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 (file)
index 0000000..2ff38de
--- /dev/null
@@ -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 (file)
index 0000000..ed7fe70
--- /dev/null
@@ -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 (file)
index 0000000..4be7c21
--- /dev/null
@@ -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 (file)
index 0000000..d0627a1
--- /dev/null
@@ -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 (file)
index 0000000..6dc243e
--- /dev/null
@@ -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 (file)
index 0000000..8e565d5
--- /dev/null
@@ -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 (file)
index 0000000..138c36b
--- /dev/null
@@ -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 (file)
index 0000000..90963b7
--- /dev/null
@@ -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 (file)
index 0000000..44f1c5b
--- /dev/null
@@ -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 (file)
index 0000000..519e3c4
--- /dev/null
@@ -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 (file)
index 0000000..c1d2a46
--- /dev/null
@@ -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 (file)
index 0000000..e931b66
--- /dev/null
@@ -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 (file)
index 0000000..3daa8f0
--- /dev/null
@@ -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 (file)
index 0000000..0f07a62
--- /dev/null
@@ -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 (file)
index 0000000..8d00837
--- /dev/null
@@ -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 (file)
index 0000000..b751d62
--- /dev/null
@@ -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 (file)
index 0000000..3911130
--- /dev/null
@@ -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>
index 1b91cc25eca6d3b618524f44499ba45a39e219b7..e0ea0fdaa1d659707719eb60218e0d11eee8c875 100644 (file)
 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 (file)
index 5b2bd44..0000000
+++ /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 (file)
index 477affd..0000000
+++ /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>
index 204e27747903ff28c6671b14bfe4e3e5197cce11..96e3b8965f8975886f978325a7270a0bbd260814 100644 (file)
@@ -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 (file)
index 0000000..24ae5ac
--- /dev/null
@@ -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 ul');
+      })
+
+      .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 ul');
+      })
+
+      .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 ul');
+      })
+
+      .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 ul');
+      })
+
+      .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 ul');
+      })
+
+      .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 (file)
index 0000000..dc1b261
--- /dev/null
@@ -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 (file)
index 0000000..9f5a49f
--- /dev/null
@@ -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 (file)
index 0000000..bbe3a9b
--- /dev/null
@@ -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 (file)
index 0000000..ac0bb15
--- /dev/null
@@ -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 (file)
index 0000000..c750b72
--- /dev/null
@@ -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 (file)
index 0000000..85e5bca
--- /dev/null
@@ -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 (file)
index 0000000..9697960
--- /dev/null
@@ -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 (file)
index 0000000..4101693
--- /dev/null
@@ -0,0 +1,5 @@
+extends layouts/main
+
+block body
+  #content
+    #provisioning