Add new component loader to load frontend-components
Add a component loader that finds components by searching for elements with the "data-icinga-component" attribute and loads the corresponding JavaScript file from the backend to render the component. refs #4456
This commit is contained in:
parent
4cdab8d904
commit
52c66893ab
|
@ -111,13 +111,18 @@ class StaticController extends ActionController
|
|||
$module = $this->_getParam('module_name');
|
||||
$file = $this->_getParam('file');
|
||||
|
||||
if (!Icinga::app()->getModuleManager()->hasEnabled($module)) {
|
||||
echo '/** Module not enabled **/';
|
||||
return;
|
||||
if ($module == 'app') {
|
||||
$basedir = Icinga::app()->getApplicationDir('../public/js/icinga/components/');
|
||||
$filePath = $basedir . $file;
|
||||
} else {
|
||||
if (!Icinga::app()->getModuleManager()->hasEnabled($module)) {
|
||||
echo "/** Module not enabled **/";
|
||||
return;
|
||||
}
|
||||
$basedir = Icinga::app()->getModuleManager()->getModule($module)->getBaseDir();
|
||||
$filePath = $basedir . '/public/js/' . $file;
|
||||
}
|
||||
$basedir = Icinga::app()->getModuleManager()->getModule($module)->getBaseDir();
|
||||
|
||||
$filePath = $basedir . '/public/js/' . $file;
|
||||
if (!file_exists($filePath)) {
|
||||
echo '/** Module has no js files **/';
|
||||
return;
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* {{LICENSE_HEADER}}
|
||||
* {{LICENSE_HEADER}}
|
||||
*/
|
||||
|
||||
/**
|
||||
* A module to load, manage and query frontend components
|
||||
*
|
||||
*/
|
||||
define(['jquery', 'logging', 'icinga/registry'], function ($, log, registry) {
|
||||
"use strict";
|
||||
|
||||
var Manager = function() {
|
||||
|
||||
/**
|
||||
* Load the component with the given type and attach it to the target
|
||||
*
|
||||
* @param {String} cmpType The component type to load '<module>/<component>'
|
||||
* @param {HTMLElement} target The targeted dom node
|
||||
* @param {function} fin The called when the component was successfully loaded
|
||||
* @param {function} err The error-callback
|
||||
*/
|
||||
var loadComponent = function(cmpType, target, fin, err) {
|
||||
requirejs(
|
||||
['modules/'+cmpType],
|
||||
function (Cmp) {
|
||||
var cmp;
|
||||
try {
|
||||
cmp = new Cmp(target);
|
||||
} catch (e) {
|
||||
log.emergency(e);
|
||||
err(e);
|
||||
return;
|
||||
}
|
||||
if (fin) {
|
||||
fin(cmp);
|
||||
}
|
||||
},
|
||||
function (ex) {
|
||||
if (!ex) {
|
||||
return;
|
||||
}
|
||||
log.emergency('Component "'+cmpType+'" could not be loaded.', ex);
|
||||
if (err) {
|
||||
err(ex);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Load all new components and remove components that were removed from
|
||||
* the DOM
|
||||
*
|
||||
* @param {function} fin Called when the loading is completed
|
||||
*/
|
||||
this.load = function(fin) {
|
||||
|
||||
/*
|
||||
* Count the amount of pending callbacks to make sure everything is loaded
|
||||
* when calling the garbage collection.
|
||||
*/
|
||||
var pendingFns = 1;
|
||||
|
||||
var finalize = function() {
|
||||
pendingFns--;
|
||||
/*
|
||||
* Only return when all components are loaded
|
||||
*/
|
||||
if (pendingFns === 0) {
|
||||
registry.removeInactive();
|
||||
if (fin) {
|
||||
fin();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
registry.markAllInactive();
|
||||
|
||||
$('div[data-icinga-component]')
|
||||
.each(function(index, el) {
|
||||
var type = $(el).attr('data-icinga-component');
|
||||
pendingFns++;
|
||||
loadComponent(
|
||||
type,
|
||||
el,
|
||||
function(cmp) {
|
||||
var id = registry.add(cmp, el.id, type);
|
||||
registry.markActive(id);
|
||||
el.id = id;
|
||||
finalize();
|
||||
},
|
||||
finalize
|
||||
);
|
||||
});
|
||||
finalize();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the id of the given component, if one is assigned
|
||||
*
|
||||
* @param {*} component The component of which the id should be retrieved
|
||||
*
|
||||
* @returns {String|null} The id of the component, or null
|
||||
*/
|
||||
this.getId = function(component) {
|
||||
return registry.getId(component);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the component that is assigned to the given id
|
||||
*
|
||||
* @param {String} id The id of the component
|
||||
*
|
||||
* @returns {*} The component or null
|
||||
*/
|
||||
this.getById = function(id) {
|
||||
return registry.getById(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all components that match the given type
|
||||
*
|
||||
* @param {String} type The component type in the form '<module>/<component>'
|
||||
*
|
||||
* @returns {*|Array} The components or an empty array
|
||||
*/
|
||||
this.getByType = function(type) {
|
||||
return registry.getByType(type);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all components
|
||||
*
|
||||
* @returns {*|Array} The components or an empty array
|
||||
*/
|
||||
this.getComponents = function() {
|
||||
return registry.getComponents();
|
||||
};
|
||||
};
|
||||
return new Manager();
|
||||
});
|
|
@ -6,16 +6,15 @@
|
|||
define(['jquery', 'datetimepicker'], function($) {
|
||||
"use strict";
|
||||
|
||||
var DateTimeBehaviour = function() {
|
||||
this.enable = function() {
|
||||
$('.datetime input')
|
||||
.attr('data-format', 'yyyy-MM-dd hh:mm:ss');
|
||||
$('.datetime')
|
||||
.addClass('input-append')
|
||||
.append('<span class="add-on">' +
|
||||
'<i data-time-icon="icon-time" data-date-icon="icon-calendar"></i></span>')
|
||||
.datetimepicker();
|
||||
}
|
||||
var DateTimePicker = function(target) {
|
||||
$(target).find('.datetime input')
|
||||
.attr('data-format', 'yyyy-MM-dd hh:mm:ss');
|
||||
|
||||
$(target).find('.datetime')
|
||||
.addClass('input-append')
|
||||
.append('<span class="add-on">' +
|
||||
'<i data-time-icon="icon-time" data-date-icon="icon-calendar"></i></span>')
|
||||
.datetimepicker();
|
||||
};
|
||||
return new DateTimeBehaviour();
|
||||
return DateTimePicker;
|
||||
});
|
||||
|
|
|
@ -5,10 +5,15 @@ define([
|
|||
'icinga/module',
|
||||
'icinga/util/async',
|
||||
'icinga/container',
|
||||
'modules/list'
|
||||
], function ($, log, moduleMgr, async, containerMgr, modules) {
|
||||
'modules/list',
|
||||
'icinga/component'
|
||||
], function ($, log, moduleMgr, async, containerMgr, modules,components) {
|
||||
'use strict';
|
||||
|
||||
$(document).ready(function(){
|
||||
components.load();
|
||||
});
|
||||
|
||||
/**
|
||||
* Icinga prototype
|
||||
*/
|
||||
|
@ -62,9 +67,6 @@ define([
|
|||
$(document).ready(initialize.bind(this));
|
||||
|
||||
return {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
loadModule: function(blubb,bla) {
|
||||
behaviour.registerBehaviour(blubb,bla);
|
||||
},
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
/**
|
||||
* {{LICENSE_HEADER}}
|
||||
* {{LICENSE_HEADER}}
|
||||
*/
|
||||
|
||||
/**
|
||||
* A component registry that maps components to unique IDs and keeps track
|
||||
* of component types to allow easy querying
|
||||
*
|
||||
*/
|
||||
define(['jquery'], function($) {
|
||||
"use strict";
|
||||
|
||||
var Registry = function() {
|
||||
var self = this;
|
||||
|
||||
/**
|
||||
* Map ids to components
|
||||
*/
|
||||
var components = {};
|
||||
|
||||
/**
|
||||
* Generate a new component id
|
||||
*/
|
||||
var createId = (function() {
|
||||
var id = 0;
|
||||
return function() {
|
||||
return 'icinga-component-'+id++;
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* Get the id of the given component, if one is assigned
|
||||
*
|
||||
* @param {*} component The component of which the id should be retrieved
|
||||
*
|
||||
* @returns {String|null} The id of the component, or null
|
||||
*/
|
||||
this.getId = function(cmp) {
|
||||
var id = null;
|
||||
$.each(components, function(key, value) {
|
||||
if (value && value.cmp === cmp) {
|
||||
id = key;
|
||||
}
|
||||
});
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the component that is assigned to the given id
|
||||
*
|
||||
* @param {String} id The id of the component
|
||||
*
|
||||
* @returns {*} The component or null
|
||||
*/
|
||||
this.getById = function(id) {
|
||||
return components[id] && components[id].cmp;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all components that match the given type
|
||||
*
|
||||
* @param {String} type The component type in the form '<module>/<component>'
|
||||
*
|
||||
* @returns {*|Array} The components or an empty array
|
||||
*/
|
||||
this.getByType = function(type) {
|
||||
return $.map(components, function(entry) {
|
||||
return entry.type === type ? entry.cmp : null;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all components
|
||||
*
|
||||
* @returns {*|Array} The components or an empty array
|
||||
*/
|
||||
this.getComponents = function() {
|
||||
return $.map(components, function(entry) {
|
||||
return entry.cmp;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the given component to the registry and return the assigned id
|
||||
*
|
||||
* @param {*} cmp The component to add
|
||||
* @param {String} id The optional id that should be assigned to that component
|
||||
* @param {String} type The component type to load '<module>/<component>'
|
||||
*
|
||||
* @returns {*|Array}
|
||||
*/
|
||||
this.add = function(cmp, id, type) {
|
||||
if (!id){
|
||||
id = self.getId(cmp) || createId();
|
||||
}
|
||||
components[id] = {
|
||||
cmp: cmp,
|
||||
type: type,
|
||||
active: true
|
||||
};
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark all components inactive
|
||||
*/
|
||||
this.markAllInactive = function() {
|
||||
$.each(components,function(index, el){
|
||||
if (el && el.active) {
|
||||
el.active = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark the component with the given id as active
|
||||
*/
|
||||
this.markActive = function(id) {
|
||||
if (components[id]) {
|
||||
components[id].active = true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Let the garbage collection remove all inactive components
|
||||
*/
|
||||
this.removeInactive = function() {
|
||||
$.each(components, function(key,value) {
|
||||
if (!value || !value.active) {
|
||||
delete components[key];
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
return new Registry();
|
||||
});
|
|
@ -45,6 +45,10 @@ define(function() {
|
|||
},
|
||||
error: function() {
|
||||
logTagged('error', arguments);
|
||||
},
|
||||
emergency: function() {
|
||||
logTagged('emergency', arguments);
|
||||
// TODO: log *emergency* errors to the backend
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
/**
|
||||
* {{LICENSE_HEADER}}
|
||||
* {{LICENSE_HEADER}}
|
||||
*/
|
||||
|
||||
require('should');
|
||||
var rjsmock = require('requiremock.js');
|
||||
|
||||
GLOBAL.document = $('body');
|
||||
var component;
|
||||
|
||||
|
||||
/**
|
||||
* Set up the test fixture
|
||||
*
|
||||
* @param registry The optional registry mock that should be used.
|
||||
*/
|
||||
var setUp = function(registry)
|
||||
{
|
||||
rjsmock.purgeDependencies();
|
||||
|
||||
requireNew('icinga/registry.js');
|
||||
registry = registry || rjsmock.getDefine();
|
||||
|
||||
rjsmock.registerDependencies({
|
||||
'icinga/registry': registry,
|
||||
|
||||
/*
|
||||
* Available components
|
||||
*/
|
||||
'modules/app/component1': function(cmp) {
|
||||
cmp.test = 'changed-by-component-1';
|
||||
this.type = function(){
|
||||
return "app/component1";
|
||||
};
|
||||
},
|
||||
'modules/app/component2': function(cmp) {
|
||||
cmp.test = 'changed-by-component-2';
|
||||
this.type = function(){
|
||||
return "app/component2";
|
||||
};
|
||||
},
|
||||
'modules/module/component3': function(cmp) {
|
||||
cmp.test = 'changed-by-component-3-from-module';
|
||||
this.type = function(){
|
||||
return "module/component3";
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
$('body').empty();
|
||||
|
||||
requireNew('icinga/component.js');
|
||||
component = rjsmock.getDefine();
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new component to the current test-DOM
|
||||
*
|
||||
* @param type {String} The type of the component in the form: "<module>/<type>"
|
||||
* @param id {String} The optional id of the component
|
||||
*/
|
||||
var addComponent = function(type,id) {
|
||||
var txt = '<div '+( id ? ( ' id= "'+id+'" ' ) : '' ) +
|
||||
' data-icinga-component="'+type+'" >test</div>';
|
||||
|
||||
$('body').append(txt);
|
||||
};
|
||||
|
||||
describe('Component loader',function(){
|
||||
|
||||
it('Component loaded with automatic id',function(){
|
||||
setUp();
|
||||
addComponent('app/component1');
|
||||
|
||||
component.load(function(){
|
||||
// loading complete
|
||||
var cmpNode = $('#icinga-component-0');
|
||||
cmpNode.length.should.equal(1);
|
||||
cmpNode[0].test.should.equal('changed-by-component-1');
|
||||
component.getById('icinga-component-0').type().should.equal('app/component1');
|
||||
});
|
||||
});
|
||||
|
||||
it('Component load with user-defined id',function(){
|
||||
setUp();
|
||||
addComponent('app/component2','some-id');
|
||||
|
||||
component.load(function(){
|
||||
// loading complete
|
||||
var cmpNode = $('#some-id');
|
||||
cmpNode.length.should.equal(1);
|
||||
cmpNode[0].test.should.equal('changed-by-component-2');
|
||||
component.getById('some-id').type().should.equal('app/component2');
|
||||
});
|
||||
});
|
||||
|
||||
it('Garbage collection removes deleted components',function(){
|
||||
setUp();
|
||||
addComponent('app/component1');
|
||||
addComponent('app/component2');
|
||||
addComponent('app/component2');
|
||||
addComponent('module/component3');
|
||||
|
||||
component.load(function(){
|
||||
// loading complete
|
||||
var components = component.getComponents();
|
||||
components.length.should.equal(4);
|
||||
$('body').empty();
|
||||
component.load(function(){
|
||||
var components = component.getComponents();
|
||||
components.length.should.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Component queries are delegated to the registry correctly',function(){
|
||||
var getByIdCalled = false;
|
||||
var getByTypeCalled = false;
|
||||
var getComponentsCalled = false;
|
||||
|
||||
var registryMock = {
|
||||
getById: function(id){
|
||||
getByIdCalled = true;
|
||||
id.should.equal('some-id');
|
||||
},
|
||||
getByType: function(type){
|
||||
getByTypeCalled = true;
|
||||
type.should.equal('some-type');
|
||||
},
|
||||
getComponents: function(){
|
||||
getComponentsCalled = true;
|
||||
}
|
||||
};
|
||||
|
||||
setUp(registryMock);
|
||||
|
||||
component.getById('some-id');
|
||||
getByIdCalled.should.be.true;
|
||||
|
||||
component.getByType('some-type');
|
||||
getByTypeCalled.should.be.true;
|
||||
|
||||
component.getComponents();
|
||||
getComponentsCalled.should.be.true;
|
||||
});
|
||||
});
|
||||
|
|
@ -11,6 +11,7 @@ var should = require("should");
|
|||
var rjsmock = require("requiremock.js");
|
||||
var asyncMock = require("asyncmock.js");
|
||||
|
||||
|
||||
requireNew("icinga/module.js");
|
||||
var module = rjsmock.getDefine();
|
||||
GLOBAL.document = $('body');
|
||||
|
@ -19,6 +20,7 @@ GLOBAL.document = $('body');
|
|||
* Test module that only uses eventhandlers and
|
||||
* no custom logic
|
||||
**/
|
||||
|
||||
var eventOnlyModule = function() {
|
||||
var thiz = this;
|
||||
this.moduleLinkClick = false;
|
||||
|
@ -82,6 +84,7 @@ var tearDownTestDOM = function() {
|
|||
$('body').empty();
|
||||
};
|
||||
|
||||
|
||||
describe('Module loader', function() {
|
||||
var err = null;
|
||||
var errorCallback = function(error) {
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* {{LICENSE_HEADER}}
|
||||
* {{LICENSE_HEADER}}
|
||||
*/
|
||||
|
||||
var should = require('should');
|
||||
var rjsmock = require('requiremock.js');
|
||||
|
||||
GLOBAL.document = $('body');
|
||||
|
||||
var registry;
|
||||
var setUp = function() {
|
||||
requireNew('icinga/registry.js');
|
||||
registry = rjsmock.getDefine();
|
||||
};
|
||||
|
||||
var cleanTestDom = function() {
|
||||
$('body').empty();
|
||||
};
|
||||
|
||||
|
||||
describe('Component registry',function(){
|
||||
it('Ids are created automatically in the form "icinga-component-<id>"',function(){
|
||||
setUp();
|
||||
|
||||
registry.add({},null,null).should.equal('icinga-component-0');
|
||||
registry.add({},null,null).should.equal('icinga-component-1');
|
||||
registry.add({},null,null).should.equal('icinga-component-2');
|
||||
|
||||
cleanTestDom();
|
||||
});
|
||||
|
||||
it('Existing ids are preserved',function(){
|
||||
setUp();
|
||||
|
||||
registry.add({},'user-defined-id',null).should.equal('user-defined-id');
|
||||
|
||||
cleanTestDom();
|
||||
});
|
||||
|
||||
it('Components are correctly added to the library',function(){
|
||||
setUp();
|
||||
|
||||
var cmp1 = { component: "cmp1" };
|
||||
registry.add(cmp1,'user-defined-id',null);
|
||||
registry.getById('user-defined-id').should.equal(cmp1);
|
||||
|
||||
var cmp2 = { component: "cmp2" };
|
||||
registry.add(cmp2,null,null);
|
||||
registry.getById('icinga-component-0').should.equal(cmp2);
|
||||
|
||||
cleanTestDom();
|
||||
});
|
||||
|
||||
it('getId(component) should return the components assigned id.',function(){
|
||||
setUp();
|
||||
|
||||
var cmp1 = { component: "cmp1" };
|
||||
registry.add(cmp1,'user-defined-id',null);
|
||||
registry.getId(cmp1).should.equal('user-defined-id');
|
||||
|
||||
var cmp2 = { component: "cmp2" };
|
||||
registry.add(cmp2,'user-defined-id-2',null);
|
||||
registry.getId(cmp2).should.equal('user-defined-id-2');
|
||||
|
||||
should.not.exist(registry.getId({}));
|
||||
|
||||
cleanTestDom();
|
||||
});
|
||||
|
||||
it('getByType() should return all components of a certain type',function(){
|
||||
setUp();
|
||||
|
||||
var cmp1 = { component: "some/type" };
|
||||
registry.add(cmp1,null,'some/type');
|
||||
|
||||
var cmp2 = { component: "some/type" };
|
||||
registry.add(cmp2,null,"some/type");
|
||||
|
||||
var cmp3 = { component: "other/type" };
|
||||
registry.add(cmp3,null,"other/type");
|
||||
|
||||
var cmps = registry.getByType('some/type');
|
||||
cmps.length.should.equal(2);
|
||||
cmps[0].component.should.equal('some/type');
|
||||
cmps[1].component.should.equal('some/type');
|
||||
|
||||
cleanTestDom();
|
||||
});
|
||||
|
||||
it('getComponents() should return all components',function(){
|
||||
setUp();
|
||||
|
||||
var cmp1 = { component: "cmp1" };
|
||||
registry.add(cmp1,null,null);
|
||||
|
||||
var cmp2 = { component: "cmp2" };
|
||||
registry.add(cmp2,null,null);
|
||||
|
||||
var cmp3 = { component: "cmp3" };
|
||||
registry.add(cmp3,null,null);
|
||||
|
||||
var cmps = registry.getComponents();
|
||||
cmps.length.should.equal(3);
|
||||
cmps[0].should.equal(cmp1);
|
||||
cmps[1].should.equal(cmp2);
|
||||
cmps[2].should.equal(cmp3);
|
||||
|
||||
cleanTestDom();
|
||||
});
|
||||
|
||||
/*
|
||||
* NOTE: The functionality of the garbage collection of this class is
|
||||
* tested in the componentTest.js
|
||||
*/
|
||||
});
|
||||
|
|
@ -35,6 +35,16 @@ var requireJsMock = function(dependencies, fn) {
|
|||
fn.apply(this,fnArgs);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock the Logger
|
||||
*/
|
||||
var logger = {
|
||||
debug: function() {},
|
||||
warn: function() {},
|
||||
error: function() {},
|
||||
emergency: function() {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock for the 'define' function of requireJS, behaves exactly the same
|
||||
* except that it looks up the dependencies in the list provided by registerDepencies()
|
||||
|
@ -82,7 +92,7 @@ function initRequireMethods() {
|
|||
GLOBAL.define = defineMock;
|
||||
registeredDependencies = {
|
||||
'jquery' : GLOBAL.$,
|
||||
'logging' : console
|
||||
'logging' : logger
|
||||
};
|
||||
}
|
||||
initRequireMethods();
|
||||
|
@ -94,7 +104,7 @@ initRequireMethods();
|
|||
function purgeDependencies() {
|
||||
registeredDependencies = {
|
||||
'jquery' : GLOBAL.$,
|
||||
'logging' : console
|
||||
'logging' : logger
|
||||
};
|
||||
}
|
||||
// helper to log debug messages with console
|
||||
|
|
Loading…
Reference in New Issue