Merge branch 'feature/javascript-autoload-components-4456'

resolves #4456
This commit is contained in:
Marius Hein 2013-08-21 09:41:31 +02:00
commit 7af982aaa5
21 changed files with 725 additions and 1171 deletions

View File

@ -30,6 +30,7 @@
use \Zend_Controller_Action_Exception as ActionException;
use \Icinga\Web\Controller\ActionController;
use \Icinga\Application\Icinga;
use \Icinga\Application\Logger;
class StaticController extends ActionController
{
@ -111,14 +112,25 @@ 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)) {
Logger::error(
'Non-existing frontend component "' . $module . '/' . $file
. '" was requested. The module "' . $module . '" does not exist or is not active.');
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)) {
Logger::error(
'Non-existing frontend component "' . $module . '/' . $file
. '" was requested, which would resolve to the the path: ' . $filePath);
echo '/** Module has no js files **/';
return;
}

130
doc/components.md Normal file
View File

@ -0,0 +1,130 @@
# Frontend components
Frontend components are JavaScript modules that can be required directly through the HTML markup of
your view, to provide additional functionality for the user. Although its best practice to
make all features available without JavaScript, these components can be used to provide a richer
and more comfortable user experience in case JavaScript is available.
There is a certain set of frontend components which come directly with the Icinga2-Web core application,
but it is also possible to define new components directly in Icinga2-Web modules.
## How do components work?
Components are defined in JavaScript files that provide a set of functionality that will be added to the
targeted HTML node. Icinga2-Web uses [RequireJS](http://requirejs.org) to load
all frontend components, so each frontend component is in fact
[defined exactly like a RequireJS-Module](http://requirejs.org/docs/api.html#define) .
The important difference to plain RequireJS is, that the loading and execution of these components is
done automatically through the HTML markup. The attribute *data-icinga-component* in a DIV
element will indicate that this element is a container for a frontend component and will trigger
the component loader to create a component instance for this HTML node. The component loader
keeps track of all available components and makes it possible to retrieve this instance when needed.
### Component names
A component name consists of two parts: the namespace and the name of the component itself. The component
is named exactly like its JavaScript file, while the namespace is the name of the Icinga2-Web module that contains
the component. Each Icinga2-Web module can contain its own components in the folder *public/js*.
<module>/<component>
NOTE: The namespace used for modules defined in the Icinga2-Web core application is "app". In opposition to
the modules the core application keeps its modules located in *public/js/icinga/components*
instead of *public/js*.
#### Example Names
The full name for the component *modules/monitoring/public/js/someComponent.js* in the module "monitoring" would be:
"monitoring/someComponent"
The full component name for the component *public/js/icinga/components/datetime.js* in the Icinga2-Web
core application would:
"app/datetime"
## Creating a component
As described in the chapters above, components are defined exactly like RequireJS modules, but
with the additional requirement that they must always return a class constructor. The component below will
search all date pickers, set the time format and create a JavaScript date picker when there is no native one
available.
/**
* Ensures that our date/time controls will work on every browser (natively or javascript based)
*/
define(['jquery', 'datetimepicker'], function($) {
"use strict";
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 DateTimePicker;
});
## Loading a component
The following code will load the module *datetime*, which will ensure that there is always a datetime-picker
with right time-format available.
<div id="date-time-picker" data-icinga-component="app/datetime">
<div class="datetime">
<input data-format="dd/MM/yyyy hh:mm:ss" type="text"/>
</div>
</div>
### Component ids
When an ID is assigned to the HTML element, it will be used by the component loader to reference this
component. Otherwise an ID in the form "icinga-component-<ID>" will be created and the ID attribute in the
HTML Element will be updated accordingly.
### Component "target"
The div-element with the *data-icinga-component* will be used as the "target" for the loaded component,
which means that the component will perform its actions on this HTML node.
# Retrieving a component
Sometimes it can be necessary to retrieve the instances of the components itself, for example when they implement
additional functions that can be called. The component loader is available in the Icinga object and can be used
to retrieve component instances using their ID or their full component name.
## By component id
var component = Icinga.components.getById("component-id");
component.doSomething();
## By full component name
var components = Icinga.components.getByType("app/datetime");
// ... do something with every component of the type app/datetime
## All components
var components = Icinga.components.getComponents();
// ... do something with every component

View File

@ -0,0 +1,148 @@
/**
* {{LICENSE_HEADER}}
* {{LICENSE_HEADER}}
*/
/**
* A module to load and manage frontend components
*
*/
define(['jquery', 'logging', 'icinga/componentRegistry'], function ($, log, registry) {
"use strict";
var ComponentLoader = 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 from the internal registry
*
* @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++;
if (!el.id || !registry.getById(el.id)) {
loadComponent(
type,
el,
function(cmp) {
var id = registry.add(cmp, el.id, type);
registry.markActive(id);
el.id = id;
finalize();
},
finalize
);
} else {
registry.markActive(el.id);
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 ComponentLoader();
});

View File

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

View File

@ -1,118 +0,0 @@
/*global Icinga:false, document: false, define:false require:false base_url:false console:false */
/**
* ActionTable behaviour as described in
* https://wiki.icinga.org/display/cranberry/Frontend+Components#FrontendComponents-ActionTable
*
* @TODO: Row selection
*/
define(['jquery','logging','icinga/util/async'],function($,log,async) {
"use strict";
var ActionTableBehaviour = function() {
var onTableHeaderClick;
var TABLE_BASE_MATCHER = '.icinga-container table.action';
var linksInActionTable = TABLE_BASE_MATCHER+" tbody tr > a";
var actionTableRow = TABLE_BASE_MATCHER+" tbody tr";
var headerRow = TABLE_BASE_MATCHER+" > th a";
var searchField = ".icinga-container .actiontable.controls input[type=search]";
onTableHeaderClick = function (ev) {
var target = ev.currentTarget,
href = $(target).attr('href'),
destination;
if ($(target).parents('.layout-main-detail').length) {
if ($(target).parents("#icinga-main").length) {
destination = 'icinga-main';
}
else {
destination = 'icinga-detail';
}
} else {
destination = 'icinga-main';
}
async.loadToTarget(destination, href);
ev.preventDefault();
ev.stopImmediatePropagation();
return false;
};
var onLinkTagClick = function(ev) {
var target = ev.currentTarget,
href = $(target).attr('href'),
destination;
if ($(target).parents('.layout-main-detail').length) {
destination = 'icinga-detail';
} else {
destination = 'icinga-main';
}
async.loadToTarget(destination,href);
ev.preventDefault();
ev.stopImmediatePropagation();
return false;
};
var onTableRowClick = function(ev) {
ev.stopImmediatePropagation();
var target = $(ev.currentTarget),
href = target.attr('href'),
destination;
$('tr.active',target.parent('tbody').first()).removeClass("active");
target.addClass('active');
// When the tr has a href, act like it is a link
if(href) {
ev.currentTarget = target.first();
return onLinkTagClick(ev);
}
// Try to find a designated row action
var links = $("a.row-action",target);
if(links.length) {
ev.currentTarget = links.first();
return onLinkTagClick(ev);
}
// otherwise use the first anchor tag
links = $("a",target);
if(links.length) {
ev.currentTarget = links.first();
return onLinkTagClick(ev);
}
log.debug("No target for this table row found");
return false;
};
var onSearchInput = function(ev) {
ev.stopImmediatePropagation();
var txt = $(this).val();
};
this.eventHandler = {};
this.eventHandler[linksInActionTable] = {
'click' : onLinkTagClick
};
this.eventHandler[actionTableRow] = {
'click' : onTableRowClick
};
this.eventHandler[headerRow] = {
'click' : onTableHeaderClick
};
this.eventHandler[searchField] = {
'keyup' : onSearchInput
};
this.enable = function() {
};
};
return new ActionTableBehaviour();
});

View File

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

View File

@ -1,97 +0,0 @@
/*global Icinga:false, document: false, define:false require:false base_url:false console:false */
/**
* Main-Detail layout behaviour as described in
* https://wiki.icinga.org/display/cranberry/Frontend+Components#FrontendComponents-Behaviour
*
*/
define(['jquery','logging','icinga/util/async'],function($,log,async) {
"use strict";
var MainDetailBehaviour = function() {
var onOuterLinkClick = function(ev) {
var a = $(ev.currentTarget),
target = a.attr("target"),
href = a.attr("href");
ev.stopImmediatePropagation();
collapseDetailView();
async.loadToTarget("icinga-main",href);
return false;
};
var onLinkTagClick = function(ev) {
var a = $(ev.currentTarget),
target = a.attr("target"),
href = a.attr("href");
// check for protocol://
if(/^[A-Z]{2,10}\:\/\//i.test(href)) {
window.open(href);
ev.stopImmediatePropagation();
return false;
}
// check for link in table header
if(a.parents('th').length > 0) {
ev.stopImmediatePropagation();
return false;
}
if(typeof target === "undefined") {
if(a.parents("#icinga-detail").length) {
log.debug("Parent is detail, loading into detail");
async.loadToTarget("icinga-detail",href);
} else {
log.debug("Parent is not detail, loading into main");
async.loadToTarget("icinga-main",href);
}
} else {
switch(target) {
case "body":
async.loadToTarget("body", href);
break;
case "main":
async.loadToTarget("icinga-main",href);
collapseDetailView();
break;
case "detail":
log.debug("Target: detail");
async.loadToTarget("icinga-detail",href);
break;
case "popup":
log.debug("No target");
async.loadToTarget(null,href);
break;
default:
return true;
}
}
ev.stopImmediatePropagation();
return false;
};
var expandDetailView = function() {
$("#icinga-detail").parents(".collapsed").removeClass('collapsed');
};
var collapseDetailView = function(elementInDetailView) {
$("#icinga-detail").parents(".layout-main-detail").addClass('collapsed');
};
this.expandDetailView = expandDetailView;
this.collapseDetailView = collapseDetailView;
this.eventHandler = {
'.layout-main-detail * a' : {
'click' : onLinkTagClick
},
'.layout-main-detail .icinga-container#icinga-detail' : {
'focus' : expandDetailView
}
};
};
return new MainDetailBehaviour();
});

View File

@ -1,113 +0,0 @@
/*global Icinga:false, document: false, define:false require:false base_url:false console:false */
(function() {
"use strict";
var containerMgrInstance = null;
var async;
var ContainerMgr = function($,log,Widgets,SubTable) {
var enhanceDetachLinks = function() {
$('a[target=_blank]',this).each(function() {
$(this).attr("target","popup");
});
};
/**
* Loading Async directly via AMD would result in a circular dependency and return null
* @param asyncMgr
*/
this.registerAsyncMgr = function(asyncMgr) {
async = asyncMgr;
};
this.updateContainer = function(id,content,req) {
var target = id;
if (typeof id === "string") {
target = this.getContainer(id);
}
var ctrl = $('.container-controls',target);
target.html(content);
if(ctrl.length) {
this.updateControlTargets(ctrl,req);
target.append(ctrl.first());
}
target.focus();
this.initializeContainers(target);
};
this.updateControlTargets = function(ctrl, req) {
$('a',ctrl).each(function() {
$(this).attr("href",req.url);
});
};
this.initControlBehaviour = function(root) {
$('div[container-id] .container-controls',root).each(function() {
enhanceDetachLinks.apply(this);
});
};
this.initExpandables = function(root) {
$('div[container-id] .expandable',root).each(function() {
var ctr = this;
$('.expand-link',this).on("click",function() {
$(ctr).removeClass('collapsed');
});
$('.collapse-link',this).on("click",function() {
$(ctr).addClass('collapsed');
});
});
};
this.drawImplicitWidgets = function(root) {
$('.icinga-widget[type="icinga/subTable"]',root).each(function() {
new SubTable(this);
});
$('div[container-id] .inlinepie',root).each(function() {
new Widgets.inlinePie(this,32,32);
});
};
this.loadAsyncContainers = function(root) {
$('.icinga-container[icingaurl]',root).each(function() {
var el = $(this);
var url = el.attr('icingaurl');
el.attr('loaded',true);
async.loadToTarget(el,url);
});
log.debug("Loading async");
};
this.initializeContainers = function(root) {
this.initControlBehaviour(root);
this.initExpandables(root);
this.drawImplicitWidgets(root);
this.loadAsyncContainers(root);
};
this.createPopupContainer = function(content,req) {
var closeButton = $('<button type="button" class="close" data-dismiss="modal" >&times;</button>');
var container = $('<div>').addClass('modal').attr('container-id','popup-'+req.url).attr("role","dialog")
.append($("<div>").addClass('modal-header').text('Header').append(closeButton))
.append($("<div>").addClass('modal-body').html(content)).appendTo(document.body);
closeButton.on("click",function() {container.remove();});
};
this.getContainer = function(id) {
if(id == 'body') {
return $(document.body);
}
return $('div[container-id='+id+']');
};
};
define(['jquery','logging','icinga/widgets/checkIcons','icinga/widgets/subTable'], function($,log,widgets,subTable) {
if (containerMgrInstance === null) {
containerMgrInstance = new ContainerMgr($,log,widgets,subTable);
}
return containerMgrInstance;
});
})();

View File

@ -2,86 +2,25 @@
define([
'jquery',
'logging',
'icinga/module',
'icinga/util/async',
'icinga/container',
'modules/list'
], function ($, log, moduleMgr, async, containerMgr, modules) {
'icinga/componentLoader'
], function ($, log, async,components) {
'use strict';
/**
* Icinga prototype
*/
var Icinga = function() {
var internalModules = ['icinga/components/actionTable',
'icinga/components/mainDetail',
'icinga/components/datetime'];
this.modules = {};
var failedModules = [];
var initialize = function () {
registerLazyModuleLoading();
enableInternalModules();
containerMgr.registerAsyncMgr(async);
containerMgr.initializeContainers(document);
components.load();
log.debug("Initialization finished");
enableModules();
};
var registerLazyModuleLoading = function() {
async.registerHeaderListener("X-Icinga-Enable-Module", loadModuleScript, this);
};
var enableInternalModules = function() {
$.each(internalModules,function(idx,module) {
moduleMgr.enableModule(module, log.error);
});
};
var loadModuleScript = function(name) {
moduleMgr.enableModule("modules/"+name+"/"+name, function(error) {
failedModules.push({
name: name,
errorMessage: error
});
});
};
var enableModules = function(moduleList) {
moduleList = moduleList || modules;
$.each(modules,function(idx,module) {
loadModuleScript(module.name);
});
};
$(document).ready(initialize.bind(this));
return {
/**
*
*/
loadModule: function(blubb,bla) {
behaviour.registerBehaviour(blubb,bla);
},
loadIntoContainer: function(ctr) {
},
loadUrl: function(url, target, params) {
target = target || "icinga-main";
async.loadToTarget(target, url, params);
},
getFailedModules: function() {
return failedModules;
}
components: components
};
};
return new Icinga();

View File

@ -1,100 +0,0 @@
/*global Icinga:false, $: false, document: false, define:false requirejs:false base_url:false console:false */
/**
This prototype encapsulates the modules registered in the module folder
**/
(function() {
"use strict";
var loaded = {};
define(['logging'],function(log) {
var registerModuleFunctions = function(module) {
var enableFn = module.enable, disableFn = module.disable;
module.enable = (function(root) {
root = root || $('body');
for (var jqMatcher in this.eventHandler) {
for (var ev in this.eventHandler[jqMatcher]) {
log.debug("Registered module: ", "'"+ev+"'", jqMatcher);
$(root).on(ev,jqMatcher,this.eventHandler[jqMatcher][ev]);
}
}
if(enableFn) {
enableFn.apply(this,arguments);
}
}).bind(module);
module.disable = (function(root) {
root = root || $('body');
for (var jqMatcher in this.eventHandler) {
for (var ev in this.eventHandler[jqMatcher]) {
log.debug("Unregistered module: ", "'"+ev+"'", jqMatcher);
$(root).off(ev,jqMatcher,this.eventHandler[jqMatcher][ev]);
}
}
if (disableFn) {
disableFn.apply(this,arguments);
}
}).bind(module);
};
var CallInterface = function() {
/**
* Loads a module and calls successCallback with the module as the parameter on success, otherwise
* the errorCallback with the errorstring as the first parameter
*
* @param name
* @param errorCallback
* @param successCallback
*/
this.enableModule = function(name,errorCallback,successCallback) {
requirejs([name],function(module) {
if (typeof module === "undefined") {
return errorCallback(new Error("Unknown module: "+name));
}
if (typeof module.eventHandler === "object") {
registerModuleFunctions(module);
}
if (typeof module.enable === "function") {
module.enable();
}
loaded[name] = {
module: module,
active: true
};
if (typeof successCallback === "function") {
successCallback(module);
}
},function(err) {
errorCallback("Could not load module "+name+" "+err,err);
});
};
this.disableModule = function(name) {
if(loaded[name] && loaded[name].active) {
loaded[name].module.disable();
}
};
/**
* This should *ONLY* be called in testcases
**/
this.resetHard = function() {
if (typeof describe !== "function") {
return;
}
loaded = {};
};
};
return new CallInterface();
});
})();

View File

@ -3,7 +3,7 @@
"use strict";
var asyncMgrInstance = null;
define(['icinga/container','logging','jquery'],function(containerMgr,log,$) {
define(['logging','jquery'],function(log,$,containerMgr) {
var headerListeners = {};

View File

@ -45,6 +45,10 @@ define(function() {
},
error: function() {
logTagged('error', arguments);
},
emergency: function() {
logTagged('emergency', arguments);
// TODO: log *emergency* errors to the backend
}
};

View File

@ -1,27 +0,0 @@
(function() {
"use strict";
var Timer = function() {
this.resolution = 1000; // 1 second resolution
this.containers = {
};
this.registerContainer = function(container) {
this.containers[container.attr('container-id')] = container;
};
var tick = function() {
for(var container in this.containers) {
var el = this.containers[container];
// document does not exist anymore
if(!jQuery.contains(document.documentElement, el[0])) {
delete this.containers[container];
continue;
}
}
};
};
})();

View File

@ -1,168 +0,0 @@
/*global Icinga:false, $:true, document: false, define:false require:false base_url:false console:false */
define(['logging','raphael'], function(log,raphael) {
"use strict";
var rad = Math.PI / 180;
var getPaper = function(el,width,height) {
if (el[0]) {
el = el[0];
}
this.paper = raphael(el,width, height);
this.paper.customAttributes.arc = function (xloc, yloc, value, total, R) {
var alpha = 360 / total * value,
a = (90 - alpha) * Math.PI / 180,
x = xloc + R * Math.cos(a),
y = yloc - R * Math.sin(a),
path;
if (total === value) {
path = [
["M", xloc, yloc - R],
["A", R, R, 0, 1, 1, xloc - 0.01, yloc - R]
];
} else {
path = [
["M", xloc, yloc - R],
["A", R, R, 0, +(alpha > 180), 1, x, y]
];
}
return {
path: path
};
};
};
var drawStatusArc = function(color,percentage,width,anim) {
anim = anim || 500;
// how much percentage this sub arc requires
var alpha = this.rot + percentage / 100 * 180; // this is the end angle for the arc
var coords = getCoordsForAngle.call(this,alpha);
var pos = getSection(alpha);
var subArc = this.paper.path().attr({
"stroke": color,
"stroke-width": width || (this.radius/4)+"px",
arc: [this.x, this.y, 0, 100, this.radius]
});
subArc.data("percentage",percentage);
subArc.transform("r" + this.rot + "," + this.x + "," + this.y).animate({
arc: [this.x, this.y, percentage, 100, this.radius]
}, anim, "easeOut");
//subArc.hover(indicateMouseOver,indicateMouseOut);
this.rot += percentage / 100 * 360;
};
var getSection = function(alpha) {
return {
right: alpha < 180,
left: alpha > 180,
top: alpha < 90 || alpha > 270,
bottom: alpha > 90 && alpha < 270
};
};
var getCoordsForAngle = function(alpha) {
var a = (90 - alpha) * Math.PI / 180;
return {
tx : this.x + (this.radius) * Math.cos(a),
ty : this.y - (this.radius) * Math.sin(a)
};
};
var sector = function(startAngle, endAngle) {
var cx = this.x, cy = this.y, r = this.radius;
var x1 = cx + r * Math.cos(-startAngle * rad),
x2 = cx + r * Math.cos(-endAngle * rad),
y1 = cy + r * Math.sin(-startAngle * rad),
y2 = cy + r * Math.sin(-endAngle * rad);
return ["M", cx, cy, "L", x1, y1, "A", r, r, 0, +(endAngle - startAngle > 180), 0, x2, y2, "z"];
};
var inlinePie = function(targetEl,h,w) {
var colors = {
0: '#11ee11',
1: '#ffff00',
2: '#ff2222',
3: '#acacac'
};
targetEl = $(targetEl);
var height = h || targetEl.height();
var width = w || targetEl.width();
this.x = width/2;
this.y = height/2;
this.radius = Math.min(width,height)/2.5;
var values = $(targetEl).html().split(",");
targetEl.html("");
getPaper.call(this,targetEl.first(),width,height);
var total = 0;
for(var i=0;i<values.length;i++) {
total += parseInt(values[i],10);
values[i] = parseInt(values[i],10);
}
var degStart = 0;
var degEnd = 0;
for(i=0;i<values.length;i++) {
degEnd = degStart+(parseInt(values[i],10)/total*360);
if(degEnd >= 360) {
degEnd = 359.9;
}
log.debug(degStart,degEnd);
this.paper.path(sector.call(this,degStart,degEnd)).attr({
fill: colors[i],
"stroke-width": '0.5px'
});
degStart = degEnd;
}
};
var diskStatus = function(targetEl,full,ok,warning,critical) {
targetEl = $(targetEl);
this.rot = 0;
var height = targetEl.height();
var width = targetEl.width();
this.x = width/2;
this.y = height/2;
this.radius = Math.min(width,height)/3;
getPaper.call(this,targetEl.first(),width,height);
for(var i = 0;i<5;i++) {
this.paper
.ellipse(this.x,this.y+(height/10)-i*(height/20),width/5,height/10)
.attr({'fill' : '90-#ddd-#666'});
}
this.radius -= 7;
drawStatusArc.call(this,'#000000',100,1,1);
this.radius += 2;
drawStatusArc.call(this,'#00ff00',ok,3 );
drawStatusArc.call(this,'#ffff00',warning,3);
drawStatusArc.call(this,'#ff0000',critical,3);
this.radius += 2;
drawStatusArc.call(this,'#000000',100,2,1);
this.radius+=4;
this.rot = 0;
drawStatusArc.call(this,'#ff0000',full);
drawStatusArc.call(this,'#0f0',100-full);
this.rot = 0;
this.radius += 5;
drawStatusArc.call(this,'#000000',100,2,1);
};
return {
inlinePie: inlinePie,
diskStatus: diskStatus
};
});

View File

@ -1,139 +0,0 @@
define(['jquery','raphael'], function($,Raphael) {
return function(el) {
this.el = $(el);
this.height = this.el.height();
this.width = this.el.width();
this.dataSet = {};
this.paper= null;
this.radius = 0;
this.total = 0;
var construct = (function(el,cfg) {
cfg = cfg || {}
this.radius = cfg.radius || Math.min(this.width,this.height)/4
this.x = cfg.x || this.width/2;
this.y = cfg.y || this.height/2;
this.paper = getPaper();
console.log(this.el);
}).bind(this);
var getSection = function(alpha) {
return {
right: alpha < 180,
left: alpha > 180,
top: alpha < 90 || alpha > 270,
bottom: alpha > 90 && alpha < 270
}
}
var getCoordsForAngle = (function(alpha) {
var a = (90 - alpha) * Math.PI / 180;
return {
tx : this.x + (this.radius) * Math.cos(a),
ty : this.y - (this.radius) * Math.sin(a)
}
}).bind(this);
var drawSubCircle = (function(coords, color,percentage) {
this.paper.circle(coords.tx,coords.ty,0).attr({
fill: color,
stroke: 'none'
}).animate({r: this.radius/2},500, "easeOut");
}).bind(this);
var indicateMouseOver = function() {
this.animate({"stroke-width": 30 }, 400, "bounce");
}
var indicateMouseOut = function() {
this.animate({"stroke-width": 18}, 400, "bounce");
}
var drawSubArcFor = (function(elem,rot) {
var percentage = elem.items.length/this.total*100; // how much percentage this sub arc requires
var alpha = rot + percentage / 100 * 180; // this is the end angle for the arc
var coords = getCoordsForAngle(alpha);
var pos = getSection(alpha);
if(elem.items.length > 10)
drawSubCircle(coords,elem.color,percentage/100);
var subArc = this.paper.path().attr({
"stroke": elem.color,
"stroke-width": 18,
arc: [this.x, this.y, 0, 100, this.radius]
});
subArc.data("percentage",percentage);
subArc.data("item",elem);
subArc.transform("r" + rot + "," + this.x + "," + this.y).animate({
arc: [this.x, this.y, percentage, 100, this.radius]
}, 500, "easeOut");
var text = this.paper
.text(coords.tx,coords.ty,elem.items.length)
.attr({'text-anchor': alpha < 180 ? 'start' : 'end'});
subArc.hover(indicateMouseOver,indicateMouseOut);
return percentage / 100 * 360;
}).bind(this);
var drawContainerCircle = (function() {
var rot = 0;
this.total = 0;
for (var i = 0;i<this.dataSet.childs.length;i++) {
this.total += this.dataSet.childs[i].items.length
}
for (var i = 0;i<this.dataSet.childs.length;i++) {
rot += drawSubArcFor(this.dataSet.childs[i],rot);
}
var innerCircleShadow = this.paper.circle(this.x, this.y + 1, this.radius - 3).attr({
fill: 'black',
stroke: 'none',
opacity: 0.4
});
var innerCircle = this.paper.circle(this.x, this.y, this.radius - 4).attr({
fill: this.dataSet.color,
stroke: 'none'
});
this.paper.text(this.x, this.y,this.dataSet.label);
}).bind(this);
var getPaper = (function() {
var paper = Raphael(this.el.first(),this.width, this.height);
paper.customAttributes.arc = function (xloc, yloc, value, total, R) {
var alpha = 360 / total * value,
a = (90 - alpha) * Math.PI / 180,
x = xloc + R * Math.cos(a),
y = yloc - R * Math.sin(a),
path;
if (total == value) {
path = [
["M", xloc, yloc - R],
["A", R, R, 0, 1, 1, xloc - 0.01, yloc - R]
];
} else {
path = [
["M", xloc, yloc - R],
["A", R, R, 0, +(alpha > 180), 1, x, y]
];
}
return {
path: path
};
};
return paper;
}).bind(this);
this.drawFor = function(dataSet) {
this.dataSet = dataSet;
drawContainerCircle();
}
construct.apply(this,arguments);
}
});

View File

@ -1,44 +0,0 @@
/*global Icinga:false, document: false, define:false require:false base_url:false console:false */
define(['jquery','logging'], function($,log) {
"use strict";
return function() {
this.count = 10;
this.offset = 0;
this.searchable = false;
var construct = function(el) {
this.el = $(el);
this.count = this.el.attr("count") || this.count;
this.searchable = this.el.attr("searchable") || false;
this.render();
};
var renderQuicksearch = (function() {
this.input = $("<input type='text' style='padding:0px;font-size:9pt;padding-left:1em;margin-bottom:2px;line-height:8px' class='search-query input-small pull-right' >");
$('.expand-title',this.el.parents('.expandable').first())
.append(this.input)
.append($("<i class='icon-search pull-right'></i>"));
this.input.on("keyup",this.updateVisible.bind(this));
}).bind(this);
this.updateVisible = function() {
var filter = this.input.val();
$("tbody tr",this.el).hide();
$("td",this.el).each(function() {
if($(this).text().match(filter)) {
$(this).parent("tbody tr").show();
}
});
};
this.render = function() {
renderQuicksearch();
};
construct.apply(this,arguments);
};
});

View File

@ -31,5 +31,4 @@ describe('The async module', function() {
},this);
var test = async.createRequest();
});
});

View File

@ -0,0 +1,145 @@
/**
* {{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/componentRegistry.js');
registry = registry || rjsmock.getDefine();
rjsmock.registerDependencies({
'icinga/componentRegistry': 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/componentLoader.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() {
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() {
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() {
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;
});
});

View File

@ -1,280 +0,0 @@
/**
* Test cases for the module loading implementation
*
*
**/
// {{LICENSE_HEADER}}
// {{LICENSE_HEADER}}
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');
/**
* Test module that only uses eventhandlers and
* no custom logic
**/
var eventOnlyModule = function() {
var thiz = this;
this.moduleLinkClick = false;
this.formFocus = false;
var onModuleLinkClick = function() {
thiz.moduleLinkClick = true;
};
var onFormFocus = function() {
thiz.formFocus = true;
};
this.eventHandler = {
'.myModule a' : {
'click': onModuleLinkClick
},
'.myModule div.test input' : {
'focus' : onFormFocus
}
};
};
/**
* Module that defines an own enable and disable function
* that is called additionally to the eventhandler setup
*
**/
var customLogicModule = function() {
var thiz = this;
this.clicked = false;
this.customEnable = false;
this.customDisable = false;
var onClick = function() {
thiz.clicked = true;
};
this.enable = function() {
thiz.customEnable = true;
};
this.disable = function() {
thiz.customDisable = true;
};
this.eventHandler = {
'.myModule a' : {
'click' : onClick
}
};
};
var setupTestDOM = function() {
tearDownTestDOM();
$('body').append($('<div class="myModule" />')
.append($('<a href="test">funkey</a>'))
.append($('<div class="test" />')
.append('<input type="button" />')));
};
var tearDownTestDOM = function() {
$('body').off();
$('body').empty();
};
describe('Module loader', function() {
var err = null;
var errorCallback = function(error) {
err = error;
};
it('Should call the errorCallback when module isn\'t found', function() {
err = null;
rjsmock.purgeDependencies();
module.resetHard();
module.enableModule("testModule", errorCallback);
should.exist(err);
});
it('Should register model event handlers when an \'eventHandler\' attribute exists', function() {
rjsmock.purgeDependencies();
var testModule = new eventOnlyModule();
rjsmock.registerDependencies({
testModule: testModule
});
err = null;
var toTest = null;
// Test event handler
setupTestDOM();
// Enable the module and asser it is recognized and enabled
module.enableModule("testModule", errorCallback, function(enabled) {
toTest = enabled;
});
should.not.exist(err, "An error occured during loading: "+err);
should.exist(toTest, "The injected test module didn't work!");
should.exist(toTest.enable, "Implicit enable method wasn't created");
$('.myModule a').click();
should.equal(toTest.moduleLinkClick, true, "Click on link should trigger handler");
$('.myModule div.test input').focus();
should.equal(toTest.formFocus, true, "Form focus should trigger handler");
tearDownTestDOM();
});
it('Should be able to deregister events handlers when disable() is called', function() {
rjsmock.purgeDependencies();
var testModule = new eventOnlyModule();
rjsmock.registerDependencies({
testModule: testModule
});
err = null;
var toTest = null;
setupTestDOM();
module.enableModule("testModule", errorCallback, function(enabled) {
toTest = enabled;
});
should.not.exist(err, "An error occured during loading: "+err);
should.exist(toTest, "The injected test module didn't work!");
should.exist(toTest.enable, "Implicit enable method wasn't created");
$('.myModule a').click();
should.equal(toTest.moduleLinkClick, true, "Click on link should trigger handler");
toTest.moduleLinkClick = false;
module.disableModule("testModule");
$('.myModule a').click();
should.equal(toTest.moduleLinkClick, false, "Click on link shouldn't trigger handler when module is disabled");
tearDownTestDOM();
$('body').unbind();
});
it('Should additionally call custom enable and disable functions', function() {
rjsmock.purgeDependencies();
var testModule = new customLogicModule();
rjsmock.registerDependencies({
testModule: testModule
});
err = null;
var toTest = null;
// Test event handler
setupTestDOM();
module.enableModule("testModule", errorCallback, function(enabled) {
toTest = enabled;
});
should.not.exist(err, "An error occured during loading: "+err);
should.exist(toTest, "The injected test module didn't work!");
should.exist(toTest.enable, "Implicit enable method wasn't created");
should.equal(toTest.customEnable, true, "Custom enable method wasn't called");
$('.myModule a').click();
should.equal(toTest.clicked, true, "Click on link should trigger handler");
toTest.clicked = false;
module.disableModule("testModule");
should.equal(toTest.customDisable, true, "Custom disable method wasn't called");
$('.myModule a').click();
should.equal(toTest.clicked, false, "Click on link shouldn't trigger handler when module is disabled");
tearDownTestDOM();
$('body').unbind();
});
});
describe('The icinga module bootstrap', function() {
it("Should automatically load all enabled modules", function() {
rjsmock.purgeDependencies();
var testClick = false;
rjsmock.registerDependencies({
"icinga/module": module,
"icinga/util/async" : {
registerHeaderListener: function() {}
},
"modules/test/test" : {
eventHandler: {
"a.test" : {
click : function() {
testClick = true;
}
}
}
},
"icinga/container" : {
registerAsyncMgr: function() {},
initializeContainers: function() {}
},
"modules/list" : [
{ name: 'test' },
{ name: 'test2'} // this one fails
]
});
tearDownTestDOM();
requireNew("icinga/icinga.js");
var icinga = rjsmock.getDefine();
$('body').append($("<a class='test'></a>"));
$('a.test').click();
should.equal(testClick, true, "Module wasn't automatically loaded!");
icinga.getFailedModules().should.have.length(1);
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,117 @@
/**
* {{LICENSE_HEADER}}
* {{LICENSE_HEADER}}
*/
var should = require('should');
var rjsmock = require('requiremock.js');
GLOBAL.document = $('body');
var registry;
var setUp = function() {
requireNew('icinga/componentRegistry.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
*/
});

View File

@ -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