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
This commit is contained in:
Matthias Jentsch 2015-06-29 18:45:14 +02:00
parent 2e375dd57c
commit aec59d9941
8 changed files with 369 additions and 207 deletions

View File

@ -26,7 +26,8 @@ class JavaScript
'js/icinga/behavior/sparkline.js',
'js/icinga/behavior/tristate.js',
'js/icinga/behavior/navigation.js',
'js/icinga/behavior/form.js'
'js/icinga/behavior/form.js',
'js/icinga/behavior/selection.js'
);
protected static $vendorFiles = array(

View File

@ -43,11 +43,14 @@ if (count($comments) === 0) {
),
'monitoring/comment/show',
array('comment_id' => $comment->id),
array('title' => sprintf(
$this->translate('Show detailed information for comment on %s for %s'),
$comment->service_display_name,
$comment->host_display_name
))) ?>
array(
'title' => sprintf(
$this->translate('Show detailed information for comment on %s for %s'),
$comment->service_display_name,
$comment->host_display_name
),
'class' => 'rowaction'
)) ?>
<?php else: ?>
<?= $this->icon('host', $this->translate('Host')); ?>

View File

@ -57,11 +57,14 @@ if (count($downtimes) === 0) {
sprintf('%s: %s', $downtime->host_display_name, $downtime->service_display_name),
'monitoring/downtime/show',
array('downtime_id' => $downtime->id),
array('title' => sprintf(
$this->translate('Show detailed information for downtime on %s for %s'),
$downtime->service_display_name,
$downtime->host_display_name
))) ?>
array(
'title' => sprintf(
$this->translate('Show detailed information for downtime on %s for %s'),
$downtime->service_display_name,
$downtime->host_display_name
),
'class' => 'rowaction'
)) ?>
<br>
<?= $this->icon('comment', $this->translate('Comment')); ?> [<?= $this->escape($downtime->author_name) ?>] <?= $this->escape($downtime->comment) ?>
<br>

View File

@ -60,7 +60,8 @@ if (count($hosts) === 0) {
$hostLink,
null,
array(
'title' => sprintf($this->translate('Show detailed information for host %s'), $host->host_display_name)
'title' => sprintf($this->translate('Show detailed information for host %s'), $host->host_display_name),
'class' => 'rowaction'
)
); ?>
<?php if (isset($host->host_unhandled_services) && $host->host_unhandled_services > 0): ?>

View File

@ -0,0 +1,341 @@
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
/**
* Icinga.Behavior.Selection
*
* A multi selection that distincts between the rows using the row action URL filter
*/
(function(Icinga, $) {
"use strict";
var stripBrackets = function (str) {
return str.replace(/^[^\(]*\(/, '').replace(/\)[^\)]*$/, '');
};
var parseSelectionQuery = function(filterString) {
var selections = [];
$.each(stripBrackets(filterString).split('|'), function(i, row) {
var tuple = {};
$.each(stripBrackets(row).split('&'), function(i, keyValue) {
var s = keyValue.split('=');
tuple[s[0]] = decodeURIComponent(s[1]);
});
selections.push(tuple);
});
return selections;
};
var toQueryPart = function(id) {
var queries = [];
$.each(id, function(key, value) {
queries.push(key + '=' + encodeURIComponent(value));
});
return queries.join('&');
};
var Table = function(table, icinga) {
this.$el = $(table);
this.icinga = icinga;
if (this.hasMultiselection()) {
if (! this.getMultiselectionKeys().length) {
icinga.logger.error('multiselect table has no data-icinga-multiselect-data');
}
if (! this.getMultiselectionUrl()) {
icinga.logger.error('multiselect table has no data-icinga-multiselect-url');
}
}
};
Table.prototype = {
rows: function() {
return this.$el.find('tr');
},
rowActions: function() {
return this.$el.find('tr a.rowaction');
},
selections: function() {
return this.$el.find('tr.active');
},
hasMultiselection: function() {
return this.$el.hasClass('multiselect');
},
getMultiselectionKeys: function() {
var data = this.$el.data('icinga-multiselect-data');
return (data && data.split(',')) || [];
},
getMultiselectionUrl: function() {
return this.$el.data('icinga-multiselect-url');
},
/**
* @param row {jQuery} The row
*
* @returns {Object} An object containing all selection data in
* this row as key-value pairs
*/
getRowData: function(row) {
var params = this.icinga.utils.parseUrl(row.attr('href')).params;
var tuple = {};
var keys = this.getMultiselectionKeys();
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (params[key]) {
tuple[key] = params[key];
}
}
return tuple;
},
/**
* If this table is currently used to control the selection
*
* @returns {Boolean}
*/
active: function() {
var loc = this.icinga.utils.parseUrl(window.location.href);
if (!loc.hash) {
return false;
}
if (this.getMultiselectionUrl()) {
var multiUrl = this.getMultiselectionUrl();
return multiUrl === loc.hash.split('?')[0].substr(1);
} else {
return this.rowActions().filter('[href="' + loc.hash.substr(1) + '"]').length > 1;
}
},
loading: function() {
},
clear: function() {
this.selections().removeClass('active');
},
select: function(filter) {
if (filter instanceof jQuery) {
filter.addClass('active');
return;
}
var self = this;
var url = this.getMultiselectionUrl();
this.rowActions()
.filter(
function (i, el) {
var params = self.getRowData($(el));
if (self.icinga.utils.objectKeys(params).length !== self.icinga.utils.objectKeys(filter).length) {
return false;
}
var equal = true;
$.each(params, function(key, value) {
if (filter[key] !== value) {
equal = false;
}
});
return equal;
}
)
.closest('tr')
.addClass('active');
},
toggle: function(filter) {
if (filter instanceof jQuery) {
filter.toggleClass('active');
return;
}
this.icinga.logger.error('toggling by filter not implemented');
},
/**
* Add a new selection range to the closest table, using the selected row as
* range target.
*
* @param row {jQuery} The target of the selected range.
*
* @returns {boolean} If the selection was changed.
*/
range: function(row) {
var from, to;
var selected = row.first().get(0);
this.rows().each(function(i, el) {
if ($(el).hasClass('active') || el === selected) {
if (!from) {
from = el;
}
to = el;
}
});
var inRange = false;
this.rows().each(function(i, el) {
if (el === from) {
inRange = true;
}
if (inRange) {
$(el).addClass('active');
}
if (el === to) {
inRange = false;
}
});
return false;
},
selectUrl: function(url) {
this.rows().filter('[href="' + url + '"]').addClass('active');
},
toQuery: function() {
var self = this;
var selections = this.selections();
var queries = [];
if (selections.length === 1) {
return $(selections[0]).attr('href');
} else if (selections.length > 1 && self.hasMultiselection()) {
selections.each(function (i, el) {
var parts = [];
$.each(self.getRowData($(el)), function(key, value) {
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
});
queries.push('(' + parts.join('&') + ')');
});
return self.getMultiselectionUrl() + '?(' + queries.join('|') + ')';
} else {
return '';
}
}
};
Icinga.Behaviors = Icinga.Behaviors || {};
var Selection = function (icinga) {
Icinga.EventListener.call(this, icinga);
/**
* The hash that is currently being loaded
*
* @var String
*/
this.loadingHash = null;
/**
* If currently loading
*
* @var Boolean
*/
this.loading = false;
this.on('rendered', this.onRendered, this);
this.on('click', 'table.action tr[href]', this.onRowClicked, this);
};
Selection.prototype = new Icinga.EventListener();
Selection.prototype.toogleTableRowSelection = function ($tr) {
// multi selection
if ($tr.hasClass('active')) {
$tr.removeClass('active');
} else {
$tr.addClass('active');
}
return true;
};
Selection.prototype.tables = function(context) {
if (context) {
return $(context).find('table.action');
}
return $('table.action');
};
Selection.prototype.onRowClicked = function(event) {
var self = event.data.self;
var $tr = $(event.target).closest('tr');
var table = new Table($tr.closest('table.action')[0], self.icinga);
// allow form actions in table rows to pass through
if ($(event.target).closest('form').length) {
return;
}
event.stopPropagation();
event.preventDefault();
// update selection
if (table.hasMultiselection()) {
if (event.ctrlKey || event.metaKey) {
// add to selection
table.toggle($tr);
} else if (event.shiftKey) {
// range selection
table.range($tr);
} else {
table.clear();
table.select($tr);
}
} else {
table.clear();
table.select($tr);
}
// update history
var url = self.icinga.utils.parseUrl(window.location.href.split('#')[0]);
if (table.selections().length > 0) {
var query = table.toQuery();
self.icinga.loader.loadUrl(query, self.icinga.events.getLinkTargetFor($tr));
self.icinga.history.pushUrl(url.path + url.query + '#!' + query);
} else {
if (self.icinga.events.getLinkTargetFor($tr).attr('id') === 'col2') {
icinga.ui.layout1col();
}
self.icinga.history.pushUrl(url.path + url.query);
}
// clear all inactive tables
this.tables().each(function () {
var t = new Table(this, self.icinga)
if (! t.active()) {
t.clear();
}
});
// update selection info
$('.selection-info-count').text(table.selections().size());
return false;
}
Selection.prototype.onRendered = function(evt) {
var container = evt.target;
var self = evt.data.self;
if (self.tables(container).length < 1) {
return;
}
// draw all selections
self.tables().each(function(i, el) {
var table = new Table(el, self.icinga);
table.clear();
if (! table.active()) {
return;
}
var hash = self.icinga.utils.parseUrl(window.location.href).hash;
if (table.hasMultiselection()) {
$.each(parseSelectionQuery(hash), function(i, selection) {
table.select(selection);
});
} else {
table.selectUrl(hash.substr(1));
}
$('.selection-info-count').text(table.selections().size());
});
};
Icinga.Behaviors.Selection = Selection;
}) (Icinga, jQuery);

View File

@ -308,68 +308,6 @@
* Handle table selection.
*/
rowSelected: function(event) {
var self = event.data.self;
var icinga = self.icinga;
var $tr = $(this);
var $table = $tr.closest('table.multiselect');
var data = self.icinga.ui.getSelectionKeys($table);
var url = $table.data('icinga-multiselect-url');
if ($(event.target).closest('form').length) {
// allow form actions in table rows to pass through
return;
}
event.stopPropagation();
event.preventDefault();
if (!data) {
icinga.logger.error('multiselect table has no data-icinga-multiselect-data');
return;
}
if (!url) {
icinga.logger.error('multiselect table has no data-icinga-multiselect-url');
return;
}
// update selection
if (event.ctrlKey || event.metaKey) {
icinga.ui.toogleTableRowSelection($tr);
// multi selection
} else if (event.shiftKey) {
// range selection
icinga.ui.addTableRowRangeSelection($tr);
} else {
// single selection
icinga.ui.setTableRowSelection($tr);
}
// focus only the current table.
icinga.ui.focusTable($table[0]);
var $target = self.getLinkTargetFor($tr);
var $trs = $table.find('tr[href].active');
if ($trs.length > 1) {
var selectionData = icinga.ui.getSelectionSetData($trs, data);
var query = icinga.ui.selectionDataToQuery(selectionData);
icinga.loader.loadUrl(url + '?' + query, $target);
icinga.ui.storeSelectionData(selectionData);
icinga.ui.provideSelectionCount();
} else if ($trs.length === 1) {
// display a single row
$tr = $trs.first();
icinga.loader.loadUrl($tr.attr('href'), $target);
icinga.ui.storeSelectionData($tr.attr('href'));
icinga.ui.provideSelectionCount();
} else {
// display nothing
if ($target.attr('id') === 'col2') {
icinga.ui.layout1col();
}
icinga.ui.storeSelectionData(null);
icinga.ui.provideSelectionCount();
}
return false;
},
/**

View File

@ -298,73 +298,6 @@
return $('#main > .container').length;
},
/**
* Add the given table-row to the selection of the closest
* table and deselect all other rows of the closest table.
*
* @param $tr {jQuery} The selected table row.
* @returns {boolean} If the selection was changed.
*/
setTableRowSelection: function ($tr) {
var $table = $tr.closest('table.multiselect');
$table.find('tr[href].active').removeClass('active');
$tr.addClass('active');
return true;
},
/**
* Toggle the given table row to "on" when not selected, or to "off" when
* currently selected.
*
* @param $tr {jQuery} The table row.
* @returns {boolean} If the selection was changed.
*/
toogleTableRowSelection: function ($tr) {
// multi selection
if ($tr.hasClass('active')) {
$tr.removeClass('active');
} else {
$tr.addClass('active');
}
return true;
},
/**
* Add a new selection range to the closest table, using the selected row as
* range target.
*
* @param $tr {jQuery} The target of the selected range.
* @returns {boolean} If the selection was changed.
*/
addTableRowRangeSelection: function ($tr) {
var $table = $tr.closest('table.multiselect');
var $rows = $table.find('tr[href]'),
from, to;
var selected = $tr.first().get(0);
$rows.each(function(i, el) {
if ($(el).hasClass('active') || el === selected) {
if (!from) {
from = el;
}
to = el;
}
});
var inRange = false;
$rows.each(function(i, el){
if (el === from) {
inRange = true;
}
if (inRange) {
$(el).addClass('active');
}
if (el === to) {
inRange = false;
}
});
return false;
},
/**
* Read the data from a whole set of selections.
*
@ -383,72 +316,6 @@
return selections;
},
getSelectionKeys: function($selection)
{
var d = $selection.data('icinga-multiselect-data') && $selection.data('icinga-multiselect-data').split(',');
return d || [];
},
/**
* Read the data from the given selected object.
*
* @param $selection {jQuery} The selected object.
* @param keys {Array} An array containing all valid keys.
* @param icinga {Icinga} The main icinga object.
* @returns {Object} An object containing all key-value pairs associated with this selection.
*/
getSelectionData: function($selection, keys, icinga)
{
var url = $selection.attr('href');
var params = this.icinga.utils.parseUrl(url).params;
var tuple = {};
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (params[key]) {
tuple[key] = params[key];
}
}
return tuple;
},
/**
* Convert a set of selection data to a single query.
*
* @param selectionData {Array} The selection data generated from getSelectionData
* @returns {String} The formatted and uri-encoded query-string.
*/
selectionDataToQuery: function (selectionData) {
var queries = [];
// create new url
if (selectionData.length < 2) {
this.icinga.logger.error('Something went wrong, we should never multiselect just one row');
} else {
$.each(selectionData, function(i, el){
var parts = []
$.each(el, function(key, value) {
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
});
queries.push('(' + parts.join('&') + ')');
});
}
return '(' + queries.join('|') + ')';
},
/**
* Create a single query-argument (not compatible to selectionDataToQuery)
*
* @param data
* @returns {string}
*/
selectionDataToQueryComp: function(data) {
var queries = [];
$.each(data, function(key, value){
queries.push(key + '=' + encodeURIComponent(value));
});
return queries.join('&');
},
/**
* Store a set of selection-data to preserve it accross page-reloads
*

View File

@ -293,6 +293,14 @@
return $element[0];
},
objectKeys: Object.keys || function (obj) {
var keys = [];
$.each(obj, function (key) {
keys.push(key);
});
return keys;
},
/**
* Cleanup
*/