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\Widget\Tabextension\OutputFormat;
use \Icinga\Module\Monitoring\Backend;
use \Icinga\Module\Monitoring\Object\Host;
use \Icinga\Module\Monitoring\Object\Service;
use \Icinga\Data\BaseQuery;
use \Icinga\Module\Monitoring\Backend\Ido\Query\StatusQuery;
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\Comment as CommentView;
use \Icinga\Module\Monitoring\DataView\Comment as CommentView;
/**
* Displays aggregations collections of multiple objects.
@ -45,7 +45,7 @@ class Monitoring_MultiController extends ActionController
{
public function init()
{
$this->view->queries = $this->getDetailQueries();
$this->view->queries = $this->getAllParamsAsArray();
$this->backend = Backend::createBackend($this->_getParam('backend'));
$this->createTabs();
}
@ -53,10 +53,10 @@ class Monitoring_MultiController extends ActionController
public function hostAction()
{
$filters = $this->view->queries;
$errors = array();
$errors = array();
// Hosts
$backendQuery = HostStatusView::fromRequest(
// Fetch Hosts
$hostQuery = HostStatusView::fromRequest(
$this->_request,
array(
'host_name',
@ -71,51 +71,69 @@ class Monitoring_MultiController extends ActionController
)
)->getQuery();
if ($this->_getParam('host') !== '*') {
$this->applyQueryFilter($backendQuery, $filters);
$this->applyQueryFilter($hostQuery, $filters);
}
$hosts = $backendQuery->fetchAll();
$hosts = $hostQuery->fetchAll();
// Comments
$commentQuery = CommentView::fromRequest($this->_request)->getQuery();
$this->applyQueryFilter($commentQuery, $filters);
$comments = array_keys($this->getUniqueValues($commentQuery->fetchAll(), 'comment_id'));
// Fetch comments
$commentQuery = $this->applyQueryFilter(
CommentView::fromRequest($this->_request)->getQuery(),
$filters,
'comment_host'
);
$comments = array_keys($this->getUniqueValues($commentQuery->fetchAll(), 'comment_internal_id'));
$this->view->objects = $this->view->hosts = $hosts;
$this->view->problems = $this->getProblems($hosts);
$this->view->comments = isset($comments) ? $comments : $this->getComments($hosts);
// Populate view
$this->view->objects = $this->view->hosts = $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->downtimes = $this->getDowntimes($hosts);
$this->view->errors = $errors;
$this->view->errors = $errors;
// Handle configuration changes
$this->handleConfigurationForm(array(
'host_passive_checks_enabled' => 'Passive Checks',
'host_active_checks_enabled' => 'Active Checks',
'host_obsessing' => 'Obsessing',
'host_notifications_enabled' => 'Notifications',
'host_event_handler_enabled' => 'Event Handler',
'host_active_checks_enabled' => 'Active Checks',
'host_obsessing' => 'Obsessing',
'host_notifications_enabled' => 'Notifications',
'host_event_handler_enabled' => 'Event Handler',
'host_flap_detection_enabled' => 'Flap Detection'
));
$this->view->form->setAction('/icinga2-web/monitoring/multi/host');
}
/**
* @param $backendQuery BaseQuery The query to apply the filter to
* @param $filter array Containing the filter expressions from the request
* Apply the query-filter received
*
* @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
foreach ($filter as $index => $expr) {
// Every query entry must define at least the host.
if (!array_key_exists('host', $expr)) {
$errors[] = 'Query ' . $index . ' misses property host.';
continue;
}
// apply filter expressions from query
$backendQuery->orWhere('host_name', $expr['host']);
$backendQuery->orWhere($hostColumn, $expr['host']);
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)) {
$unique[$value[$key]] = $value[$key];
} else {
$unique[$value->{$key}] = $value->{$key};
$unique[$value->$key] = $value->$key;
}
}
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)
{
$problems = 0;
foreach ($objects as $object) {
if (property_exists($object, 'host_unhandled_service_count')) {
$problems += $object->{'host_unhandled_service_count'};
$problems += $object->host_unhandled_service_count;
} else if (
property_exists($object, 'service_handled') &&
!$object->service_handled &&
@ -175,7 +195,7 @@ class Monitoring_MultiController extends ActionController
{
$objectnames = array();
foreach ($objects as $object) {
$objectnames[] = $object->{$property};
$objectnames[] = $object->$property;
}
return $objectnames;
}
@ -224,7 +244,7 @@ class Monitoring_MultiController extends ActionController
// Comments
$commentQuery = CommentView::fromRequest($this->_request)->getQuery();
$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->problems = $this->getProblems($services);
@ -236,9 +256,9 @@ class Monitoring_MultiController extends ActionController
$this->handleConfigurationForm(array(
'service_passive_checks_enabled' => 'Passive Checks',
'service_active_checks_enabled' => 'Active Checks',
'service_notifications_enabled' => 'Notifications',
'service_event_handler_enabled' => 'Event Handler',
'service_active_checks_enabled' => 'Active Checks',
'service_notifications_enabled' => 'Notifications',
'service_event_handler_enabled' => 'Event Handler',
'service_flap_detection_enabled' => 'Flap Detection'
));
$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
* the filter arguments as properties.
* Regularly, _getAllParams would return queries like <b>host[0]=value1&service[0]=value2</b> as
* 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();
$objects = array();
$queries = array();
foreach ($details as $property => $values) {
if (!is_array($values)) {
continue;
}
foreach ($values as $index => $value) {
if (!array_key_exists($index, $objects)) {
$objects[$index] = array();
if (!array_key_exists($index, $queries)) {
$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>
<?php foreach($hosts as $host):
$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();
if (!$host->host_handled && $host->host_state > 0){

View File

@ -13,6 +13,12 @@
Icinga.Events.prototype = {
keyboard: {
ctrlKey: false,
altKey: false,
shiftKey: false
},
/**
* Icinga will call our initialize() function once it's ready
*/
@ -65,7 +71,7 @@
type: 'pie',
sliceColors: ['#44bb77', '#ffaa44', '#ff5566', '#dcd'],
width: '2em',
height: '2em',
height: '2em'
});
},
@ -90,7 +96,10 @@
$(document).on('click', 'a', { self: this }, this.linkClicked);
// 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);
@ -277,6 +286,151 @@
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]
*/

View File

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

View File

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