Merge branch 'feature/javascript-autoload-components-4456'
resolves #4456
This commit is contained in:
commit
7af982aaa5
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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();
|
||||
});
|
|
@ -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();
|
||||
});
|
|
@ -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();
|
||||
|
||||
});
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -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" >×</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;
|
||||
});
|
||||
})();
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
})();
|
|
@ -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 = {};
|
||||
|
||||
|
|
|
@ -45,6 +45,10 @@ define(function() {
|
|||
},
|
||||
error: function() {
|
||||
logTagged('error', arguments);
|
||||
},
|
||||
emergency: function() {
|
||||
logTagged('emergency', arguments);
|
||||
// TODO: log *emergency* errors to the backend
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
})();
|
|
@ -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
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
};
|
||||
});
|
|
@ -31,5 +31,4 @@ describe('The async module', function() {
|
|||
},this);
|
||||
var test = async.createRequest();
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
||||
});
|
||||
});
|
|
@ -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
|
||||
*/
|
||||
});
|
||||
|
|
@ -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