diff --git a/jstests/tests/home/mock_remoteappapi.js b/jstests/tests/home/mock_remoteappapi.js new file mode 100644 index 000000000..e4d4a5897 --- /dev/null +++ b/jstests/tests/home/mock_remoteappapi.js @@ -0,0 +1,45 @@ +define(function (require) { + "use strict"; + var MockApi = function () { + this.available_applications_info = function () { + return [{ + image: { + name: "app1", + ui_name: "Application 1", + icon_128: "", + description: "description", + policy: { + allow_home: true, + volume_source: "", + volume_target: "", + volume_mode: "" + }, + configurables: [ + "resolution" + ] + }, + mapping_id: "12345" + }, + { + image: { + name: "app2", + ui_name: "Application 2", + icon_128: "", + description: "description", + policy: { + allow_home: true, + volume_source: "", + volume_target: "", + volume_mode: "" + }, + configurables: [] + }, + mapping_id: "67890" + }]; + }; + }; + + return { + MockApi: MockApi + }; +}); diff --git a/jstests/tests/home/test_configurables.js b/jstests/tests/home/test_configurables.js new file mode 100644 index 000000000..a07eac250 --- /dev/null +++ b/jstests/tests/home/test_configurables.js @@ -0,0 +1,25 @@ +define(function (require) { + "use strict"; + var configurables = require("home/configurables"); + + QUnit.module("home.configurables"); + QUnit.test("instantiation", function (assert) { + var resolution_class = configurables.from_tag("resolution"); + var resolution = new resolution_class(); + assert.equal(resolution.tag, "resolution"); + assert.equal(resolution.resolution, "Window"); + + resolution.resolution = "1024x768"; + assert.equal(resolution.as_config_dict().resolution, "1024x768"); + }); + + QUnit.test("view", function (assert) { + var resolution_class = configurables.from_tag("resolution"); + var resolution = new resolution_class(); + + var view = resolution.view(); + assert.notEqual(view.find("select"), null); + assert.equal(view.find("option").length, + resolution.resolution_options.length); + }); +}); diff --git a/jstests/tests/home/test_models.js b/jstests/tests/home/test_models.js index 965237e0e..4c9837696 100644 --- a/jstests/tests/home/test_models.js +++ b/jstests/tests/home/test_models.js @@ -1,20 +1,19 @@ define(function (require) { "use strict"; var models = require("home/models"); - - var MockApi = function () { - this.available_applications_info = function() { - return [{}, {}]; - }; - }; + var mock_api = require("../../../../../jstests/tests/home/mock_remoteappapi"); QUnit.module("home.models"); QUnit.test("instantiation", function (assert) { - var mock_api = new MockApi(); - var model = new models.ApplicationListModel(mock_api); - assert.equal(model.data.length, 0); + var api = new mock_api.MockApi(); + var model = new models.ApplicationListModel(api); + assert.equal(model.app_data.length, 0); model.update().done(function() { - assert.equal(model.data.length, 2); + assert.equal(model.app_data.length, 2); + assert.equal(model.app_data[0].image.configurables[0], "resolution"); + assert.notEqual(model.configurables[0].resolution, null); + assert.equal(model.configurables[0].resolution.resolution, "Window"); }); }); }); + diff --git a/jstests/tests/home/test_views.js b/jstests/tests/home/test_views.js index 61e95bff2..68c5b55ed 100644 --- a/jstests/tests/home/test_views.js +++ b/jstests/tests/home/test_views.js @@ -2,43 +2,26 @@ define(function (require) { "use strict"; var models = require("home/models"); var views = require("home/views"); + var mock_api = require("../../../../../jstests/tests/home/mock_remoteappapi"); var $ = require("jquery"); - var MockApi = function () { - this.available_applications_info = function() { - return [ - { - image : { - ui_name : "foo", - policy : { - allow_home: true - } - } - }, - { - image: { - ui_name : "bar", - policy : { - allow_home: true - } - } - }]; - }; - }; - QUnit.module("home.views"); QUnit.test("rendering", function (assert) { - var mock_api = new MockApi(); - var model = new models.ApplicationListModel(mock_api); + var api = new mock_api.MockApi(); + var model = new models.ApplicationListModel(api); var view = new views.ApplicationListView(model); model.update() .done(function() { view.render(); } ) .done(function() { var applist = $("#applist"); assert.equal(applist.children().length, 2); - assert.equal($("#applist > div:nth-child(1) > div > h4").text(), "foo"); - assert.equal($("#applist > div:nth-child(2) > div > h4").text(), "bar"); + assert.equal($("#applist > div:nth-child(1) > div > h4").text(), "Application 1"); + assert.equal($("#applist > div:nth-child(2) > div > h4").text(), "Application 2"); + }) + .done(function() { + model.app_data[0].image.ui_name = "Hello"; + view.update_entry(0); + assert.equal($("#applist > div:nth-child(1) > div > h4").text(), "Hello"); }); - }); }); diff --git a/jstests/testsuite.js b/jstests/testsuite.js index 33d06a1a8..21b734963 100644 --- a/jstests/testsuite.js +++ b/jstests/testsuite.js @@ -16,6 +16,7 @@ }); require([ + "tests/home/test_configurables.js", "tests/home/test_models.js", "tests/home/test_views.js", "tests/test_remoteappapi.js", diff --git a/remoteappmanager/static/js/home/configurables.js b/remoteappmanager/static/js/home/configurables.js new file mode 100644 index 000000000..5687e567a --- /dev/null +++ b/remoteappmanager/static/js/home/configurables.js @@ -0,0 +1,88 @@ +define(["jquery"], function($) { + "use strict"; + + var ResolutionModel = function () { + // Model for the resolution configurable. + var self = this; + this.resolution = "Window"; + this.resolution_options = ["Window", "1920x1080", "1280x1024", "1280x800", "1024x768"]; + + self.view = function () { + // Creates the View to add to the application entry. + var opts = ""; + for (var i = 0; i < self.resolution_options.length; ++i) { + var opt = self.resolution_options[i]; + opts += ""; + } + var widget = $("

" + + "Resolution: " + + "

" + ); + + widget.find("select").change(function() { + if (this.selectedIndex) { + self.resolution = this.options[this.selectedIndex].value; + } + }); + + return widget; + + }; + }; + + ResolutionModel.prototype.tag = "resolution"; + + ResolutionModel.prototype.as_config_dict = function() { + // Returns the configuration dict to hand over to the API request. + // e.g. + // { + // "resolution" : "1024x768" + // } + // The returned dictionary must be added to the configurable + // parameter under the key given by the tag member. + var resolution = this.resolution; + + if (resolution === 'Window') { + resolution = this._viewport_resolution(); + } + + return { + "resolution": resolution + }; + }; + + ResolutionModel.prototype._viewport_resolution = function () { + // Returns the current viewport resolution as a "WxH" string + var e = window, a = 'inner'; + if ( !( 'innerWidth' in window ) ) { + a = 'client'; + e = document.documentElement || document.body; + } + return e[ a+'Width' ]+"x"+e[ a+'Height' ]; + }; + + // Define all your configurables here. + var configurables = { + ResolutionModel: ResolutionModel + }; + + var from_tag = function (tag) { + // Given a tag, lookup the appropriate configurable and + // return it. If the tag matches no configurable, returns null + for (var conf in configurables) { + if (configurables[conf].prototype.tag === tag) { + return configurables[conf]; + } + } + return null; + }; + + var ns = { + from_tag: from_tag + }; + + return $.extend(ns, configurables); + +}); diff --git a/remoteappmanager/static/js/home/home.js b/remoteappmanager/static/js/home/home.js index 1cf9d3d27..dca5b084d 100644 --- a/remoteappmanager/static/js/home/home.js +++ b/remoteappmanager/static/js/home/home.js @@ -20,7 +20,7 @@ require( }; view.view_button_clicked = function (index) { - var app_info = model.data[index]; + var app_info = model.app_data[index]; window.location = utils.url_path_join( base_url, @@ -29,13 +29,13 @@ require( }; view.stop_button_clicked = function (index) { - var app_info = model.data[index]; + var app_info = model.app_data[index]; var url_id = app_info.container.url_id; return appapi.stop_application(url_id, { success: function () { - view.reset_buttons_to_start(index); app_info.container = null; + view.update_entry(index); }, error: function (jqXHR, status, error) { report_error(jqXHR, status, error); @@ -44,8 +44,20 @@ require( view.start_button_clicked = function (index) { // The container is not running. This is a start button. - var mapping_id = model.data[index].mapping_id; - return appapi.start_application(mapping_id, { + var mapping_id = model.app_data[index].mapping_id; + var configurables_data = {}; + var configurables = model.configurables[index]; + configurables_data = {}; + + Object.getOwnPropertyNames(configurables).forEach( + function(val, idx, array) { + var configurable = configurables[val]; + var tag = configurable.tag; + configurables_data[tag] = configurable.as_config_dict(); + } + ); + + return appapi.start_application(mapping_id, configurables_data, { error: function(jqXHR, status, error) { report_error(jqXHR, status, error); }, diff --git a/remoteappmanager/static/js/home/models.js b/remoteappmanager/static/js/home/models.js index 3324542d5..310d11334 100644 --- a/remoteappmanager/static/js/home/models.js +++ b/remoteappmanager/static/js/home/models.js @@ -1,6 +1,6 @@ -define(['jquery'], function ($) { +define(['jquery', 'home/configurables'], function ($, configurables) { "use strict"; - + var ApplicationListModel = function(remote_app_api) { // (constructor) Model for the application list. // Parameters @@ -9,7 +9,13 @@ define(['jquery'], function ($) { this._appapi = remote_app_api; // Contains the data retrieved from the remote API - this.data = []; + this.app_data = []; + + // Contains the submodels for the configurables. + // The values are aligned to the app_data index, and contain + // a dictionary that maps a supported (by the image) configurable tag + // to its client-side model. + this.configurables = []; }; ApplicationListModel.prototype.update = function() { @@ -19,10 +25,32 @@ define(['jquery'], function ($) { // data. Note that, in error conditions, this routine resolves // successfully in any case, and the data is set to empty list var self = this; + return $.when( self._appapi.available_applications_info() ).done(function (app_data) { - self.data = app_data; + self.app_data = app_data; + self.configurables = []; + + // Add the options for some image types + for (var data_idx = 0; data_idx < self.app_data.length; ++data_idx) { + var image = self.app_data[data_idx].image; + self.configurables[data_idx] = {}; + + for (var cfg_idx = 0; cfg_idx < image.configurables.length; ++cfg_idx) { + var tag = image.configurables[cfg_idx]; + + // If this returns null, the tag has not been recognized + // by the client. skip it and let the server deal with the + // missing data, either by using a default or throwing + // an error. + var ConfigurableCls = configurables.from_tag(tag); + + if (ConfigurableCls !== null) { + self.configurables[data_idx][tag] = new ConfigurableCls(); + } + } + } }); }; diff --git a/remoteappmanager/static/js/home/views.js b/remoteappmanager/static/js/home/views.js index 14f08901c..367bc88f1 100644 --- a/remoteappmanager/static/js/home/views.js +++ b/remoteappmanager/static/js/home/views.js @@ -1,6 +1,6 @@ define(["jquery", "utils"], function ($, utils) { "use strict"; - + var ApplicationListView = function(model) { // (Constructor) Represents the application list. In charge of // rendering in on the div with id #applist @@ -24,27 +24,39 @@ define(["jquery", "utils"], function ($, utils) { // Triggered when the button X (left side) is clicked var button = this; var index = $(button).data("index"); - $(button).find(".fa-spinner").show(); - var app_info = self.model.data[index]; + var icon_elem = $(button).find(".x-icon"); + var icons = ['fa-start', 'fa-eye']; + var icon_type; + + for (var i = 0; i < icons.length; ++i) { + if (icon_elem.hasClass(icons[i])) { + icon_type = icons[i]; + } + } - var hide_spinner = function () { - $(button).find(".fa-spinner").hide(); + icon_elem.removeClass(icon_type).addClass("fa-spinner fa-spin"); + + var restore_icon = function () { + icon_elem.removeClass("fa-spinner fa-spin").addClass(icon_type); }; + + var app_info = self.model.app_data[index]; if (app_info.container !== null) { - self.view_button_clicked(index).done(hide_spinner); + self.view_button_clicked(index).done(restore_icon); } else { - self.start_button_clicked(index).done(hide_spinner); + self.start_button_clicked(index).done(restore_icon); } }; this._y_button_clicked = function () { var button = this; var index = $(button).data("index"); - $(button).find(".fa-spinner").show(); + $(button).find(".y-icon").removeClass("fa-stop").addClass("fa-spinner fa-spin"); + self.stop_button_clicked(index).done(function () { - $(button).find(".fa-spinner").hide(); + $(button).find(".y-icon").removeClass("fa-spinner fa-spin").addClass("fa-stop"); }); }; @@ -56,46 +68,103 @@ define(["jquery", "utils"], function ($, utils) { ApplicationListView.prototype.render = function () { // Renders the full application list and adds it to the DOM. - var num_entries = this.model.data.length; - var html = ""; + var num_entries = this.model.app_data.length; + var row; + var applist = $("#applist"); + applist.empty(); if (num_entries === 0) { - html = '

No applications found

'; + row = $('

No applications found

'); + applist.append(row); } else { for (var i = 0; i < num_entries; i++) { - var info = this.model.data[i]; - html += this.render_applist_entry(i, info); + row = this.render_applist_entry(i); + applist.append(row); } } - $("#applist").html(html); - this.register_button_eventhandlers(); }; - ApplicationListView.prototype.render_applist_entry = function (index, info) { + ApplicationListView.prototype.render_applist_entry = function (index) { // Returns a HTML snippet for a single application entry // index: // a progressive index for the entry. - // info: - // A dictionary containing the retrieved data about the application - // and (possibly) the container. - var html = '
'; + + var app_data = this.model.app_data[index]; + var configurables = this.model.configurables[index]; + + var html_template = '' + + '
' + + ' ' + + '
' + + '

{image_name}

' + + '
{policy_html}
' + + '
' + + '
' + + '
' + + ' ' + + '
' + + '
' + + ' ' + + '
' + + '
'; - if (info.image.icon_128) { - html += ''; - } else { - html += ''; - } + var icon = app_data.image.icon_128 ? + "data:image/png;base64,"+app_data.image.icon_128 : + utils.url_path_join(this.base_url, + "static", "images", "generic_appicon_128.png"); - var name; - if (info.image.ui_name) { - name = info.image.ui_name; + var image_name = app_data.image.ui_name ? app_data.image.ui_name : app_data.image.name; + var policy_html = this._policy_html(app_data.image.policy); + var cls, text, stop_style, x_icon; + if (app_data.container !== null) { + cls = "view-button btn-success"; + text = " View"; + x_icon = "fa-eye"; + stop_style = ""; } else { - name = info.image.name; + cls = "start-button btn-primary"; + x_icon = "fa-play"; + text = " Start"; + stop_style = 'visibility: hidden;'; } - html += '

'+name+'

'; - - var policy = info.image.policy; + + var row = $(html_template + .replace(/\{icon\}/g, icon) + .replace(/\{image_name\}/g, image_name) + .replace(/\{policy\}/g, policy_html) + .replace(/\{index\}/g, index) + .replace(/\{button_x_class\}/g, cls) + .replace(/\{button_x_icon\}/g, x_icon) + .replace(/\{button_x_text\}/g, text) + .replace(/\{button_y_style\}/g, stop_style) + .replace(/\{policy_html\}/g, policy_html)); + + row.find(".bnx").click(this._x_button_clicked); + row.find(".bny").click(this._y_button_clicked); + + if (app_data.container === null) { + var ul = $("
'; - - var cls, text, stop_style; - if (info.container !== null) { - cls = "view-button btn-success"; - text = " View"; - stop_style = ""; - } else { - cls = "start-button btn-primary"; - text = " Start"; - stop_style = 'style="visibility: hidden;"'; + mount_html = ""; } - html += '
'; - html += ''; - html += '
'; - html += '
'; - html += ''; - html += '
'; - html += '
'; - return html; - }; - - ApplicationListView.prototype.reset_buttons_to_start = function (index) { - // Used to revert the buttons to their "start" state when the - // User clicks on "stop". - $("#bnx_"+index) - .removeClass("view-button btn-success") - .addClass("start-button btn-primary"); - $("#bnx_"+index+" > span").text(" Start"); - $("#bny_"+index).hide(); - }; - - ApplicationListView.prototype.register_button_eventhandlers = function () { - // Registers the event handlers on the buttons after addition - // of the new entries to the list. - $(".bnx").click(this._x_button_clicked); - $(".bny").click(this._y_button_clicked); + return mount_html; }; return { diff --git a/remoteappmanager/static/js/remoteappapi.js b/remoteappmanager/static/js/remoteappapi.js index e865fae2c..36061df1c 100644 --- a/remoteappmanager/static/js/remoteappapi.js +++ b/remoteappmanager/static/js/remoteappapi.js @@ -24,17 +24,22 @@ define(['jquery', 'utils'], function ($, utils) { return d; }; - RemoteAppAPI.prototype.start_application = function(id, options) { + RemoteAppAPI.prototype.start_application = function(id, configurables_data, options) { // Starts an application with a given id. (async) // // @param id : the mapping id of the application to start. + // @param configurables: a dictionary of values to configure the + // image startup according to its startup + // configurability options. // @param options : the options for the request. Optional. // @return a deferred object for the request. + configurables_data = configurables_data || {}; options = options || {}; options = utils.update(options, { type: 'POST', data: JSON.stringify({ - mapping_id: id + mapping_id: id, + configurables: configurables_data })}); return this._api_request( 'containers',