From aec59d9941c5f216f03edbcfbc1fd33e97c635b0 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Mon, 29 Jun 2015 18:45:14 +0200 Subject: [PATCH 1/6] 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 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' + )) ?> 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' + )) ?>
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 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 */ From 96677fb6c7faeb4c3792774e112cbff833968c7f Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Mon, 29 Jun 2015 18:51:32 +0200 Subject: [PATCH 2/6] Fix selection when clicking on regular links refs #9054 refs #9346 --- public/js/icinga/events.js | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js index 406a7e298..677c91af1 100644 --- a/public/js/icinga/events.js +++ b/public/js/icinga/events.js @@ -117,9 +117,6 @@ $(document).on('click', 'a', { self: this }, this.linkClicked); $(document).on('click', 'tr[href]', { self: this }, this.linkClicked); - // Select a table row - $(document).on('click', 'table.multiselect tr[href]', { self: this }, this.rowSelected); - // We catch all form submit events $(document).on('submit', 'form', { self: this }, this.submitForm); @@ -304,12 +301,6 @@ return false; }, - /** - * Handle table selection. - */ - rowSelected: function(event) { - }, - /** * Handle anchor, i.e. focus the element which is referenced by the anchor * @@ -345,16 +336,16 @@ // Special checks for link clicks in multiselect rows if (! $a.is('tr[href]') && $a.closest('tr[href]').length > 0 && $a.closest('table.multiselect').length > 0) { - // Forward clicks to ANY link with special key pressed to rowSelected + // ignoray clicks to ANY link with special key pressed if (event.ctrlKey || event.metaKey || event.shiftKey) { - return self.rowSelected.call($a.closest('tr[href]'), event); + return true; } - // Forward inner links matching the row URL to rowSelected + // ignore inner links matching the row URL if ($a.attr('href') === $a.closest('tr[href]').attr('href')) { - return self.rowSelected.call($a.closest('tr[href]'), event); + return true; } } @@ -513,8 +504,6 @@ $(window).off('beforeunload', this.onUnload); $(document).off('scroll', '.container', this.onContainerScroll); $(document).off('click', 'a', this.linkClicked); - $(document).off('click', 'table.action tr[href]', this.rowSelected); - $(document).off('click', 'table.action tr a', this.rowSelected); $(document).off('submit', 'form', this.submitForm); $(document).off('change', 'form select.autosubmit', this.submitForm); $(document).off('change', 'form input.autosubmit', this.submitForm); From e0d588cda3a9f1bbd79e3473483f3033059f2385 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Tue, 30 Jun 2015 11:53:38 +0200 Subject: [PATCH 3/6] Fix look and feel of row selection Always refresh all tables on render and click events, to display or remove active rows without any delay. refs #9054 refs #9346 --- public/js/icinga/behavior/selection.js | 98 ++++++++++---------------- 1 file changed, 36 insertions(+), 62 deletions(-) diff --git a/public/js/icinga/behavior/selection.js b/public/js/icinga/behavior/selection.js index a4f2a80b5..7efc60791 100644 --- a/public/js/icinga/behavior/selection.js +++ b/public/js/icinga/behavior/selection.js @@ -93,28 +93,6 @@ 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'); }, @@ -210,6 +188,25 @@ } else { return ''; } + }, + + refresh: function() { + this.clear(); + var hash = this.icinga.utils.parseUrl(window.location.href).hash; + if (this.hasMultiselection()) { + var query = parseSelectionQuery(hash); + if (query.length > 1 && this.getMultiselectionUrl() === this.icinga.utils.parseUrl(hash.substr(1)).path) { + // select all rows with matching filters + var self = this; + $.each(query, function(i, selection) { + self.select(selection); + }); + } + if (query.length > 1) { + return; + } + } + this.selectUrl(hash.substr(1)); } }; @@ -237,16 +234,6 @@ }; 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'); @@ -275,6 +262,7 @@ // range selection table.range($tr); } else { + // single selection table.clear(); table.select($tr); } @@ -285,25 +273,24 @@ // update history var url = self.icinga.utils.parseUrl(window.location.href.split('#')[0]); - if (table.selections().length > 0) { + var count = table.selections().length; + var state = url.path + url.query; + if (count > 0) { var query = table.toQuery(); - self.icinga.loader.loadUrl(query, self.icinga.events.getLinkTargetFor($tr)); - self.icinga.history.pushUrl(url.path + url.query + '#!' + query); + self.icinga.loader.loadUrl(query, self.icinga.events.getLinkTargetFor($tr)); + state += '#!' + query; } else { if (self.icinga.events.getLinkTargetFor($tr).attr('id') === 'col2') { - icinga.ui.layout1col(); + self.icinga.ui.layout1col(); } - self.icinga.history.pushUrl(url.path + url.query); } + self.icinga.history.pushUrl(state); - // clear all inactive tables - this.tables().each(function () { - var t = new Table(this, self.icinga) - if (! t.active()) { - t.clear(); - } + // re draw all table selections + self.tables().each(function () { + new Table(this, self.icinga).refresh(); }); - + // update selection info $('.selection-info-count').text(table.selections().size()); return false; @@ -312,28 +299,15 @@ 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()); + new Table(el, self.icinga).refresh(); }); + + // update displayed selection count + var table = new Table(self.tables(container).first()); + $(container).find('.selection-info-count').text(table.selections().size()); }; Icinga.Behaviors.Selection = Selection; From 975a834bd45b476eed1047a392e452e1e0780f84 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Tue, 30 Jun 2015 12:28:41 +0200 Subject: [PATCH 4/6] Remove unused code and clean up --- public/js/icinga/events.js | 2 - public/js/icinga/loader.js | 36 -------------- public/js/icinga/ui.js | 97 +------------------------------------- 3 files changed, 1 insertion(+), 134 deletions(-) diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js index 677c91af1..e4ad54183 100644 --- a/public/js/icinga/events.js +++ b/public/js/icinga/events.js @@ -407,8 +407,6 @@ icinga.ui.layout1col(); } $('table tr[href].active').removeClass('active'); - icinga.ui.storeSelectionData(null); - icinga.ui.loadSelectionData(); icinga.history.pushCurrentState(); } } diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js index 082b41e87..5b345a308 100644 --- a/public/js/icinga/loader.js +++ b/public/js/icinga/loader.js @@ -504,42 +504,6 @@ this.icinga.ui.fixDebugVisibility().triggerWindowResize(); } self.cacheLoadedIcons(req.$target); - - if (active) { - var focusedUrl = this.icinga.ui.getFocusedContainerDataUrl(); - var oldSelectionData = this.icinga.ui.loadSelectionData(); - if (typeof oldSelectionData === 'string') { - $('[href="' + oldSelectionData + '"]', req.$target).addClass('active'); - - } else if (oldSelectionData !== null) { - var $container; - if (!focusedUrl) { - $container = $('document').first(); - } else { - $container = $('.container[data-icinga-url="' + focusedUrl + '"]'); - } - - var $table = $container.find('table.action').first(); - var keys = self.icinga.ui.getSelectionKeys($table); - - // build map of selected queries - var oldSelectionQueries = {}; - $.each(oldSelectionData, function(i, query){ - oldSelectionQueries[self.icinga.ui.selectionDataToQueryComp(query)] = true; - }); - - // set all new selections to active - $table.find('tr[href]').filter(function(){ - var $tr = $(this); - var rowData = self.icinga.ui.getSelectionData($tr, keys, self.icinga); - var newSelectionQuery = self.icinga.ui.selectionDataToQueryComp(rowData); - if (oldSelectionQueries[newSelectionQuery]) { - return true; - } - return false; - }).addClass('active'); - } - } }, /** diff --git a/public/js/icinga/ui.js b/public/js/icinga/ui.js index e8909559f..d2f9ab138 100644 --- a/public/js/icinga/ui.js +++ b/public/js/icinga/ui.js @@ -9,13 +9,6 @@ '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; @@ -38,8 +31,7 @@ this.fadeNotificationsAway(); }, - fadeNotificationsAway: function() - { + fadeNotificationsAway: function() { var icinga = this.icinga; $('#notifications li') .not('.fading-out') @@ -298,93 +290,6 @@ 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 * From 6a7e3fe4404cf02b5f6ea7c5f1fdce791b55a2b9 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Tue, 30 Jun 2015 13:03:41 +0200 Subject: [PATCH 5/6] Document multi selection and use clearer names refs #9054 --- library/Icinga/Web/JavaScript.php | 2 +- .../behavior/{selection.js => actiontable.js} | 149 ++++++++++++++---- 2 files changed, 116 insertions(+), 35 deletions(-) rename public/js/icinga/behavior/{selection.js => actiontable.js} (69%) diff --git a/library/Icinga/Web/JavaScript.php b/library/Icinga/Web/JavaScript.php index 8a0edf565..ed39c1b39 100644 --- a/library/Icinga/Web/JavaScript.php +++ b/library/Icinga/Web/JavaScript.php @@ -27,7 +27,7 @@ class JavaScript 'js/icinga/behavior/tristate.js', 'js/icinga/behavior/navigation.js', 'js/icinga/behavior/form.js', - 'js/icinga/behavior/selection.js' + 'js/icinga/behavior/actiontable.js' ); protected static $vendorFiles = array( diff --git a/public/js/icinga/behavior/selection.js b/public/js/icinga/behavior/actiontable.js similarity index 69% rename from public/js/icinga/behavior/selection.js rename to public/js/icinga/behavior/actiontable.js index 7efc60791..d3df54614 100644 --- a/public/js/icinga/behavior/selection.js +++ b/public/js/icinga/behavior/actiontable.js @@ -1,18 +1,31 @@ /* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ /** - * Icinga.Behavior.Selection + * Icinga.Behavior.ActionTable * - * A multi selection that distincts between the rows using the row action URL filter + * A multi selection that distincts between the table rows using the row action URL filter */ (function(Icinga, $) { "use strict"; + /** + * Remove one leading and trailing bracket and all text outside those brackets + * + * @param str {String} + * @returns {string} + */ var stripBrackets = function (str) { return str.replace(/^[^\(]*\(/, '').replace(/\)[^\)]*$/, ''); }; + /** + * Parse the filter query contained in the given url filter string + * + * @param filterString {String} + * + * @returns {Array} An object containing each row filter + */ var parseSelectionQuery = function(filterString) { var selections = []; $.each(stripBrackets(filterString).split('|'), function(i, row) { @@ -26,15 +39,15 @@ 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) { + /** + * Handle the selection of an action table + * + * @param table {HTMLElement} The table + * @param {Icinga} + * + * @constructor + */ + var Selection = function(table, icinga) { this.$el = $(table); this.icinga = icinga; @@ -48,37 +61,71 @@ } }; - Table.prototype = { + Selection.prototype = { + + /** + * Return all rows as jQuery selector + * + * @returns {jQuery} + */ rows: function() { return this.$el.find('tr'); }, + /** + * Return all row action links as jQuery selector + * + * @returns {jQuery} + */ rowActions: function() { return this.$el.find('tr a.rowaction'); }, + /** + * Return all selected rows as jQuery selector + * + * @returns {jQuery} + */ selections: function() { return this.$el.find('tr.active'); }, + /** + * If this selection allows selecting multiple rows + * + * @returns {Boolean} + */ hasMultiselection: function() { return this.$el.hasClass('multiselect'); }, + /** + * Return all filter keys that are significant when applying the selection + * + * @returns {Array} + */ getMultiselectionKeys: function() { var data = this.$el.data('icinga-multiselect-data'); return (data && data.split(',')) || []; }, + /** + * Return the target URL that is used when multi selecting rows + * + * This URL may differ from the url that is used when applying single rows + * + * @returns {String} + */ getMultiselectionUrl: function() { return this.$el.data('icinga-multiselect-url'); }, /** - * @param row {jQuery} The row + * Read all filter data from the given row * - * @returns {Object} An object containing all selection data in - * this row as key-value pairs + * @param row {jQuery} The row element + * + * @returns {Object} An object containing all filter data in this row as key-value pairs */ getRowData: function(row) { var params = this.icinga.utils.parseUrl(row.attr('href')).params; @@ -93,10 +140,18 @@ return tuple; }, + /** + * Deselect all selected rows + */ clear: function() { this.selections().removeClass('active'); }, + /** + * Add all rows that match the given filter to the selection + * + * @param filter {jQuery|Object} Either an object containing filter variables or the actual row to select + */ select: function(filter) { if (filter instanceof jQuery) { filter.addClass('active'); @@ -124,21 +179,22 @@ .addClass('active'); }, - toggle: function(filter) { - if (filter instanceof jQuery) { - filter.toggleClass('active'); - return; - } - this.icinga.logger.error('toggling by filter not implemented'); + /** + * Toggle the selection of the row between on and off + * + * @param row {jQuery} The row to toggle + */ + toggle: function(row) { + row.toggleClass('active'); }, /** * 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. + * @param row {jQuery} The target of the selected range. * - * @returns {boolean} If the selection was changed. + * @returns {boolean} If the selection was changed. */ range: function(row) { var from, to; @@ -166,10 +222,20 @@ return false; }, + /** + * Select rows that target the given url + * + * @param url {String} The target url + */ selectUrl: function(url) { this.rows().filter('[href="' + url + '"]').addClass('active'); }, + /** + * Convert all currently selected rows into an url query string + * + * @returns {String} The filter string + */ toQuery: function() { var self = this; var selections = this.selections(); @@ -190,6 +256,9 @@ } }, + /** + * Refresh the displayed active columns using the current page location + */ refresh: function() { this.clear(); var hash = this.icinga.utils.parseUrl(window.location.href).hash; @@ -212,7 +281,7 @@ Icinga.Behaviors = Icinga.Behaviors || {}; - var Selection = function (icinga) { + var ActionTable = function (icinga) { Icinga.EventListener.call(this, icinga); /** @@ -232,19 +301,28 @@ this.on('rendered', this.onRendered, this); this.on('click', 'table.action tr[href]', this.onRowClicked, this); }; - Selection.prototype = new Icinga.EventListener(); + ActionTable.prototype = new Icinga.EventListener(); - Selection.prototype.tables = function(context) { + /** + * Return all active tables in this table, or in the context as jQuery selector + * + * @param context {HTMLElement} + * @returns {jQuery} + */ + ActionTable.prototype.tables = function(context) { if (context) { return $(context).find('table.action'); } return $('table.action'); }; - Selection.prototype.onRowClicked = function(event) { + /** + * Handle clicks on table rows and update selection and history + */ + ActionTable.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); + var table = new Selection($tr.closest('table.action')[0], self.icinga); // allow form actions in table rows to pass through if ($(event.target).closest('form').length) { @@ -288,28 +366,31 @@ // re draw all table selections self.tables().each(function () { - new Table(this, self.icinga).refresh(); + new Selection(this, self.icinga).refresh(); }); // update selection info $('.selection-info-count').text(table.selections().size()); return false; - } + }; - Selection.prototype.onRendered = function(evt) { + /** + * Ensure that + */ + ActionTable.prototype.onRendered = function(evt) { var container = evt.target; var self = evt.data.self; // draw all selections self.tables().each(function(i, el) { - new Table(el, self.icinga).refresh(); + new Selection(el, self.icinga).refresh(); }); // update displayed selection count - var table = new Table(self.tables(container).first()); + var table = new Selection(self.tables(container).first()); $(container).find('.selection-info-count').text(table.selections().size()); }; - Icinga.Behaviors.Selection = Selection; + Icinga.Behaviors.ActionTable = ActionTable; }) (Icinga, jQuery); From 0fcb054be44a752b106c234c55eb80ad0e7a0094 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Tue, 30 Jun 2015 14:07:12 +0200 Subject: [PATCH 6/6] Fix service selection in event history Add rowaction class to all service rows, to make services selectable again. --- .../application/views/helpers/Link.php | 16 ++++++++++------ .../views/scripts/list/eventhistory.phtml | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/modules/monitoring/application/views/helpers/Link.php b/modules/monitoring/application/views/helpers/Link.php index 35c5a4cbc..e37375d14 100644 --- a/modules/monitoring/application/views/helpers/Link.php +++ b/modules/monitoring/application/views/helpers/Link.php @@ -45,10 +45,11 @@ class Zend_View_Helper_Link extends Zend_View_Helper_Abstract * @param string $serviceLinkText Text for the service link, e.g. the service's display name * @param string $host Hostname * @param string $hostLinkText Text for the host link, e.g. the host's display name + * @param string $class An optional class to use for this link * * @return string */ - public function service($service, $serviceLinkText, $host, $hostLinkText) + public function service($service, $serviceLinkText, $host, $hostLinkText, $class = null) { return sprintf( '%s: %s', @@ -57,11 +58,14 @@ class Zend_View_Helper_Link extends Zend_View_Helper_Abstract $serviceLinkText, 'monitoring/service/show', array('host' => $host, 'service' => $service), - array('title' => sprintf( - $this->view->translate('Show detailed information for service %s on host %s'), - $service, - $host - )) + array( + 'title' => sprintf( + $this->view->translate('Show detailed information for service %s on host %s'), + $service, + $host + ), + 'class' => $class + ) ) ); } diff --git a/modules/monitoring/application/views/scripts/list/eventhistory.phtml b/modules/monitoring/application/views/scripts/list/eventhistory.phtml index 538702a64..ab9cb2331 100644 --- a/modules/monitoring/application/views/scripts/list/eventhistory.phtml +++ b/modules/monitoring/application/views/scripts/list/eventhistory.phtml @@ -82,7 +82,7 @@ if (count($history) === 0) { link()->service( - $event->service_description, $event->service_display_name, $event->host_name, $event->host_display_name + $event->service_description, $event->service_display_name, $event->host_name, $event->host_display_name, 'rowaction' ) ?> link()->host($event->host_name, $event->host_display_name) ?>