A few JS files where missing

This commit is contained in:
Thomas Gelf 2014-02-18 19:00:00 +00:00
parent f9f522d599
commit ed2b330843
6 changed files with 1050 additions and 0 deletions

239
public/js/icinga/events.js Normal file
View File

@ -0,0 +1,239 @@
(function(Icinga) {
Icinga.Events = function(icinga) {
this.icinga = icinga;
};
Icinga.Events.prototype = {
/**
* Icinga will call our initialize() function once it's ready
*/
initialize: function()
{
this.applyGlobalDefaults();
this.icinga.ui.prepareContainers();
},
// TODO: What's this?
applyHandlers: function(el)
{
var icinga = this.icinga;
$('.dashboard > div', el).each(function(idx, el) {
var url = $(el).attr('data-icinga-url');
if (typeof url === 'undefined') return;
icinga.loader.loadUrl(url, $(el));
});
// Set first links href in a action table tr as row href:
$('table.action tr', el).each(function(idx, el) {
var $a = $('a[href]', el).first();
if ($a.length) {
$(el).attr('href', $a.attr('href'));
}
});
$('.icinga-module', el).each(function(idx, mod) {
$mod = $(mod);
var moduleName = $mod.data('icinga-module');
if (icinga.hasModule(moduleName)) {
var module = icinga.module(moduleName);
// NOT YET, the applyOnloadDings: module.applyEventHandlers(mod);
}
});
},
/**
* Global default event handlers
*/
applyGlobalDefaults: function()
{
// We catch resize events
$(window).on('resize', { self: this }, this.onWindowResize);
// We catch scroll events in our containers
$('.container').on('scroll', icinga.events.onContainerScroll);
// We want to catch each link click
$(document).on('click', 'a', { self: this }, this.linkClicked);
// We treat tr's with a href attribute like links
$(document).on('click', 'tr[href]', { self: this }, this.linkClicked);
// We catch all form submit events
$(document).on('submit', 'form', { self: this }, this.submitForm);
// We support an 'autosubmit' class on dropdown form elements
$(document).on('change', 'form select.autosubmit', { self: this }, this.submitForm);
$(window).on('popstate', { self: this }, this.historyChanged);
// TBD: a global autocompletion handler
// $(document).on('keyup', 'form.auto input', this.formChangeDelayed);
// $(document).on('change', 'form.auto input', this.formChanged);
// $(document).on('change', 'form.auto select', this.submitForm);
},
historyChanged: function(event)
{
var icinga = event.data.self.icinga;
if (event.originalEvent.state === null) {
icinga.logger.debug('No more history steps available');
} else {
icinga.logger.debug(event.originalEvent.state);
}
icinga.loader.loadUrl(
document.location.pathname + document.location.search,
$('#col1')
).historyTriggered = true;
},
/**
* Our window got resized, let's fix our UI
*/
onWindowResize: function(event)
{
var icinga = event.data.self.icinga;
icinga.ui.fixControls();
},
/**
* A scroll event happened in one of our containers
*/
onContainerScroll: function(event)
{
// Yet ugly. And PLEASE, not so often
icinga.ui.fixControls();
},
/**
*
*/
submitForm: function (event)
{
var icinga = event.data.self.icinga;
event.stopPropagation();
event.preventDefault();
// .closest is not required unless subelements to trigger this
var $form = $(event.currentTarget).closest('form');
var url = $form.attr('action');
var method = $form.attr('method');
var data = $form.serializeArray();
// TODO: Check button
data.push({ name: 'btn_submit', value: 'yesss' });
icinga.logger.debug('Submitting form: ' + method + ' ' + url);
// We should move this to a generic target-finder:
var $target = $form.closest('.container');
if ($target.length == 0) {
$target = $('#body');
}
icinga.loader.loadUrl(url, $target, data, method);
// TODO: Do we really need to return false with stop/preventDefault?
return false;
},
/**
* Someone clicked a link or tr[href]
*/
linkClicked: function(event)
{
var icinga = event.data.self.icinga;
var $a = $(this);
var href = $a.attr('href');
event.stopPropagation();
event.preventDefault();
if (href === '#') {
if ($a.closest('#menu')) {
var $li = $a.closest('li');
$li.siblings('li.active').removeClass('active');
$li.addClass('active');
}
return;
}
var $target = $('#col1');
var $container = $a.closest('.container');
// If link is hash tag...
if ($a.closest('table').length) {
$target = $('#col2');
$('#layout').addClass('twocols');
icinga.ui.fixControls();
}
if ($a.closest('[data-base-target]').length) {
$target = $('#' + $a.closest('[data-base-target]').data('base-target'));
$('#layout').addClass('twocols');
icinga.ui.fixControls();
}
if ($a.closest('.tree').length) {
var $li = $a.closest('li');
if ($li.find('li').length) {
if ($li.hasClass('collapsed')) {
$li.removeClass('collapsed');
} else {
$li.addClass('collapsed');
$li.find('li').addClass('collapsed');
}
return false;
} else {
$target = $('#col2');
$('#layout').addClass('twocols');
icinga.ui.fixControls();
}
}
icinga.loader.loadUrl(href, $target);
event.stopPropagation();
event.preventDefault();
if ($a.closest('#menu').length) {
$('#layout').removeClass('twocols');
$('#col2').html('<ul class="tabs"></ul>');
icinga.ui.fixControls();
return false;
}
if ($a.closest('table').length) {
if ($('#layout').hasClass('twocols')) {
if ($target.attr('id') === 'col2') return;
icinga.logger.debug('Switching to single col');
$('#layout').removeClass('twocols');
icinga.ui.fixControls();
} else {
icinga.logger.debug('Switching to double col');
$('#layout').addClass('twocols');
icinga.ui.fixControls();
}
return false;
}
},
/*
hrefIsHashtag: function(href)
{
// WARNING: IE gives full URL :(
// Also it doesn't support negativ indexes in substr
return href.substr(href.length - 1, 1) == '#';
},
*/
unbindGlobalHandlers: function()
{
$(window).off('popstate', this.historyChanged);
$(window).off('resize', this.onWindowResize);
$(document).off('scroll', '.container', this.onContainerScroll);
$(document).off('click', 'a', this.linkClicked);
$(document).off('click', 'tr[href]', this.linkClicked);
$(document).off('submit', 'form', this.submitForm);
$(document).off('change', 'form select.autosubmit', this.submitForm);
},
destroy: function() {
// This is gonna be hard, clean up the mess
this.unbindGlobalHandlers();
this.icinga = null;
}
};
}(Icinga));

338
public/js/icinga/loader.js Normal file
View File

@ -0,0 +1,338 @@
/**
* Icinga.Loader
*
* This is where we take care of XHR requests, responses and failures.
*/
(function(Icinga) {
Icinga.Loader = function(icinga) {
/**
* YES, we need Icinga
*/
this.icinga = icinga;
/**
* Our base url
*/
this.baseUrl = icinga.config.baseUrl;
this.failureNotice = null;
this.exception = null;
/**
* Pending requests
*/
this.requests = {};
this.autorefreshEnabled = true;
};
Icinga.Loader.prototype = {
initialize: function()
{
this.icinga.timer.register(this.autorefresh, this, 10000);
},
/**
* Load the given URL to the given target
*
* @param {string} url URL to be loaded
* @param {object} target Target jQuery element
* @param {object} data Optional parameters, usually for POST requests
* @param {string} method HTTP method, default is 'GET'
*/
loadUrl: function (url, $target, data, method)
{
var id = null;
// Default method is GET
if (typeof method === 'undefined') {
method = 'GET';
}
this.icinga.logger.debug('Loading ', url, ' to ', $target);
// We should do better and ignore requests without target and/or id
if (typeof $target !== 'undefined' && $target.attr('id')) {
id = $target.attr('id');
}
if (typeof $target !== 'undefined') {
// TODO: We shouldn't use data but keep this information somewhere else.
if ($target.data('icingaUrl') !== url) {
$target.removeAttr('data-icinga-url');
$target.removeAttr('data-icinga-refresh');
$target.removeData('icingaUrl');
$target.removeData('icingaRefresh');
}
}
// If we have a pending request for the same target...
if (id in this.requests) {
// ...ignore the new request if it is already pending with the same URL
if (this.requests[id].url === url) {
this.icinga.logger.debug('Request to ', url, ' is already running for ', $target);
return this.requests[id];
}
// ...or abort the former request otherwise
this.icinga.logger.debug('Aborting pending request loading ', url, ' to ', $target);
this.requests[id].abort();
}
// Not sure whether we need this Accept-header
var headers = { 'X-Icinga-Accept': 'text/html' };
// Ask for a new window id in case we don't already have one
if (this.icinga.hasWindowId()) {
headers['X-Icinga-WindowId'] = this.icinga.getWindowId();
} else {
headers['X-Icinga-WindowId'] = 'undefined';
}
var self = this;
var req = $.ajax({
type : method,
url : url,
data : data,
headers: headers,
context: self
});
req.$target = $target;
req.url = url;
req.done(this.onResponse);
req.fail(this.onFailure);
req.historyTriggered = false;
req.autorefresh = false;
if (id) {
this.requests[id] = req;
}
return req;
},
/**
* Create an URL relative to the Icinga base Url, still unused
*
* @param {string} url Relative url
*/
url: function(url)
{
if (typeof url === 'undefined') {
return this.baseUrl;
}
return this.baseUrl + url;
},
autorefresh: function()
{
var self = this;
if (self.autorefreshEnabled !== true) {
return;
}
$('.container[data-icinga-refresh]').each(function(idx, el) {
var $el = $(el);
self.loadUrl($el.data('icingaUrl'), $el).autorefresh = true;
el = null;
});
},
disableAutorefresh: function()
{
this.autorefreshEnabled = false;
},
enableAutorefresh: function()
{
this.autorefreshEnabled = true;
},
/**
* Handle successful XHR response
*/
onResponse: function (data, textStatus, req)
{
if (this.failureNotice !== null) {
this.failureNotice.remove();
this.failureNotice = null;
}
if (this.exception !== null) {
this.exception.remove();
this.exception = null;
req.$target.removeClass('impact');
}
var url = req.url;
var targetId = req.$target.attr('id');
this.icinga.logger.debug('Got response for ', req.$target, ', URL was ' + url);
if (! req.autorefresh) {
// TODO: Hook for response/url?
var $matches = $('[href="' + url + '"]');
$matches.each(function(idx, el) {
if ($(el).closest('#menu').length) {
$(el).closest('#menu').find('li.active').removeClass('active');
} else if ($(el).closest('table.action').length) {
$(el).closest('table.action').find('.active').removeClass('active');
}
});
$matches.each(function(idx, el) {
if ($(el).closest('#menu').length) {
$(el).closest('li').addClass('active');
$(el).parents('li').addClass('active');
} else if ($(el).closest('table.action').length) {
$(el).addClass('active');
}
});
}
delete this.requests[targetId];
req.$target.attr('icingaurl', this.url);
//
var target = req.getResponseHeader('X-Icinga-Container');
if (target) {
req.$target = $('body');
}
var refresh = req.getResponseHeader('X-Icinga-Refresh');
if (refresh) {
// Hmmmm... .data() doesn't work here?
req.$target.attr('data-icinga-refresh', refresh);
req.$target.attr('data-icinga-url', req.url);
}
// Set a window identifier if the server asks us to do so
var windowId = req.getResponseHeader('X-Icinga-WindowId');
if (windowId) {
this.icinga.setWindowId(windowId);
}
// Update history when necessary. Don't do so for requests triggered
// by history or autorefresh events
if (! req.historyTriggered && ! req.autorefresh) {
// We only want to care about top-level containers
if (req.$target.parent().closest('.container').length === 0) {
this.icinga.logger.debug('Pushing ', req.url, ' to history');
window.history.pushState({icinga: true}, null, req.url);
}
}
$resp = $(req.responseText);
/* Should we try to fiddle with responses containing full HTML? */
/*
if ($('body', $resp).length) {
req.responseText = $('script', $('body', $resp).html()).remove();
}
*/
this.renderContentToContainer(req.responseText, req.$target);
},
/**
* Handle failed XHR response
*/
onFailure: function (req, textStatus, errorThrown)
{
var url = req.url;
delete this.requests[req.$target.attr('id')];
if (req.status === 500) {
if (this.exception === null) {
req.$target.addClass('impact');
this.exception = this.createNotice(
'error',
$('h1', $(req.responseText)).first().html()
/* 'The connection to the Icinga web server has been lost at ' +
this.icinga.utils.timeShort() +
'.'
*/
);
this.icinga.ui.fixControls();
}
} else if (req.status > 0) {
this.icinga.logger.debug(req.responseText.slice(0, 100));
this.renderContentToContainer(
'<h1>' + req.status + ' ' + errorThrown + '</h1> ' +
req.responseText,
req.$target
);
// Header example:
// Icinga.debug(req.getResponseHeader('X-Icinga-Redirect'));
} else {
if (errorThrown === 'abort') {
this.icinga.logger.info('Request to ', url, ' has been aborted for ', req.$target);
} else {
if (this.failureNotice === null) {
this.failureNotice = this.createNotice(
'error',
'The connection to the Icinga web server has been lost at ' +
this.icinga.utils.timeShort() +
'.'
);
this.icinga.ui.fixControls();
}
this.icinga.logger.error(
'Failed to contact web server loading ',
url,
' for ',
req.$target
);
}
}
},
createNotice: function(severity, message) {
return $('<li class="' + severity + '">' + message + '</li>').appendTo($('#notifications'));
},
/**
* Smoothly render given HTML to given container
*/
renderContentToContainer: function (content, $container)
{
// Disable all click events while rendering
$('*').click(function( event ) {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
});
// Container update happens here
var scrollPos = $container.scrollTop();
$container.html(content);
$container.scrollTop(scrollPos);
// TODO: this.icinga.events.refreshContainer(container);
var icinga = this.icinga;
icinga.events.applyHandlers($container);
icinga.ui.initializeControls($container);
icinga.ui.fixControls();
// Re-enable all click events
$('*').off('click');
},
/**
* On shutdown we kill all pending requests
*/
destroy: function() {
$.each(this.requests, function(id, request) {
request.abort();
});
this.icinga = null;
this.requests = {};
}
};
}(Icinga));

112
public/js/icinga/module.js Normal file
View File

@ -0,0 +1,112 @@
/**
* This is how we bootstrap JS code in our modules
*/
(function(Icinga) {
Icinga.Module = function(icinga, name, prototyp) {
// The Icinga instance
this.icinga = icinga;
// Event handlers registered by this module
this.handlers = [];
this.registeredHandlers = {};
// The module name
this.name = name;
// The JS prototype for this module
this.prototyp = prototyp;
// Once initialized, this will be an instance of the modules prototype
this.object = {};
// Initialize this module
this.initialize();
};
Icinga.Module.prototype = {
initialize: function()
{
try {
// The constructor of the modules prototype must be prepared to get an
// instance of Icinga.Module
this.object = new this.prototyp(this);
this.applyRegisteredEventHandlers();
} catch(e) {
this.icinga.logger.error('Failed to load module ', this.name, ': ', e);
return false;
}
// That's all, the module is ready
this.icinga.logger.debug('Module ' + this.name + ' has been initialized');
return true;
},
/**
* Globally register this modules event handlers
*/
registerEventHandlers: function(handlers)
{
this.registeredHandlers = handlers;
return this;
},
applyRegisteredEventHandlers: function()
{
var self = this;
$.each(this.registeredHandlers, function(filter, events) {
$.each(events, function (event, handler) {
// TODO: if (event[1] === 'each') {
// $(event[0], $(el)).each(event[2]);
self.bindEventHandler(
event,
'.module-' + self.name + ' ' + filter,
handler
);
});
});
self = null;
return this;
},
/**
* Effectively bind the given event handler
*/
bindEventHandler: function(event, filter, handler)
{
var self = this;
this.icinga.logger.debug('Bound ' + filter + ' .' + event + '()');
this.handlers.push([event, filter, handler]);
$(document).on(event, filter, handler.bind(self.object));
},
/**
* Unbind all event handlers bound by this module
*/
unbindEventHandlers: function()
{
$.each(this.handlers, function(idx, handler) {
$(document).off(handler[0], handler[1], handler[2]);
});
},
/**
* Allow to destroy and clean up this module
*/
destroy: function()
{
this.unbindEventHandlers();
if (typeof this.object.destroy === 'function') {
this.object.destroy();
}
this.object = null;
this.icinga = null;
this.prototyp = null;
}
};
}(Icinga));

147
public/js/icinga/timer.js Normal file
View File

@ -0,0 +1,147 @@
/**
* Icinga.Timer
*
* Timer events are triggered once a second. Runs all reegistered callback
* functions and is able to preserve a desired scope.
*/
(function(Icinga) {
Icinga.Timer = function(icinga) {
/**
* We keep a reference to the Icinga instance even if we don't need it
*/
this.icinga = icinga;
/**
* The Interval object
*/
this.ticker = null;
/**
* Fixed default interval is 250ms
*/
this.interval = 250;
/**
* Our registerd observers
*/
this.observers = [];
/**
* Counter
*/
this.stepCounter = 0;
this.start = (new Date()).getTime();
this.lastRuntime = [];
};
Icinga.Timer.prototype = {
/**
* The initialization function starts our ticker
*/
initialize: function(icinga)
{
var self = this;
this.ticker = setInterval(function() { self.tick(); }, this.interval);
},
/**
* We will trigger our tick function once a second. It will call each
* registered observer.
*/
tick: function()
{
var icinga = this.icinga;
$.each(this.observers, function(idx, observer) {
if (observer.isDue()) {
observer.run();
} else {
// Not due
}
});
icinga = null;
},
/**
* Register a given callback function to be run within an optional scope.
*/
register: function(callback, scope, interval)
{
try {
if (typeof scope === 'undefined') {
this.observers.push(new Icinga.Timer.Interval(callback, interval));
} else {
this.observers.push(
new Icinga.Timer.Interval(
callback.bind(scope),
interval
)
);
}
} catch(err) {
this.icinga.logger.error(err);
}
},
/**
* Our destroy function will clean up everything. Unused right now.
*/
destroy: function()
{
if (this.ticker !== null) {
clearInterval(this.ticker);
}
this.icinga = null;
$.each(this.observers, function(idx, observer) {
observer.destroy();
});
this.observers = [];
}
};
Icinga.Timer.Interval = function(callback, interval) {
if (typeof interval === 'undefined') {
throw 'Timer interval is required';
}
if (interval < 100) {
throw 'Timer interval cannot be less than 100ms, got ' + interval;
}
this.lastRun = (new Date()).getTime();
this.interval = interval;
this.scheduledNextRun = this.lastRun + interval;
this.callback = callback;
};
Icinga.Timer.Interval.prototype = {
isDue: function()
{
return this.scheduledNextRun < (new Date()).getTime();
},
run: function()
{
this.lastRun = (new Date()).getTime();
while (this.scheduledNextRun < this.lastRun) {
this.scheduledNextRun += this.interval;
}
this.callback();
},
destroy: function()
{
this.callback = null;
}
};
}(Icinga));

117
public/js/icinga/ui.js Normal file
View File

@ -0,0 +1,117 @@
(function(Icinga) {
Icinga.UI = function(icinga) {
this.icinga = icinga;
};
Icinga.UI.prototype = {
initialize: function()
{
this.icinga.timer.register(this.refreshDebug, this, 1000);
this.refreshDebug();
},
prepareContainers: function ()
{
var icinga = this.icinga;
$('.container').each(function(idx, el) {
icinga.events.applyHandlers($(el));
icinga.ui.initializeControls($(el));
});
/*
$('#icinga-main').attr(
'icingaurl',
window.location.pathname + window.location.search
);
*/
},
refreshDebug: function()
{
var size = this.icinga.ui.getDefaultFontSize().toString();
var winWidth = $( window ).width();
var winHeight = $( window ).height();
$('#responsive-debug').html(
'Time: ' +
this.icinga.ui.formatHHiiss(new Date) +
'<br />&nbsp;1em: ' +
size +
'px<br />&nbsp;Win: ' +
winWidth +
'x'+
winHeight +
'px<br />'
).css({display: 'block'});
},
formatHHiiss: function(date)
{
var hours = date.getHours();
var minutes = date.getMinutes();
var seconds = date.getSeconds();
if (hours < 10) hours = '0' + hours;
if (minutes < 10) minutes = '0' + minutes;
if (seconds < 10) seconds = '0' + seconds;
return hours + ':' + minutes + ':' + seconds;
},
createFontSizeCalculator: function()
{
var $el = $('<div id="fontsize-calc">&nbsp;</div>');
$('#main').append($el);
return $el;
},
getDefaultFontSize: function()
{
var $calc = $('#fontsize-calc');
if (! $calc.length) {
$calc = this.createFontSizeCalculator();
}
return $calc.width() / 1000;
},
initializeControls: function(parent)
{
var self = this;
$('.controls', parent).each(function(idx, el) {
var $el = $(el);
if (! $el.next('.fake-controls').length) {
var newdiv = $('<div class="fake-controls"></div>');
newdiv.css({
height: $el.css('height')
});
$el.after(newdiv);
}
});
this.fixControls(parent);
},
fixControls: function($parent)
{
var self = this;
if (typeof $parent === 'undefined') {
$('.container').each(function(idx, container) {
self.fixControls($(container));
});
return;
}
self.icinga.logger.debug('Fixing controls for ', $parent);
$('.controls', $parent).each(function(idx, el) {
var $el = $(el);
var $fake = $el.next('.fake-controls');
var y = $parent.scrollTop();
$el.css({
position: 'fixed',
top: $parent.offset().top,
width: $fake.css('width')
});
$fake.css({
height: $el.css('height'),
display: 'block'
});
});
},
destroy: function() {
// This is gonna be hard, clean up the mess
this.icinga = null;
}
};
}(Icinga));

97
public/js/icinga/utils.js Normal file
View File

@ -0,0 +1,97 @@
/**
* Icinga utility functions
*/
(function(Icinga) {
Icinga.Utils = function(icinga) {
/**
* Utility functions may need access to their Icinga instance
*/
this.icinga = icinga;
/**
* We will use this to create an URL helper only once
*/
this.url_helper = null;
};
Icinga.Utils.prototype = {
timeWithMs: function(now)
{
if (typeof now === 'undefined') {
now = new Date();
}
var ms = now.getMilliseconds() + '';
while (ms.length < 3) {
ms = '0' + ms;
}
return now.toLocaleTimeString() + '.' + ms;
},
timeShort: function(now)
{
if (typeof now === 'undefined') {
now = new Date();
}
return now.toLocaleTimeString().replace(/:\d{2}$/, '');
},
/**
* Parse a given Url and return an object
*/
parseUrl: function(url)
{
if (this.url_helper === null) {
this.url_helper = document.createElement('a');
}
var a = this.url_helper;
a.href = url;
var result = {
source : url,
protocol: a.protocol.replace(':', ''),
host : a.hostname,
port : a.port,
query : a.search,
file : (a.pathname.match(/\/([^\/?#]+)$/i) || [,''])[1],
hash : a.hash.replace('#',''),
path : a.pathname.replace(/^([^\/])/,'/$1'),
relative: (a.href.match(/tps?:\/\/[^\/]+(.+)/) || [,''])[1],
segments: a.pathname.replace(/^\//,'').split('/'),
params : this.parseParams(a),
};
a = null;
return result;
},
/**
* Parse url params
*/
parseParams: function(a) {
var params = {},
segment = a.search.replace(/^\?/,'').split('&'),
len = segment.length,
i = 0,
s;
for (; i < len; i++) {
if (! segment[i]) { continue; }
s = segment[i].split('=');
params[s[0]] = decodeURIComponent(s[1]);
}
return params;
},
/**
* Cleanup
*/
destroy: function()
{
this.url_helper = null;
this.icinga = null;
}
};
}(Icinga));