summaryrefslogtreecommitdiffstats
path: root/public/js/icinga/ui.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/js/icinga/ui.js')
-rw-r--r--public/js/icinga/ui.js645
1 files changed, 645 insertions, 0 deletions
diff --git a/public/js/icinga/ui.js b/public/js/icinga/ui.js
new file mode 100644
index 0000000..0e6ee82
--- /dev/null
+++ b/public/js/icinga/ui.js
@@ -0,0 +1,645 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.UI
+ *
+ * Our user interface
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.UI = function (icinga) {
+
+ this.icinga = icinga;
+
+ this.currentLayout = 'default';
+
+ this.debug = false;
+
+ this.debugTimer = null;
+
+ this.timeCounterTimer = null;
+
+ // detect currentLayout
+ var classList = $('#layout').attr('class').split(/\s+/);
+ var _this = this;
+ var matched;
+ $.each(classList, function(index, item) {
+ if (null !== (matched = item.match(/^([a-z]+)-layout$/))) {
+ var layout = matched[1];
+ if (layout !== 'fullscreen') {
+ _this.currentLayout = layout;
+ // Break loop
+ return false;
+ }
+ }
+ });
+ };
+
+ Icinga.UI.prototype = {
+
+ initialize: function () {
+ $('html').removeClass('no-js').addClass('js');
+ this.enableTimeCounters();
+ this.triggerWindowResize();
+ this.fadeNotificationsAway();
+
+ $(document).on('click', '#mobile-menu-toggle', this.toggleMobileMenu);
+ $(document).on('keypress', '#search',{ self: this, type: 'key' }, this.closeMobileMenu);
+ $(document).on('mouseleave', '#sidebar', { self: this, type: 'leave' }, this.closeMobileMenu);
+ $(document).on('click', '#sidebar a', { self: this, type: 'navigate' }, this.closeMobileMenu);
+ },
+
+ fadeNotificationsAway: function() {
+ var icinga = this.icinga;
+ $('#notifications li')
+ .not('.fading-out')
+ .not('.persist')
+ .addClass('fading-out')
+ .delay(7000)
+ .fadeOut('slow',
+ function() {
+ $(this).remove();
+ });
+ },
+
+ toggleDebug: function() {
+ if (this.debug) {
+ return this.disableDebug();
+ } else {
+ return this.enableDebug();
+ }
+ },
+
+ enableDebug: function () {
+ if (this.debug === true) { return this; }
+ this.debug = true;
+ this.debugTimer = this.icinga.timer.register(
+ this.refreshDebug,
+ 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;
+ },
+
+ reloadCss: function () {
+ var icinga = this.icinga;
+ icinga.logger.info('Reloading CSS');
+ $('link').each(function() {
+ var $oldLink = $(this);
+ if ($oldLink.hasAttr('type') && $oldLink.attr('type').indexOf('css') > -1) {
+ var $newLink = $oldLink.clone().attr(
+ 'href',
+ icinga.utils.addUrlParams(
+ $oldLink.attr('href'),
+ { id: new Date().getTime() } // Only required for Firefox to reload CSS automatically
+ )
+ ).on('load', function() {
+ $oldLink.remove();
+ $('head').trigger('css-reloaded');
+ });
+
+ $newLink.appendTo($('head'));
+ }
+ });
+ },
+
+ enableTimeCounters: function () {
+ this.timeCounterTimer = this.icinga.timer.register(
+ this.refreshTimeSince,
+ this,
+ 1000
+ );
+ return this;
+ },
+
+ disableTimeCounters: function () {
+ this.icinga.timer.unregister(this.timeCounterTimer);
+ this.timeCounterTimer = null;
+ return this;
+ },
+
+ /**
+ * Focus the given element and scroll to its position
+ *
+ * @param {string} element The name or id of the element to focus
+ * @param {object} [$container] The container containing the element
+ * @param {boolean} [scroll] Whether the viewport should be scrolled to the focused element
+ */
+ focusElement: function(element, $container, scroll) {
+ var $element = element;
+
+ if (typeof scroll === 'undefined') {
+ scroll = true;
+ }
+
+ if (typeof element === 'string') {
+ if ($container && $container.length) {
+ $element = $container.find('#' + element);
+ } else {
+ $element = $('#' + element);
+ }
+
+ if (! $element.length) {
+ // The name attribute is actually deprecated, on anchor tags,
+ // but we'll possibly handle links from another source
+ // (module etc) so that's used as a fallback
+ if ($container && $container.length) {
+ $element = $container.find('[name="' + element.replace(/'/, '\\\'') + '"]');
+ } else {
+ $element = $('[name="' + element.replace(/'/, '\\\'') + '"]');
+ }
+ }
+ }
+
+ if ($element.length) {
+ if (! this.isFocusable($element)) {
+ $element.attr('tabindex', -1);
+ }
+
+ $element[0].focus();
+
+ if (scroll && $container && $container.length) {
+ if (! $container.is('.container')) {
+ $container = $container.closest('.container');
+ }
+
+ if ($container.css('display') === 'flex' && $container.is('.container')) {
+ var $controls = $container.find('.controls');
+ var $content = $container.find('.content');
+ $content.scrollTop($element.offsetTopRelativeTo($content) - $controls.outerHeight() - (
+ $element.outerHeight(true) - $element.innerHeight()
+ ));
+ } else {
+ $container.scrollTop($element.first().position().top);
+ }
+ }
+ }
+ },
+
+ isFocusable: function ($element) {
+ return $element.is('*[tabindex], a[href], input:not([disabled]), button:not([disabled])' +
+ ', select:not([disabled]), textarea:not([disabled]), iframe, area[href], object' +
+ ', embed, *[contenteditable]');
+ },
+
+ moveToLeft: function () {
+ var col2 = this.cutContainer($('#col2'));
+ var kill = this.cutContainer($('#col1'));
+ this.pasteContainer($('#col1'), col2);
+ this.icinga.behaviors.navigation.trySetActiveAndSelectedByUrl($('#col1').data('icingaUrl'));
+ $('#col1').trigger('column-moved', 'col2');
+ },
+
+ moveToRight: function () {
+ let col1 = document.getElementById('col1'),
+ col2 = document.getElementById('col2'),
+ col1Backup = this.cutContainer($(col1));
+
+ this.cutContainer($(col2)); // Clear col2 states
+ this.pasteContainer($(col2), col1Backup);
+ this.layout2col();
+ $(col2).trigger('column-moved', 'col1');
+ },
+
+ cutContainer: function ($col) {
+ var props = {
+ 'elements': $('#' + $col.attr('id') + ' > *').detach(),
+ 'data': {
+ 'data-icinga-url': $col.data('icingaUrl'),
+ 'data-icinga-title': $col.data('icingaTitle'),
+ 'data-icinga-refresh': $col.data('icingaRefresh'),
+ 'data-last-update': $col.data('lastUpdate'),
+ 'data-icinga-module': $col.data('icingaModule'),
+ 'data-icinga-container-id': $col[0].dataset.icingaContainerId
+ },
+ 'class': $col.attr('class')
+ };
+ this.icinga.loader.stopPendingRequestsFor($col);
+ $col.removeData('icingaUrl');
+ $col.removeData('icingaTitle');
+ $col.removeData('icingaRefresh');
+ $col.removeData('lastUpdate');
+ $col.removeData('icingaModule');
+ delete $col[0].dataset.icingaContainerId;
+ $col.removeAttr('class').attr('class', 'container');
+ return props;
+ },
+
+ pasteContainer: function ($col, backup) {
+ backup['elements'].appendTo($col);
+ $col.attr('class', backup['class']); // TODO: ie memleak? remove first?
+ $col.data('icingaUrl', backup['data']['data-icinga-url']);
+ $col.data('icingaTitle', backup['data']['data-icinga-title']);
+ $col.data('icingaRefresh', backup['data']['data-icinga-refresh']);
+ $col.data('lastUpdate', backup['data']['data-last-update']);
+ $col.data('icingaModule', backup['data']['data-icinga-module']);
+ $col[0].dataset.icingaContainerId = backup['data']['data-icinga-container-id'];
+ },
+
+ triggerWindowResize: function () {
+ this.onWindowResize({data: {self: this}});
+ },
+
+ /**
+ * Our window got resized, let's fix our UI
+ */
+ onWindowResize: function (event) {
+ var _this = event.data.self;
+
+ if (_this.layoutHasBeenChanged()) {
+ _this.icinga.logger.info(
+ 'Layout change detected, switching to',
+ _this.currentLayout
+ );
+ }
+
+ _this.refreshDebug();
+ },
+
+ /**
+ * Returns whether the layout is too small for more than one column
+ *
+ * @returns {boolean} True when more than one column is available
+ */
+ hasOnlyOneColumn: function () {
+ return this.currentLayout === 'poor' || this.currentLayout === 'minimal';
+ },
+
+ 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').removeClass(this.currentLayout + '-layout').addClass(layout);
+ this.currentLayout = matched[1];
+ if (this.currentLayout === 'poor' || this.currentLayout === 'minimal') {
+ this.layout1col();
+ } else if (this.icinga.initialized) {
+ // layout1col() also triggers this, that's why an else is required
+ $('#layout').trigger('layout-change');
+ }
+ return true;
+ }
+ }
+ this.icinga.logger.error(
+ 'Someone messed up our responsiveness hacks, html font-family is',
+ layout
+ );
+ return false;
+ },
+
+ /**
+ * Returns whether only one column is displayed
+ *
+ * @returns {boolean} True when only one column is displayed
+ */
+ isOneColLayout: function () {
+ return ! $('#layout').hasClass('twocols');
+ },
+
+ layout1col: function () {
+ if (this.isOneColLayout()) { return; }
+ this.icinga.logger.debug('Switching to single col');
+ $('#layout').removeClass('twocols');
+ this.closeContainer($('#col2'));
+
+ if (this.icinga.initialized) {
+ $('#layout').trigger('layout-change');
+ }
+
+ // one-column layouts never have any selection active
+ $('#col1').removeData('icinga-actiontable-former-href');
+ this.icinga.behaviors.actiontable.clearAll();
+ },
+
+ closeContainer: function($c) {
+ this.icinga.loader.stopPendingRequestsFor($c);
+ $c.removeData('icingaUrl');
+ $c.removeData('icingaTitle');
+ $c.removeData('icingaRefresh');
+ $c.removeData('lastUpdate');
+ $c.removeData('icingaModule');
+ delete $c[0].dataset.icingaContainerId;
+ $c.removeAttr('class').attr('class', 'container');
+ $c.trigger('close-column');
+ this.icinga.history.pushCurrentState();
+ $c.html('');
+ },
+
+ layout2col: function () {
+ if (! this.isOneColLayout()) { return; }
+ this.icinga.logger.debug('Switching to double col');
+ $('#layout').addClass('twocols');
+
+ if (this.icinga.initialized) {
+ $('#layout').trigger('layout-change');
+ }
+ },
+
+ prepareColumnFor: function ($el, $target) {
+ var explicitTarget;
+
+ if ($target.attr('id') === 'col2') {
+ if ($el.closest('#col2').length) {
+ explicitTarget = $el.closest('[data-base-target]').data('baseTarget');
+ if (typeof explicitTarget !== 'undefined' && explicitTarget === '_next') {
+ this.moveToLeft();
+ }
+ } else {
+ this.layout2col();
+ }
+ } else { // if ($target.attr('id') === 'col1')
+ explicitTarget = $el.closest('[data-base-target]').data('baseTarget');
+ if (typeof explicitTarget !== 'undefined' && explicitTarget === '_main') {
+ this.layout1col();
+ }
+ }
+ },
+
+ 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;
+ },
+
+ /**
+ * Assign a unique ID to each .container without such
+ *
+ * This usually applies to dashlets
+ */
+ assignUniqueContainerIds: function() {
+ var currentMax = 0;
+ $('.container').each(function() {
+ var $el = $(this);
+ var m;
+ if (!$el.attr('id')) {
+ return;
+ }
+ if (m = $el.attr('id').match(/^ciu_(\d+)$/)) {
+ if (parseInt(m[1]) > currentMax) {
+ currentMax = parseInt(m[1]);
+ }
+ }
+ });
+ $('.container').each(function() {
+ var $el = $(this);
+ if (!!$el.attr('id')) {
+ return;
+ }
+ currentMax++;
+ $el.attr('id', 'ciu_' + currentMax);
+ });
+ },
+
+ refreshDebug: function () {
+ if (! this.debug) {
+ return;
+ }
+
+ 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 = '<br />Loading:<br />';
+ }
+ loading += el + ' => ' + encodeURI(req.url);
+ });
+
+ $('#responsive-debug').html(
+ ' Time: ' +
+ this.icinga.utils.formatHHiiss(new Date()) +
+ '<br /> 1em: ' +
+ size +
+ 'px<br /> Win: ' +
+ winWidth +
+ 'x'+
+ winHeight +
+ 'px<br />' +
+ ' Layout: ' +
+ this.currentLayout +
+ loading
+ );
+ },
+
+ /**
+ * Refresh partial time counters
+ *
+ * This function runs every second.
+ */
+ refreshTimeSince: function () {
+ $('.time-ago, .time-since').each(function (idx, el) {
+ var partialTime = /(\d{1,2})m (\d{1,2})s/.exec(el.innerHTML);
+ if (partialTime !== null) {
+ var minute = parseInt(partialTime[1], 10),
+ second = parseInt(partialTime[2], 10);
+ if (second < 59) {
+ ++second;
+ } else {
+ ++minute;
+ second = 0;
+ }
+ el.innerHTML = el.innerHTML.substring(0, partialTime.index) + minute.toString() + 'm '
+ + second.toString() + 's' + el.innerHTML.substring(partialTime.index + partialTime[0].length);
+ }
+ });
+
+ $('.time-until').each(function (idx, el) {
+ var partialTime = /(-?)(\d{1,2})m (\d{1,2})s/.exec(el.innerHTML);
+ if (partialTime !== null) {
+ var minute = parseInt(partialTime[2], 10),
+ second = parseInt(partialTime[3], 10),
+ invert = partialTime[1];
+ if (invert.length) {
+ // Count up because partial time is negative
+ if (second < 59) {
+ ++second;
+ } else {
+ ++minute;
+ second = 0;
+ }
+ } else {
+ // Count down because partial time is positive
+ if (second === 0) {
+ if (minute === 0) {
+ // Invert counter
+ minute = 0;
+ second = 1;
+ invert = '-';
+ } else {
+ --minute;
+ second = 59;
+ }
+ } else {
+ --second;
+ }
+
+ if (minute === 0 && second === 0 && el.dataset.agoLabel) {
+ el.innerText = el.dataset.agoLabel;
+ el.classList.remove('time-until');
+ el.classList.add('time-ago');
+
+ return;
+ }
+ }
+ el.innerHTML = el.innerHTML.substring(0, partialTime.index) + invert + minute.toString() + 'm '
+ + second.toString() + 's' + el.innerHTML.substring(partialTime.index + partialTime[0].length);
+ }
+ });
+ },
+
+ createFontSizeCalculator: function () {
+ var $el = $('<div id="fontsize-calc">&nbsp;</div>');
+ $('#layout').append($el);
+ return $el;
+ },
+
+ getDefaultFontSize: function () {
+ var $calc = $('#fontsize-calc');
+ if (! $calc.length) {
+ $calc = this.createFontSizeCalculator();
+ }
+ return $calc.width() / 1000;
+ },
+
+ /**
+ * Toggle mobile menu
+ *
+ * @param {object} e Event
+ */
+ toggleMobileMenu: function(e) {
+ $('#sidebar').toggleClass('expanded');
+ },
+
+ /**
+ * Close mobile menu when the enter key is pressed during search or the user leaves the sidebar
+ *
+ * @param {object} e Event
+ */
+ closeMobileMenu: function(e) {
+ if (e.data.self.currentLayout !== 'minimal') {
+ return;
+ }
+
+ if (e.data.type === 'key') {
+ if (e.which === 13) {
+ $('#sidebar').removeClass('expanded');
+ $(e.target)[0].blur();
+ }
+ } else {
+ $('#sidebar').removeClass('expanded');
+ }
+ },
+
+ toggleFullscreen: function () {
+ $('#layout').toggleClass('fullscreen-layout');
+ },
+
+ getUniqueContainerId: function (container) {
+ if (typeof container.jquery !== 'undefined') {
+ if (! container.length) {
+ return null;
+ }
+
+ container = container[0];
+ } else if (typeof container === 'undefined') {
+ return null;
+ }
+
+ var containerId = container.dataset.icingaContainerId || null;
+ if (containerId === null) {
+ /**
+ * Only generate an id if it's not for col1 or the menu (which are using the non-suffixed window id).
+ * This is based on the assumption that the server only knows about the menu and first column
+ * and therefore does not need to protect its ids. (As the menu is most likely part of the sidebar)
+ */
+ var col1 = document.getElementById('col1');
+ if (container.id !== 'menu' && col1 !== null && ! col1.contains(container)) {
+ containerId = this.icinga.utils.generateId(6); // Random because the content may move
+ container.dataset.icingaContainerId = containerId;
+ }
+ }
+
+ return containerId;
+ },
+
+ getWindowId: function () {
+ if (! this.hasWindowId()) {
+ return undefined;
+ }
+ return window.name.match(/^Icinga-([a-zA-Z0-9]+)$/)[1];
+ },
+
+ hasWindowId: function () {
+ var res = window.name.match(/^Icinga-([a-zA-Z0-9]+)$/);
+ return typeof res === 'object' && null !== res;
+ },
+
+ setWindowId: function (id) {
+ this.icinga.logger.debug('Setting new window id', id);
+ window.name = 'Icinga-' + id;
+ },
+
+ destroy: function () {
+ // This is gonna be hard, clean up the mess
+ this.icinga = null;
+ this.debugTimer = null;
+ this.timeCounterTimer = null;
+ }
+ };
+
+}(Icinga, jQuery));