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 committed by Marius Hein
parent c25ea37fd8
commit e9ade7a339
7 changed files with 179 additions and 14 deletions

View File

@ -35,7 +35,11 @@ class ModulesController extends ActionController
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()

View File

@ -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) {
@ -37,6 +40,7 @@ define([
};
var loadModuleScript = function(name) {
console.log("Loading ", name);
moduleMgr.enableModule("modules/"+name+"/"+name, function(error) {
failedModules.push({
name: name,

View File

@ -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 = {
@ -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);
}
};
@ -49,6 +68,8 @@
var CallInterface = function() {
this.__internalXHRImplementation = $.ajax;
this.clearPendingRequestsFor = function(destination) {
if(!$.isArray(pending)) {
pending = [];
@ -68,7 +89,7 @@
};
this.createRequest = function(url,data) {
var req = $.ajax({
var req = this.__internalXHRImplementation({
type : data ? 'POST' : 'GET',
url : url,
data : data,
@ -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();
});

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

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.
*
**/
var path = require('path');
var registeredDependencies = {};
/**
@ -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]]);
}
@ -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()");
}
@ -91,7 +94,6 @@ initRequireMethods();
function purgeDependencies() {
registeredDependencies = {
'jquery' : GLOBAL.$,
'__define__' : registeredDependencies.__define__,
'logging' : console
};
}
@ -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
@ -122,3 +130,4 @@ module.exports = {
}
}
};