/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ /** * Icinga.Loader * * This is where we take care of XHR requests, responses and failures. */ (function(Icinga, $) { 'use strict'; Icinga.Loader = function (icinga) { /** * YES, we need Icinga */ this.icinga = icinga; /** * Our base url */ this.baseUrl = icinga.config.baseUrl; this.failureNotice = null; /** * Pending requests */ this.requests = {}; this.iconCache = {}; /** * Whether auto-refresh is enabled */ this.autorefreshEnabled = true; /** * Whether auto-refresh is suspended due to visibility of page */ this.autorefreshSuspended = false; }; Icinga.Loader.prototype = { initialize: function () { this.icinga.timer.register(this.autorefresh, this, 500); }, submitForm: function ($form, $autoSubmittedBy, $button) { var icinga = this.icinga; var url = $form.attr('action'); var method = $form.attr('method'); var encoding = $form.attr('enctype'); var progressTimer; var $target; var data; if (typeof method === 'undefined') { method = 'POST'; } else { method = method.toUpperCase(); } if (typeof encoding === 'undefined') { encoding = 'application/x-www-form-urlencoded'; } if (typeof $autoSubmittedBy === 'undefined') { $autoSubmittedBy = false; } if (typeof $button === 'undefined') { $button = $('input[type=submit]:focus', $form).add('button[type=submit]:focus', $form); } if ($button.length === 0) { $button = $('input[type=submit]', $form).add('button[type=submit]', $form).first(); } if ($button.length) { // Activate spinner if ($button.hasClass('spinner')) { $button.addClass('active'); } $target = this.getLinkTargetFor($button); } else { $target = this.getLinkTargetFor($form); } // Overwrite the URL only if the form is not auto submitted if (! $autoSubmittedBy && $button.hasAttr('formaction')) { url = $button.attr('formaction'); } if (! url) { // Use the URL of the target container if the form's action is not set url = $target.closest('.container').data('icinga-url'); } icinga.logger.debug('Submitting form: ' + method + ' ' + url, method); if (method === 'GET') { var dataObj = $form.serializeObject(); if (! $autoSubmittedBy) { if ($button.length && $button.attr('name') !== 'undefined') { dataObj[$button.attr('name')] = $button.attr('value'); } } url = icinga.utils.addUrlParams(url, dataObj); } else { if (encoding === 'multipart/form-data') { data = new window.FormData($form[0]); } else { data = $form.serializeArray(); } if (! $autoSubmittedBy) { if ($button.length && $button.attr('name') !== 'undefined') { if (encoding === 'multipart/form-data') { data.append($button.attr('name'), $button.attr('value')); } else { data.push({ name: $button.attr('name'), value: $button.attr('value') }); } } } } // Disable all form controls to prevent resubmission except for our search input // Note that disabled form inputs will not be enabled via JavaScript again if (! $autoSubmittedBy && ! $form.is('[role="search"]') && $target.attr('id') === $form.closest('.container').attr('id') ) { $form.find('input[type=submit],button[type=submit],button:not([type])').prop('disabled', true); } // Show a spinner depending on how the form is being submitted if ($autoSubmittedBy && $autoSubmittedBy.siblings('.spinner').length) { $autoSubmittedBy.siblings('.spinner').first().addClass('active'); } else if ($button.length && $button.is('button') && $button.hasClass('animated')) { $button.addClass('active'); } else if ($button.length && $button.attr('data-progress-label')) { var isInput = $button.is('input'); if (isInput) { $button.prop('value', $button.attr('data-progress-label') + '...'); } else { $button.html($button.attr('data-progress-label') + '...'); } // Use a fixed width to prevent the button from wobbling $button.css('width', $button.css('width')); progressTimer = icinga.timer.register(function () { var label = isInput ? $button.prop('value') : $button.html(); var dots = label.substr(-3); // Using empty spaces here to prevent centered labels from wobbling if (dots === '...') { label = label.slice(0, -2) + ' '; } else if (dots === '.. ') { label = label.slice(0, -1) + '.'; } else if (dots === '. ') { label = label.slice(0, -2) + '. '; } if (isInput) { $button.prop('value', label); } else { $button.html(label); } }, null, 100); } else if ($button.length && $button.next().hasClass('spinner')) { $('i', $button.next()).addClass('active'); } else if ($form.attr('data-progress-element')) { var $progressElement = $('#' + $form.attr('data-progress-element')); if ($progressElement.length) { if ($progressElement.hasClass('spinner')) { $('i', $progressElement).addClass('active'); } else { $('i.spinner', $progressElement).addClass('active'); } } } let extraHeaders = {}; if ($autoSubmittedBy) { let id; if (($autoSubmittedBy.attr('name') || $autoSubmittedBy.attr('id'))) { id = $autoSubmittedBy.attr('name') || $autoSubmittedBy.attr('id'); } else { let formSelector = icinga.utils.getCSSPath($form); let nearestKnownParent = $autoSubmittedBy.closest( formSelector + ' [name],' + formSelector + ' [id]' ); if (nearestKnownParent) { id = nearestKnownParent.attr('name') || nearestKnownParent.attr('id'); } } if (id) { extraHeaders['X-Icinga-AutoSubmittedBy'] = id; } } var req = this.loadUrl(url, $target, data, method, undefined, undefined, undefined, extraHeaders); req.forceFocus = $autoSubmittedBy ? $autoSubmittedBy : $button.length ? $button : null; req.autosubmit = !! $autoSubmittedBy; req.addToHistory = method === 'GET'; req.progressTimer = progressTimer; if ($autoSubmittedBy) { if ($autoSubmittedBy.closest('.controls').length) { $('.content', req.$target).addClass('impact'); } else { req.$target.addClass('impact'); } } return req; }, /** * 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' * @param {string} action How to handle the response ('replace' or 'append'), default is 'replace' * @param {boolean} autorefresh Whether the cause is a autorefresh or not * @param {object} progressTimer A timer to be stopped when the request is done * @param {object} extraHeaders Extra header entries */ loadUrl: function (url, $target, data, method, action, autorefresh, progressTimer, extraHeaders) { var id = null; // Default method is GET if ('undefined' === typeof method) { method = 'GET'; } if ('undefined' === typeof action) { action = 'replace'; } if ('undefined' === typeof autorefresh) { autorefresh = false; } 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 we have a pending request for the same target... if (typeof this.requests[id] !== 'undefined') { // ... ignore the new request if it is already pending with the same URL. Only abort GETs, as those // are the only methods that are guaranteed to return the same value if (this.requests[id].url === url && method === 'GET') { if (autorefresh) { return false; } 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' }; if (!! id) { headers['X-Icinga-Container'] = id; } if (autorefresh) { headers['X-Icinga-Autorefresh'] = '1'; } if ($target.is('#col2')) { headers['X-Icinga-Col1-State'] = this.icinga.history.getCol1State(); headers['X-Icinga-Col2-State'] = this.icinga.history.getCol2State().replace(/^#!/, ''); } // Ask for a new window id in case we don't already have one if (this.icinga.ui.hasWindowId()) { var windowId = this.icinga.ui.getWindowId(); var containerId = this.icinga.ui.getUniqueContainerId($target); if (containerId) { windowId = windowId + '_' + containerId; } headers['X-Icinga-WindowId'] = windowId; } else { headers['X-Icinga-WindowId'] = 'undefined'; } if (typeof extraHeaders !== 'undefined') { headers = $.extend(headers, extraHeaders); } // This is jQuery's default content type var contentType = 'application/x-www-form-urlencoded; charset=UTF-8'; var isFormData = typeof window.FormData !== 'undefined' && data instanceof window.FormData; if (isFormData) { // Setting false is mandatory as the form's data // won't be recognized by the server otherwise contentType = false; } var _this = this; var req = $.ajax({ type : method, url : url, data : data, headers: headers, context: _this, contentType: contentType, processData: ! isFormData }); req.$target = $target; req.$redirectTarget = $target; req.url = url; req.done(this.onResponse); req.fail(this.onFailure); req.always(this.onComplete); req.autorefresh = autorefresh; req.autosubmit = false; req.scripted = false; req.method = method; req.action = action; req.addToHistory = true; req.progressTimer = progressTimer; if (url.match(/#/)) { req.forceFocus = url.split(/#/)[1]; } if (id) { this.requests[id] = req; } if (! autorefresh) { setTimeout(function () { // The column may have not been shown before. To make the transition // delay working we have to wait for the column getting rendered if (! req.autosubmit && req.state() === 'pending') { req.$target.addClass('impact'); } }, 0); } 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 (typeof this.requests[id] !== 'undefined') { this.requests[id].abort(); } }, filterAutorefreshingContainers: function () { return $(this).data('icingaRefresh') > 0 && ! $(this).is('[data-suspend-autorefresh]'); }, autorefresh: function () { var _this = this; $('.container').filter(this.filterAutorefreshingContainers).each(function (idx, el) { var $el = $(el); var id = $el.attr('id'); // Always request application-state if (id !== 'application-state' && (! _this.autorefreshEnabled || _this.autorefreshSuspended)) { // Continue return true; } if (typeof _this.requests[id] !== 'undefined') { _this.icinga.logger.debug('No refresh, request pending for ', id); return; } var interval = $el.data('icingaRefresh'); var lastUpdate = $el.data('lastUpdate'); if (typeof interval === 'undefined' || ! interval) { _this.icinga.logger.info('No interval, setting default', id); interval = 10; } if (typeof lastUpdate === 'undefined' || ! lastUpdate) { _this.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; } if (_this.loadUrl($el.data('icingaUrl'), $el, undefined, undefined, undefined, true) === false) { _this.icinga.logger.debug( 'NOT autorefreshing ' + id + ', even if ' + interval + ' ms passed. Request pending?' ); } else { _this.icinga.logger.debug( 'Autorefreshing ' + id + ' ' + interval + ' ms passed' ); } el = null; }); }, /** * Disable the autorefresh mechanism */ disableAutorefresh: function () { this.autorefreshEnabled = false; }, /** * Enable the autorefresh mechanism */ enableAutorefresh: function () { this.autorefreshEnabled = true; }, processNotificationHeader: function(req) { var header = req.getResponseHeader('X-Icinga-Notification'); var _this = this; if (! header) return false; var list = header.split('&'); $.each(list, function(idx, el) { var parts = decodeURIComponent(el).split(' '); _this.createNotice(parts.shift(), parts.join(' ')); }); return true; }, /** * Process the X-Icinga-Redirect HTTP Response Header * * If the response includes the X-Icinga-Redirect header, redirects to the URL associated with the header. * * @param {object} req Current request * * @returns {boolean} Whether we're about to redirect */ processRedirectHeader: function(req) { var icinga = this.icinga, $redirectTarget = req.$redirectTarget, redirect = req.getResponseHeader('X-Icinga-Redirect'); if (! redirect) { return false; } redirect = decodeURIComponent(redirect); if (redirect.match(/__SELF__/)) { if (req.autorefresh) { // Redirect to the current window's URL in case it's an auto-refresh request. If authenticated // externally this ensures seamless re-login if the session's expired redirect = redirect.replace( /__SELF__/, encodeURIComponent( document.location.pathname + document.location.search + document.location.hash ) ); } else { // Redirect to the URL which required authentication. When clicking a link this ensures that we // redirect to the link's URL instead of the current window's URL (see above) redirect = redirect.replace(/__SELF__/, req.url); } } else if (redirect.match(/__BACK__/)) { if (req.$redirectTarget.is('#col1')) { icinga.logger.warn('Cannot navigate back. Redirect target is #col1'); return false; } var originUrl = req.$target.data('icingaUrl'); $(window).on('popstate.__back__', { self: this }, function (event) { const _this = event.data.self; let $refreshTarget = $('#col2'); let refreshUrl; const hash = icinga.history.getCol2State(); if (hash && hash.match(/^#!/)) { // TODO: These three lines are copied from history.js, I don't like this var parts = hash.split(/#!/); if (parts[1] === originUrl) { // After a page load a call to back() seems not to have an effect icinga.ui.layout1col(); } else { refreshUrl = parts[1]; } } if (typeof refreshUrl === 'undefined' && icinga.ui.isOneColLayout()) { refreshUrl = icinga.history.getCol1State(); $refreshTarget = $('#col1'); } const refreshReq = _this.loadUrl(refreshUrl, $refreshTarget); refreshReq.autoRefreshInterval = req.getResponseHeader('X-Icinga-Refresh'); refreshReq.autorefresh = true; refreshReq.scripted = true; $(window).off('popstate.__back__'); }); // Navigate back, no redirect desired window.history.back(); return true; } else if (redirect.match(/__CLOSE__/)) { if (req.$target.is('#col1') && req.$redirectTarget.is('#col1')) { icinga.logger.warn('Cannot close #col1'); return false; } if (req.$redirectTarget.is('.container') && ! req.$redirectTarget.is('#main > :scope')) { // If it is a container that is not a top level container, we just empty it req.$redirectTarget.empty(); return true; } if (! req.$redirectTarget.is('#col2')) { icinga.logger.debug('Cannot close container', req.$redirectTarget); return false; } // Close right column as requested icinga.ui.layout1col(); if (!! req.getResponseHeader('X-Icinga-Extra-Updates')) { icinga.logger.debug('Not refreshing #col1 due to outstanding extra updates'); return true; } $redirectTarget = $('#col1'); redirect = icinga.history.getCol1State(); } else if (redirect.match(/__REFRESH__/)) { if (req.$redirectTarget.is('#col1')) { redirect = icinga.history.getCol1State(); } else if (req.$redirectTarget.is('#col2')) { redirect = icinga.history.getCol2State().replace(/^#!/, ''); } else { icinga.logger.error('Unable to refresh. Not a primary column: ', req.$redirectTarget); return false; } } var useHttp = req.getResponseHeader('X-Icinga-Redirect-Http'); if (useHttp === 'yes') { window.location.replace(redirect); return true; } this.redirectToUrl(redirect, $redirectTarget, req); return true; }, /** * Redirect to the given url * * @param {string} url * @param {object} $target * @param {XMLHttpRequest} referrer */ redirectToUrl: function (url, $target, referrer) { var icinga = this.icinga, rerenderLayout, autoRefreshInterval, forceFocus, origin; if (typeof referrer !== 'undefined') { rerenderLayout = referrer.getResponseHeader('X-Icinga-Rerender-Layout'); autoRefreshInterval = referrer.getResponseHeader('X-Icinga-Refresh'); forceFocus = referrer.forceFocus; origin = referrer.url; } icinga.logger.debug( 'Got redirect for ', $target, ', URL was ' + url ); if (rerenderLayout) { var parts = url.split(/#!/); url = parts.shift(); var redirectionUrl = icinga.utils.addUrlFlag(url, 'renderLayout'); var r = this.loadUrl(redirectionUrl, $('#layout')); r.historyUrl = url; r.referrer = referrer; if (parts.length) { r.loadNext = parts; } else if (!! document.location.hash) { // Retain detail URL if the layout is rerendered parts = document.location.hash.split('#!').splice(1); if (parts.length) { r.loadNext = $.grep(parts, function (url) { if (url !== origin) { icinga.logger.debug('Retaining detail url ' + url); return true; } icinga.logger.debug('Discarding detail url ' + url + ' as it\'s the origin of the redirect'); return false; }); } } } else { if (url.match(/#!/)) { var parts = url.split(/#!/); icinga.ui.layout2col(); this.loadUrl(parts.shift(), $('#col1')); this.loadUrl(parts.shift(), $('#col2')); } else { var req = this.loadUrl(url, $target); req.forceFocus = url === origin ? forceFocus : null; req.autoRefreshInterval = autoRefreshInterval; req.referrer = referrer; } } }, /** * Handle successful XHR response */ onResponse: function (data, textStatus, req) { var _this = this; if (this.failureNotice !== null) { if (! this.failureNotice.hasClass('fading-out')) { this.failureNotice.remove(); } this.failureNotice = null; } var target = req.getResponseHeader('X-Icinga-Container'); var newBody = false; var oldNotifications = false; var isRedirect = !! req.getResponseHeader('X-Icinga-Redirect'); if (target) { if (target === 'ignore') { return; } var $newTarget = this.identifyLinkTarget(target, req.$target); if ($newTarget.length) { if (isRedirect) { req.$redirectTarget = $newTarget; } else { // If we change the target, oncomplete will fail to clean up. // This fixes the problem, not using req.$target would be better delete this.requests[req.$target.attr('id')]; req.$target = $newTarget; } if (target === 'layout') { oldNotifications = $('#notifications li').detach(); this.icinga.ui.layout1col(); newBody = true; } else if ($newTarget.attr('id') === 'col2') { if (_this.icinga.ui.isOneColLayout()) { _this.icinga.ui.layout2col(); } else if (target === '_next') { _this.icinga.ui.moveToLeft(); } } } } if (req.autorefresh && req.$target.is('[data-suspend-autorefresh]')) { return; } this.icinga.logger.debug( 'Got response for ', req.$target, ', URL was ' + req.url ); this.processNotificationHeader(req); var cssreload = req.getResponseHeader('X-Icinga-Reload-Css'); if (cssreload) { this.icinga.ui.reloadCss(); } if (isRedirect) { return; } if (req.getResponseHeader('X-Icinga-Announcements') === 'refresh') { var announceReq = _this.loadUrl(_this.url('/layout/announcements'), $('#announcements')); announceReq.addToHistory = false; announceReq.scripted = true; } var classes; if (target !== 'layout') { var moduleName = req.getResponseHeader('X-Icinga-Module'); classes = $.grep(req.$target.classes(), function (el) { if (el === 'icinga-module' || el.match(/^module\-/)) { return false; } return true; }); if (moduleName) { // Lazy load module javascript (Applies only to module.js code) _this.icinga.ensureModule(moduleName); req.$target.data('icingaModule', moduleName); classes.push('icinga-module'); classes.push('module-' + moduleName); } else { req.$target.removeData('icingaModule'); if (req.$target.attr('data-icinga-module')) { req.$target.removeAttr('data-icinga-module'); } } req.$target.attr('class', classes.join(' ')); var refresh = req.autoRefreshInterval || req.getResponseHeader('X-Icinga-Refresh'); if (refresh) { req.$target.data('icingaRefresh', refresh); } else { req.$target.removeData('icingaRefresh'); if (req.$target.attr('data-icinga-refresh')) { req.$target.removeAttr('data-icinga-refresh'); } } } var title = req.getResponseHeader('X-Icinga-Title'); if (title && (target === 'layout' || req.$target.is('#layout'))) { this.icinga.ui.setTitle(decodeURIComponent(title)); } else if (title && ! req.autorefresh && req.$target.closest('.dashboard').length === 0) { req.$target.data('icingaTitle', decodeURIComponent(title)); } // 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); } var autoSubmit = false; var currentUrl = this.icinga.utils.parseUrl(req.$target.data('icingaUrl')); if (req.method === 'POST') { var newUrl = this.icinga.utils.parseUrl(req.url); if (newUrl.path === currentUrl.path && this.icinga.utils.arraysEqual(newUrl.params, currentUrl.params)) { autoSubmit = true; } } req.$target.data('icingaUrl', req.url); if (typeof req.progressTimer !== 'undefined') { this.icinga.timer.unregister(req.progressTimer); } var contentSeparator = req.getResponseHeader('X-Icinga-Multipart-Content'); if (!! contentSeparator) { var locationQuery = req.getResponseHeader('X-Icinga-Location-Query'); if (locationQuery !== null) { let url = currentUrl.path + (locationQuery ? '?' + locationQuery : ''); if (req.autosubmit || autoSubmit) { // Also update a form's action if it doesn't differ from the container's url var $form = $(req.forceFocus).closest('form'); var formAction = $form.attr('action'); if (!! formAction) { formAction = this.icinga.utils.parseUrl(formAction); if (formAction.path === currentUrl.path && this.icinga.utils.arraysEqual(formAction.params, currentUrl.params) ) { $form.attr('action', url); } } } req.$target.data('icingaUrl', url); this.icinga.history.replaceCurrentState(); } $.each(req.responseText.split(contentSeparator), function (idx, el) { var match = el.match(/for=(Behavior:)?(\S+)\s+([^]*)/m); if (!! match) { if (match[1]) { var behavior = _this.icinga.behaviors[match[2].toLowerCase()]; if (typeof behavior !== 'undefined' && typeof behavior.update === 'function') { behavior.update(JSON.parse(match[3])); } else { _this.icinga.logger.warn( 'Invalid behavior. Cannot update behavior "' + match[2] + '"'); } } else { var $target = $('#' + match[2]); if ($target.length) { var forceFocus; if (req.forceFocus && typeof req.forceFocus.jquery !== 'undefined' && $.contains($target[0], req.forceFocus[0]) ) { forceFocus = req.forceFocus; } _this.renderContentToContainer( match[3], $target, 'replace', req.autorefresh, forceFocus, req.autosubmit || autoSubmit, req.scripted ); } else { _this.icinga.logger.warn( 'Invalid target ID. Cannot render multipart to #' + match[2]); } } } else { _this.icinga.logger.error('Ill-formed multipart', el); } }) } else { this.renderContentToContainer( req.responseText, req.$target, req.action, req.autorefresh, req.forceFocus, req.autosubmit || autoSubmit, req.scripted ); } if (oldNotifications) { oldNotifications.appendTo($('#notifications')); } if (newBody) { this.icinga.ui.fixDebugVisibility().triggerWindowResize(); } }, /** * Regardless of whether a request succeeded of failed, clean up */ onComplete: function (dataOrReq, textStatus, reqOrError) { var _this = this; var req; if (typeof dataOrReq === 'object') { req = dataOrReq; } else { req = reqOrError; } if (req.getResponseHeader('X-Icinga-Reload-Window') === 'yes') { window.location.reload(); return; } req.$target.data('lastUpdate', (new Date()).getTime()); delete this.requests[req.$target.attr('id')]; this.icinga.ui.fadeNotificationsAway(); var extraUpdates = req.getResponseHeader('X-Icinga-Extra-Updates'); if (!! extraUpdates && req.getResponseHeader('X-Icinga-Redirect-Http') !== 'yes') { $.each(extraUpdates.split(','), function (idx, el) { var parts = el.trim().split(';'); var $target; var url; if (parts.length === 2) { $target = $(parts[0].startsWith('#') ? parts[0] : '#' + parts[0]); if (! $target.length) { _this.icinga.logger.warn('Invalid target ID. Cannot load extra URL', el); return; } url = parts[1]; } else if (parts.length === 1) { $target = $(parts[0]).closest(".container").not(req.$target); if (! $target.length) { _this.icinga.logger.warn('Invalid target ID. Cannot load extra URL', el); return; } url = $target.data('icingaUrl'); if (! url) { _this.icinga.logger.debug( 'Superfluous extra update. The target\'s container has no url', el); return; } } else { _this.icinga.logger.error('Invalid extra update', el); return; } if (url === '__CLOSE__') { if ($target.is('#col2')) { _this.icinga.ui.layout1col(); } else if ($target.is('#main > :scope')) { _this.icinga.logger.warn('Invalid target ID. Cannot close ', $target); } else if ($target.is('.container')) { // If it is a container that is not a top level container, we just empty it $target.empty(); } } else { _this.loadUrl(url, $target).addToHistory = false; } }); } if ((textStatus === 'abort' && typeof req.referrer !== 'undefined') || this.processRedirectHeader(req)) { return; } // Remove 'impact' class if there was such if (req.$target.hasClass('impact')) { req.$target.removeClass('impact'); } else { var $impact = req.$target.find('.impact').first(); if ($impact.length) { $impact.removeClass('impact'); } } if (! req.autorefresh && ! req.autosubmit) { // TODO: Hook for response/url? var url = req.url; if (req.$target[0].id === 'col1') { this.icinga.behaviors.navigation.trySetActiveAndSelectedByUrl(url); } var $forms = $('[action="' + this.icinga.utils.parseUrl(url).path + '"]'); var $matches = $.merge($('[href="' + url + '"]'), $forms); $matches.each(function (idx, el) { var $el = $(el); if ($el.closest('#menu').length) { if ($el.is('form')) { $('input', $el).addClass('active'); } // Interrupt .each, only one menu item shall be active return false; } }); } // Update history when necessary if (! req.autorefresh && req.addToHistory) { if (req.$target.hasClass('container')) { // We only want to care about top-level containers if (req.$target.parent().closest('.container').length === 0) { this.icinga.history.pushCurrentState(); } } else { // Request wasn't for a container, so it's usually the body // or the full layout. Push request URL to history: var url = typeof req.historyUrl !== 'undefined' ? req.historyUrl : req.url; this.icinga.history.pushUrl(url); } } if (typeof req.loadNext !== 'undefined' && req.loadNext.length) { if ($('#col2').length) { var r = this.loadUrl(req.loadNext[0], $('#col2')); r.addToHistory = req.addToHistory; this.icinga.ui.layout2col(); } else { this.icinga.logger.error('Failed to load URL for #col2', req.loadNext); } } // Lazy load module javascript (Applies only to module.js code) this.icinga.ensureSubModules(req.$target); req.$target.find('.container').each(function () { $(this).trigger('rendered', [req.autorefresh, req.scripted, req.autosubmit]); }); req.$target.trigger('rendered', [req.autorefresh, req.scripted, req.autosubmit]); this.icinga.ui.refreshDebug(); }, /** * Handle failed XHR response */ onFailure: function (req, textStatus, errorThrown) { var url = req.url; /* * Test if a manual actions comes in and autorefresh is active: Stop refreshing */ if (req.addToHistory && ! req.autorefresh) { req.$target.data('icingaRefresh', 0); req.$target.data('icingaUrl', url); } if (typeof req.progressTimer !== 'undefined') { this.icinga.timer.unregister(req.progressTimer); } if (req.status > 0 && req.status < 501) { this.icinga.logger.error( req.status, errorThrown + ':', $(req.responseText).text().replace(/\s+/g, ' ').slice(0, 100) ); this.renderContentToContainer( req.responseText, req.$target, req.action, req.autorefresh, undefined, req.autosubmit, req.scripted ); } else { if (errorThrown === 'abort') { this.icinga.logger.debug( 'Request to ' + url + ' has been aborted for ', req.$target ); if (req.scripted) { req.addToHistory = false; } } else { if (this.failureNotice === null) { var now = new Date(); var padString = this.icinga.utils.padString; this.failureNotice = this.createNotice( 'error', 'The connection to the Icinga web server was lost at ' + now.getFullYear() + '-' + padString(now.getMonth() + 1, 0, 2) + '-' + padString(now.getDate(), 0, 2) + ' ' + padString(now.getHours(), 0, 2) + ':' + padString(now.getMinutes(), 0, 2) + '.', true ); } this.icinga.logger.error( 'Failed to contact web server loading ', url, ' for ', req.$target ); } } }, /** * Create a notification. Can be improved. */ createNotice: function (severity, message, persist) { var c = severity, icon; if (persist) { c += ' persist'; } switch (severity) { case 'success': icon = 'check-circle'; break; case 'error': icon = 'times'; break; case 'warning': icon = 'exclamation-triangle'; break; case 'info': icon = 'info-circle'; break; } var $notice = $( '
  • ' + '' + this.icinga.utils.escape(message) + '
  • ' ).appendTo($('#notifications')); if (!persist) { this.icinga.ui.fadeNotificationsAway(); } return $notice; }, /** * Detect the link/form target for a given element (link, form, whatever) * * @param {object} $el jQuery set with the element * @param {boolean} prepare Pass `false` to disable column preparation */ getLinkTargetFor: function($el, prepare) { if (typeof prepare === 'undefined') { prepare = true; } // If everything else fails, our target is the first column... var $target = $('#col1'); // ...but usually we will use our own container... var $container = $el.closest('.container'); if ($container.length) { $target = $container; } // You can of course override the default behaviour: if ($el.closest('[data-base-target]').length) { var targetId = $el.closest('[data-base-target]').data('baseTarget'); $target = this.identifyLinkTarget(targetId, $el); if (! $target.length) { this.icinga.logger.warn('Link target "#' + targetId + '" does not exist in DOM.'); } } if (prepare) { this.icinga.ui.prepareColumnFor($el, $target); } return $target; }, /** * Identify link target by the given id * * The id may also be one of the column aliases: `_next`, `_self` and `_main` * * @param {string} id * @param {object} $of * @return {object} */ identifyLinkTarget: function (id, $of) { var $target; if (id === '_next') { if (this.icinga.ui.hasOnlyOneColumn()) { $target = $('#col1'); } else { $target = $('#col2'); } } else if (id === '_self') { $target = $of.closest('.container'); } else if (id === '_main') { $target = $('#col1'); } else { $target = $('#' + id); } return $target; }, /** * Smoothly render given HTML to given container */ renderContentToContainer: function (content, $container, action, autorefresh, forceFocus, autoSubmit, scripted) { // Container update happens here var scrollPos = false; var _this = this; var containerId = $container.attr('id'); var activeElementPath = false; var navigationAnchor = false; var focusFallback = false; if (forceFocus && forceFocus.length) { if (typeof forceFocus === 'string') { navigationAnchor = forceFocus; } else { activeElementPath = this.icinga.utils.getCSSPath($(forceFocus)); } } else if (document.activeElement && document.activeElement.id === 'search') { activeElementPath = '#search'; } else if (document.activeElement && document.activeElement !== document.body && $.contains($container[0], document.activeElement) ) { // Active element in container var $activeElement = $(document.activeElement); var $pagination = $activeElement.closest('.pagination-control'); if ($pagination.length) { focusFallback = { 'parent': this.icinga.utils.getCSSPath($pagination), 'child': '.active > a' }; } activeElementPath = this.icinga.utils.getCSSPath($activeElement); } var scrollTarget = $container; if (typeof containerId !== 'undefined') { if (autorefresh || autoSubmit) { if ($container.css('display') === 'flex' && $container.is('.container')) { var $scrollableContent = $container.children('.content'); scrollPos = { x: $scrollableContent.scrollTop(), y: $scrollableContent.scrollLeft() }; scrollTarget = _this.icinga.utils.getCSSPath($scrollableContent); } else { scrollPos = { x: $container.scrollTop(), y: $container.scrollLeft() }; } } else { scrollPos = { x: 0, y: 0 } } } $container.trigger('beforerender', [content, action, autorefresh, scripted, autoSubmit]); var discard = false; $.each(_this.icinga.behaviors, function(name, behavior) { if (behavior.renderHook) { var changed = behavior.renderHook(content, $container, action, autorefresh, autoSubmit); if (changed === null) { discard = true; } else { content = changed; } } }); $('.container', $container).each(function() { _this.stopPendingRequestsFor($(this)); }); if (! discard) { if ($container.closest('.dashboard').length) { var title = $('h1', $container).first().detach(); $container.html(title).append(content); } else if (action === 'replace') { $container.html(content); } else { $container.append(content); } } this.icinga.ui.assignUniqueContainerIds(); if (! discard && navigationAnchor) { var $element = $container.find('#' + navigationAnchor); if ($element.length) { // data-icinga-no-scroll-on-focus is NOT designed to avoid scrolling for non-XHR requests setTimeout(this.icinga.ui.focusElement.bind(this.icinga.ui), 0, $element, $container, ! $element.is('[data-icinga-no-scroll-on-focus]')); } } else if (! activeElementPath) { // Active element was not in this container if (! autorefresh && ! autoSubmit && ! scripted) { setTimeout(function() { if (typeof $container.attr('tabindex') === 'undefined') { $container.attr('tabindex', -1); } // Do not touch focus in case a module or component already placed it if ($(document.activeElement).closest('.container').attr('id') !== containerId) { _this.icinga.ui.focusElement($container); } }, 0); } } else { setTimeout(function() { var $activeElement = $(activeElementPath); if ($activeElement.length && $activeElement.is(':visible')) { $activeElement[0].focus({preventScroll: autorefresh || autoSubmit}); } else if (! autorefresh && ! autoSubmit && ! scripted) { if (focusFallback) { _this.icinga.ui.focusElement($(focusFallback.parent).find(focusFallback.child)); } else if (typeof $container.attr('tabindex') === 'undefined') { $container.attr('tabindex', -1); } _this.icinga.ui.focusElement($container); } }, 0); } if (scrollPos !== false) { var $scrollTarget = $(scrollTarget); // Fallback for browsers without support for focus({preventScroll: true}) requestAnimationFrame(() => { if ($scrollTarget.scrollTop() !== scrollPos.x) { $scrollTarget.scrollTop(scrollPos.x); } if ($scrollTarget.scrollLeft() !== scrollPos.y) { $scrollTarget.scrollLeft(scrollPos.y); } }); } // Re-enable all click events (disabled as of performance reasons) // $('*').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, jQuery));