diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js
new file mode 100644
index 000000000..d61b786d3
--- /dev/null
+++ b/public/js/icinga/events.js
@@ -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('
');
+ 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));
diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js
new file mode 100644
index 000000000..22998c718
--- /dev/null
+++ b/public/js/icinga/loader.js
@@ -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(
+ '' + req.status + ' ' + errorThrown + '
' +
+ 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 $('' + message + '').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));
diff --git a/public/js/icinga/module.js b/public/js/icinga/module.js
new file mode 100644
index 000000000..c2134d96c
--- /dev/null
+++ b/public/js/icinga/module.js
@@ -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));
diff --git a/public/js/icinga/timer.js b/public/js/icinga/timer.js
new file mode 100644
index 000000000..e1a92551f
--- /dev/null
+++ b/public/js/icinga/timer.js
@@ -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));
diff --git a/public/js/icinga/ui.js b/public/js/icinga/ui.js
new file mode 100644
index 000000000..74a04d0d3
--- /dev/null
+++ b/public/js/icinga/ui.js
@@ -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) +
+ '
1em: ' +
+ size +
+ 'px
Win: ' +
+ winWidth +
+ 'x'+
+ winHeight +
+ 'px
'
+ ).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 = $('
');
+ $('#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 = $('');
+ 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));
diff --git a/public/js/icinga/utils.js b/public/js/icinga/utils.js
new file mode 100644
index 000000000..c80e24189
--- /dev/null
+++ b/public/js/icinga/utils.js
@@ -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));