aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/update-center
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-06-22 17:57:54 +0200
committerStas Vilchik <vilchiks@gmail.com>2015-06-25 17:02:34 +0200
commitcef2bec878828926143f7e27010ce039c0e1a7a2 (patch)
tree5cbaea3c0527bf08f086ae3f5bd1824dd25da75d /server/sonar-web/src/main/js/apps/update-center
parent0d9fcaa4415ee996e423a97cfe0d965586ca59a5 (diff)
downloadsonarqube-cef2bec878828926143f7e27010ce039c0e1a7a2.tar.gz
sonarqube-cef2bec878828926143f7e27010ce039c0e1a7a2.zip
SONAR-6661 rewrite update center
Diffstat (limited to 'server/sonar-web/src/main/js/apps/update-center')
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/app.js62
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/controller.js25
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/footer-view.js19
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/header-view.js28
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/layout.js16
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/list-item-view.js81
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/list-view.js10
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/plugin-changelog-view.js15
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/plugin.js67
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/plugins.js200
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/router.js32
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/search-view.js65
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/templates/update-center-footer.hbs3
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/templates/update-center-header.hbs28
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/templates/update-center-layout.hbs6
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/templates/update-center-plugin-changelog.hbs42
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/templates/update-center-plugin.hbs120
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/templates/update-center-search.hbs27
-rw-r--r--server/sonar-web/src/main/js/apps/update-center/templates/update-center-system-update.hbs52
19 files changed, 898 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/apps/update-center/app.js b/server/sonar-web/src/main/js/apps/update-center/app.js
new file mode 100644
index 00000000000..f69199a44d0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/app.js
@@ -0,0 +1,62 @@
+define([
+ './layout',
+ './header-view',
+ './search-view',
+ './list-view',
+ './footer-view',
+ './controller',
+ './router',
+ './plugins'
+], function (Layout, HeaderView, SearchView, ListView, FooterView, Controller, Router, Plugins) {
+
+ var App = new Marionette.Application(),
+ init = function (options) {
+ // State
+ this.state = new Backbone.Model();
+
+ // Layout
+ this.layout = new Layout({ el: options.el });
+ this.layout.render();
+
+ // Plugins
+ this.plugins = new Plugins();
+
+ // Controller
+ this.controller = new Controller({ collection: this.plugins, state: this.state });
+
+ // Router
+ this.router = new Router({ controller: this.controller});
+
+ // Header
+ this.headerView = new HeaderView({ collection: this.plugins });
+ this.layout.headerRegion.show(this.headerView);
+
+ // Search
+ this.searchView = new SearchView({ collection: this.plugins, router: this.router, state: this.state });
+ this.layout.searchRegion.show(this.searchView);
+ this.searchView.focusSearch();
+
+ // List
+ this.listView = new ListView({ collection: this.plugins });
+ this.layout.listRegion.show(this.listView);
+
+ // Footer
+ this.footerView = new FooterView({ collection: this.plugins });
+ this.layout.footerRegion.show(this.footerView);
+
+ // Go
+ Backbone.history.start({
+ pushState: true,
+ root: options.urlRoot || (baseUrl + '/updatecenter_new')
+ });
+ };
+
+ 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/update-center/controller.js b/server/sonar-web/src/main/js/apps/update-center/controller.js
new file mode 100644
index 00000000000..bc05151bab7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/controller.js
@@ -0,0 +1,25 @@
+define(function () {
+
+ return Marionette.Controller.extend({
+ initialize: function (options) {
+ this.collection = options.collection;
+ this.state = options.state;
+ },
+
+ showInstalled: function () {
+ this.state.set({ section: 'installed' });
+ this.collection.fetchInstalled();
+ },
+
+ showUpdates: function () {
+ this.state.set({ section: 'updates' });
+ this.collection.fetchUpdates();
+ },
+
+ showAvailable: function () {
+ this.state.set({ section: 'available' });
+ this.collection.fetchAvailable();
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/update-center/footer-view.js b/server/sonar-web/src/main/js/apps/update-center/footer-view.js
new file mode 100644
index 00000000000..2f83d509dc7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/footer-view.js
@@ -0,0 +1,19 @@
+define([
+ './templates'
+], function () {
+
+ return Marionette.ItemView.extend({
+ template: Templates['update-center-footer'],
+
+ collectionEvents: {
+ 'all': 'render'
+ },
+
+ serializeData: function () {
+ return _.extend(this._super(), {
+ total: this.collection.where({ _hidden: false }).length
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/update-center/header-view.js b/server/sonar-web/src/main/js/apps/update-center/header-view.js
new file mode 100644
index 00000000000..99e1095ad7f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/header-view.js
@@ -0,0 +1,28 @@
+define([
+ './templates'
+], function () {
+
+ return Marionette.ItemView.extend({
+ template: Templates['update-center-header'],
+
+ collectionEvents: {
+ all: 'render'
+ },
+
+ events: {
+ 'click .js-cancel-all': 'cancelAll'
+ },
+
+ cancelAll: function () {
+ this.collection.cancelAll();
+ },
+
+ serializeData: function () {
+ return _.extend(this._super(), {
+ installing: this.collection._installedCount,
+ uninstalling: this.collection._uninstalledCount
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/update-center/layout.js b/server/sonar-web/src/main/js/apps/update-center/layout.js
new file mode 100644
index 00000000000..58e480d16de
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/layout.js
@@ -0,0 +1,16 @@
+define([
+ './templates'
+], function () {
+
+ return Marionette.LayoutView.extend({
+ template: Templates['update-center-layout'],
+
+ regions: {
+ headerRegion: '#update-center-header',
+ searchRegion: '#update-center-search',
+ listRegion: '#update-center-plugins',
+ footerRegion: '#update-center-footer'
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/update-center/list-item-view.js b/server/sonar-web/src/main/js/apps/update-center/list-item-view.js
new file mode 100644
index 00000000000..456d83b9597
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/list-item-view.js
@@ -0,0 +1,81 @@
+define([
+ './plugin-changelog-view',
+ './templates'
+], function (PluginChangelogView) {
+
+ var $ = jQuery;
+
+ return Marionette.ItemView.extend({
+ tagName: 'li',
+ className: 'panel panel-vertical',
+ template: Templates['update-center-plugin'],
+ systemTemplate: Templates['update-center-system-update'],
+
+ modelEvents: {
+ 'change:_hidden': 'toggleDisplay',
+ 'change': 'onModelChange',
+ 'request': 'onRequest'
+ },
+
+ events: {
+ 'click .js-changelog': 'onChangelogClick',
+ 'click .js-install': 'install',
+ 'click .js-update': 'update',
+ 'click .js-uninstall': 'uninstall'
+ },
+
+ getTemplate: function () {
+ return this.model.get('_system') ? this.systemTemplate : this.template;
+ },
+
+ onRender: function () {
+ this.$el.attr('data-id', this.model.id);
+ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' });
+ },
+
+ onDestroy: function () {
+ this.$('[data-toggle="tooltip"]').tooltip('destroy');
+ },
+
+ onModelChange: function () {
+ if (!this.model.hasChanged('_hidden')) {
+ this.render();
+ }
+ },
+
+ onChangelogClick: function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ $('body').click();
+ var index = $(e.currentTarget).data('idx'),
+ update = this.model.get('updates')[index],
+ popup = new PluginChangelogView({
+ triggerEl: $(e.currentTarget),
+ model: new Backbone.Model(update)
+ });
+ popup.render();
+ },
+
+ onRequest: function () {
+ this.$('.js-actions').addClass('hidden');
+ this.$('.js-spinner').removeClass('hidden');
+ },
+
+ toggleDisplay: function () {
+ this.$el.toggleClass('hidden', this.model.get('_hidden'));
+ },
+
+ install: function () {
+ this.model.install();
+ },
+
+ update: function () {
+ this.model.update();
+ },
+
+ uninstall: function () {
+ this.model.uninstall();
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/update-center/list-view.js b/server/sonar-web/src/main/js/apps/update-center/list-view.js
new file mode 100644
index 00000000000..e188597e511
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/list-view.js
@@ -0,0 +1,10 @@
+define([
+ './list-item-view'
+], function (ListItemView) {
+
+ return Marionette.CollectionView.extend({
+ tagName: 'ul',
+ childView: ListItemView
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/update-center/plugin-changelog-view.js b/server/sonar-web/src/main/js/apps/update-center/plugin-changelog-view.js
new file mode 100644
index 00000000000..6ea23463630
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/plugin-changelog-view.js
@@ -0,0 +1,15 @@
+define([
+ 'components/common/popup',
+ './templates'
+], function (Popup) {
+
+ return Popup.extend({
+ template: Templates['update-center-plugin-changelog'],
+
+ onRender: function () {
+ this._super();
+ this.$('.bubble-popup-container').isolatedScroll();
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/update-center/plugin.js b/server/sonar-web/src/main/js/apps/update-center/plugin.js
new file mode 100644
index 00000000000..e5d4f254d78
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/plugin.js
@@ -0,0 +1,67 @@
+define(function () {
+
+ return Backbone.Model.extend({
+ idAttribute: 'key',
+
+ defaults: {
+ _hidden: false,
+ _system: false
+ },
+
+ _matchAttribute: function (attr, query) {
+ var value = this.get(attr) || '';
+ return value.search(new RegExp(query, 'i')) !== -1;
+ },
+
+ match: function (query) {
+ return this._matchAttribute('name', query) ||
+ this._matchAttribute('category', query) ||
+ this._matchAttribute('description', query);
+ },
+
+ _action: function (options) {
+ var that = this;
+ var opts = _.extend({}, options, {
+ type: 'POST',
+ data: { key: this.id },
+ beforeSend: function () {
+ // disable global ajax notifications
+ },
+ success: function () {
+ options.success(that);
+ }
+ });
+ var xhr = Backbone.ajax(opts);
+ this.trigger('request', this, xhr);
+ return xhr;
+ },
+
+ install: function () {
+ return this._action({
+ url: baseUrl + '/api/plugins/install',
+ success: function (model) {
+ model.set({ _status: 'installing' });
+ }
+ });
+ },
+
+ update: function () {
+ return this._action({
+ url: baseUrl + '/api/plugins/update',
+ success: function (model) {
+ model.set({ _status: 'installing' });
+ }
+ });
+ },
+
+ uninstall: function () {
+ return this._action({
+ url: baseUrl + '/api/plugins/uninstall',
+ success: function (model) {
+ model.set({ _status: 'uninstalling' });
+ }
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/update-center/plugins.js b/server/sonar-web/src/main/js/apps/update-center/plugins.js
new file mode 100644
index 00000000000..6763c04e250
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/plugins.js
@@ -0,0 +1,200 @@
+define([
+ './plugin'
+], function (Plugin) {
+
+ var $ = jQuery;
+
+ var Plugins = Backbone.Collection.extend({
+ model: Plugin,
+
+ comparator: function (model) {
+ return model.get('name') || '';
+ },
+
+ initialize: function () {
+ this._installedCount = 0;
+ this._uninstalledCount = 0;
+ this.listenTo(this, 'change:_status', this.onStatusChange);
+ },
+
+ parse: function (r) {
+ var that = this;
+ return r.plugins.map(function (plugin) {
+ var updates = [
+ that._getLastWithStatus(plugin.updates, 'COMPATIBLE'),
+ that._getLastWithStatus(plugin.updates, 'REQUIRES_SYSTEM_UPGRADE'),
+ that._getLastWithStatus(plugin.updates, 'DEPS_REQUIRE_SYSTEM_UPGRADE')
+ ].filter(_.identity);
+ updates = updates.map(function (update) {
+ return that._extendChangelog(plugin.updates, update);
+ });
+ return _.extend(plugin, { updates: updates });
+ });
+ },
+
+ _getLastWithStatus: function (updates, status) {
+ var index = _.findLastIndex(updates, function (update) {
+ return update.status === status;
+ });
+ return index !== -1 ? updates[index] : null;
+ },
+
+ _extendChangelog: function (updates, update) {
+ var index = updates.indexOf(update);
+ var previousUpdates = index > 0 ? updates.slice(0, index) : [];
+ return _.extend(update, { previousUpdates: previousUpdates });
+ },
+
+ _fetchInstalled: function () {
+ if (this._installed) {
+ return $.Deferred().resolve().promise();
+ }
+ var that = this;
+ var opts = {
+ type: 'GET',
+ url: baseUrl + '/api/plugins/installed',
+ success: function (r) {
+ that._installed = that.parse(r);
+ }
+ };
+ return Backbone.ajax(opts);
+ },
+
+ _fetchUpdates: function () {
+ if (this._updates) {
+ return $.Deferred().resolve().promise();
+ }
+ var that = this;
+ var opts = {
+ type: 'GET',
+ url: baseUrl + '/api/plugins/updates',
+ success: function (r) {
+ that._updates = that.parse(r);
+ }
+ };
+ return Backbone.ajax(opts);
+ },
+
+ _fetchAvailable: function () {
+ if (this._available) {
+ return $.Deferred().resolve().promise();
+ }
+ var that = this;
+ var opts = {
+ type: 'GET',
+ url: baseUrl + '/api/plugins/available',
+ success: function (r) {
+ that._available = that.parse(r);
+ }
+ };
+ return Backbone.ajax(opts);
+ },
+
+ _fetchPending: function () {
+ var that = this;
+ var opts = {
+ type: 'GET',
+ url: baseUrl + '/api/plugins/pending',
+ success: function (r) {
+ var installing = r.installing.map(function (plugin) {
+ return { key: plugin.key, _status: 'installing' };
+ }),
+ uninstalling = r.removing.map(function (plugin) {
+ return { key: plugin.key, _status: 'uninstalling' };
+ });
+ that._installedCount = installing.length;
+ that._uninstalledCount = uninstalling.length;
+ that._pending = new Plugins([].concat(installing, uninstalling)).models;
+ }
+ };
+ return Backbone.ajax(opts);
+ },
+
+ _fetchSystemUpdates: function () {
+ if (this._systemUpdates) {
+ return $.Deferred().resolve().promise();
+ }
+ var that = this;
+ var opts = {
+ type: 'GET',
+ url: baseUrl + '/api/system/upgrades',
+ success: function (r) {
+ that._systemUpdates = r.upgrades.map(function (update) {
+ return _.extend(update, { _system: true });
+ });
+ }
+ };
+ return Backbone.ajax(opts);
+ },
+
+ fetchInstalled: function () {
+ var that = this;
+ return $.when(this._fetchInstalled(), this._fetchUpdates(), this._fetchPending()).done(function () {
+ var plugins = new Plugins();
+ plugins.set(that._installed);
+ plugins.set(that._updates, { remove: false });
+ plugins.set(that._pending, { add: false, remove: false });
+ that.reset(plugins.models);
+ });
+ },
+
+ fetchUpdates: function () {
+ var that = this;
+ return $.when(this._fetchInstalled(), this._fetchUpdates(), this._fetchPending(), this._fetchSystemUpdates())
+ .done(function () {
+ var plugins = new Plugins();
+ plugins.set(that._installed);
+ plugins.set(that._updates, { remove: true });
+ plugins.set(that._pending, { add: false, remove: false });
+ plugins.add(that._systemUpdates);
+ that.reset(plugins.models);
+ });
+ },
+
+ fetchAvailable: function () {
+ var that = this;
+ return $.when(this._fetchAvailable(), this._fetchPending()).done(function () {
+ var plugins = new Plugins();
+ plugins.set(that._available);
+ plugins.set(that._pending, { add: false, remove: false });
+ that.reset(plugins.models);
+ });
+ },
+
+ search: function (query) {
+ this.filter(function (model) {
+ model.set({ _hidden: !model.match(query) });
+ });
+ },
+
+ cancelAll: function () {
+ var that = this;
+ var opts = {
+ type: 'POST',
+ url: baseUrl + '/api/plugins/cancel_all',
+ success: function () {
+ that._installedCount = 0;
+ that._uninstalledCount = 0;
+ that.forEach(function (model) {
+ model.unset('_status');
+ });
+ that.trigger('change');
+ }
+ };
+ return Backbone.ajax(opts);
+ },
+
+ onStatusChange: function (model, status) {
+ if (status === 'installing') {
+ this._installedCount++;
+ }
+ if (status === 'uninstalling') {
+ this._uninstalledCount++;
+ }
+ this.trigger('change');
+ }
+ });
+
+ return Plugins;
+
+});
diff --git a/server/sonar-web/src/main/js/apps/update-center/router.js b/server/sonar-web/src/main/js/apps/update-center/router.js
new file mode 100644
index 00000000000..b35b12df39e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/router.js
@@ -0,0 +1,32 @@
+define(function () {
+
+ return Backbone.Router.extend({
+ routes: {
+ '': 'index',
+ 'installed': 'showInstalled',
+ 'updates': 'showUpdates',
+ 'available': 'showAvailable'
+ },
+
+ initialize: function (options) {
+ this.controller = options.controller;
+ },
+
+ index: function () {
+ this.navigate('installed', { trigger: true, replace: true });
+ },
+
+ showInstalled: function () {
+ this.controller.showInstalled();
+ },
+
+ showUpdates: function () {
+ this.controller.showUpdates();
+ },
+
+ showAvailable: function () {
+ this.controller.showAvailable();
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/update-center/search-view.js b/server/sonar-web/src/main/js/apps/update-center/search-view.js
new file mode 100644
index 00000000000..3ace406ba06
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/search-view.js
@@ -0,0 +1,65 @@
+define([
+ './templates'
+], function () {
+
+ return Marionette.ItemView.extend({
+ template: Templates['update-center-search'],
+
+ events: {
+ 'change [name="update-center-filter"]': 'onFilterChange',
+
+ 'submit #update-center-search-form': 'onFormSubmit',
+ 'search #update-center-search-query': 'debouncedOnKeyUp',
+ 'keyup #update-center-search-query': 'debouncedOnKeyUp'
+ },
+
+ initialize: function () {
+ this._bufferedValue = null;
+ this.debouncedOnKeyUp = _.debounce(this.onKeyUp, 50);
+ this.listenTo(this.options.state, 'change', this.render);
+ },
+
+ onFilterChange: function () {
+ var value = this.$('[name="update-center-filter"]:checked').val();
+ this.filter(value);
+ },
+
+ filter: function (value) {
+ this.options.router.navigate(value, { trigger: true });
+ },
+
+ onFormSubmit: function (e) {
+ e.preventDefault();
+ this.debouncedOnKeyUp();
+ },
+
+ onKeyUp: function () {
+ var q = this.getQuery();
+ if (q === this._bufferedValue) {
+ return;
+ }
+ this._bufferedValue = this.getQuery();
+ this.search(q);
+ },
+
+ getQuery: function () {
+ return this.$('#update-center-search-query').val();
+ },
+
+ search: function (q) {
+ this.collection.search(q);
+ },
+
+ focusSearch: function () {
+ var that = this;
+ setTimeout(function () {
+ that.$('#update-center-search-query').focus();
+ }, 0);
+ },
+
+ serializeData: function () {
+ return _.extend(this._super(), { state: this.options.state.toJSON() });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/update-center/templates/update-center-footer.hbs b/server/sonar-web/src/main/js/apps/update-center/templates/update-center-footer.hbs
new file mode 100644
index 00000000000..861df440805
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/templates/update-center-footer.hbs
@@ -0,0 +1,3 @@
+<footer class="spacer-top note text-center">
+ {{total}} shown
+</footer>
diff --git a/server/sonar-web/src/main/js/apps/update-center/templates/update-center-header.hbs b/server/sonar-web/src/main/js/apps/update-center/templates/update-center-header.hbs
new file mode 100644
index 00000000000..d358d3dfdd7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/templates/update-center-header.hbs
@@ -0,0 +1,28 @@
+<header class="page-header">
+ <h1 class="page-title">{{t 'update_center.page'}}</h1>
+ <p class="page-description">{{t 'update_center.page.description'}}</p>
+</header>
+
+{{#any installing uninstalling}}
+ <div class="panel panel-warning big-spacer-bottom">
+ <div class="display-inline-block">
+ <p>
+ SonarQube needs to be restarted in order to
+ {{#if installing}}
+ install
+ <strong class="big text-success little-spacer-left little-spacer-right">{{installing}}</strong> plugins
+ {{/if}}
+ {{#all installing uninstalling}}
+ and
+ {{/all}}
+ {{#if uninstalling}}
+ uninstall
+ <strong class="big text-danger little-spacer-left little-spacer-right">{{uninstalling}}</strong> plugins
+ {{/if}}
+ </p>
+ </div>
+ <div class="button-group pull-right">
+ <button class="js-cancel-all button-red">Revert</button>
+ </div>
+ </div>
+{{/any}}
diff --git a/server/sonar-web/src/main/js/apps/update-center/templates/update-center-layout.hbs b/server/sonar-web/src/main/js/apps/update-center/templates/update-center-layout.hbs
new file mode 100644
index 00000000000..5dae4a0854a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/templates/update-center-layout.hbs
@@ -0,0 +1,6 @@
+<div class="page">
+ <div id="update-center-header"></div>
+ <div id="update-center-search"></div>
+ <div id="update-center-plugins"></div>
+ <div id="update-center-footer"></div>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/update-center/templates/update-center-plugin-changelog.hbs b/server/sonar-web/src/main/js/apps/update-center/templates/update-center-plugin-changelog.hbs
new file mode 100644
index 00000000000..6a59095f2b5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/templates/update-center-plugin-changelog.hbs
@@ -0,0 +1,42 @@
+<div class="bubble-popup-container">
+ <div class="bubble-popup-title">Changelog</div>
+
+ <ul>
+ {{#each previousUpdates}}
+ <li class="spacer-bottom">
+ <div class="pull-left spacer-right">
+ <span class="badge badge-success little-spacer-bottom">{{release.version}}</span>
+ </div>
+ <div class="pull-left spacer-right">
+ <p class="note">{{d release.date}}</p>
+ </div>
+ {{#if release.changeLogUrl}}
+ <div class="pull-right spacer-left">
+ <a href="{{release.changeLogUrl}}">Release Notes</a>
+ </div>
+ {{/if}}
+ <div class="overflow-hidden">
+ {{{release.description}}}
+ </div>
+ </li>
+ {{/each}}
+ <li class="spacer-bottom">
+ <div class="pull-left spacer-right">
+ <span class="badge badge-success little-spacer-bottom">{{release.version}}</span>
+ </div>
+ <div class="pull-left spacer-right">
+ <p class="note">{{d release.date}}</p>
+ </div>
+ {{#if release.changeLogUrl}}
+ <div class="pull-right spacer-left">
+ <a href="{{release.changeLogUrl}}">Release Notes</a>
+ </div>
+ {{/if}}
+ <div class="overflow-hidden">
+ {{{release.description}}}
+ </div>
+ </li>
+ </ul>
+</div>
+
+<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/apps/update-center/templates/update-center-plugin.hbs b/server/sonar-web/src/main/js/apps/update-center/templates/update-center-plugin.hbs
new file mode 100644
index 00000000000..aebd742b21d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/templates/update-center-plugin.hbs
@@ -0,0 +1,120 @@
+<div class="display-inline-block text-top width-20">
+ <div>
+ <strong class="js-plugin-name">{{name}}</strong>
+ {{#if category}}
+ <span class="badge spacer-left">{{category}}</span>
+ {{/if}}
+ </div>
+ <div class="js-plugin-description little-spacer-top">{{{description}}}</div>
+</div>
+
+<div class="display-inline-block text-top width-40 big-spacer-left">
+ <div class="pull-left spacer-right">
+ <strong>Versions</strong>
+ </div>
+ <ul class="overflow-hidden bordered-left">
+ {{#if version}}
+ <li class="spacer-left little-spacer-bottom">
+ <strong>{{version}}</strong>&nbsp;installed
+ </li>
+ {{/if}}
+ {{#notEmpty updates}}
+ <li class="spacer-left little-spacer-bottom spacer-top">
+ <strong>Updates:</strong>
+ </li>
+ {{#each updates}}
+ <li class="spacer-left little-spacer-bottom">
+ <div class="pull-left spacer-right">
+ {{#notEq status 'COMPATIBLE'}}
+ <span class="badge badge-warning" data-toggle="tooltip" title="{{t 'update_center.status' status}}">
+ {{release.version}}
+ </span>
+ {{else}}
+ <span class="badge badge-success">{{release.version}}</span>
+ {{/notEq}}
+ </div>
+ <div class="overflow-hidden">
+ {{{release.description}}}
+ <button class="button-link js-changelog issue-rule icon-ellipsis-h" data-idx="{{@index}}"></button>
+ </div>
+ </li>
+ {{/each}}
+ {{/notEmpty}}
+ {{#if release}}
+ <li class="spacer-left little-spacer-bottom">
+ <div class="pull-left spacer-right">
+ <span class="badge badge-success">{{release.version}}</span>
+ </div>
+ <div class="overflow-hidden">
+ {{{release.description}}}
+ {{#notEmpty update.requires}}
+ <p class="little-spacer-top">
+ <strong>Requires</strong>: {{#each update.requires}}{{name}}{{/each}}
+ </p>
+ {{/notEmpty}}
+ </div>
+ </li>
+ {{/if}}
+ </ul>
+</div>
+
+<div class="display-inline-block text-top width-20 big-spacer-left">
+ <ul>
+ {{#any homepageUrl issueTrackerUrl termsAndConditionsUrl}}
+ <li class="little-spacer-bottom">
+ <ul class="list-inline">
+ {{#if homepageUrl}}
+ <li><a href="{{homepageUrl}}">Homepage</a></li>
+ {{/if}}
+ {{#if issueTrackerUrl}}
+ <li><a href="{{issueTrackerUrl}}">Issue Tracker</a></li>
+ {{/if}}
+ {{#if termsAndConditionsUrl}}
+ <li><a href="{{termsAndConditionsUrl}}">Terms and Conditions</a></li>
+ {{/if}}
+ </ul>
+ </li>
+ {{/any}}
+
+ {{#if license}}
+ <li class="little-spacer-bottom">Licensed under {{license}}</li>
+ {{/if}}
+
+ {{#if organizationName}}
+ <li class="little-spacer-bottom">
+ Developed by
+ {{#if organizationUrl}}
+ <a href="{{organizationUrl}}">{{organizationName}}</a>
+ {{else}}
+ {{organizationName}}
+ {{/if}}
+ </li>
+ {{/if}}
+ </ul>
+</div>
+
+<div class="pull-right big-spacer-left nowrap text-right">
+ {{#eq _status 'installing'}}
+ <p class="text-success">To Be Installed</p>
+ {{/eq}}
+
+ {{#eq _status 'uninstalling'}}
+ <p class="text-danger">To Be Uninstalled</p>
+ {{/eq}}
+
+ {{#unless _status}}
+ <i class="js-spinner spinner hidden"></i>
+ <div class="js-actions button-group">
+ {{#each updates}}
+ {{#eq status 'COMPATIBLE'}}
+ <button class="js-update" data-verion="{{release.version}}">Update to {{release.version}}</button>
+ {{/eq}}
+ {{/each}}
+ {{#if version}}
+ <button class="js-uninstall button-red">Uninstall</button>
+ {{else}}
+ <button class="js-install">Install</button>
+ {{/if}}
+ </div>
+ {{/unless}}
+</div>
diff --git a/server/sonar-web/src/main/js/apps/update-center/templates/update-center-search.hbs b/server/sonar-web/src/main/js/apps/update-center/templates/update-center-search.hbs
new file mode 100644
index 00000000000..6c7d20d15b1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/templates/update-center-search.hbs
@@ -0,0 +1,27 @@
+<div class="panel panel-vertical bordered-bottom spacer-bottom">
+ <div class="display-inline-block text-top big-spacer-right">
+ <ul class="radio-toggle">
+ <li>
+ <input type="radio" name="update-center-filter" value="installed" id="update-center-filter-installed"
+ {{#eq state.section 'installed'}}checked{{/eq}}>
+ <label for="update-center-filter-installed">Installed</label>
+ </li>
+ <li>
+ <input type="radio" name="update-center-filter" value="updates" id="update-center-filter-updates"
+ {{#eq state.section 'updates'}}checked{{/eq}}>
+ <label for="update-center-filter-updates">Updates</label>
+ </li>
+ <li>
+ <input type="radio" name="update-center-filter" value="available" id="update-center-filter-available"
+ {{#eq state.section 'available'}}checked{{/eq}}>
+ <label for="update-center-filter-available">Available</label>
+ </li>
+ </ul>
+ </div>
+
+ <form id="update-center-search-form" class="search-box display-inline-block text-top">
+ <button id="update-center-search-submit" class="search-box-submit button-clean"><i class="icon-search"></i></button>
+ <input id="update-center-search-query" class="search-box-input" type="search" name="q" placeholder="Search"
+ maxlength="100" autocomplete="off">
+ </form>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/update-center/templates/update-center-system-update.hbs b/server/sonar-web/src/main/js/apps/update-center/templates/update-center-system-update.hbs
new file mode 100644
index 00000000000..adc28664f78
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/update-center/templates/update-center-system-update.hbs
@@ -0,0 +1,52 @@
+<div class="display-inline-block text-top big-spacer-right">
+ <svg width="60" height="60" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
+ <g id="Layer1">
+ <path d="M52.0841,58.4777L48.8003,58.4777C48.8003,32.0551 26.9663,10.4603 0,10.4603L0,7.06778C28.7278,7.06778 52.1493,30.2719 52.1493,58.4777L52.084,58.4777L52.0841,58.4777Z" style="fill:rgb(75,159,213);fill-rule:nonzero;"/>
+ <path d="M54.3675,40.7974C50.4096,24.2044 36.97,10.2646 20.0072,5.45851L20.7684,2.7619C38.6228,7.95943 52.8453,22.5952 56.9554,40.2755L54.3675,40.7974L54.3675,40.7974L54.3675,40.7974Z" style="fill:rgb(75,159,213);fill-rule:nonzero;"/>
+ <path d="M58.0863,25.27C53.9978,16.332 47.1475,8.65532 38.5792,3.47949L39.6666,1.52228C48.561,6.87202 55.8898,15.0924 60,24.3784L58.0863,25.27L58.0863,25.27L58.0863,25.27L58.0863,25.27Z" style="fill:rgb(75,159,213);fill-rule:nonzero;"/>
+ </g>
+</svg>
+</div>
+
+<div class="display-inline-block text-top width-20">
+ <div>
+ <strong class="js-plugin-name">SonarQube {{version}}</strong>
+ <span class="badge badge-success spacer-left">System Update</span>
+ </div>
+ <div class="js-plugin-description little-spacer-top">{{{description}}}</div>
+
+ <ul class="big-spacer-top">
+ {{#if changeLogUrl}}
+ <li class="little-spacer-bottom">
+ <a href="{{changeLogUrl}}">Release Notes</a>
+ </li>
+ {{/if}}
+ {{#if releaseDate}}
+ <li class="little-spacer-bottom">Released: {{d releaseDate}}</li>
+ {{/if}}
+ </ul>
+</div>
+
+<div class="display-inline-block text-top width-60 big-spacer-left">
+ <div class="pull-left spacer-right">
+ <strong>How to upgrade</strong>
+ </div>
+ <ul class="list-styled overflow-hidden bordered-left">
+ <li class="little-spacer-bottom">Stop SonarQube</li>
+ <li class="little-spacer-bottom"><a href="{{downloadUrl}}">Download</a> and install
+ SonarQube {{version}} after having carefully read the
+ <a href="http://redirect.sonarsource.com/doc/upgrading.html">upgrade guide</a>.
+ </li>
+ {{#each plugins.incompatible}}
+ <li class="little-spacer-bottom">
+ Uninstall the plugin {{name}} which is not compatible with SonarQube {{../version}}.
+ </li>
+ {{/each}}
+ {{#each plugins.requireUpdate}}
+ <li class="little-spacer-bottom">
+ Replace current version of plugin {{name}} by version {{version}}.
+ </li>
+ {{/each}}
+ <li>Start SonarQube</li>
+ </ul>
+</div>