Add support for lazy module loading

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
This commit is contained in:
Jannis Moßhammer 2013-06-21 15:33:06 +02:00
parent 36c8e0df4a
commit 35c43446d8
7 changed files with 179 additions and 14 deletions

View File

@ -35,7 +35,11 @@ class ModulesController extends ActionController
public function enableAction() public function enableAction()
{ {
$this->manager->enableModule($this->_getParam('name')); $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() public function disableAction()

View File

@ -19,6 +19,7 @@ define([
var failedModules = []; var failedModules = [];
var initialize = function () { var initialize = function () {
registerLazyModuleLoading();
enableInternalModules(); enableInternalModules();
containerMgr.registerAsyncMgr(async); containerMgr.registerAsyncMgr(async);
@ -28,7 +29,9 @@ define([
enableModules(); enableModules();
}; };
var registerLazyModuleLoading = function() {
async.registerHeaderListener("X-Icinga-Enable-Module", loadModuleScript, this);
};
var enableInternalModules = function() { var enableInternalModules = function() {
$.each(internalModules,function(idx,module) { $.each(internalModules,function(idx,module) {
@ -37,6 +40,7 @@ define([
}; };
var loadModuleScript = function(name) { var loadModuleScript = function(name) {
console.log("Loading ", name);
moduleMgr.enableModule("modules/"+name+"/"+name, function(error) { moduleMgr.enableModule("modules/"+name+"/"+name, function(error) {
failedModules.push({ failedModules.push({
name: name, name: name,

View File

@ -3,7 +3,8 @@
"use strict"; "use strict";
var asyncMgrInstance = null; 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 = { var pending = {
@ -18,12 +19,30 @@
return target; 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) { if(this.destination) {
containerMgr.updateContainer(this.destination,html,this); containerMgr.updateContainer(this.destination,html,this);
} else { } else {
containerMgr.createPopupContainer(html,this); // tbd
// containerMgr.createPopupContainer(html,this);
} }
}; };
@ -49,6 +68,8 @@
var CallInterface = function() { var CallInterface = function() {
this.__internalXHRImplementation = $.ajax;
this.clearPendingRequestsFor = function(destination) { this.clearPendingRequestsFor = function(destination) {
if(!$.isArray(pending)) { if(!$.isArray(pending)) {
pending = []; pending = [];
@ -68,7 +89,7 @@
}; };
this.createRequest = function(url,data) { this.createRequest = function(url,data) {
var req = $.ajax({ var req = this.__internalXHRImplementation({
type : data ? 'POST' : 'GET', type : data ? 'POST' : 'GET',
url : url, url : url,
data : data, data : data,
@ -101,6 +122,11 @@
this.loadCSS = function(name) { this.loadCSS = function(name) {
}; };
this.registerHeaderListener = function(header, fn, scope) {
headerListeners[header] = headerListeners[header] || [];
headerListeners[header].push({fn: fn, scope:scope});
};
}; };
return new CallInterface(); return new CallInterface();
}); });

View File

@ -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();
});
});

View File

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

View File

@ -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)
};
};
}
};

View File

@ -13,6 +13,7 @@
* to console. * to console.
* *
**/ **/
var path = require('path');
var registeredDependencies = {}; var registeredDependencies = {};
/** /**
@ -21,10 +22,12 @@ var registeredDependencies = {};
* in dependencies and calls fn with them as the parameter * in dependencies and calls fn with them as the parameter
* *
**/ **/
var debug = false;
var requireJsMock = function(dependencies, fn) { var requireJsMock = function(dependencies, fn) {
var fnArgs = []; var fnArgs = [];
for (var i=0;i<dependencies.length;i++) { for (var i=0;i<dependencies.length;i++) {
if (typeof registeredDependencies[dependencies[i]] === "undefined") { if (typeof registeredDependencies[dependencies[i]] === "undefined") {
if (debug === true)
console.warn("Unknown dependency "+dependencies[i]+" in define()"); console.warn("Unknown dependency "+dependencies[i]+" in define()");
} }
fnArgs.push(registeredDependencies[dependencies[i]]); fnArgs.push(registeredDependencies[dependencies[i]]);
@ -55,7 +58,7 @@ var defineMock = function() {
var argList = arguments[currentArg]; var argList = arguments[currentArg];
fn = arguments[currentArg+1]; fn = arguments[currentArg+1];
for (var i=0;i<argList.length;i++) { 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()"); console.warn("Unknown dependency "+argList[i]+" in define()");
} }
@ -91,7 +94,6 @@ initRequireMethods();
function purgeDependencies() { function purgeDependencies() {
registeredDependencies = { registeredDependencies = {
'jquery' : GLOBAL.$, 'jquery' : GLOBAL.$,
'__define__' : registeredDependencies.__define__,
'logging' : console 'logging' : console
}; };
} }
@ -107,6 +109,12 @@ function registerDependencies(obj) {
registeredDependencies[name] = obj[name]; 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 * The API for this module
@ -122,3 +130,4 @@ module.exports = {
} }
} }
}; };