Johannes Meyer 2a24f69fd8 loader.js: Remove legacy search request handling
Search requests are handled elsewhere in a more dynamic
way since .. some time. This part also used to be active
only in case Icinga Web is hosted at the root of the web
server, which many users don't do as otherwise we would
have gotten more reports in the past regarding the reload
animation. Or rather, about the login not working due to
the same reason.

It's gone for good now.

fixes #4834
2022-06-29 15:05:14 +02:00

1311 lines
51 KiB
JavaScript

/*! 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);
}
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 (! $form.is('[role="search"]') && $target.attr('id') === $form.closest('.container').attr('id')) {
$form.find('input:not(:disabled)').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');
}
}
}
var req = this.loadUrl(url, $target, data, method);
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
*/
loadUrl: function (url, $target, data, method, action, autorefresh, progressTimer) {
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';
}
// 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';
}
// 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,
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) {
var _this = event.data.self;
var $refreshTarget = $('#col2');
var refreshUrl;
var 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');
}
// loadUrl won't run this request, as due to the history handling it's already running.
// The important bit though is that it returns this (already running) request. This way
// it's possible to set `scripted = true` on the request. (`addToHistory = true` should
// already be the case, though it's added again just in case it's not already `true`..)
var req = _this.loadUrl(refreshUrl, $refreshTarget);
req.addToHistory = false;
req.scripted = true;
setTimeout(function () {
// TODO: Find a better solution than a hardcoded one
// This is still the *cheat* to get live results
// (in case there's a delay and a change is not instantly effective)
var req = _this.loadUrl(refreshUrl, $refreshTarget);
req.addToHistory = false;
req.scripted = true;
}, 1000);
$(window).off('popstate.__back__');
});
// Navigate back, no redirect desired
window.history.back();
return true;
} else if (redirect.match(/__CLOSE__/)) {
if (req.$redirectTarget.is('#col1')) {
icinga.logger.warn('Cannot close #col1');
return false;
}
// Close right column as requested
icinga.ui.layout1col();
// Refresh left column and produce a new history state for it
var $col1 = $('#col1');
var col1Url = icinga.history.getCol1State();
var refresh = this.loadUrl(col1Url, $col1);
refresh.scripted = true;
var _this = this;
setTimeout(function () {
// TODO: Find a better solution than a hardcoded one
// This is still the *cheat* to get live results
// (in case there's a delay and a change is not instantly effective)
var secondRefresh = _this.loadUrl(col1Url, $col1);
if (secondRefresh !== refresh) {
// Only change these properties if it's not still the first refresh
secondRefresh.addToHistory = false;
secondRefresh.scripted = true;
}
}, 1000);
return true;
}
var useHttp = req.getResponseHeader('X-Icinga-Redirect-Http');
if (useHttp === 'yes') {
window.location.replace(redirect);
return true;
}
this.redirectToUrl(redirect, req.$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;
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 {
if ($target.attr('id') === 'col2') { // TODO: multicol
if (($target.data('icingaUrl') || '').split('?')[0] === url.split('?')[0]) {
// Don't do anything in this case
} else if ($('#col1').data('icingaUrl').split('?')[0] === url.split('?')[0]) {
icinga.ui.layout1col();
$target = $('#col1');
delete(this.requests['col2']);
}
}
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;
if (target) {
if (target === 'ignore') {
return;
}
var $newTarget = this.identifyLinkTarget(target, req.$target);
if ($newTarget.length) {
// 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;
req.$redirectTarget = $newTarget;
if (target === 'layout') {
oldNotifications = $('#notifications li').detach();
this.icinga.ui.layout1col();
newBody = true;
}
}
}
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 (req.getResponseHeader('X-Icinga-Redirect')) {
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 referrer = req.referrer;
if (typeof referrer === 'undefined') {
referrer = req;
}
var autoSubmit = false;
var currentUrl = this.icinga.utils.parseUrl(req.$target.data('icingaUrl'));
if (referrer.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 = $(referrer.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;
}
// 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.getResponseHeader('X-Icinga-Reload-Window') === 'yes') {
window.location.reload();
return;
}
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);
}
}
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]);
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');
} else {
_this.icinga.logger.error('Invalid extra update', el);
return;
}
_this.loadUrl(url, $target).addToHistory = false;
});
}
if (this.processRedirectHeader(req)) {
return;
}
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);
icinga.history.pushCurrentState();
}
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
);
// Aborted requests should not be added to browser history
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 = $(
'<li class="' + c + '">' +
'<i class="icon fa fa-' + icon + '"></i>' +
this.icinga.utils.escape(message) + '</li>'
).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 = $scrollableContent.scrollTop();
scrollTarget = _this.icinga.utils.getCSSPath($scrollableContent);
} else {
scrollPos = $container.scrollTop();
}
} else {
scrollPos = 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);
$scrollTarget.scrollTop(scrollPos);
// Fallback for browsers without support for focus({preventScroll: true})
setTimeout(function () {
if ($scrollTarget.scrollTop() !== scrollPos) {
$scrollTarget.scrollTop(scrollPos);
}
}, 0);
}
// 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));