Matthias Jentsch aec59d9941 Use current filter to highlight active rows instead of storing active rows in JS
Clean up selection code and move it into separate behavior and parse filter query to fetch selectable rows.

refs #9054
refs #9346
2015-06-29 18:48:42 +02:00

689 lines
23 KiB
JavaScript

/*! Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
/**
* Icinga.UI
*
* Our user interface
*/
(function(Icinga, $) {
'use strict';
// Stores the icinga-data-url of the last focused table.
var focusedTableDataUrl = null;
// The stored selection data, useful for preserving selections over
// multiple reload-cycles.
var selectionData = null;
Icinga.UI = function (icinga) {
this.icinga = icinga;
this.currentLayout = 'default';
this.debug = false;
this.debugTimer = null;
this.timeCounterTimer = null;
};
Icinga.UI.prototype = {
initialize: function () {
$('html').removeClass('no-js').addClass('js');
this.enableTimeCounters();
this.triggerWindowResize();
this.fadeNotificationsAway();
},
fadeNotificationsAway: function()
{
var icinga = this.icinga;
$('#notifications li')
.not('.fading-out')
.not('.persist')
.addClass('fading-out')
.delay(7000)
.fadeOut('slow',
function() {
icinga.ui.fixControls();
$(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(
$(this).attr('href'),
{ id: new Date().getTime() }
)
).on('load', function() {
icinga.ui.fixControls();
$oldLink.remove();
});
$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;
},
moveToLeft: function () {
var col2 = this.cutContainer($('#col2'));
var kill = this.cutContainer($('#col1'));
this.pasteContainer($('#col1'), col2);
this.fixControls();
this.icinga.behaviors.navigation.setActiveByUrl($('#col1').data('icingaUrl'));
},
cutContainer: function ($col) {
var props = {
'elements': $('#' + $col.attr('id') + ' > *').detach(),
'data': {
'data-icinga-url': $col.data('icingaUrl'),
'data-icinga-refresh': $col.data('icingaRefresh'),
'data-last-update': $col.data('lastUpdate'),
'data-icinga-module': $col.data('icingaModule')
},
'class': $col.attr('class')
};
this.icinga.loader.stopPendingRequestsFor($col);
$col.removeData('icingaUrl');
$col.removeData('icingaRefresh');
$col.removeData('lastUpdate');
$col.removeData('icingaModule');
$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('icingaRefresh', backup['data']['data-icinga-refresh']);
$col.data('lastUpdate', backup['data']['data-last-update']);
$col.data('icingaModule', backup['data']['data-icinga-module']);
},
scrollContainerToAnchor: function ($container, anchorName) {
// TODO: Generic issue -> we probably should escape attribute value selectors!?
var $anchor = $("a[name='" + anchorName.replace(/'/, '\\\'') + "']", $container);
if ($anchor.length) {
$container.scrollTop(0);
$container.scrollTop($anchor.first().position().top);
this.icinga.logger.debug('Scrolling ', $container, ' to ', anchorName);
} else {
this.icinga.logger.info('Anchor "' + anchorName + '" not found in ', $container);
}
},
triggerWindowResize: function () {
this.onWindowResize({data: {self: this}});
},
/**
* Our window got resized, let's fix our UI
*/
onWindowResize: function (event) {
var self = event.data.self;
if (self.layoutHasBeenChanged()) {
self.icinga.logger.info(
'Layout change detected, switching to',
self.currentLayout
);
}
self.fixControls();
self.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();
}
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'));
this.disableCloseButtons();
},
closeContainer: function($c) {
$c.removeData('icingaUrl');
$c.removeData('icingaRefresh');
$c.removeData('lastUpdate');
$c.removeData('icingaModule');
this.icinga.loader.stopPendingRequestsFor($c);
$c.html('');
this.fixControls();
},
layout2col: function () {
if (! this.isOneColLayout()) { return; }
this.icinga.logger.debug('Switching to double col');
$('#layout').addClass('twocols');
this.fixControls();
this.enableCloseButtons();
},
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;
},
/**
* Read the data from a whole set of selections.
*
* @param $selections {jQuery} All selected rows in a jQuery-selector.
* @param keys {Array} An array containing all valid keys.
* @returns {Array} An array containing an object with the data for each selection.
*/
getSelectionSetData: function($selections, keys) {
var selections = [];
var icinga = this.icinga;
// read all current selections
$selections.each(function(ind, selected) {
selections.push(icinga.ui.getSelectionData($(selected), keys, icinga));
});
return selections;
},
/**
* Store a set of selection-data to preserve it accross page-reloads
*
* @param data {Array|String|Null} The selection-data be an Array of Objects,
* containing the selection data (when multiple rows where selected), a
* String containing a single url (when only a single row was selected) or
* Null when nothing was selected.
*/
storeSelectionData: function(data) {
selectionData = data;
},
/**
* Load the last stored set of selection-data
*
* @returns {Array|String|Null} May be an Array of Objects, containing the selection data
* (when multiple rows where selected), a String containing a single url
* (when only a single row was selected) or Null when nothing was selected.
*/
loadSelectionData: function() {
this.provideSelectionCount();
return selectionData;
},
/**
* Set the selections row count hint info
*/
provideSelectionCount: function() {
var $count = $('.selection-info-count');
if (typeof selectionData === 'undefined' || selectionData === null) {
$count.text(0);
return;
}
if (typeof selectionData === 'string') {
$count.text(1);
} else if (selectionData.length > 1) {
$count.text(selectionData.length);
} else {
$count.text(0);
}
},
/**
* Focus the given table by deselecting all selections on all other tables.
*
* Focusing a table is important for environments with multiple tables like
* the dashboard. It should only be possible to select rows at one table at a time,
* when a user selects a row on a table all rows that are not child of the given table
* will be removed from the selection.
*
* @param table {htmlElement} The table to focus.
*/
focusTable: function (table) {
$('table').filter(function(){ return this !== table; }).find('tr[href]').removeClass('active');
var n = $(table).closest('div.container').attr('data-icinga-url');
focusedTableDataUrl = n;
},
/**
* Return the URL of the last focused table container.
*
* @returns {String} The data-icinga-url of the last focused table, which should be unique in each site.
*/
getFocusedContainerDataUrl: function() {
return focusedTableDataUrl;
},
/**
* 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 () {
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 + ' => ' + 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.substr(0, partialTime.index) + minute.toString() + 'm '
+ second.toString() + 's' + el.innerHTML.substr(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;
}
}
el.innerHTML = el.innerHTML.substr(0, partialTime.index) + invert + minute.toString() + 'm '
+ second.toString() + 's' + el.innerHTML.substr(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;
},
/**
* Initialize all TriStateCheckboxes in the given html
*/
initializeTriStates: function ($html) {
var self = this;
$('div.tristate', $html).each(function(index, item) {
var $target = $(item);
// hide input boxess and remove text nodes
$target.find("input").hide();
$target.contents().filter(function() { return this.nodeType === 3; }).remove();
// has three states?
var triState = $target.find('input[value="unchanged"]').size() > 0 ? 1 : 0;
// fetch current value from radiobuttons
var value = $target.find('input:checked').first().val();
$target.append(
'<input class="tristate-dummy" ' +
' data-icinga-old="' + value + '" data-icinga-tristate="' + triState + '" type="checkbox" ' +
(value === '1' ? 'checked ' : ( value === 'unchanged' ? 'indeterminate="true" ' : ' ' )) +
'/> <b style="visibility: hidden;" class="tristate-changed"> (changed) </b>'
);
if (triState) {
// TODO: find a better way to activate indeterminate checkboxes after load.
$target.append(
'<script type="text/javascript"> ' +
' $(\'input.tristate-dummy[indeterminate="true"]\').each(function(i, el){ el.indeterminate = true; }); ' +
'</script>'
);
}
});
},
/**
* Set the value of the given TriStateCheckbox
*
* @param value {String} The value to set, can be '1', '0' and 'unchanged'
* @param $checkbox {jQuery} The checkbox
*/
setTriState: function(value, $checkbox)
{
switch (value) {
case ('1'):
$checkbox.prop('checked', true).prop('indeterminate', false);
break;
case ('0'):
$checkbox.prop('checked', false).prop('indeterminate', false);
break;
case ('unchanged'):
$checkbox.prop('checked', false).prop('indeterminate', true);
break;
}
},
initializeControls: function (parent) {
if ($(parent).closest('.dashboard').length || $('#layout').hasClass('fullscreen-layout')) {
return;
}
$('.controls', parent).each(function (idx, el) {
var $el = $(el);
if (! $el.next('.fake-controls').length) {
var newdiv = $('<div class="fake-controls"></div>');
newdiv.css({
height: $el.css('height')
});
$el.after(newdiv);
}
});
this.fixControls(parent);
},
disableCloseButtons: function () {
$('a.close-toggle').hide();
},
enableCloseButtons: function () {
$('a.close-toggle').show();
},
fixControls: function ($parent) {
var self = this;
if ($('#layout').hasClass('fullscreen-layout')) {
return;
}
if ('undefined' === typeof $parent) {
if (! $('#layout').hasClass('fullscreen-layout')) {
$('#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;
}
if ($parent.closest('.dashboard').length) {
return;
}
// Enable this only in case you want to track down UI problems
// self.icinga.logger.debug('Fixing controls for ', $parent);
$('.controls', $parent).each(function (idx, el) {
var $el = $(el);
if ($el.closest('.dashboard').length) {
return;
}
var $fake = $el.next('.fake-controls');
var y = $parent.scrollTop();
$el.css({
position : 'fixed',
top : $parent.offset().top,
// Firefox gives 1px too much depending on total width.
// TODO: find a better solution for -1
width : ($fake.width() - 1) + 'px'
});
$fake.css({
height : $el.css('height'),
display : 'block'
});
});
},
toggleFullscreen: function () {
$('#layout').toggleClass('fullscreen-layout');
this.fixControls();
},
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));