diff --git a/library/Icinga/Web/JavaScript.php b/library/Icinga/Web/JavaScript.php
index 6a378559f..8a0edf565 100644
--- a/library/Icinga/Web/JavaScript.php
+++ b/library/Icinga/Web/JavaScript.php
@@ -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(
diff --git a/modules/monitoring/application/views/scripts/list/comments.phtml b/modules/monitoring/application/views/scripts/list/comments.phtml
index 43b94be66..7c3863d17 100644
--- a/modules/monitoring/application/views/scripts/list/comments.phtml
+++ b/modules/monitoring/application/views/scripts/list/comments.phtml
@@ -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'
+ )) ?>
= $this->icon('host', $this->translate('Host')); ?>
diff --git a/modules/monitoring/application/views/scripts/list/downtimes.phtml b/modules/monitoring/application/views/scripts/list/downtimes.phtml
index e0836aa26..0c587a03f 100644
--- a/modules/monitoring/application/views/scripts/list/downtimes.phtml
+++ b/modules/monitoring/application/views/scripts/list/downtimes.phtml
@@ -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'
+ )) ?>
= $this->icon('comment', $this->translate('Comment')); ?> [= $this->escape($downtime->author_name) ?>] = $this->escape($downtime->comment) ?>
diff --git a/modules/monitoring/application/views/scripts/list/hosts.phtml b/modules/monitoring/application/views/scripts/list/hosts.phtml
index 15dbb5a6f..0d033eaad 100644
--- a/modules/monitoring/application/views/scripts/list/hosts.phtml
+++ b/modules/monitoring/application/views/scripts/list/hosts.phtml
@@ -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'
)
); ?>
host_unhandled_services) && $host->host_unhandled_services > 0): ?>
diff --git a/public/js/icinga/behavior/selection.js b/public/js/icinga/behavior/selection.js
new file mode 100644
index 000000000..a4f2a80b5
--- /dev/null
+++ b/public/js/icinga/behavior/selection.js
@@ -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);
diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js
index 775b99eff..406a7e298 100644
--- a/public/js/icinga/events.js
+++ b/public/js/icinga/events.js
@@ -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;
},
/**
diff --git a/public/js/icinga/ui.js b/public/js/icinga/ui.js
index cd5290f32..e8909559f 100644
--- a/public/js/icinga/ui.js
+++ b/public/js/icinga/ui.js
@@ -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
*
diff --git a/public/js/icinga/utils.js b/public/js/icinga/utils.js
index 4b04b3d44..b447454e3 100644
--- a/public/js/icinga/utils.js
+++ b/public/js/icinga/utils.js
@@ -293,6 +293,14 @@
return $element[0];
},
+ objectKeys: Object.keys || function (obj) {
+ var keys = [];
+ $.each(obj, function (key) {
+ keys.push(key);
+ });
+ return keys;
+ },
+
/**
* Cleanup
*/