diff --git a/public/js/icinga.js b/public/js/icinga.js index 31bad4ff0..67b960797 100644 --- a/public/js/icinga.js +++ b/public/js/icinga.js @@ -9,200 +9,151 @@ * }); * */ -(function() { +(function(window, $) { - var Icinga = function(config) { + 'use strict'; - /** - * Our config object - */ - this.config = config; + var Icinga = function (config) { - /** - * Icinga.Logger - */ - this.logger = null; + /** + * Our config object + */ + this.config = config; - /** - * Icinga.UI - */ - this.ui = null; + /** + * Icinga.Logger + */ + this.logger = null; - /** - * Icinga.Loader - */ - this.loader = null; + /** + * Icinga.UI + */ + this.ui = null; - /** - * Icinga.Events - */ - this.events = null; + /** + * Icinga.Loader + */ + this.loader = null; - /** - * Icinga.Timer - */ - this.timer = null; + /** + * Icinga.Events + */ + this.events = null; - /** - * Icinga.Utils - */ - this.utils = null; + /** + * Icinga.Timer + */ + this.timer = null; - /** - * Loaded modules - */ - this.modules = {}; + /** + * Icinga.History + */ + this.history = null; - var self = this; - $(document).ready(function() { - self.initialize(); - self = null; - }); - }; + /** + * Icinga.Utils + */ + this.utils = null; - Icinga.prototype = { + /** + * Loaded modules + */ + this.modules = {}; - /** - * Icinga startup, will be triggerd once the document is ready - */ - initialize: function() - { - $('html').removeClass('no-js').addClass('js'); + var self = this; + $(document).ready(function () { + self.initialize(); + self = null; + }); + }; - this.utils = new Icinga.Utils(this); - this.logger = new Icinga.Logger(this); - this.timer = new Icinga.Timer(this); - this.ui = new Icinga.UI(this); - this.loader = new Icinga.Loader(this); - this.events = new Icinga.Events(this); + Icinga.prototype = { - this.timer.initialize(); - this.events.initialize(); - this.ui.initialize(); - this.loader.initialize(); - this.logger.setLevel('info'); - this.logger.info('Icinga is ready'); - this.timer.register(this.refreshTimeSince, this, 1000); - }, + /** + * Icinga startup, will be triggerd once the document is ready + */ + initialize: function () { - toggleFullscreen: function() - { - $('#layout').toggleClass('fullscreen'); - this.ui.fixControls(); - }, + this.utils = new Icinga.Utils(this); + this.logger = new Icinga.Logger(this); + this.timer = new Icinga.Timer(this); + this.ui = new Icinga.UI(this); + this.loader = new Icinga.Loader(this); + this.events = new Icinga.Events(this); + this.history = new Icinga.History(this); - flipContent: function() - { - var col1 = $('#col1 > div').detach(); - var col2 = $('#col2 > div').detach(); - $('#col2').html(''); - $('#col1').html(''); + this.timer.initialize(); + this.events.initialize(); + this.history.initialize(); + this.ui.initialize(); + this.loader.initialize(); + this.logger.info('Icinga is ready'); + }, - col1.appendTo('#col2'); - col2.appendTo('#col1'); - this.ui.fixControls(); - }, + /** + * Load a given module by name + */ + loadModule: function (name) { - refreshTimeSince: function() - { - $('.timesince').each(function(idx, el) { - var m = el.innerHTML.match(/^(\d+)m\s(\d+)s/); - if (m !== null) { - var nm = parseInt(m[1]); - var ns = parseInt(m[2]); - if (ns < 59) { - ns++; - } else { - ns = 0; - nm++; - } - $(el).html(nm + 'm ' + ns + 's'); + if (this.hasModule(name)) { + this.logger.error('Cannot load module ' + name + ' twice'); + return; + } + + this.modules[name] = new Icinga.Module(this, name); + }, + + /** + * Whether a module matching the given name exists + */ + hasModule: function (name) { + return 'undefined' !== typeof this.modules[name] || + 'undefined' !== typeof Icinga.availableModules[name]; + }, + + /** + * Get a module by name + */ + module: function (name) { + + if ('undefined' === typeof this.modules[name]) { + if ('undefined' !== typeof Icinga.availableModules[name]) { + this.modules[name] = new Icinga.Module( + this, + name, + Icinga.availableModules[name] + ); + } + } + + return this.modules[name]; + }, + + /** + * Clean up and unload all Icinga components + */ + destroy: function () { + + $.each(this.modules, function (name, module) { + module.destroy(); + }); + + this.timer.destroy(); + this.events.destroy(); + this.loader.destroy(); + this.ui.destroy(); + this.logger.debug('Icinga has been destroyed'); + this.logger.destroy(); + this.utils.destroy(); + + this.modules = []; + this.timer = this.events = this.loader = this.ui = this.logger = + this.utils = null; } - }); - }, + }; - getWindowId: function() - { - var res = window.name.match(/^Icinga_([a-zA-Z0-9])$/); - if (res) { - return res[1]; - } - return null; - }, + window.Icinga = Icinga; - hasWindowId: function() - { - var res = window.name.match(/^Icinga_([a-zA-Z0-9])$/); - return typeof res === 'object'; - }, - - setWindowId: function(id) - { - window.name = 'Icinga_' + id; - }, - - /** - * Load a given module by name - */ - loadModule: function(name) - { - if (this.hasModule(name)) { - this.logger.error('Cannot load module ' + name + ' twice'); - return; - } - this.modules[name] = new Icinga.Module(this, name); - }, - - /** - * Whether a module matching the given name exists - */ - hasModule: function(name) - { - return typeof this.modules[name] !== 'undefined' || - typeof Icinga.availableModules[name] !== 'undefined'; - }, - - /** - * Get a module by name - */ - module: function(name) - { - if (typeof this.modules[name] === 'undefined') { - if (typeof Icinga.availableModules[name] !== 'undefined') { - this.modules[name] = new Icinga.Module( - this, - name, - Icinga.availableModules[name] - ); - } - } - return this.modules[name]; - }, - - /** - * Clean up and unload all Icinga components - */ - destroy: function() - { - $.each(this.modules, function(name, module) { - module.destroy(); - }); - this.timer.destroy(); - this.events.destroy(); - this.loader.destroy(); - this.ui.destroy(); - this.logger.debug('Icinga has been destroyed'); - this.logger.destroy(); - this.utils.destroy(); - - this.modules = []; - this.timer = this.events = this.loader = this.ui = this.logger = this.utils = null; - } - }; - - window.Icinga = Icinga; - - Icinga.availableModules = {}; - -})(window); + Icinga.availableModules = {}; +})(window, window.jQuery); diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js index 0d62188b1..ce60426b9 100644 --- a/public/js/icinga/events.js +++ b/public/js/icinga/events.js @@ -1,266 +1,272 @@ -(function(Icinga) { +/** + * Icinga.Events + * + * Event handlers + */ +(function (Icinga, $) { - Icinga.Events = function(icinga) { - this.icinga = icinga; - }; + 'use strict'; - Icinga.Events.prototype = { + Icinga.Events = function (icinga) { + this.icinga = icinga; + }; - /** - * Icinga will call our initialize() function once it's ready - */ - initialize: function() - { - this.applyGlobalDefaults(); - this.icinga.ui.prepareContainers(); - }, + Icinga.Events.prototype = { - // 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)).autorefresh = true; - }); - // 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); + /** + * Icinga will call our initialize() function once it's ready + */ + initialize: function () { + this.applyGlobalDefaults(); + this.applyHandlers($('#layout')); + 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)).autorefresh = true; + }); + + // 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) { + var $mod = $(mod); + var moduleName = $mod.data('icinga-module'); + if (icinga.hasModule(moduleName)) { + var module = icinga.module(moduleName); + // NOT YET, the applyOnloadDings: module.applyEventHandlers(mod); + } + }); + + $('input.autofocus', el).focus(); + + $('.inlinepie', el).sparkline('html', { + type: 'pie', + sliceColors: ['#44bb77', '#ffaa44', '#ff5566', '#dcd'], + width: '2em', + height: '2em', + }); + + }, + + /** + * Global default event handlers + */ + applyGlobalDefaults: function () { + // We catch resize events + $(window).on('resize', { self: this.icinga.ui }, this.icinga.ui.onWindowResize); + + // Destroy Icinga, clean up and interrupt pending requests on unload + $( window ).on('unload', { self: this }, this.onUnload); + $( window ).on('beforeunload', { self: this }, this.onUnload); + + // 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); + + $(document).on('keyup', '#menu input.search', {self: this}, this.submitForm); + + // 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); + }, + + onUnload: function (event) { + var icinga = event.data.self.icinga; + icinga.logger.info('Unloading Icinga'); + icinga.destroy(); + }, + + /** + * A scroll event happened in one of our containers + */ + onContainerScroll: function (event) { + // 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 = null; + if ($form.closest('[data-base-target]').length) { + $target = $( + '#' + $form.closest('[data-base-target]').data('baseTarget') + ); + } else if ($form.closest('.container').length) { + $target = $form.closest('.container'); + } else { + icinga.logger.error('No form target found, stopping here'); + return false; } - }); - $('.inlinepie', el).sparkline('html', { - type: 'pie', - sliceColors: ['#44bb77', '#ffaa44', '#ff5566', '#dcd'], - width: '2em', - height: '2em', - }); + icinga.loader.loadUrl(url, $target, data, method); + // TODO: Do we really need to return false with stop/preventDefault? + return false; + }, - }, - /** - * Global default event handlers - */ - applyGlobalDefaults: function() - { - // We catch resize events - $(window).on('resize', { self: this }, this.onWindowResize); + layout1col: function () { + if (! $('#layout').hasClass('twocols')) { return; } + var $col2 = $('#col2'); + icinga.logger.debug('Switching to single col'); + $('#layout').removeClass('twocols'); + $col2.removeAttr('data-icinga-url'); + $col2.removeAttr('data-icinga-refresh'); + $col2.removeData('icingaUrl'); + $col2.removeData('icingaRefresh'); + this.icinga.loader.stopPendingRequestsFor($col2); + $col2.html(''); + this.icinga.ui.fixControls(); + }, - // Destroy Icinga, clean up and interrupt pending requests on unload - $( window ).on('unload', { self: this }, this.onUnload); - $( window ).on('beforeunload', { self: this }, this.onUnload); + layout2col: function () { + if ($('#layout').hasClass('twocols')) { return; } + icinga.logger.debug('Switching to double col'); + $('#layout').addClass('twocols'); + this.icinga.ui.fixControls(); + }, - // We catch scroll events in our containers - $('.container').on('scroll', icinga.events.onContainerScroll); + /** + * Someone clicked a link or tr[href] + */ + linkClicked: function (event) { + var icinga = event.data.self.icinga; - // We want to catch each link click - $(document).on('click', 'a', { self: this }, this.linkClicked); + var $a = $(this); + var href = $a.attr('href'); + var $li; + if ($a.attr('target') === '_blank') { + return true; + } + event.stopPropagation(); + event.preventDefault(); - // We treat tr's with a href attribute like links - $(document).on('click', 'tr[href]', { self: this }, this.linkClicked); + // If link is hash tag... + if (href === '#') { + if ($a.closest('#menu')) { + $li = $a.closest('li'); + $('#menu .active').removeClass('active'); + $li.addClass('active'); + } + return; + } + var $target = $('#col1'); + var $container = $a.closest('.container'); + if ($container.length) { + $target = $container; + } - // We catch all form submit events - $(document).on('submit', 'form', { self: this }, this.submitForm); + if ($a.closest('table').length) { + $target = $('#col2'); + icinga.events.layout2col(); + } + if ($a.closest('[data-base-target]').length) { + $target = $('#' + $a.closest('[data-base-target]').data('baseTarget')); + icinga.events.layout2col(); + } + if ($a.closest('.tree').length) { + $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'); + icinga.events.layout2col(); + } + } + + icinga.loader.loadUrl(href, $target); + event.stopPropagation(); + event.preventDefault(); + + if ($a.closest('#menu').length) { + icinga.events.layout1col(); + return false; + } + + if ($a.closest('table.action').length) { + if ($('#layout').hasClass('twocols')) { + if ($target.attr('id') === 'col2') { + return; + } + icinga.events.layout1col(); + } else { + icinga.events.layout2col(); + } + return false; + } + }, - // We support an 'autosubmit' class on dropdown form elements - $(document).on('change', 'form select.autosubmit', { self: this }, this.submitForm); + /* + hrefIsHashtag: function(href) { + // WARNING: IE gives full URL :( + // Also it doesn't support negativ indexes in substr + return href.substr(href.length - 1, 1) == '#'; + }, + */ - $(window).on('popstate', { self: this }, this.historyChanged); + unbindGlobalHandlers: function () { + $(window).off('resize', this.onWindowResize); + $(window).off('unload', this.onUnload); + $(window).off('beforeunload', this.onUnload); + $(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); + }, - // 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); - }, - - onUnload: function(event) - { - var icinga = event.data.self.icinga; - icinga.logger.info('Unloading Icinga'); - icinga.destroy(); - }, - - 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'); + destroy: function() { + // This is gonna be hard, clean up the mess + this.unbindGlobalHandlers(); + this.icinga = null; } + }; - 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'); - if ($a.attr('target') === '_blank') { - return true; - } - 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 ($container.length) { - $target = $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('baseTarget')); - $('#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); - $(window).off('unload', this.onUnload); - $(window).off('beforeunload', this.onUnload); - $(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)); +}(Icinga, jQuery)); diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js index 22998c718..9124093c1 100644 --- a/public/js/icinga/loader.js +++ b/public/js/icinga/loader.js @@ -3,336 +3,499 @@ * * This is where we take care of XHR requests, responses and failures. */ -(function(Icinga) { +(function(Icinga, $) { - Icinga.Loader = function(icinga) { + 'use strict'; - /** - * YES, we need Icinga - */ - this.icinga = icinga; + Icinga.Loader = function (icinga) { - /** - * Our base url - */ - this.baseUrl = icinga.config.baseUrl; + /** + * YES, we need Icinga + */ + this.icinga = icinga; - this.failureNotice = null; + /** + * Our base url + */ + this.baseUrl = icinga.config.baseUrl; - this.exception = null; + this.failureNotice = null; - /** - * Pending requests - */ - this.requests = {}; + this.exception = null; - this.autorefreshEnabled = true; - }; + /** + * Pending requests + */ + this.requests = {}; - Icinga.Loader.prototype = { + this.autorefreshEnabled = true; + }; - initialize: function() - { - this.icinga.timer.register(this.autorefresh, this, 10000); - }, + Icinga.Loader.prototype = { - /** - * 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; + initialize: function () { + this.icinga.timer.register(this.autorefresh, this, 500); + }, - // Default method is GET - if (typeof method === 'undefined') { - method = 'GET'; - } + /** + * 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; - 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'); + // Default method is GET + if ('undefined' === typeof method) { + method = 'GET'; } - }); - - $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'); + 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'); } - }); - } - 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(); + 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'); } - this.icinga.logger.error( - 'Failed to contact web server loading ', - url, - ' for ', - req.$target - ); } + + // 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.ui.hasWindowId()) { + headers['X-Icinga-WindowId'] = this.icinga.ui.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.complete(this.onComplete); + req.historyTriggered = false; + req.autorefresh = false; + if (id) { + this.requests[id] = req; + } + this.icinga.ui.refreshDebug(); + 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; + }, + + stopPendingRequestsFor: function ($el) { + var id; + if (typeof $el !== 'undefined' || ! (id = $el.attr('id'))) { + return; + } + + if (id in this.requests) { + this.requests[id].abort(); + } + }, + + autorefresh: function () { + var self = this; + if (self.autorefreshEnabled !== true) { + return; + } + + $('.container[data-icinga-refresh]').each(function (idx, el) { + var $el = $(el); + var id = $el.attr('id'); + if (id in self.requests) { + self.icinga.logger.debug('No refresh, request pending', id); + return; + } + + var interval = $el.data('icingaRefresh'); + var lastUpdate = $el.data('lastUpdate'); + + if (typeof interval === 'undefined' || ! interval) { + self.icinga.logger.info('No interval, setting default', id); + interval = 10; + } + + if (typeof lastUpdate === 'undefined' || ! lastUpdate) { + self.icinga.logger.info('No lastUpdate, setting one', id); + $el.data('lastUpdate',(new Date()).getTime()); + return; + } + interval = interval * 1000; + + // TODO: + if ((lastUpdate + interval) > (new Date()).getTime()) { + // self.icinga.logger.info( + // 'Skipping refresh', + // id, + // lastUpdate, + // interval, + // (new Date()).getTime() + // ); + return; + } + + self.icinga.logger.info( + 'Autorefreshing ' + id + ' ' + interval + ' ms passed' + ); + 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) { + var self = this; + 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; + this.icinga.logger.debug( + 'Got response for ', req.$target, ', URL was ' + url + ); + + var $resp = $(req.responseText); + var active = false; + + if (! req.autorefresh) { + // TODO: Hook for response/url? + var $forms = $('[action="' + url + '"]'); + var $matches = $.merge($('[href="' + url + '"]'), $forms); + $matches.each(function (idx, el) { + if ($(el).closest('#menu').length) { + $('#menu .active').removeClass('active'); + } else if ($(el).closest('table.action').length) { + $(el).closest('table.action').find('.active').removeClass('active'); + } + }); + + $matches.each(function (idx, el) { + var $el = $(el); + if ($el.closest('#menu').length) { + if ($el.is('form')) { + $('input', $el).addClass('active'); + } else { + $el.closest('li').addClass('active'); + $el.parents('li').addClass('active'); + } + } else if ($(el).closest('table.action').length) { + $el.addClass('active'); + } + }); + } else { + // TODO: next container url + active = $('[href].active', req.$target).attr('href'); + } + + req.$target.attr('data-icinga-url', url); + + // + var target = req.getResponseHeader('X-Icinga-Container'); + var newBody = false; + if (target) { + req.$target = $('#' + target); + newBody = true; + } + + var title = req.getResponseHeader('X-Icinga-Title'); + if (title && req.$target.closest('.dashboard').length === 0) { + this.icinga.ui.setTitle(title); + } + + 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-last-update', (new Date()).getTime()); + req.$target.data('lastUpdate', (new Date()).getTime()); + req.$target.data('icingaRefresh', refresh); + } else { + req.$target.removeAttr('data-icinga-refresh'); + req.$target.removeAttr('data-last-update'); + req.$target.removeData('icingaRefresh'); + req.$target.removeData('lastUpdate'); + } + + // Set a window identifier if the server asks us to do so + var windowId = req.getResponseHeader('X-Icinga-WindowId'); + if (windowId) { + this.icinga.ui.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.history.pushCurrentState(); + /* + this.icinga.logger.debug('Pushing ', req.url, ' to history'); + if (typeof window.history.pushState !== 'undefined') { + window.history.pushState({icinga: true}, null, req.url); + } + */ + } + } + + // Handle search requests, still hardcoded + if (req.url === '/search' && + req.$target.data('icingaUrl') === '/search') + { + // TODO: We need dashboard pane and container identifiers (not ids) + var targets = []; + $('.dashboard .container').each(function (idx, el) { + targets.push($(el)); + }); + + var i = 0; + $('.dashboard .container', $resp).each(function (idx, el) { + var $el = $(el); + var url = $el.data('icingaUrl'); + targets[i].data('icingaUrl', url); + + var title = $('h1', $el).first(); + $('h1', targets[i]).first().replaceWith(title); + + self.loadUrl(url, targets[i]); + i++; + }); + return; + } + + req.$target.attr('data-icinga-url', req.url); + req.$target.data('icingaUrl', req.url); + + /* Should we try to fiddle with responses containing full HTML? */ + /* + if ($('body', $resp).length) { + req.responseText = $('script', $('body', $resp).html()).remove(); + } + */ + /* + + var containers = []; + + $('.dashboard .container').each(function(idx, el) { + urls.push($(el).data('icingaUrl')); + }); + console.log(urls); + $('.container[data-icinga-refresh]').each(function(idx, el) { + var $el = $(el); + self.loadUrl($el.data('icingaUrl'), $el).autorefresh = true; + el = null; + }); + */ + + this.renderContentToContainer($resp, req.$target); + if (newBody) { + this.icinga.ui.fixDebugVisibility().triggerWindowResize(); + } + + if (active) { + $('[href="' + active + '"]', req.$target).addClass('active'); + } + }, + + onComplete: function (req, textStatus) { + delete this.requests[req.$target.attr('id')]; + this.icinga.ui.refreshDebug(); + }, + + /** + * Handle failed XHR response + */ + onFailure: function (req, textStatus, errorThrown) { + var url = req.url; + + if (req.status === 500) { + if (this.exception === null) { + req.$target.addClass('impact'); + + this.exception = this.createNotice( + 'error', + $('h1', $(req.responseText)).first().html() + ); + 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 = false; + var containerId = $container.attr('id'); + if (typeof containerId !== 'undefined') { + scrollPos = $container.scrollTop(); + } + + var origFocus = document.activeElement; + var $content = $(content); + if (false && + $('.dashboard', $content).length > 0 && + $('.dashboard', $container).length === 0 + ) { + // $('.dashboard', $content) + // $container.html(content); + + } else { + if ($container.closest('.dashboard').length && + ! $('h1', $content).length + ) { + var title = $('h1', $container).first().detach(); + $('h1', $content).first().detach(); + $container.html(title).append(content); + } else { + $container.html(content); + } + } + if (scrollPos !== false) { + $container.scrollTop(scrollPos); + } + if (origFocus) { + origFocus.focus(); + } + + // 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 = {}; } - }, - 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)); +}(Icinga, jQuery)); diff --git a/public/js/icinga/logger.js b/public/js/icinga/logger.js index e8a7be909..f5226c82c 100644 --- a/public/js/icinga/logger.js +++ b/public/js/icinga/logger.js @@ -1,96 +1,117 @@ -(function(Icinga) { +/** + * Icinga.Logger + * + * Well, log output. Rocket science. + */ +(function (Icinga) { - Icinga.Logger = function(icinga) { + 'use strict'; - // Well... we don't really need Icinga right now - this.icinga = icinga; + Icinga.Logger = function (icinga) { - this.logLevel = 'info'; + this.icinga = icinga; + + this.logLevel = 'info'; + + this.logLevels = { + 'debug': 0, + 'info' : 1, + 'warn' : 2, + 'error': 3 + }; - this.logLevels = { - 'debug': 0, - 'info' : 1, - 'warn' : 2, - 'error': 3 }; - // Let's get started - this.initialize(); - }; + Icinga.Logger.prototype = { - Icinga.Logger.prototype = { + /** + * Whether the browser has a console object + */ + hasConsole: function () { + return 'undefined' !== typeof console; + }, - /** - * Logger initialization - */ - initialize: function() - { - }, + /** + * Raise or lower current log level + * + * Messages blow this threshold will be silently discarded + */ + setLevel: function (level) { + if ('undefined' !== typeof this.numericLevel(level)) { + this.logLevel = level; + } + return this; + }, - /** - * Whether the browser has a console object - */ - hasConsole: function() - { - return typeof console !== 'undefined'; - }, + /** + * Log a debug message + */ + debug: function () { + return this.writeToConsole('debug', arguments); + }, - debug: function(msg) - { - this.writeToConsole('debug', arguments); - }, + /** + * Log an informational message + */ + info: function () { + return this.writeToConsole('info', arguments); + }, - setLevel: function(level) - { - if (this.numericLevel(level) !== 'undefined') { - this.logLevel = level; - } - }, + /** + * Log a warning message + */ + warn: function () { + return this.writeToConsole('warn', arguments); + }, - info: function() - { - this.writeToConsole('info', arguments); - }, + /** + * Log an error message + */ + error: function () { + return this.writeToConsole('error', arguments); + }, - warn: function() - { - this.writeToConsole('warn', arguments); - }, + /** + * Write a log message with the given level to the console + */ + writeToConsole: function (level, args) { - error: function() - { - this.writeToConsole('error', arguments); - }, + args = Array.prototype.slice.call(args); - writeToConsole: function(level, args) { - args = Array.prototype.slice.call(args); - args.unshift(this.icinga.utils.timeWithMs()); - if (this.hasConsole() && this.hasLogLevel(level)) { - console[level].apply(console, args); - } - }, + // We want our log messages to carry precise timestamps + args.unshift(this.icinga.utils.timeWithMs()); - numericLevel: function(level) - { - var ret = this.logLevels[level]; - if (typeof ret === 'undefined') { - throw 'Got invalid log level ' + level; - } - return ret; - }, + if (this.hasConsole() && this.hasLogLevel(level)) { + console[level].apply(console, args); + } + return this; + }, - hasLogLevel: function(level) - { - return this.numericLevel(level) >= this.numericLevel(this.logLevel); - }, + /** + * Return the numeric identifier fot a given log level + */ + numericLevel: function (level) { + var ret = this.logLevels[level]; + if ('undefined' === typeof ret) { + throw 'Got invalid log level ' + level; + } + return ret; + }, - /** - * There isn't much to clean up here - */ - destroy: function() { - this.enabled = false; - this.icinga = null; - } - }; + /** + * Whether a given log level exists + */ + hasLogLevel: function (level) { + return this.numericLevel(level) >= this.numericLevel(this.logLevel); + }, + + /** + * There isn't much to clean up here + */ + destroy: function () { + this.enabled = false; + this.icinga = null; + } + }; }(Icinga)); diff --git a/public/js/icinga/module.js b/public/js/icinga/module.js index c2134d96c..f9ae3d69b 100644 --- a/public/js/icinga/module.js +++ b/public/js/icinga/module.js @@ -1,112 +1,124 @@ /** * This is how we bootstrap JS code in our modules */ -(function(Icinga) { +(function(Icinga, $) { - Icinga.Module = function(icinga, name, prototyp) { + 'use strict'; - // The Icinga instance - this.icinga = icinga; + Icinga.Module = function (icinga, name, prototyp) { - // Event handlers registered by this module - this.handlers = []; + // The Icinga instance + this.icinga = icinga; - this.registeredHandlers = {}; + // Event handlers registered by this module + this.handlers = []; - // The module name - this.name = name; + this.registeredHandlers = {}; - // The JS prototype for this module - this.prototyp = prototyp; + // The module name + this.name = name; - // Once initialized, this will be an instance of the modules prototype - this.object = {}; + // The JS prototype for this module + this.prototyp = prototyp; - // Initialize this module - this.initialize(); - }; + // Once initialized, this will be an instance of the modules prototype + this.object = {}; - Icinga.Module.prototype = { + // Initialize this module + this.initialize(); + }; - 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; - } + Icinga.Module.prototype = { - // That's all, the module is ready - this.icinga.logger.debug('Module ' + this.name + ' has been initialized'); - return true; - }, + initialize: function () { - /** - * Globally register this modules event handlers - */ - registerEventHandlers: function(handlers) - { - this.registeredHandlers = handlers; - return this; - }, + try { - 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; - }, + // 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 + ); - /** - * 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)); - }, + return false; + } - /** - * Unbind all event handlers bound by this module - */ - unbindEventHandlers: function() - { - $.each(this.handlers, function(idx, handler) { - $(document).off(handler[0], handler[1], handler[2]); - }); - }, + // That's all, the module is ready + this.icinga.logger.debug( + 'Module ' + this.name + ' has been initialized' + ); - /** - * 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; - } + return true; + }, - }; + /** + * Globally register this modules event handlers + */ + registerEventHandlers: function (handlers) { + this.registeredHandlers = handlers; + return this; + }, -}(Icinga)); + 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, jQuery)); diff --git a/public/js/icinga/timer.js b/public/js/icinga/timer.js index e1a92551f..5ae77fa74 100644 --- a/public/js/icinga/timer.js +++ b/public/js/icinga/timer.js @@ -4,144 +4,164 @@ * Timer events are triggered once a second. Runs all reegistered callback * functions and is able to preserve a desired scope. */ -(function(Icinga) { +(function(Icinga, $) { - Icinga.Timer = function(icinga) { + 'use strict'; - /** - * We keep a reference to the Icinga instance even if we don't need it - */ - this.icinga = icinga; + Icinga.Timer = function (icinga) { - /** - * The Interval object - */ - this.ticker = null; + /** + * We keep a reference to the Icinga instance even if we don't need it + */ + this.icinga = icinga; - /** - * Fixed default interval is 250ms - */ - this.interval = 250; + /** + * The Interval object + */ + this.ticker = null; - /** - * Our registerd observers - */ - this.observers = []; + /** + * Fixed default interval is 250ms + */ + this.interval = 250; - /** - * Counter - */ - this.stepCounter = 0; + /** + * Our registerd observers + */ + this.observers = []; - this.start = (new Date()).getTime(); + /** + * Counter + */ + this.stepCounter = 0; + + this.start = (new Date()).getTime(); - this.lastRuntime = []; - }; + this.lastRuntime = []; + }; - Icinga.Timer.prototype = { + Icinga.Timer.prototype = { - /** - * The initialization function starts our ticker - */ - initialize: function(icinga) - { - var self = this; - this.ticker = setInterval(function() { self.tick(); }, this.interval); - }, + /** + * The initialization function starts our ticker + */ + initialize: function () { + 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 + /** + * 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) { + + var observer; + + try { + + if (typeof scope === 'undefined') { + observer = new Icinga.Timer.Interval(callback, interval); + } else { + observer = new Icinga.Timer.Interval( + callback.bind(scope), + interval + ); + } + + this.observers.push(observer); + + } catch(err) { + this.icinga.logger.error(err); + } + + return observer; + }, + + unregister: function (observer) { + + var idx = $.inArray(observer, this.observers); + if (idx > -1) { + this.observers.splice(idx, 1); + } + + return this; + }, + + /** + * 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 = 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 - ) - ); + Icinga.Timer.Interval = function (callback, interval) { + + if ('undefined' === typeof interval) { + throw 'Timer interval is required'; } - } 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 = []; - } - }; + if (interval < 100) { + throw 'Timer interval cannot be less than 100ms, got ' + interval; + } - 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.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; } - this.callback(); - }, + }; - destroy: function() - { - this.callback = null; - } - }; - -}(Icinga)); +}(Icinga, jQuery)); diff --git a/public/js/icinga/ui.js b/public/js/icinga/ui.js index 74a04d0d3..c40245a91 100644 --- a/public/js/icinga/ui.js +++ b/public/js/icinga/ui.js @@ -1,117 +1,335 @@ -(function(Icinga) { +/** + * Icinga.UI + * + * Our user interface + */ +(function(Icinga, $) { - Icinga.UI = function(icinga) { - this.icinga = icinga; - }; + 'use strict'; - Icinga.UI.prototype = { - initialize: function() - { - this.icinga.timer.register(this.refreshDebug, this, 1000); - this.refreshDebug(); - }, + Icinga.UI = function (icinga) { - 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(); + this.icinga = icinga; + + this.currentLayout = 'default'; + + this.debug = false; + + this.debugTimer = null; + + }; + + Icinga.UI.prototype = { + + initialize: function () { + $('html').removeClass('no-js').addClass('js'); + this.triggerWindowResize(); + }, + + enableDebug: function () { + this.debug = true; + this.debugTimer = this.icinga.timer.register( + this.refreshDebug, + this, + 1000 + ); + this.icinga.timer.register(this.refreshTimeSince, this, 1000); + this.fixDebugVisibility(); + + return this; + }, + + fixDebugVisibility: function () { + if (this.debug) { + $('#responsive-debug').css({display: 'block'}); + } else { + $('#responsive-debug').css({display: 'none'}); + } + return this; + }, + + disableDebug: function () { + if (this.debug === false) { return; } + + this.debug = false; + this.icinga.timer.unregister(this.debugTimer); + this.debugTimer = null; + this.fixDebugVisibility(); + return this; + }, + + flipContent: function () { + var col1 = $('#col1 > div').detach(); + var col2 = $('#col2 > div').detach(); + $('#col2').html(''); + $('#col1').html(''); + + col1.appendTo('#col2'); + col2.appendTo('#col1'); + this.fixControls(); + }, + + triggerWindowResize: function () { + this.onWindowResize({data: {self: this}}); + }, + + /** + * Our window got resized, let's fix our UI + */ + onWindowResize: function (event) { + var self = event.data.self; + self.fixControls(); + + if (self.layoutHasBeenChanged()) { + self.icinga.logger.info( + 'Layout change detected, switching to', + self.currentLayout + ); + } + self.refreshDebug(); + }, + + layoutHasBeenChanged: function () { + + var layout = $('html').css('fontFamily').replace(/['",]/g, ''); + var matched; + + if (null !== (matched = layout.match(/^([a-z]+)-layout$/))) { + if (matched[1] === this.currentLayout && + $('#layout').hasClass(layout) + ) { + + return false; + } else { + $('#layout').attr('class', layout); + this.currentLayout = matched[1]; + + return true; + } + } + + this.icinga.logger.error( + 'Someone messed up our responsiveness hacks, html font-family is', + layout + ); + + return false; + }, + + getAvailableColumnSpace: function () { + return $('#main').width() / this.getDefaultFontSize(); + }, + + setColumnCount: function (count) { + if (count === 3) { + $('#main > .container').css({ + width: '33.33333%' + }); + } else if (count === 2) { + $('#main > .container').css({ + width: '50%' + }); + } else { + $('#main > .container').css({ + width: '100%' + }); + } + }, + + setTitle: function (title) { + document.title = title; + return this; + }, + + getColumnCount: function () { + return $('#main > .container').length; + }, + + 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.getDefaultFontSize().toString(); + var winWidth = $( window ).width(); + var winHeight = $( window ).height(); + var loading = ''; + + $.each(this.icinga.loader.requests, function (el, req) { + if (loading === '') { + loading = '
    Loading:
    '; + } + loading += el + ' => ' + req.url; + }); + + $('#responsive-debug').html( + ' Time: ' + + this.icinga.utils.formatHHiiss(new Date()) + + '
    1em: ' + + size + + 'px
    Win: ' + + winWidth + + 'x'+ + winHeight + + 'px
    ' + + ' Layout: ' + + this.currentLayout + + loading + ); + }, + + refreshTimeSince: function () { + + $('.timesince').each(function (idx, el) { + var m = el.innerHTML.match(/^(-?\d+)m\s(-?\d+)s/); + if (m !== null) { + var nm = parseInt(m[1]); + var ns = parseInt(m[2]); + if (ns < 59) { + ns++; + } else { + ns = 0; + nm++; + } + $(el).html(nm + 'm ' + ns + 's'); + } + }); + + $('.timeunless').each(function (idx, el) { + var m = el.innerHTML.match(/^(-?\d+)m\s(-?\d+)s/); + if (m !== null) { + var nm = parseInt(m[1]); + var ns = parseInt(m[2]); + if (nm >= 0) { + if (ns > 0) { + ns--; + } else { + ns = 59; + nm--; + } + } else { + if (ns < 59) { + ns++; + } else { + ns = 0; + nm--; + } + } + $(el).html(nm + 'm ' + ns + 's'); + } + }); + }, + + createFontSizeCalculator: function () { + var $el = $('
     
    '); + $('#layout').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 ('undefined' === typeof $parent) { + + $('#header').css({height: 'auto'}); + $('#main').css({top: $('#header').css('height')}); + $('#sidebar').css({top: $('#header').height() + 'px'}); + $('#header').css({height: $('#header').height() + 'px'}); + $('#inner-layout').css({top: $('#header').css('height')}); + $('.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' + }); + }); + }, + + toggleFullscreen: function () { + $('#layout').toggleClass('fullscreen-layout'); + this.fixControls(); + }, + + getWindowId: function () { + var res = window.name.match(/^Icinga_([a-zA-Z0-9])$/); + if (res) { + return res[1]; + } + return null; + }, + + hasWindowId: function () { + var res = window.name.match(/^Icinga_([a-zA-Z0-9])$/); + return typeof res === 'object'; + }, + + setWindowId: function (id) { + window.name = 'Icinga_' + id; + }, + + destroy: function () { + // This is gonna be hard, clean up the mess + this.icinga = null; } - 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)); +}(Icinga, jQuery)); diff --git a/public/js/icinga/utils.js b/public/js/icinga/utils.js index c80e24189..111fca61b 100644 --- a/public/js/icinga/utils.js +++ b/public/js/icinga/utils.js @@ -3,95 +3,113 @@ */ (function(Icinga) { - Icinga.Utils = function(icinga) { + 'use strict'; - /** - * Utility functions may need access to their Icinga instance - */ - this.icinga = icinga; + Icinga.Utils = function (icinga) { - /** - * We will use this to create an URL helper only once - */ - this.url_helper = null; - }; + /** + * Utility functions may need access to their Icinga instance + */ + this.icinga = icinga; - Icinga.Utils.prototype = { + /** + * We will use this to create an URL helper only once + */ + this.urlHelper = null; + }; - 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; - }, + Icinga.Utils.prototype = { - timeShort: function(now) - { - if (typeof now === 'undefined') { - now = new Date(); - } - return now.toLocaleTimeString().replace(/:\d{2}$/, ''); - }, + timeWithMs: function (now) { - /** - * 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; + if (typeof now === 'undefined') { + now = new Date(); + } - 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; + var ms = now.getMilliseconds() + ''; + while (ms.length < 3) { + ms = '0' + ms; + } - return result; - }, + return now.toLocaleTimeString() + '.' + ms; + }, - /** - * 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; - }, + timeShort: function (now) { - /** - * Cleanup - */ - destroy: function() - { - this.url_helper = null; - this.icinga = null; - } - }; + if (typeof now === 'undefined') { + now = new Date(); + } + + return now.toLocaleTimeString().replace(/:\d{2}$/, ''); + }, + + 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; + }, + + /** + * Parse a given Url and return an object + */ + parseUrl: function (url) { + + if (this.urlHelper === null) { + this.urlHelper = document.createElement('a'); + } + + var a = this.urlHelper; + 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.urlHelper = null; + this.icinga = null; + } + }; }(Icinga));