From aec59d9941c5f216f03edbcfbc1fd33e97c635b0 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Mon, 29 Jun 2015 18:45:14 +0200 Subject: [PATCH] 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 --- library/Icinga/Web/JavaScript.php | 3 +- .../views/scripts/list/comments.phtml | 13 +- .../views/scripts/list/downtimes.phtml | 13 +- .../views/scripts/list/hosts.phtml | 3 +- public/js/icinga/behavior/selection.js | 341 ++++++++++++++++++ public/js/icinga/events.js | 62 ---- public/js/icinga/ui.js | 133 ------- public/js/icinga/utils.js | 8 + 8 files changed, 369 insertions(+), 207 deletions(-) create mode 100644 public/js/icinga/behavior/selection.js 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 */