Add abillity for multi and range-selection to events.js

Add the abillity to select multiple rows, with a multi-selection using the
CTRL-key or a range-selection using the shift-key. Also fix several issues in
the Multi-Controller of the Backend.

refs #5765
This commit is contained in:
Matthias Jentsch 2014-04-02 18:23:01 +02:00
parent 6973b04211
commit 6d303f1c42
5 changed files with 240 additions and 51 deletions

View File

@ -31,12 +31,12 @@ use \Icinga\Web\Form;
use \Icinga\Web\Controller\ActionController; use \Icinga\Web\Controller\ActionController;
use \Icinga\Web\Widget\Tabextension\OutputFormat; use \Icinga\Web\Widget\Tabextension\OutputFormat;
use \Icinga\Module\Monitoring\Backend; use \Icinga\Module\Monitoring\Backend;
use \Icinga\Module\Monitoring\Object\Host; use \Icinga\Data\BaseQuery;
use \Icinga\Module\Monitoring\Object\Service; use \Icinga\Module\Monitoring\Backend\Ido\Query\StatusQuery;
use \Icinga\Module\Monitoring\Form\Command\MultiCommandFlagForm; use \Icinga\Module\Monitoring\Form\Command\MultiCommandFlagForm;
use \Icinga\Module\Monitoring\DataView\HostStatus as HostStatusView; use \Icinga\Module\Monitoring\DataView\HostStatus as HostStatusView;
use \Icinga\Module\Monitoring\DataView\ServiceStatus as ServiceStatusView; use \Icinga\Module\Monitoring\DataView\ServiceStatus as ServiceStatusView;
use \Icinga\Module\Monitoring\DataView\Comment as CommentView; use \Icinga\Module\Monitoring\DataView\Comment as CommentView;
/** /**
* Displays aggregations collections of multiple objects. * Displays aggregations collections of multiple objects.
@ -45,7 +45,7 @@ class Monitoring_MultiController extends ActionController
{ {
public function init() public function init()
{ {
$this->view->queries = $this->getDetailQueries(); $this->view->queries = $this->getAllParamsAsArray();
$this->backend = Backend::createBackend($this->_getParam('backend')); $this->backend = Backend::createBackend($this->_getParam('backend'));
$this->createTabs(); $this->createTabs();
} }
@ -53,10 +53,10 @@ class Monitoring_MultiController extends ActionController
public function hostAction() public function hostAction()
{ {
$filters = $this->view->queries; $filters = $this->view->queries;
$errors = array(); $errors = array();
// Hosts // Fetch Hosts
$backendQuery = HostStatusView::fromRequest( $hostQuery = HostStatusView::fromRequest(
$this->_request, $this->_request,
array( array(
'host_name', 'host_name',
@ -71,51 +71,69 @@ class Monitoring_MultiController extends ActionController
) )
)->getQuery(); )->getQuery();
if ($this->_getParam('host') !== '*') { if ($this->_getParam('host') !== '*') {
$this->applyQueryFilter($backendQuery, $filters); $this->applyQueryFilter($hostQuery, $filters);
} }
$hosts = $backendQuery->fetchAll(); $hosts = $hostQuery->fetchAll();
// Comments // Fetch comments
$commentQuery = CommentView::fromRequest($this->_request)->getQuery(); $commentQuery = $this->applyQueryFilter(
$this->applyQueryFilter($commentQuery, $filters); CommentView::fromRequest($this->_request)->getQuery(),
$comments = array_keys($this->getUniqueValues($commentQuery->fetchAll(), 'comment_id')); $filters,
'comment_host'
);
$comments = array_keys($this->getUniqueValues($commentQuery->fetchAll(), 'comment_internal_id'));
$this->view->objects = $this->view->hosts = $hosts; // Populate view
$this->view->problems = $this->getProblems($hosts); $this->view->objects = $this->view->hosts = $hosts;
$this->view->comments = isset($comments) ? $comments : $this->getComments($hosts); $this->view->problems = $this->getProblems($hosts);
$this->view->comments = isset($comments) ? $comments : $this->getComments($hosts);
$this->view->hostnames = $this->getProperties($hosts, 'host_name'); $this->view->hostnames = $this->getProperties($hosts, 'host_name');
$this->view->downtimes = $this->getDowntimes($hosts); $this->view->downtimes = $this->getDowntimes($hosts);
$this->view->errors = $errors; $this->view->errors = $errors;
// Handle configuration changes
$this->handleConfigurationForm(array( $this->handleConfigurationForm(array(
'host_passive_checks_enabled' => 'Passive Checks', 'host_passive_checks_enabled' => 'Passive Checks',
'host_active_checks_enabled' => 'Active Checks', 'host_active_checks_enabled' => 'Active Checks',
'host_obsessing' => 'Obsessing', 'host_obsessing' => 'Obsessing',
'host_notifications_enabled' => 'Notifications', 'host_notifications_enabled' => 'Notifications',
'host_event_handler_enabled' => 'Event Handler', 'host_event_handler_enabled' => 'Event Handler',
'host_flap_detection_enabled' => 'Flap Detection' 'host_flap_detection_enabled' => 'Flap Detection'
)); ));
$this->view->form->setAction('/icinga2-web/monitoring/multi/host'); $this->view->form->setAction('/icinga2-web/monitoring/multi/host');
} }
/** /**
* @param $backendQuery BaseQuery The query to apply the filter to * Apply the query-filter received
* @param $filter array Containing the filter expressions from the request *
* @param $backendQuery BaseQuery The query to apply the filter to
* @param $filter array Containing the queries of the current request, converted into an
* array-structure.
* @param $hostColumn string The name of the host-column in the BaseQuery, defaults to 'host_name'
* @param $serviceColumn string The name of the service-column in the BaseQuery, defaults to 'service-description'
*
* @return BaseQuery The given BaseQuery
*/ */
private function applyQueryFilter($backendQuery, $filter) private function applyQueryFilter(
{ BaseQuery $backendQuery,
array $filter,
$hostColumn = 'host_name',
$serviceColumn = 'service_description'
) {
// fetch specified hosts // fetch specified hosts
foreach ($filter as $index => $expr) { foreach ($filter as $index => $expr) {
// Every query entry must define at least the host.
if (!array_key_exists('host', $expr)) { if (!array_key_exists('host', $expr)) {
$errors[] = 'Query ' . $index . ' misses property host.'; $errors[] = 'Query ' . $index . ' misses property host.';
continue; continue;
} }
// apply filter expressions from query // apply filter expressions from query
$backendQuery->orWhere('host_name', $expr['host']); $backendQuery->orWhere($hostColumn, $expr['host']);
if (array_key_exists('service', $expr)) { if (array_key_exists('service', $expr)) {
$backendQuery->andWhere('service_description', $expr['service']); $backendQuery->andWhere($serviceColumn, $expr['service']);
} }
} }
return $backendQuery;
} }
/** /**
@ -134,23 +152,25 @@ class Monitoring_MultiController extends ActionController
if (is_array($value)) { if (is_array($value)) {
$unique[$value[$key]] = $value[$key]; $unique[$value[$key]] = $value[$key];
} else { } else {
$unique[$value->{$key}] = $value->{$key}; $unique[$value->$key] = $value->$key;
} }
} }
return $unique; return $unique;
} }
/** /**
* Get the numbers of problems in the given objects * Get the numbers of problems of the given objects
* *
* @param $object array The hosts or services * @param $objects The objects containing the problems
*
* @return int The problem count
*/ */
private function getProblems($objects) private function getProblems($objects)
{ {
$problems = 0; $problems = 0;
foreach ($objects as $object) { foreach ($objects as $object) {
if (property_exists($object, 'host_unhandled_service_count')) { if (property_exists($object, 'host_unhandled_service_count')) {
$problems += $object->{'host_unhandled_service_count'}; $problems += $object->host_unhandled_service_count;
} else if ( } else if (
property_exists($object, 'service_handled') && property_exists($object, 'service_handled') &&
!$object->service_handled && !$object->service_handled &&
@ -175,7 +195,7 @@ class Monitoring_MultiController extends ActionController
{ {
$objectnames = array(); $objectnames = array();
foreach ($objects as $object) { foreach ($objects as $object) {
$objectnames[] = $object->{$property}; $objectnames[] = $object->$property;
} }
return $objectnames; return $objectnames;
} }
@ -224,7 +244,7 @@ class Monitoring_MultiController extends ActionController
// Comments // Comments
$commentQuery = CommentView::fromRequest($this->_request)->getQuery(); $commentQuery = CommentView::fromRequest($this->_request)->getQuery();
$this->applyQueryFilter($commentQuery, $filters); $this->applyQueryFilter($commentQuery, $filters);
$comments = array_keys($this->getUniqueValues($commentQuery->fetchAll(), 'comment_id')); $comments = array_keys($this->getUniqueValues($commentQuery->fetchAll(), 'comment_internal_id'));
$this->view->objects = $this->view->services = $services; $this->view->objects = $this->view->services = $services;
$this->view->problems = $this->getProblems($services); $this->view->problems = $this->getProblems($services);
@ -236,9 +256,9 @@ class Monitoring_MultiController extends ActionController
$this->handleConfigurationForm(array( $this->handleConfigurationForm(array(
'service_passive_checks_enabled' => 'Passive Checks', 'service_passive_checks_enabled' => 'Passive Checks',
'service_active_checks_enabled' => 'Active Checks', 'service_active_checks_enabled' => 'Active Checks',
'service_notifications_enabled' => 'Notifications', 'service_notifications_enabled' => 'Notifications',
'service_event_handler_enabled' => 'Event Handler', 'service_event_handler_enabled' => 'Event Handler',
'service_flap_detection_enabled' => 'Flap Detection' 'service_flap_detection_enabled' => 'Flap Detection'
)); ));
$this->view->form->setAction('/icinga2-web/monitoring/multi/service'); $this->view->form->setAction('/icinga2-web/monitoring/multi/service');
@ -263,28 +283,37 @@ class Monitoring_MultiController extends ActionController
} }
/** /**
* Fetch all requests from the 'detail' parameter. * "Flips" the structure of the objects created by _getAllParams
* *
* @return array An array of request that contain * Regularly, _getAllParams would return queries like <b>host[0]=value1&service[0]=value2</b> as
* the filter arguments as properties. * two entirely separate arrays. Instead, we want it as one single array, containing one single object
* for each index, containing all of its members as keys.
*
* @return array An array of all query parameters (See example above)
* <b>
* array( <br />
* 0 => array(host => value1, service => value2), <br />
* ... <br />
* )
* </b>
*/ */
private function getDetailQueries() private function getAllParamsAsArray()
{ {
$details = $this->_getAllParams(); $details = $this->_getAllParams();
$objects = array(); $queries = array();
foreach ($details as $property => $values) { foreach ($details as $property => $values) {
if (!is_array($values)) { if (!is_array($values)) {
continue; continue;
} }
foreach ($values as $index => $value) { foreach ($values as $index => $value) {
if (!array_key_exists($index, $objects)) { if (!array_key_exists($index, $queries)) {
$objects[$index] = array(); $queries[$index] = array();
} }
$objects[$index][$property] = $value; $queries[$index][$property] = $value;
} }
} }
return $objects; return $queries;
} }
/** /**

View File

@ -19,12 +19,17 @@ if ($hosts->count() === 0) {
} }
?> ?>
<table data-base-target="_next" class="action multiselect"> <table
data-base-target="_next"
class="action multiselect"
data-icinga-multiselect-url="<?= $this->href("/monitoring/multi/host") ?>"
data-icinga-multiselect-data="host"
>
<tbody> <tbody>
<?php foreach($hosts as $host): <?php foreach($hosts as $host):
$hostStateName = strtolower($this->util()->getHostStateName($host->host_state)); $hostStateName = strtolower($this->util()->getHostStateName($host->host_state));
$hostLink = $this->href('monitoring/show/host', array('host' => $host->host_name)); $hostLink = $this->href('/monitoring/show/host', array('host' => $host->host_name));
$icons = array(); $icons = array();
if (!$host->host_handled && $host->host_state > 0){ if (!$host->host_handled && $host->host_state > 0){

View File

@ -13,6 +13,12 @@
Icinga.Events.prototype = { Icinga.Events.prototype = {
keyboard: {
ctrlKey: false,
altKey: false,
shiftKey: false
},
/** /**
* Icinga will call our initialize() function once it's ready * Icinga will call our initialize() function once it's ready
*/ */
@ -65,7 +71,7 @@
type: 'pie', type: 'pie',
sliceColors: ['#44bb77', '#ffaa44', '#ff5566', '#dcd'], sliceColors: ['#44bb77', '#ffaa44', '#ff5566', '#dcd'],
width: '2em', width: '2em',
height: '2em', height: '2em'
}); });
}, },
@ -90,7 +96,10 @@
$(document).on('click', 'a', { self: this }, this.linkClicked); $(document).on('click', 'a', { self: this }, this.linkClicked);
// We treat tr's with a href attribute like links // We treat tr's with a href attribute like links
$(document).on('click', 'tr[href]', { self: this }, this.linkClicked); $(document).on('click', ':not(table) tr[href]', { self: this }, this.linkClicked);
// When tables have the class 'multiselect', multiple selection is possible.
$(document).on('click', 'table tr[href]', { self: this }, this.rowSelected);
$(document).on('click', 'button', { self: this }, this.submitForm); $(document).on('click', 'button', { self: this }, this.submitForm);
@ -277,6 +286,151 @@
return false; return false;
}, },
handleExternalTarget: function($node) {
var linkTarget = $node.attr('target');
// TODO: Let remote links pass through. Right now they only work
// combined with target="_blank" or target="_self"
// window.open is used as return true; didn't work reliable
if (linkTarget === '_blank' || linkTarget === '_self') {
window.open(href, linkTarget);
return true;
}
return false;
},
/**
* 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 = $table.data('icinga-multiselect-data').split(',');
var multisel = $table.hasClass('multiselect');
var url = $table.data('icinga-multiselect-url');
var $trs, $target;
event.stopPropagation();
event.preventDefault();
if (icinga.events.handleExternalTarget($tr)) {
// link handled externally
return false;
}
if (!data) {
icinga.logger.error('A table with multiselection must define the attribute "data-icinga-multiselect-data"');
return;
}
if (!url) {
icinga.logger.error('A table with multiselection must define the attribute "data-icinga-multiselect-url"');
return;
}
// Update selection
if (event.ctrlKey && multisel) {
// multi selection
if ($tr.hasClass('active')) {
$tr.removeClass('active');
} else {
$tr.addClass('active');
}
} else if (event.shiftKey && multisel) {
// range selection
var $rows = $table.find('tr[href]'),
from, to;
var selected = this;
// TODO: find a better place for this
$rows.find('td').attr('unselectable', 'on')
.css('user-select', 'none')
.css('-webkit-user-select', 'none')
.css('-moz-user-select', 'none')
.css('-ms-user-select', 'none');
$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;
}
});
} else {
// single selection
if ($tr.hasClass('active')) {
return false;
}
$table.find('tr[href].active').removeClass('active');
$tr.addClass('active');
}
$trs = $table.find('tr[href].active');
// Update url
$target = self.getLinkTargetFor($tr);
if ($trs.length > 1) {
// display multiple rows
var query = icinga.events.selectionToQuery($trs, data, icinga);
icinga.loader.loadUrl(url + '?' + query, $target);
} else if ($trs.length === 1) {
// display a single row
icinga.loader.loadUrl($tr.attr('href'), $target);
} else {
// display nothing
icinga.loader.loadUrl('#');
}
return false;
},
selectionToQuery: function ($selection, data, icinga) {
var selections = [], queries = [];
if ($selection.length === 0) {
return '';
}
// read all current selections
$selection.each(function(ind, selected) {
var url = $(selected).attr('href');
var params = icinga.utils.parseUrl(url).params;
var tuple = {};
for (var i = 0; i < data.length; i++) {
var key = data[i];
if (params[key]) {
tuple[key] = params[key];
}
}
selections.push(tuple);
});
// create new url
if (selections.length < 2) {
// single-selection
$.each(selections[0], function(key, value){
queries.push(key + '=' + encodeURIComponent(value));
});
} else {
// multi-selection
$.each(selections, function(i, el){
$.each(el, function(key, value) {
queries.push(key + '[' + i + ']=' + encodeURIComponent(value));
});
});
}
return queries.join('&');
},
/** /**
* Someone clicked a link or tr[href] * Someone clicked a link or tr[href]
*/ */

View File

@ -99,6 +99,7 @@
} }
var self = this; var self = this;
console.log("$.ajax({ url = " + url + " })");
var req = $.ajax({ var req = $.ajax({
type : method, type : method,
url : url, url : url,

View File

@ -104,7 +104,7 @@
path : a.pathname.replace(/^([^\/])/,'/$1'), path : a.pathname.replace(/^([^\/])/,'/$1'),
relative: (a.href.match(/tps?:\/\/[^\/]+(.+)/) || [,''])[1], relative: (a.href.match(/tps?:\/\/[^\/]+(.+)/) || [,''])[1],
segments: a.pathname.replace(/^\//,'').split('/'), segments: a.pathname.replace(/^\//,'').split('/'),
params : this.parseParams(a), params : this.parseParams(a)
}; };
a = null; a = null;