diff --git a/library/Icinga/Web/JavaScript.php b/library/Icinga/Web/JavaScript.php index 6a378559fc..8a0edf565c 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 43b94be66f..7c3863d17d 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' + )) ?> 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 e0836aa26d..0c587a03fe 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' + )) ?>
icon('comment', $this->translate('Comment')); ?> [escape($downtime->author_name) ?>] escape($downtime->comment) ?>
diff --git a/modules/monitoring/application/views/scripts/list/hosts.phtml b/modules/monitoring/application/views/scripts/list/hosts.phtml index 15dbb5a6ff..0d033eaad5 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 0000000000..a4f2a80b54 --- /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 775b99effe..406a7e2981 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 cd5290f32c..e8909559f8 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 4b04b3d44f..b447454e37 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 */