Skip to content

Commit

Permalink
Add support for lazy module loading
Browse files Browse the repository at this point in the history
When the X-Icinga-Module-Enable header is send, the
modulemanager automatically tries to load javascript files for
that module. This is realized by adding the 'registerHeaderListener'
method to the async manager, which allows to listen to specific headers
and firing callbacks if a response with the specified header is retrieved.

Also the tests have changed a bit, requireNow should be used when using
the requiremock, so a require always loads files new.

refs #4092
refs #3753
  • Loading branch information
Jannis Moßhammer committed Jun 21, 2013
1 parent 36c8e0d commit 35c4344
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 14 deletions.
6 changes: 5 additions & 1 deletion application/controllers/ModulesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ public function overviewAction()
public function enableAction()
{
$this->manager->enableModule($this->_getParam('name'));
$this->redirectNow('modules/overview?_render=body');
$this->manager->loadModule($this->_getParam('name'));
$this->getResponse()->setHeader('X-Icinga-Enable-Module', $this->_getParam('name'));
$this->replaceLayout = true;
$this->indexAction();

}

public function disableAction()
Expand Down
8 changes: 6 additions & 2 deletions public/js/icinga/icinga.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,19 @@ define([
var failedModules = [];

var initialize = function () {
registerLazyModuleLoading();
enableInternalModules();

containerMgr.registerAsyncMgr(async);
containerMgr.initializeContainers(document);
log.debug("Initialization finished");

enableModules();
};


var registerLazyModuleLoading = function() {
async.registerHeaderListener("X-Icinga-Enable-Module", loadModuleScript, this);
};

var enableInternalModules = function() {
$.each(internalModules,function(idx,module) {
Expand All @@ -37,6 +40,7 @@ define([
};

var loadModuleScript = function(name) {
console.log("Loading ", name);
moduleMgr.enableModule("modules/"+name+"/"+name, function(error) {
failedModules.push({
name: name,
Expand Down
34 changes: 30 additions & 4 deletions public/js/icinga/util/async.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"use strict";
var asyncMgrInstance = null;

define(['icinga/container','logging','icinga/behaviour','jquery'],function(containerMgr,log,behaviour,$) {
define(['icinga/container','logging','jquery'],function(containerMgr,log,$) {
var headerListeners = {};

var pending = {

Expand All @@ -18,12 +19,30 @@
return target;
};

var handleResponse = function(html) {
var applyHeaderListeners = function(headers) {
for (var header in headerListeners) {
if (headers.getResponseHeader(header) === null) {
// see if the browser/server converts headers to lowercase
if (headers.getResponseHeader(header.toLowerCase()) === null) {
continue;
}
header = header.toLowerCase();
}
var value = headers.getResponseHeader(header);
var listeners = headerListeners[header];
for (var i=0;i<listeners.length;i++) {
listeners[i].fn.apply(listeners[i].scope, [value, header, headers]);
}
}
};

var handleResponse = function(html, status, response) {
applyHeaderListeners(response);
if(this.destination) {
containerMgr.updateContainer(this.destination,html,this);
} else {
containerMgr.createPopupContainer(html,this);
// tbd
// containerMgr.createPopupContainer(html,this);
}
};

Expand All @@ -49,6 +68,8 @@

var CallInterface = function() {

this.__internalXHRImplementation = $.ajax;

this.clearPendingRequestsFor = function(destination) {
if(!$.isArray(pending)) {
pending = [];
Expand All @@ -68,7 +89,7 @@
};

this.createRequest = function(url,data) {
var req = $.ajax({
var req = this.__internalXHRImplementation({
type : data ? 'POST' : 'GET',
url : url,
data : data,
Expand Down Expand Up @@ -101,6 +122,11 @@
this.loadCSS = function(name) {

};

this.registerHeaderListener = function(header, fn, scope) {
headerListeners[header] = headerListeners[header] || [];
headerListeners[header].push({fn: fn, scope:scope});
};
};
return new CallInterface();
});
Expand Down
35 changes: 35 additions & 0 deletions test/js/test/icinga/asyncTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

// {{LICENSE_HEADER}}
// {{LICENSE_HEADER}}
var should = require("should");
var rjsmock = require("requiremock.js");
var asyncMock = require("asyncmock.js");

GLOBAL.document = $('body');


describe('The async module', function() {
it("Allows to react on specific headers", function(done) {
rjsmock.purgeDependencies();
rjsmock.registerDependencies({
'icinga/container' : {
updateContainer : function() {},
createPopupContainer: function() {}
}
});

requireNew("icinga/util/async.js");
var async = rjsmock.getDefine();
var headerValue = null;
asyncMock.setNextAsyncResult(async, "result", false, {
'X-Dont-Care' : 'Ignore-me',
'X-Test-Header' : 'Testme123'
});
async.registerHeaderListener("X-Test-Header", function(value, header) {
should.equal("Testme123", value);
done();
},this);
var test = async.createRequest();
});

});
62 changes: 58 additions & 4 deletions test/js/test/icinga/moduleTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
// {{LICENSE_HEADER}}
var should = require("should");
var rjsmock = require("requiremock.js");
var asyncMock = require("asyncmock.js");

var BASE = "../../../../public/js/";
require(BASE+"icinga/module.js");

requireNew("icinga/module.js");
var module = rjsmock.getDefine();
GLOBAL.document = $('body');

/**
* Test module that only uses eventhandlers and
* no custom logic
Expand Down Expand Up @@ -195,6 +195,9 @@ describe('The icinga module bootstrap', function() {
var testClick = false;
rjsmock.registerDependencies({
"icinga/module": module,
"icinga/util/async" : {
registerHeaderListener: function() {}
},
"modules/test/test" : {
eventHandler: {
"a.test" : {
Expand All @@ -214,7 +217,7 @@ describe('The icinga module bootstrap', function() {
]
});
tearDownTestDOM();
require(BASE+"icinga/icinga.js");
requireNew("icinga/icinga.js");
var icinga = rjsmock.getDefine();
$('body').append($("<a class='test'></a>"));
$('a.test').click();
Expand All @@ -223,4 +226,55 @@ describe('The icinga module bootstrap', function() {
should.equal(icinga.getFailedModules()[0].name, "test2");
tearDownTestDOM();
});

it("Should load modules lazily when discovering a X-Icinga-Enable-Module header", function() {
rjsmock.purgeDependencies();

requireNew("icinga/util/async.js");
var async = rjsmock.getDefine();

rjsmock.registerDependencies({
"icinga/module": module,
"icinga/util/async": async,
"modules/test/test" : {
eventHandler: {
"a.test" : {
click : function() {
testClick = true;
}
}
}
},
"icinga/container" : {
registerAsyncMgr: function() {},
initializeContainers: function() {}
},
"modules/list" : [
]
});

tearDownTestDOM();

requireNew("icinga/icinga.js");
var icinga = rjsmock.getDefine();

var testClick = false;
// The module shouldn't be loaded
$('body').append($("<a class='test'></a>"));
$('a.test').click();
should.equal(testClick, false, "Unregistered module was loaded");

asyncMock.setNextAsyncResult(async,"result", false, {
"X-Icinga-Enable-Module" : "test"
});
async.createRequest();
// The module shouldn't be loaded
$('body').append($("<a class='test'></a>"));
$('a.test').click();
should.equal(testClick, true, "Module wasn't automatically loaded on header!");


tearDownTestDOM();

});
});
33 changes: 33 additions & 0 deletions test/js/testlib/asyncmock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Helper for mocking $.async's XHR requests
*
*/


var getCallback = function(empty, response, succeed, headers) {
if (empty)
return function() {};
return function(callback) {
callback(response, succeed, {
getAllResponseHeaders: function() {
return headers;
},
getResponseHeader: function(header) {
return headers[header] || null;
}
});
};
};

module.exports = {
setNextAsyncResult: function(async, response, fails, headers) {
headers = headers || {};
var succeed = fails ? "fail" : "success";
async.__internalXHRImplementation = function(config) {
return {
done: getCallback(fails, response, succeed, headers),
fail: getCallback(!fails, response, succeed, headers)
};
};
}
};
15 changes: 12 additions & 3 deletions test/js/testlib/requiremock.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* to console.
*
**/
var path = require('path');
var registeredDependencies = {};

/**
Expand All @@ -21,11 +22,13 @@ var registeredDependencies = {};
* in dependencies and calls fn with them as the parameter
*
**/
var debug = false;
var requireJsMock = function(dependencies, fn) {
var fnArgs = [];
for (var i=0;i<dependencies.length;i++) {
if (typeof registeredDependencies[dependencies[i]] === "undefined") {
console.warn("Unknown dependency "+dependencies[i]+" in define()");
if (debug === true)
console.warn("Unknown dependency "+dependencies[i]+" in define()");
}
fnArgs.push(registeredDependencies[dependencies[i]]);
}
Expand Down Expand Up @@ -55,7 +58,7 @@ var defineMock = function() {
var argList = arguments[currentArg];
fn = arguments[currentArg+1];
for (var i=0;i<argList.length;i++) {
if (typeof registerDependencies[argList[i]] === "undefined") {
if (typeof registerDependencies[argList[i]] === "undefined" && debug) {
console.warn("Unknown dependency "+argList[i]+" in define()");
}

Expand Down Expand Up @@ -91,7 +94,6 @@ initRequireMethods();
function purgeDependencies() {
registeredDependencies = {
'jquery' : GLOBAL.$,
'__define__' : registeredDependencies.__define__,
'logging' : console
};
}
Expand All @@ -107,6 +109,12 @@ function registerDependencies(obj) {
registeredDependencies[name] = obj[name];
}
}
var base = path.normalize(__dirname+"../../../../public/js");
GLOBAL.requireNew = function(key) {
key = path.normalize(base+"/"+key);
delete require.cache[key];
return require(key);
};

/**
* The API for this module
Expand All @@ -122,3 +130,4 @@ module.exports = {
}
}
};

0 comments on commit 35c4344

Please sign in to comment.