Semantic search implementation

- Only implemented for hosts as an example
- URL behaviour still has to be normalized

refs #4469
This commit is contained in:
Jannis Moßhammer 2013-10-14 13:25:25 +02:00
parent dac61eda19
commit d33cec78de
36 changed files with 1540 additions and 784 deletions

View File

@ -25,6 +25,7 @@
* @author Icinga Development Team <info@icinga.org>
*/
// {{{ICINGA_LICENSE_HEADER}}}
// @codingStandardsIgnoreStart
use Icinga\Web\Form;
use Icinga\Web\Controller\ActionController;
@ -34,69 +35,82 @@ use Icinga\Filter\Type\TextFilter;
use Icinga\Application\Logger;
use Icinga\Module\Monitoring\Filter\Type\StatusFilter;
use Icinga\Module\Monitoring\Filter\UrlViewFilter;
use Icinga\Module\Monitoring\DataView\HostStatus;
use Icinga\Web\Url;
/**
* Application wide interface for filtering
*/
class FilterController extends ActionController
{
/**
* The current filter registry
*
* @var Filter
*/
private $registry;
/**
* Entry point for filtering, uses the filter_domain and filter_module request parameter
* to determine which filter registry should be used
*/
public function indexAction()
{
$this->registry = new Filter();
$filter = new UrlViewFilter();
$this->view->form = new Form();
$this->view->form->addElement(
'text',
'query',
array(
'name' => 'query',
'label' => 'search',
'type' => 'search',
'data-icinga-component' => 'app/semanticsearch',
'data-icinga-target' => 'host',
'helptext' => 'Filter test'
)
);
$this->view->form->addElement(
'submit',
'btn_submit',
array(
'name' => 'submit'
)
);
$this->setupQueries();
$this->view->form->setRequest($this->getRequest());
if ($this->view->form->isSubmittedAndValid()) {
$tree = $this->registry->createQueryTreeForFilter($this->view->form->getValue('query'));
$this->view->tree = new \Icinga\Web\Widget\FilterBadgeRenderer($tree);
$view = \Icinga\Module\Monitoring\DataView\HostAndServiceStatus::fromRequest($this->getRequest());
$cv = new \Icinga\Module\Monitoring\Filter\Backend\IdoQueryConverter($view);
$this->view->sqlString = $cv->treeToSql($tree);
$this->view->params = $cv->getParams();
} else if ($this->getRequest()->getHeader('accept') == 'application/json') {
if ($this->getRequest()->getHeader('accept') == 'application/json') {
$this->getResponse()->setHeader('Content-Type', 'application/json');
$this->setupQueries(
$this->getParam('filter_domain', ''),
$this->getParam('filter_module', '')
);
$this->_helper->json($this->parse($this->getRequest()->getParam('query', '')));
} else {
$this->redirect('index/welcome');
}
}
private function setupQueries()
/**
* Set up the query handler for the given domain and module
*
* @param string $domain The domain to use
* @param string $module The module to use
*/
private function setupQueries($domain, $module = 'default')
{
$this->registry->addDomain(\Icinga\Module\Monitoring\Filter\MonitoringFilter::hostFilter());
$class = '\\Icinga\\Module\\' . ucfirst($module) . '\\Filter\\Registry';
$factory = strtolower($domain) . 'Filter';
$this->registry->addDomain($class::$factory());
}
/**
* Parse the given query text and returns the json as expected by the semantic search box
*
* @param String $text The query to parse
* @return array The result structure to be returned in json format
*/
private function parse($text)
{
try {
return $this->registry->getProposalsForQuery($text);
$view = HostStatus::fromRequest($this->getRequest());
$urlParser = new UrlViewFilter($view);
$queryTree = $this->registry->createQueryTreeForFilter($text);
return array(
'state' => 'success',
'proposals' => $this->registry->getProposalsForQuery($text),
'urlParam' => $urlParser->fromTree($queryTree)
);
} catch (\Exception $exc) {
Logger::error($exc);
$this->getResponse()->setHttpResponseCode(500);
return array(
'state' => 'error',
'message' => 'Search service is currently not available'
);
}
}
}
// @codingStandardsIgnoreEnd

View File

@ -1,11 +0,0 @@
All critical hosts starting with 'MySql'
All services with status warning that have been checked in the last two days
Services with open Problems and with critical hosts
with services that are not ok
[(SUBJECT)] [(SPECIFIED)] [(OP)] [FILTER] [(ADDITIONAL)]

View File

@ -26,7 +26,6 @@
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Filter;
use Icinga\Filter\Query\Node;
@ -132,11 +131,11 @@ class Domain extends QueryProposer
}
foreach ($this->attributes as $attributeHandler) {
if ($attributeHandler->isValidQuery($query)) {
$node = $attributeHandler->convertToTreeNode($query);
return $node;
}
if ($attributeHandler->isValidQuery($query)) {
$node = $attributeHandler->convertToTreeNode($query);
return $node;
}
}
return null;
}
}
}

View File

@ -28,7 +28,6 @@
namespace Icinga\Filter;
use Icinga\Filter\Query\Tree;
use Icinga\Filter\Query\Node;
@ -38,7 +37,8 @@ use Icinga\Filter\Query\Node;
* This class handles the top level parsing of queries, i.e.
* - Splitting queries at conjunctions and parsing them part by part
* - Delegating the query parts to specific filter domains handling this filters
* - Building a query tree that allows to convert a filter representation into others (url to string, string to url, sql..)
* - Building a query tree that allows to convert a filter representation into others
* (url to string, string to url, sql..)
*
* Filters are split in Filter Domains, Attributes and Types:
*
@ -110,7 +110,7 @@ class Filter extends QueryProposer
{
if ($this->defaultDomain !== null) {
return $this->defaultDomain;
} else if (count($this->domains) > 0) {
} elseif (count($this->domains) > 0) {
return $this->domains[0];
}
return null;
@ -240,7 +240,7 @@ class Filter extends QueryProposer
$right = $query;
do {
list($left, $conjuction, $right) = $this->splitQueryAtNextConjunction($right);
} while($conjuction !== null);
} while ($conjuction !== null);
return $left;
}
@ -275,7 +275,7 @@ class Filter extends QueryProposer
if ($conjunction === 'AND') {
$tree->insert(Node::createAndNode());
} elseif($conjunction === 'OR') {
} elseif ($conjunction === 'OR') {
$tree->insert(Node::createOrNode());
}
@ -292,4 +292,4 @@ class Filter extends QueryProposer
{
return $this->ignoredQueryParts;
}
}
}

View File

@ -26,7 +26,6 @@
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Filter;
use Icinga\Filter\Query\Node;
@ -88,7 +87,7 @@ class FilterAttribute extends QueryProposer
if (!$this->field) {
$this->field = $attr;
}
foreach(func_get_args() as $arg) {
foreach (func_get_args() as $arg) {
$this->attributes[] = trim($arg);
}
return $this;
@ -121,7 +120,7 @@ class FilterAttribute extends QueryProposer
$query = trim($query);
foreach ($this->attributes as $attribute) {
if (stripos($query, $attribute) === 0) {
return $attribute;
return $attribute;
}
}
return null;
@ -134,7 +133,8 @@ class FilterAttribute extends QueryProposer
*
* @return bool True when this query contains an attribute mapped by this filter
*/
public function queryHasSupportedAttribute($query) {
public function queryHasSupportedAttribute($query)
{
return $this->getMatchingAttribute($query) !== null;
}
@ -230,6 +230,4 @@ class FilterAttribute extends QueryProposer
{
return new FilterAttribute($type);
}
}

View File

@ -27,12 +27,14 @@
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Data;
namespace Icinga\Filter;
use Icinga\Filter\Query\Tree;
interface Filterable
{
public function isValidFilterTarget($targetOrColumn);
public function resolveFilterTarget($targetOrColumn);
}
public function isValidFilterTarget($field);
public function getMappedField($field);
public function applyFilter(Tree $filter);
}

View File

@ -26,7 +26,6 @@
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Filter\Query;
/**
@ -102,7 +101,8 @@ class Node
* Factory method for creating operator nodes
*
* @param String $operator The operator to use
* @param String $left The left side of the node, i.e. target (mostly attribute) to query for with this node
* @param String $left The left side of the node, i.e. target (mostly attribute)
* to query for with this node
* @param String $right The right side of the node, i.e. the value to use for querying
*
* @return Node An operator Node instance

View File

@ -26,9 +26,10 @@
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Filter\Query;
use Icinga\Filter\Filterable;
/**
* A binary tree representing queries in an interchangeable way
*
@ -75,7 +76,7 @@ class Tree
$node->parent = $this->lastNode;
if ($this->lastNode->left == null) {
$this->lastNode->left = $node;
} else if($this->lastNode->right == null) {
} elseif ($this->lastNode->right == null) {
$this->lastNode->right = $node;
}
break;
@ -99,7 +100,7 @@ class Tree
if ($currentNode->type != Node::TYPE_AND) {
// No AND node, insert into tree
if($currentNode->parent !== null) {
if ($currentNode->parent !== null) {
$node->parent = $currentNode->parent;
if ($currentNode->parent->left === $currentNode) {
$currentNode->parent->left = $node;
@ -144,7 +145,7 @@ class Tree
{
if ($currentNode->type === Node::TYPE_OPERATOR) {
// Always insert when encountering an operator node
if($currentNode->parent !== null) {
if ($currentNode->parent !== null) {
$node->parent = $currentNode->parent;
if ($currentNode->parent->left === $currentNode) {
$currentNode->parent->left = $node;
@ -168,4 +169,215 @@ class Tree
$this->insertOrNode($node, $currentNode->right);
}
}
/**
* Return a copy of this tree that only contains filters that can be applied for the given Filterable
*
* @param Filterable $filter The Filterable to test element nodes agains
* @return Tree A copy of this tree that only contains nodes for the given filter
*/
public function getCopyForFilterable(Filterable $filter)
{
$copy = $this->createCopy();
if (!$this->root) {
return $copy;
}
$copy->root = $this->removeInvalidFilter($copy->root, $filter);
return $copy;
}
/**
* Remove all tree nodes that are not applicable ot the given Filterable
*
* @param Node $node The root node to use
* @param Filterable $filter The Filterable to test nodes against
* @return Node The normalized tree node
*/
public function removeInvalidFilter($node, Filterable $filter)
{
if ($node === null) {
return $node;
}
if ($node->type === Node::TYPE_OPERATOR) {
if (!$filter->isValidFilterTarget($node->left)) {
return null;
} else {
return $node;
}
}
$node->left = $this->removeInvalidFilter($node->left, $filter);
$node->right = $this->removeInvalidFilter($node->right, $filter);
if ($node->left && $node->right) {
return $node;
} elseif ($node->left) {
return $node->left;
} elseif ($node->right) {
return $node->right;
}
return null;
}
/**
* Normalize this tree and fix incomplete nodes
*
* @param Node $node The root node to normalize
* @return Node The normalized root node
*/
public static function normalizeTree($node)
{
if ($node->type === Node::TYPE_OPERATOR) {
return $node;
}
if ($node === null) {
return null;
}
if ($node->left && $node->right) {
$node->left = self::normalizeTree($node->left);
$node->right = self::normalizeTree($node->right);
return $node;
} elseif ($node->left) {
return $node->left;
} elseif ($node->right) {
return $node->right;
}
}
/**
* Return an array of all attributes in this tree
*
* @param Node $ctx The root node to use instead of the tree root
* @return array An array of attribute names
*/
public function getAttributes($ctx = null)
{
$result = array();
$ctx = $ctx ? $ctx : $this->root;
if ($ctx == null) {
return $result;
}
if ($ctx->type === Node::TYPE_OPERATOR) {
$result[] = $ctx->left;
} else {
$result = $result + $this->getAttributes($ctx->left) + $this->getAttributes($ctx->right);
}
return $result;
}
/**
* Create a copy of this tree without the given node
*
* @param Node $node The node to remove
* @return Tree A copy of the given tree
*/
public function withoutNode(Node $node)
{
$tree = $this->createCopy();
$toRemove = $tree->findNode($node);
if ($toRemove !== null) {
if ($toRemove === $tree->root) {
$tree->root = null;
return $tree;
}
if ($toRemove->parent->left === $toRemove) {
$toRemove->parent->left = null;
} else {
$toRemove->parent->right = null;
}
}
$tree->root = $tree->normalizeTree($tree->root);
return $tree;
}
/**
* Create an independent copy of this tree
*
* @return Tree A copy of this tree
*/
public function createCopy()
{
$tree = new Tree();
if ($this->root === null) {
return $tree;
}
$this->copyBranch($this->root, $tree);
return $tree;
}
/**
* Copy the given node or branch into the given tree
*
* @param Node $node The node to copy
* @param Tree $tree The tree to insert the copied node and it's subnodes to
*/
private function copyBranch(Node $node, Tree &$tree)
{
if ($node->type === Node::TYPE_OPERATOR) {
$copy = Node::createOperatorNode($node->operator, $node->left, $node->right);
$copy->context = $node->context;
$tree->insert($copy);
} else {
if ($node->left) {
$this->copyBranch($node->left, $tree);
}
$tree->insert($node->type === Node::TYPE_OR ? Node::createOrNode() : Node::createAndNode());
if ($node->right) {
$this->copyBranch($node->right, $tree);
}
}
}
/**
* Look for a given node in the tree and return it if exists
*
* @param Node $node The node to look for
* @param Node $ctx The node to use as the root of the tree
*
* @return Node The node that matches $node in the tree or null
*/
public function findNode(Node $node, $ctx = null)
{
$ctx = $ctx ? $ctx : $this->root;
if ($ctx === null) {
return null;
}
if ($ctx->type === Node::TYPE_OPERATOR) {
if ($ctx->left == $node->left && $ctx->right == $node->right && $ctx->operator == $node->operator) {
return $ctx;
}
return null;
} else {
$result = $this->findNode($node, $ctx->left);
if ($result === null) {
$result = $this->findNode($node, $ctx->right);
}
return $result;
}
}
/**
* Return true if A node with the given attribute on the left side exists
*
* @param String $name The attribute to test for existence
* @param Node $ctx The current root node
*
* @return bool True if a node contains $name on the left side, otherwise false
*/
public function hasNodeWithAttribute($name, $ctx = null)
{
$ctx = $ctx ? $ctx : $this->root;
if ($ctx === null) {
return false;
}
if ($ctx->type === Node::TYPE_OPERATOR) {
return $ctx->left === $name;
} else {
return $this->hasNodeWithAttribute($name, $ctx->left) || $this->hasNodeWithAttribute($name, $ctx->right);
}
}
}

View File

@ -26,7 +26,6 @@
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Filter;
/**
@ -61,5 +60,4 @@ abstract class QueryProposer
* @return array An array containing 0..* proposal text tokens
*/
abstract public function getProposalsForQuery($query);
}
}

View File

@ -121,7 +121,7 @@ class BooleanFilter extends FilterType
if (self::startsWith($query, $match) && $this->subFilter) {
$subQuery = trim(substr($query, strlen($match)));
$proposals = $proposals + $this->subFilter->getProposalsForQuery($subQuery);
} else if (strtolower($query) !== strtolower($match)) {
} elseif (strtolower($query) !== strtolower($match)) {
$proposals[] = self::markDifference($match, $query);
}
}
@ -146,9 +146,9 @@ class BooleanFilter extends FilterType
foreach ($operators as $operator) {
if (strtolower($operator) === strtolower($query)) {
$proposals += array_values($this->fields);
} else if (self::startsWith($operator, $query)) {
} elseif (self::startsWith($operator, $query)) {
$proposals[] = self::markDifference($operator, $query);
} else if (self::startsWith($query, $operator)) {
} elseif (self::startsWith($query, $operator)) {
$fieldPart = trim(substr($query, strlen($operator)));
$proposals = $proposals + $this->getFieldProposals($fieldPart);
}
@ -232,5 +232,4 @@ class BooleanFilter extends FilterType
}
return $node;
}
}
}

View File

@ -73,7 +73,7 @@ abstract class FilterType extends QueryProposer
*
* @return bool True when $string starts with $substring
*/
static public function startsWith($string, $substring)
public static function startsWith($string, $substring)
{
return stripos($string, $substring) === 0;
}
@ -90,11 +90,11 @@ abstract class FilterType extends QueryProposer
$matchingOperator = '';
foreach ($this->getOperators() as $operator) {
if (stripos($query, $operator) === 0) {
if (strlen($matchingOperator) < strlen($operator) ){
if (strlen($matchingOperator) < strlen($operator)) {
$matchingOperator = $operator;
}
}
}
return $matchingOperator;
}
}
}

View File

@ -26,10 +26,8 @@
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Filter\Type;
use Icinga\Filter\Query\Node;
class TextFilter extends FilterType
@ -78,7 +76,7 @@ class TextFilter extends FilterType
foreach ($operators as $operator) {
if (strtolower($operator) === strtolower($query)) {
$proposals += array('\'' . $this->getProposalsForValues($operator) . '\'');
} else if (self::startsWith($operator, $query)) {
} elseif (self::startsWith($operator, $query)) {
$proposals[] = self::markDifference($operator, $query);
}
}
@ -166,10 +164,10 @@ class TextFilter extends FilterType
}
switch (strtolower($operator)) {
case 'starts with':
case 'ends with':
$value = '*' . $value;
break;
case 'ends with':
case 'starts with':
$value = $value . '*';
break;
case 'matches':
@ -208,4 +206,4 @@ class TextFilter extends FilterType
return '...value...';
}
}
}
}

View File

@ -27,6 +27,7 @@
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Filter\Type;
use Icinga\Filter\Query\Node;
/**
@ -104,8 +105,8 @@ class TimeRangeSpecifier extends FilterType
* Return a two element array with the operator and the timestring parsed from the given query part
*
* @param String $query The query to extract the operator and time value from
* @return array An array containing the operator as the first and the string for strotime as the second
* value or (null,null) if the query is invalid
* @return array An array containing the operator as the first and the string for
* strotime as the second value or (null,null) if the query is invalid
*/
private function getOperatorAndTimeStringFromQuery($query)
{
@ -123,9 +124,9 @@ class TimeRangeSpecifier extends FilterType
}
if (is_numeric($query[0])) {
if($this->forcedPrefix) {
if ($this->forcedPrefix) {
$prefix = $this->forcedPrefix;
} elseif($currentOperator === Node::OPERATOR_GREATER_EQ) {
} elseif ($currentOperator === Node::OPERATOR_GREATER_EQ) {
$prefix = '-';
} else {
$prefix = '+';
@ -214,4 +215,4 @@ class TimeRangeSpecifier extends FilterType
}
return $this;
}
}
}

View File

@ -26,43 +26,85 @@
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Web\Widget;
use Icinga\Filter\Query\Tree;
use Icinga\Filter\Query\Node;
use Icinga\Module\Monitoring\Filter\UrlViewFilter;
use Icinga\Web\Url;
use Zend_View_Abstract;
/**
* A renderer for filter badges that allow to disable specific filters
*/
class FilterBadgeRenderer implements Widget
{
private $tree;
/**
* @var Url
*/
private $baseUrl;
private $conjunctionCellar = '';
private $urlFilter;
/**
* Create a new badge renderer for this tree
*
* @param Tree $tree
*/
public function __construct(Tree $tree)
{
$this->tree = $tree;
}
/**
* Create a removable badge from a query tree node
*
* @param Node $node The node to create the badge for
* @return string The html for the badge
*/
private function nodeToBadge(Node $node)
{
$basePath = $this->baseUrl->getAbsoluteUrl();
$allParams = $this->baseUrl->getParams();
if ($node->type === Node::TYPE_OPERATOR) {
return ' <a class="btn btn-default btn-xs">'
$newTree = $this->tree->withoutNode($node);
$url = $this->urlFilter->fromTree($newTree);
$url = $basePath . (empty($allParams) ? '?' : '&') . $url;
return ' <a class="btn btn-default btn-xs" href="' . $url . '">'
. $this->conjunctionCellar . ' '
. ucfirst($node->left) . ' '
. $node->operator . ' '
. $node->right . '</a>';
}
$result = '';
$result .= $this->nodeToBadge($node->left);
$this->conjunctionCellar = $node->type;
$result .= $this->nodeToBadge($node->right);
return $result;
}
/**
* Initialize $this->baseUrl with an Url instance containing all non-filter parameter
*/
private function buildBaseUrl()
{
$baseUrl = Url::fromRequest();
foreach ($baseUrl->getParams() as $key => $param) {
$translated = preg_replace('/[^0-9A-Za-z_]{1,2}$/', '', $key);
if ($this->tree->hasNodeWithAttribute($translated) === true) {
$baseUrl->removeKey($key);
}
}
$this->baseUrl = $baseUrl;
}
/**
* Renders this widget via the given view and returns the
@ -73,9 +115,11 @@ class FilterBadgeRenderer implements Widget
*/
public function render(Zend_View_Abstract $view)
{
$this->urlFilter = new UrlViewFilter();
if ($this->tree->root == null) {
return '';
}
$this->buildBaseUrl();
return $this->nodeToBadge($this->tree->root);
}
}
}

View File

@ -0,0 +1,119 @@
<?php
// {{{ICINGA_LICENSE_HEADER}}}
/**
* This file is part of Icinga 2 Web.
*
* Icinga 2 Web - Head for multiple monitoring backends.
* Copyright (C) 2013 Icinga Development Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* @copyright 2013 Icinga Development Team <info@icinga.org>
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
* @author Icinga Development Team <info@icinga.org>
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Web\Widget;
use Zend_View_Abstract;
use Icinga\Web\Form;
use Icinga\Web\Url;
use Icinga\Filter\Query\Tree;
/**
* Widget that renders a filter input box together with an FilterBadgeRenderer widget
*/
class FilterBox implements Widget
{
/**
* An optional initial filter to use
*
* @var \Icinga\Filter\Query\Tree
*/
private $initialFilter;
/**
* The domain of the filter, set in the data-icinga-filter-domain attribute
* @var string
*/
private $domain;
/**
* The module of the filter, set in the data-icinga-filter-module attribute
* @var string
*/
private $module;
/**
* The template used for rendering the form and badges
* @var string
*/
private static $TPL = <<<'EOT'
<div class="row">
<div class="col-md-12">{{FORM}}</div>
<div class="col-md-12">{{BADGES}}</div>
</div>
EOT;
/**
* Create a new FilterBox widget
*
* @param Tree $initialFilter The tree to use for initial population
* @param String $domain The filter domain
* @param String $module The filter module
*/
public function __construct(Tree $initialFilter, $domain, $module)
{
$this->initialFilter = $initialFilter;
$this->domain = $domain;
$this->module = $module;
}
/**
* Render this widget
*
* @param Zend_View_Abstract $view The view to use for rendering the widget
* @return string The HTML of the widget as a string
*/
public function render(Zend_View_Abstract $view)
{
$form = new Form();
$form->setAttrib('class', 'form-inline');
$form->setMethod('GET');
$form->setAction(Url::fromPath('/filter'));
$form->setTokenDisabled();
$form->addElement(
'text',
'filter',
array(
'label' => 'Filter Results',
'name' => 'filter',
'data-icinga-component' => 'app/semanticsearch',
'data-icinga-filter-domain' => $this->domain,
'data-icinga-filter-module' => $this->module
)
);
$form->removeAttrib('data-icinga-component');
$form->setIgnoreChangeDiscarding(true);
$badges = new FilterBadgeRenderer($this->initialFilter);
$html = str_replace('{{FORM}}', $form->render($view), self::$TPL);
return str_replace('{{BADGES}}', $badges->render($view), $html);
}
}

View File

@ -38,16 +38,21 @@ use Icinga\Web\Widget\Tabextension\OutputFormat;
use Icinga\Web\Widget\Tabs;
use Icinga\Module\Monitoring\Backend;
use Icinga\Web\Widget\SortBox;
use Icinga\Web\Widget\FilterBox;
use Icinga\Application\Config as IcingaConfig;
use Icinga\Module\Monitoring\DataView\DataView;
use Icinga\Module\Monitoring\DataView\Notification as NotificationView;
use Icinga\Module\Monitoring\DataView\Downtime as DowntimeView;
use Icinga\Module\Monitoring\DataView\Contact as ContactView;
use Icinga\Module\Monitoring\DataView\Contactgroup as ContactgroupView;
use Icinga\Module\Monitoring\DataView\HostAndServiceStatus as HostAndServiceStatusView;
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\Groupsummary as GroupsummaryView;
use Icinga\Module\Monitoring\DataView\EventHistory as EventHistoryView;
use Icinga\Module\Monitoring\Filter\UrlViewFilter;
use Icinga\Filter\Filterable;
class Monitoring_ListController extends MonitoringController
{
@ -96,8 +101,9 @@ class Monitoring_ListController extends MonitoringController
*/
public function hostsAction()
{
$this->compactView = 'hosts-compact';
$query = HostAndServiceStatusView::fromRequest(
$dataview = HostStatusView::fromRequest(
$this->_request,
array(
'host_icon_image',
@ -123,8 +129,9 @@ class Monitoring_ListController extends MonitoringController
'host_current_check_attempt',
'host_max_check_attempts'
)
)->getQuery();
$this->view->hosts = $query->paginate();
);
$query = $dataview->getQuery();
$this->setupFilterControl($dataview);
$this->setupSortControl(array(
'host_last_check' => 'Last Host Check',
'host_severity' => 'Host Severity',
@ -134,6 +141,8 @@ class Monitoring_ListController extends MonitoringController
'host_state' => 'Hard State'
));
$this->handleFormatRequest($query);
$this->view->hosts = $query->paginate();
}
/**
@ -390,7 +399,8 @@ class Monitoring_ListController extends MonitoringController
$this->_helper->viewRenderer($this->compactView);
}
if ($this->_getParam('format') === 'sql'
if ($this->getParam('format') === 'sql'
&& IcingaConfig::app()->global->get('environment', 'production') === 'development') {
echo '<pre>'
. htmlspecialchars(wordwrap($query->dump()))
@ -426,6 +436,17 @@ class Monitoring_ListController extends MonitoringController
$this->view->sortControl->applyRequest($this->getRequest());
}
private function setupFilterControl(Filterable $dataview)
{
$parser = new UrlViewFilter($dataview);
$this->view->filterBox = new FilterBox(
$parser->parseUrl(),
'host',
'monitoring'
);
}
/**
* Return all tabs for this controller
*

View File

@ -5,8 +5,19 @@ $viewHelper = $this->getHelper('MonitoringState');
<?= $this->tabs->render($this); ?>
<h1>Hosts Status</h1>
<div data-icinga-component="app/mainDetailGrid">
<?= $this->sortControl->render($this); ?>
<?= $this->paginationControl($hosts, null, null, array('preserve' => $this->preserve)); ?>
<div class="container">
<div class="row">
<div class="col-md-5">
<?= $this->filterBox->render($this); ?>
</div>
<div class="col-md-7">
<?= $this->sortControl->render($this); ?>
</div>
</div>
<div class="row">
<?= $this->paginationControl($hosts, null, null, array('preserve' => $this->preserve)); ?>
</div>
</div>
<table class="table table-condensed">
<tbody>

View File

@ -1,23 +1,50 @@
<?php
// {{{ICINGA_LICENSE_HEADER}}}
/**
* This file is part of Icinga 2 Web.
*
* Icinga 2 Web - Head for multiple monitoring backends.
* Copyright (C) 2013 Icinga Development Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* @copyright 2013 Icinga Development Team <info@icinga.org>
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
* @author Icinga Development Team <info@icinga.org>
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\Backend\Ido\Query;
use Icinga\Data\Db\Query;
use Icinga\Application\Benchmark;
use Icinga\Exception\ProgrammingError;
use Icinga\Filter\Query\Tree;
use Icinga\Filter\Filterable;
use Icinga\Module\Monitoring\Filter\Backend\IdoQueryConverter;
use Icinga\Module\Monitoring\Filter\UrlViewFilter;
abstract class AbstractQuery extends Query
abstract class AbstractQuery extends Query implements Filterable
{
protected $prefix;
protected $idxAliasColumn;
protected $idxAliasTable;
protected $columnMap = array();
protected $query;
protected $customVars = array();
protected $joinedVirtualTables = array();
protected $object_id = 'object_id';
protected $host_id = 'host_id';
protected $hostgroup_id = 'hostgroup_id';
@ -25,16 +52,62 @@ abstract class AbstractQuery extends Query
protected $servicegroup_id = 'servicegroup_id';
protected $contact_id = 'contact_id';
protected $contactgroup_id = 'contactgroup_id';
protected $aggregateColumnIdx = array();
protected $allowCustomVars = false;
protected function isAggregateColumn($column)
public function isAggregateColumn($column)
{
return array_key_exists($column, $this->aggregateColumnIdx);
}
public function order($col, $dir = null)
{
$this->requireColumn($col);
if ($this->isCustomvar($col)) {
// TODO: Doesn't work right now. Does it?
$col = $this->getCustomvarColumnName($col);
} elseif ($this->hasAliasName($col)) {
$col = $this->aliasToColumnName($col);
} else {
throw new \InvalidArgumentException('Can\'t order by column '.$col);
}
$this->order_columns[] = array($col, $dir);
return $this;
}
public function applyFilter(Tree $filter)
{
foreach ($filter->getAttributes() as $target) {
$this->requireColumn($target);
}
$converter = new IdoQueryConverter($this);
$converter->treeToSql($filter, $this->baseQuery);
}
public function isValidFilterTarget($field)
{
return $this->getMappedField($field) !== null;
}
public function getMappedField($field)
{
foreach ($this->columnMap as $columnSource => $columnSet) {
if (isset($columnSet[$field])) {
return $columnSet[$field];
}
}
return null;
}
public function isTimestamp($field)
{
$mapped = $this->getMappedField($field);
if ($mapped === null) {
return false;
}
return stripos($mapped, 'UNIX_TIMESTAMP') !== false;
}
protected function init()
{
parent::init();
@ -65,11 +138,136 @@ abstract class AbstractQuery extends Query
$this->prepareAliasIndexes();
}
protected function joinBaseTables()
{
reset($this->columnMap);
$table = key($this->columnMap);
$this->baseQuery = $this->db->select()->from(
array($table => $this->prefix . $table),
array()
);
$this->joinedVirtualTables = array($table => true);
}
protected function prepareAliasIndexes()
{
foreach ($this->columnMap as $tbl => & $cols) {
foreach ($cols as $alias => $col) {
$this->idxAliasTable[$alias] = $tbl;
$this->idxAliasColumn[$alias] = preg_replace('~\n\s*~', ' ', $col);
}
}
}
protected function beforeCreatingCountQuery()
{
}
protected function beforeCreatingSelectQuery()
{
$this->setRealColumns();
$classParts = explode('\\', get_class($this));
Benchmark::measure(sprintf('%s ready to run', array_pop($classParts)));
}
public function setRealColumns()
{
$columns = $this->columns;
$this->columns = array();
if (empty($columns)) {
$columns = $this->getDefaultColumns();
}
foreach ($columns as $alias => $col) {
$this->requireColumn($col);
if ($this->isCustomvar($col)) {
$name = $this->getCustomvarColumnName($col);
} else {
$name = $this->aliasToColumnName($col);
}
if (is_int($alias)) {
$alias = $col;
}
$this->columns[$alias] = preg_replace('|\n|', ' ', $name);
}
return $this;
}
protected function getDefaultColumns()
{
reset($this->columnMap);
$table = key($this->columnMap);
return array_keys($this->columnMap[$table]);
}
protected function requireColumn($alias)
{
if ($this->hasAliasName($alias)) {
$this->requireVirtualTable($this->aliasToTableName($alias));
} elseif ($this->isCustomVar($alias)) {
$this->requireCustomvar($alias);
} else {
throw new ProgrammingError(sprintf('Got invalid column: %s', $alias));
}
return $this;
}
protected function hasAliasName($alias)
{
return array_key_exists($alias, $this->idxAliasColumn);
}
protected function requireVirtualTable($name)
{
if ($this->hasJoinedVirtualTable($name)) {
return $this;
}
return $this->joinVirtualTable($name);
}
protected function joinVirtualTable($table)
{
$func = 'join' . ucfirst($table);
if (method_exists($this, $func)) {
$this->$func();
} else {
throw new ProgrammingError(
sprintf(
'Cannot join "%s", no such table found',
$table
)
);
}
$this->joinedVirtualTables[$table] = true;
return $this;
}
protected function aliasToTableName($alias)
{
return $this->idxAliasTable[$alias];
}
protected function isCustomVar($alias)
{
return $this->allowCustomVars && $alias[0] === '_';
}
protected function requireCustomvar($customvar)
{
if (! $this->hasCustomvar($customvar)) {
$this->joinCustomvar($customvar);
}
return $this;
}
protected function hasCustomvar($customvar)
{
return array_key_exists($customvar, $this->customVars);
}
protected function joinCustomvar($customvar)
{
// TODO: This is not generic enough yet
@ -103,200 +301,6 @@ abstract class AbstractQuery extends Query
return $this;
}
protected function prepareAliasIndexes()
{
foreach ($this->columnMap as $tbl => & $cols) {
foreach ($cols as $alias => $col) {
$this->idxAliasTable[$alias] = $tbl;
$this->idxAliasColumn[$alias] = preg_replace('~\n\s*~', ' ', $col);
}
}
}
protected function getDefaultColumns()
{
reset($this->columnMap);
$table = key($this->columnMap);
return array_keys($this->columnMap[$table]);
}
protected function joinBaseTables()
{
reset($this->columnMap);
$table = key($this->columnMap);
$this->baseQuery = $this->db->select()->from(
array($table => $this->prefix . $table),
array()
);
$this->joinedVirtualTables = array($table => true);
}
protected function beforeCreatingCountQuery()
{
$this->applyAllFilters();
}
protected function beforeCreatingSelectQuery()
{
$this->setRealColumns();
$classParts = explode('\\', get_class($this));
Benchmark::measure(sprintf('%s ready to run', array_pop($classParts)));
}
protected function applyAllFilters()
{
$filters = array();
foreach ($this->filters as $f) {
$alias = $f[0];
$value = $f[1];
$this->requireColumn($alias);
if ($this->isCustomvar($alias)) {
$col = $this->getCustomvarColumnName($alias);
} elseif ($this->hasAliasName($alias)) {
$col = $this->aliasToColumnName($alias);
} else {
throw new ProgrammingError(
'If you finished here, code has been messed up'
);
}
$func = 'filter' . ucfirst($alias);
if (method_exists($this, $func)) {
$this->$func($value);
return;
}
if ($this->isAggregateColumn($alias)) {
$this->baseQuery->having($this->prepareFilterStringForColumn($col, $value));
} else {
$this->baseQuery->where($this->prepareFilterStringForColumn($col, $value));
}
}
}
public function order($col, $dir = null)
{
$this->requireColumn($col);
if ($this->isCustomvar($col)) {
// TODO: Doesn't work right now. Does it?
$col = $this->getCustomvarColumnName($col);
} elseif ($this->hasAliasName($col)) {
$col = $this->aliasToColumnName($col);
} else {
throw new \InvalidArgumentException('Can\'t order by column '.$col);
}
$this->order_columns[] = array($col, $dir);
return $this;
}
public function setRealColumns()
{
$columns = $this->columns;
$this->columns = array();
if (empty($columns)) {
$colums = $this->getDefaultColumns();
}
foreach ($columns as $alias => $col) {
$this->requireColumn($col);
if ($this->isCustomvar($col)) {
$name = $this->getCustomvarColumnName($col);
} else {
$name = $this->aliasToColumnName($col);
}
if (is_int($alias)) {
$alias = $col;
}
$this->columns[$alias] = preg_replace('|\n|', ' ' , $name);
}
return $this;
}
protected function requireColumn($alias)
{
if ($this->hasAliasName($alias)) {
$this->requireVirtualTable($this->aliasToTableName($alias));
} elseif ($this->isCustomVar($alias)) {
$this->requireCustomvar($alias);
} else {
throw new ProgrammingError(sprintf('Got invalid column: %s', $alias));
}
return $this;
}
protected function hasAliasName($alias)
{
return array_key_exists($alias, $this->idxAliasColumn);
}
public function aliasToColumnName($alias)
{
return $this->idxAliasColumn[$alias];
}
protected function aliasToTableName($alias)
{
return $this->idxAliasTable[$alias];
}
protected function hasJoinedVirtualTable($name)
{
return array_key_exists($name, $this->joinedVirtualTables);
}
protected function requireVirtualTable($name)
{
if ($this->hasJoinedVirtualTable($name)) {
return $this;
}
return $this->joinVirtualTable($name);
}
protected function joinVirtualTable($table)
{
$func = 'join' . ucfirst($table);
if (method_exists($this, $func)) {
$this->$func();
} else {
throw new ProgrammingError(sprintf(
'Cannot join "%s", no such table found',
$table
));
}
$this->joinedVirtualTables[$table] = true;
return $this;
}
protected function requireCustomvar($customvar)
{
if (! $this->hasCustomvar($customvar)) {
$this->joinCustomvar($customvar);
}
return $this;
}
protected function hasCustomvar($customvar)
{
return array_key_exists($customvar, $this->customVars);
}
protected function getCustomvarColumnName($customvar)
{
return $this->customVars[$customvar] . '.varvalue';
}
protected function createSubQuery($queryName, $columns = array())
{
$class = '\\'
. substr(__CLASS__, 0, strrpos(__CLASS__, '\\') + 1)
. ucfirst($queryName) . 'Query';
$query = new $class($this->ds, $columns);
return $query;
}
protected function customvarNameToTypeName($customvar)
{
// TODO: Improve this:
@ -311,106 +315,27 @@ abstract class AbstractQuery extends Query
return array($m[1], $m[2]);
}
protected function prepareFilterStringForColumn($column, $value)
protected function hasJoinedVirtualTable($name)
{
$filter = '';
$filters = array();
$or = array();
$and = array();
if (
! is_array($value) &&
(strpos($value, ',') !== false || strpos($value, '|') !== false)
) {
$value = preg_split('~[,|]~', $value, -1, PREG_SPLIT_NO_EMPTY);
}
if (! is_array($value)) {
$value = array($value);
}
// Go through all given values
foreach ($value as $val) {
if ($val === '') {
// TODO: REALLY??
continue;
}
$not = false;
$force = false;
$op = '=';
$wildcard = false;
if ($val[0] === '-' || $val[0] === '!') {
// Value starting with minus or !: negation
$val = substr($val, 1);
$not = true;
}
if ($val[0] === '+') {
// Value starting with +: enforces AND
// TODO: depends on correct URL handling, not given in all
// ZF versions.
$val = substr($val, 1);
$force = true;
}
if ($val[0] === '<' || $val[0] === '>') {
$op = $val[0];
$val = substr($val, 1);
}
if (strpos($val, '*') !== false) {
$wildcard = true;
$val = str_replace('*', '%', $val);
}
$operator = null;
switch ($op) {
case '=':
if ($not) {
$operator = $wildcard ? 'NOT LIKE' : '!=';
} else {
$operator = $wildcard ? 'LIKE' : '=';
}
break;
case '>':
$operator = $not ? '<=' : '>';
break;
case '<':
$operator = $not ? '>=' : '<';
break;
default:
throw new ProgrammingError("'$op' is not a valid operator");
}
if ($not || $force) {
$and[] = $this->db->quoteInto($column . ' ' . $operator . ' ?', $val);
} else {
$or[] = $this->db->quoteInto($column . ' ' . $operator . ' ?', $val);
}
}
if (! empty($or)) {
$filters[] = implode(' OR ', $or);
}
if (! empty($and)) {
$filters[] = implode(' AND ', $and);
}
if (! empty($filters)) {
$filter = '(' . implode(') AND (', $filters) . ')';
}
return $filter;
return array_key_exists($name, $this->joinedVirtualTables);
}
public function getMappedColumn($name)
protected function getCustomvarColumnName($customvar)
{
foreach ($this->columnMap as $column => $results) {
if (isset($results[$name])) {
return $results[$name];
}
}
return $this->customVars[$customvar] . '.varvalue';
}
return null;
public function aliasToColumnName($alias)
{
return $this->idxAliasColumn[$alias];
}
protected function createSubQuery($queryName, $columns = array())
{
$class = '\\'
. substr(__CLASS__, 0, strrpos(__CLASS__, '\\') + 1)
. ucfirst($queryName) . 'Query';
$query = new $class($this->ds, $columns);
return $query;
}
}

View File

@ -1,49 +1,73 @@
<?php
// {{{ICINGA_LICENSE_HEADER}}}
/**
* This file is part of Icinga 2 Web.
*
* Icinga 2 Web - Head for multiple monitoring backends.
* Copyright (C) 2013 Icinga Development Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* @copyright 2013 Icinga Development Team <info@icinga.org>
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
* @author Icinga Development Team <info@icinga.org>
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\Backend\Ido\Query;
class HoststatusQuery extends AbstractQuery
{
protected $allowCustomVars = true;
protected $columnMap = array(
'hosts' => array(
'host' => 'ho.name1 COLLATE latin1_general_ci',
'host_name' => 'ho.name1 COLLATE latin1_general_ci',
'host_display_name' => 'h.display_name',
'host_alias' => 'h.alias',
'host_address' => 'h.address',
'host_ipv4' => 'INET_ATON(h.address)',
'host_icon_image' => 'h.icon_image',
'host' => 'ho.name1 COLLATE latin1_general_ci',
'host_name' => 'ho.name1 COLLATE latin1_general_ci',
'host_display_name' => 'h.display_name',
'host_alias' => 'h.alias',
'host_address' => 'h.address',
'host_ipv4' => 'INET_ATON(h.address)',
'host_icon_image' => 'h.icon_image',
),
'hoststatus' => array(
'problems' => 'CASE WHEN hs.current_state = 0 THEN 0 ELSE 1 END',
'handled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) > 0 THEN 1 ELSE 0 END',
'unhandled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) = 0 THEN 1 ELSE 0 END',
'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END',
'host_output' => 'hs.output',
'host_long_output' => 'hs.long_output',
'host_perfdata' => 'hs.perfdata',
'host_problem' => 'CASE WHEN hs.current_state = 0 THEN 0 ELSE 1 END',
'host_acknowledged' => 'hs.problem_has_been_acknowledged',
'host_in_downtime' => 'CASE WHEN (hs.scheduled_downtime_depth = 0) THEN 0 ELSE 1 END',
'host_handled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) > 0 THEN 1 ELSE 0 END',
'host_does_active_checks' => 'hs.active_checks_enabled',
'problems' => 'CASE WHEN hs.current_state = 0 THEN 0 ELSE 1 END',
'handled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) > 0 THEN 1 ELSE 0 END',
'unhandled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) = 0 THEN 1 ELSE 0 END',
'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END',
'host_output' => 'hs.output',
'host_long_output' => 'hs.long_output',
'host_perfdata' => 'hs.perfdata',
'host_problem' => 'CASE WHEN hs.current_state = 0 THEN 0 ELSE 1 END',
'host_acknowledged' => 'hs.problem_has_been_acknowledged',
'host_in_downtime' => 'CASE WHEN (hs.scheduled_downtime_depth = 0) THEN 0 ELSE 1 END',
'host_handled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) > 0 THEN 1 ELSE 0 END',
'host_does_active_checks' => 'hs.active_checks_enabled',
'host_accepts_passive_checks' => 'hs.passive_checks_enabled',
'host_last_state_change' => 'UNIX_TIMESTAMP(hs.last_state_change)',
'host_last_hard_state' => 'hs.last_hard_state',
'host_check_command' => 'hs.check_command',
'host_last_check' => 'UNIX_TIMESTAMP(hs.last_check)',
'host_next_check' => 'CASE WHEN hs.should_be_scheduled THEN UNIX_TIMESTAMP(hs.next_check) ELSE NULL END',
'host_check_execution_time' => 'hs.execution_time',
'host_check_latency' => 'hs.latency',
'host_notifications_enabled' => 'hs.notifications_enabled',
'host_last_time_up' => 'hs.last_time_up',
'host_last_time_down' => 'hs.last_time_down',
'host_last_time_unreachable' => 'hs.last_time_unreachable',
'host_current_check_attempt' => 'hs.current_check_attempt',
'host_max_check_attempts' => 'hs.max_check_attempts',
'host_last_state_change' => 'UNIX_TIMESTAMP(hs.last_state_change)',
'host_last_hard_state' => 'hs.last_hard_state',
'host_check_command' => 'hs.check_command',
'host_last_check' => 'UNIX_TIMESTAMP(hs.last_check)',
'host_next_check' => 'CASE WHEN hs.should_be_scheduled THEN UNIX_TIMESTAMP(hs.next_check) ELSE NULL END',
'host_check_execution_time' => 'hs.execution_time',
'host_check_latency' => 'hs.latency',
'host_notifications_enabled' => 'hs.notifications_enabled',
'host_last_time_up' => 'hs.last_time_up',
'host_last_time_down' => 'hs.last_time_down',
'host_last_time_unreachable' => 'hs.last_time_unreachable',
'host_current_check_attempt' => 'hs.current_check_attempt',
'host_max_check_attempts' => 'hs.max_check_attempts',
'host_severity' => 'CASE WHEN hs.current_state = 0
THEN
CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL
@ -86,37 +110,35 @@ class HoststatusQuery extends AbstractQuery
'contact' => 'hco.name1 COLLATE latin1_general_ci',
),
'services' => array(
'services_cnt' => 'SUM(1)',
'services_ok' => 'SUM(CASE WHEN ss.current_state = 0 THEN 1 ELSE 0 END)',
'services_warning' => 'SUM(CASE WHEN ss.current_state = 1 THEN 1 ELSE 0 END)',
'services_cnt' => 'SUM(1)',
'services_ok' => 'SUM(CASE WHEN ss.current_state = 0 THEN 1 ELSE 0 END)',
'services_warning' => 'SUM(CASE WHEN ss.current_state = 1 THEN 1 ELSE 0 END)',
'services_critical' => 'SUM(CASE WHEN ss.current_state = 2 THEN 1 ELSE 0 END)',
'services_unknown' => 'SUM(CASE WHEN ss.current_state = 3 THEN 1 ELSE 0 END)',
'services_pending' => 'SUM(CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 1 ELSE 0 END)',
'services_problem' => 'SUM(CASE WHEN ss.current_state > 0 THEN 1 ELSE 0 END)',
'services_problem_handled' => 'SUM(CASE WHEN ss.current_state > 0 AND (ss.problem_has_been_acknowledged = 1 OR ss.scheduled_downtime_depth > 0) THEN 1 ELSE 0 END)',
'services_problem_unhandled' => 'SUM(CASE WHEN ss.current_state > 0 AND (ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0) THEN 1 ELSE 0 END)',
'services_warning_handled' => 'SUM(CASE WHEN ss.current_state = 1 AND (ss.problem_has_been_acknowledged = 1 OR ss.scheduled_downtime_depth > 0) THEN 1 ELSE 0 END)',
'services_unknown' => 'SUM(CASE WHEN ss.current_state = 3 THEN 1 ELSE 0 END)',
'services_pending' => 'SUM(CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 1 ELSE 0 END)',
'services_problem' => 'SUM(CASE WHEN ss.current_state > 0 THEN 1 ELSE 0 END)',
'services_problem_handled' => 'SUM(CASE WHEN ss.current_state > 0 AND (ss.problem_has_been_acknowledged = 1 OR ss.scheduled_downtime_depth > 0) THEN 1 ELSE 0 END)',
'services_problem_unhandled' => 'SUM(CASE WHEN ss.current_state > 0 AND (ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0) THEN 1 ELSE 0 END)',
'services_warning_handled' => 'SUM(CASE WHEN ss.current_state = 1 AND (ss.problem_has_been_acknowledged = 1 OR ss.scheduled_downtime_depth > 0) THEN 1 ELSE 0 END)',
'services_critical_handled' => 'SUM(CASE WHEN ss.current_state = 2 AND (ss.problem_has_been_acknowledged = 1 OR ss.scheduled_downtime_depth > 0) THEN 1 ELSE 0 END)',
'services_unknown_handled' => 'SUM(CASE WHEN ss.current_state = 3 AND (ss.problem_has_been_acknowledged = 1 OR ss.scheduled_downtime_depth > 0) THEN 1 ELSE 0 END)',
'services_warning_unhandled' => 'SUM(CASE WHEN ss.current_state = 1 AND (ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0) THEN 1 ELSE 0 END)',
'services_unknown_handled' => 'SUM(CASE WHEN ss.current_state = 3 AND (ss.problem_has_been_acknowledged = 1 OR ss.scheduled_downtime_depth > 0) THEN 1 ELSE 0 END)',
'services_warning_unhandled' => 'SUM(CASE WHEN ss.current_state = 1 AND (ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0) THEN 1 ELSE 0 END)',
'services_critical_unhandled' => 'SUM(CASE WHEN ss.current_state = 2 AND (ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0) THEN 1 ELSE 0 END)',
'services_unknown_unhandled' => 'SUM(CASE WHEN ss.current_state = 3 AND (ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0) THEN 1 ELSE 0 END)',
'services_unknown_unhandled' => 'SUM(CASE WHEN ss.current_state = 3 AND (ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0) THEN 1 ELSE 0 END)',
),
);
protected $aggregateColumnIdx = array(
'services_cnt' => true,
'services_problem' => true,
'services_problem_handled' => true,
'services_cnt' => true,
'services_problem' => true,
'services_problem_handled' => true,
'services_problem_unhandled' => true,
);
protected $hcgSub;
protected function getDefaultColumns()
{
return $this->columnMap['hosts']
+ $this->columnMap['hoststatus'];
+ $this->columnMap['hoststatus'];
}
protected function joinBaseTables()
@ -126,16 +148,16 @@ class HoststatusQuery extends AbstractQuery
array('ho' => $this->prefix . 'objects'),
array()
)->join(
array('hs' => $this->prefix . 'hoststatus'),
'ho.' . $this->object_id . ' = hs.host_object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
array()
)->join(
array('h' => $this->prefix . 'hosts'),
'hs.host_object_id = h.host_object_id',
array()
);
array('hs' => $this->prefix . 'hoststatus'),
'ho.' . $this->object_id . ' = hs.host_object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
array()
)->join(
array('h' => $this->prefix . 'hosts'),
'hs.host_object_id = h.host_object_id',
array()
);
$this->joinedVirtualTables = array(
'hosts' => true,
'hosts' => true,
'hoststatus' => true,
);
}
@ -157,14 +179,14 @@ class HoststatusQuery extends AbstractQuery
's.host_object_id = h.host_object_id',
array()
)->join(
array('so' => $this->prefix . 'objects'),
"so.$this->object_id = s.service_object_id AND so.is_active = 1",
array()
)->joinLeft(
array('ss' => $this->prefix . 'servicestatus'),
"so.$this->object_id = ss.service_object_id",
array()
);
array('so' => $this->prefix . 'objects'),
"so.$this->object_id = s.service_object_id AND so.is_active = 1",
array()
)->joinLeft(
array('ss' => $this->prefix . 'servicestatus'),
"so.$this->object_id = ss.service_object_id",
array()
);
foreach ($this->columns as $col) {
$real = $this->aliasToColumnName($col);
if (substr($real, 0, 4) === 'SUM(') {
@ -184,25 +206,65 @@ class HoststatusQuery extends AbstractQuery
}
}
protected function joinServiceHostgroups()
{
$this->baseQuery->join(
array('hgm' => $this->prefix . 'hostgroup_members'),
'hgm.host_object_id = s.host_object_id',
array()
)->join(
array('hg' => $this->prefix . 'hostgroups'),
'hgm.hostgroup_id = hg.' . $this->hostgroup_id,
array()
)->join(
array('hgo' => $this->prefix . 'objects'),
'hgo.' . $this->object_id . ' = hg.hostgroup_object_id'
. ' AND hgo.is_active = 1',
array()
);
return $this;
}
protected function joinHostHostgroups()
{
$this->baseQuery->join(
array('hgm' => $this->prefix . 'hostgroup_members'),
'hgm.host_object_id = h.host_object_id',
array()
)->join(
array('hg' => $this->prefix . 'hostgroups'),
"hgm.hostgroup_id = hg.$this->hostgroup_id",
array()
)->join(
array('hgo' => $this->prefix . 'objects'),
'hgo.' . $this->object_id . ' = hg.hostgroup_object_id'
. ' AND hgo.is_active = 1',
array()
);
return $this;
}
protected function joinContacts()
{
$this->hcgcSub = $this->db->select()->distinct()->from(
array('hcgc' => $this->prefix . 'host_contactgroups'),
array('host_name' => 'ho.name1')
)->join(
array('cgo' => $this->prefix . 'objects'),
'hcg.contactgroup_object_id = cgo.' . $this->object_id
. ' AND cgo.is_active = 1',
array()
)->join(
array('h' => $this->prefix . 'hosts'),
'hcg.host_id = h.host_id',
array()
)->join(
array('ho' => $this->prefix . 'objects'),
'h.host_object_id = ho.' . $this->object_id . ' AND ho.is_active = 1',
array()
);
array('cgo' => $this->prefix . 'objects'),
'hcg.contactgroup_object_id = cgo.' . $this->object_id
. ' AND cgo.is_active = 1',
array()
)->join(
array('h' => $this->prefix . 'hosts'),
'hcg.host_id = h.host_id',
array()
)->join(
array('ho' => $this->prefix . 'objects'),
'h.host_object_id = ho.' . $this->object_id . ' AND ho.is_active = 1',
array()
);
$this->baseQuery->join(
array('hcg' => $this->hcgSub),
'hcg.host_name = ho.name1',
@ -212,48 +274,6 @@ class HoststatusQuery extends AbstractQuery
return $this;
}
/*
protected function joinContacts()
{
$this->baseQuery->join(
array('hc' => $this->prefix . 'host_contacts'),
'hc.host_id = h.host_id',
array()
)->join(
array('hco' => $this->prefix . 'objects'),
'hco.' . $this->object_id. ' = hc.contact_object_id'
. ' AND hco.is_active = 1',
array()
);
$this->baseQuery->join(
array('hcg' => $this->prefix . 'host_contactgroups'),
'hcg.host_id = h.host_id',
array()
)->join(
array('hcgo' => $this->prefix . 'objects'),
'hcgo.' . $this->object_id. ' = hcg.contactgroup_object_id'
. ' AND hcgo.is_active = 1',
array()
);
$this->baseQuery->join(
array('cgm' => $this->prefix . 'contactgroup_members'),
'cgm.contactgroup_id = cg.contactgroup_id',
array()
)->join(
array('co' => $this->prefix . 'objects'),
'cgm.contact_object_id = co.object_id AND co.is_active = 1',
array()
);
}
return $this;
}
*/
protected function filterContactgroup($value)
{
$this->hcgSub->where(
@ -265,28 +285,6 @@ class HoststatusQuery extends AbstractQuery
return $this;
}
protected function createContactgroupFilterSubselect()
{
die((string) $this->db->select()->distinct()->from(
array('hcg' => $this->prefix . 'host_contactgroups'),
array('object_id' => 'ho.object_id')
)->join(
array('cgo' => $this->prefix . 'objects'),
'hcg.contactgroup_object_id = cgo.' . $this->object_id
. ' AND cgo.is_active = 1',
array()
)->join(
array('h' => $this->prefix . 'hosts'),
'hcg.host_id = h.host_id',
array()
)->join(
array('ho' => $this->prefix . 'objects'),
'h.host_object_id = ho.' . $this->object_id . ' AND ho.is_active = 1',
array()
));
}
protected function joinContactgroups()
{
$this->hcgSub = $this->createContactgroupFilterSubselect();
@ -299,44 +297,25 @@ class HoststatusQuery extends AbstractQuery
return $this;
}
protected function joinHostHostgroups()
protected function createContactgroupFilterSubselect()
{
$this->baseQuery->join(
array('hgm' => $this->prefix . 'hostgroup_members'),
'hgm.host_object_id = h.host_object_id',
array()
die((string)$this->db->select()->distinct()->from(
array('hcg' => $this->prefix . 'host_contactgroups'),
array('object_id' => 'ho.object_id')
)->join(
array('hg' => $this->prefix . 'hostgroups'),
"hgm.hostgroup_id = hg.$this->hostgroup_id",
array()
)->join(
array('hgo' => $this->prefix . 'objects'),
'hgo.' . $this->object_id. ' = hg.hostgroup_object_id'
. ' AND hgo.is_active = 1',
array()
);
return $this;
}
protected function joinServiceHostgroups()
{
$this->baseQuery->join(
array('hgm' => $this->prefix . 'hostgroup_members'),
'hgm.host_object_id = s.host_object_id',
array()
)->join(
array('hg' => $this->prefix . 'hostgroups'),
'hgm.hostgroup_id = hg.' . $this->hostgroup_id,
array()
)->join(
array('hgo' => $this->prefix . 'objects'),
'hgo.' . $this->object_id. ' = hg.hostgroup_object_id'
. ' AND hgo.is_active = 1',
array()
);
return $this;
array('cgo' => $this->prefix . 'objects'),
'hcg.contactgroup_object_id = cgo.' . $this->object_id
. ' AND cgo.is_active = 1',
array()
)->join(
array('h' => $this->prefix . 'hosts'),
'hcg.host_id = h.host_id',
array()
)->join(
array('ho' => $this->prefix . 'objects'),
'h.host_object_id = ho.' . $this->object_id . ' AND ho.is_active = 1',
array()
));
}
protected function joinServicegroups()
@ -348,15 +327,15 @@ class HoststatusQuery extends AbstractQuery
'sgm.service_object_id = s.service_object_id',
array()
)->join(
array('sg' => $this->prefix . 'servicegroups'),
'sgm.servicegroup_id = sg.' . $this->servicegroup_id,
array()
)->join(
array('sgo' => $this->prefix . 'objects'),
'sgo.' . $this->object_id. ' = sg.servicegroup_object_id'
. ' AND sgo.is_active = 1',
array()
);
array('sg' => $this->prefix . 'servicegroups'),
'sgm.servicegroup_id = sg.' . $this->servicegroup_id,
array()
)->join(
array('sgo' => $this->prefix . 'objects'),
'sgo.' . $this->object_id . ' = sg.servicegroup_object_id'
. ' AND sgo.is_active = 1',
array()
);
return $this;
}

View File

@ -4,7 +4,7 @@ namespace \Icinga\Module\Monitoring\Backend\Livestatus\Query;
use Icinga\Data\AbstractQuery;
class StatusQuery extends AbstractQuery
class StatusQuery extends AbstractQuery implements Filterable
{
protected $available_columns = array(
'host_name',

View File

@ -28,17 +28,19 @@
namespace Icinga\Module\Monitoring\Backend\Statusdat\Query;
use \Icinga\Module\Monitoring\Backend\Statusdat\Criteria\Order;
use Icinga\Filter\Query\Tree;
use Icinga\Protocol\Statusdat;
use Icinga\Exception;
use Icinga\Data\AbstractQuery;
use Icinga\Protocol\Statusdat\View\MonitoringObjectList as MList;
use Icinga\Protocol\Statusdat\Query as StatusdatQuery;
use Icinga\Filter\Filterable;
/**
* Class Query
* @package Icinga\Backend\Statusdat
*/
abstract class Query extends AbstractQuery
abstract class Query extends AbstractQuery implements Filterable
{
/**
* @var null
@ -284,7 +286,22 @@ abstract class Query extends AbstractQuery
*/
public function count()
{
return count($this->baseQuery->getResult());
}
public function isValidFilterTarget($field)
{
// TODO: Implement isValidFilterTarget() method.
}
public function getMappedField($field)
{
// TODO: Implement getMappedField() method.
}
public function applyFilter(Tree $filter)
{
// TODO: Implement applyFilter() method.
}
}

View File

@ -5,14 +5,25 @@
namespace Icinga\Module\Monitoring\DataView;
use Icinga\Data\AbstractQuery;
use Icinga\Filter\Filterable;
use Icinga\Filter\Query\Tree;
use Icinga\Module\Monitoring\Backend;
use Icinga\Module\Monitoring\Filter\UrlViewFilter;
use Icinga\Web\Request;
/**
* A read-only view of an underlying Query
*/
abstract class DataView
abstract class DataView implements Filterable
{
/**
* Sort in ascending order, default
*/
const SORT_ASC = AbstractQuery::SORT_ASC;
/**
* Sort in reverse order
*/
const SORT_DESC = AbstractQuery::SORT_DESC;
/**
* The query used to populate the view
*
@ -20,25 +31,17 @@ abstract class DataView
*/
private $query;
/**
* Sort in ascending order, default
*/
const SORT_ASC = AbstractQuery::SORT_ASC;
/**
* Sort in reverse order
*/
const SORT_DESC = AbstractQuery::SORT_DESC;
/**
* Create a new view
*
* @param Backend $ds Which backend to query
* @param array $columns Select columns
* @param Backend $ds Which backend to query
* @param array $columns Select columns
*/
public function __construct(Backend $ds, array $columns = null)
{
$filter = new UrlViewFilter($this);
$this->query = $ds->select()->from(static::getTableName(), $columns === null ? $this->getColumns() : $columns);
$this->applyFilter($filter->parseUrl());
}
/**
@ -60,23 +63,16 @@ abstract class DataView
*/
abstract public function getColumns();
/**
* Retrieve default sorting rules for particular columns. These involve sort order and potential additional to sort
*
* @return array
*/
abstract public function getSortRules();
public function getFilterColumns()
public function applyFilter(Tree $filter)
{
return array();
return $this->query->applyFilter($filter);
}
/**
* Create view from request
*
* @param Request $request
* @param array $columns
* @param array $columns
*
* @return static
*/
@ -102,8 +98,8 @@ abstract class DataView
/**
* Create view from params
*
* @param array $params
* @param array $columns
* @param array $params
* @param array $columns
*
* @return static
*/
@ -131,12 +127,12 @@ abstract class DataView
*
* @param array $filters
*
* @see isValidFilterColumn()
* @see Filterable::isValidFilterTarget()
*/
public function filter(array $filters)
{
foreach ($filters as $column => $filter) {
if ($this->isValidFilterColumn($column)) {
if ($this->isValidFilterTarget($column)) {
$this->query->where($column, $filter);
}
}
@ -150,16 +146,21 @@ abstract class DataView
*
* @return bool
*/
public function isValidFilterColumn($column)
public function isValidFilterTarget($column)
{
return in_array($column, $this->getColumns()) || in_array($column, $this->getFilterColumns());
}
public function getFilterColumns()
{
return array();
}
/**
* Sort the rows, according to the specified sort column and order
*
* @param string $column Sort column
* @param int $order Sort order, one of the SORT_ constants
* @param string $column Sort column
* @param int $order Sort order, one of the SORT_ constants
*
* @see DataView::SORT_ASC
* @see DataView::SORT_DESC
@ -180,8 +181,8 @@ abstract class DataView
}
} else {
$sortColumns = array(
'columns' => array($column),
'order' => $order
'columns' => array($column),
'order' => $order
);
};
}
@ -191,6 +192,18 @@ abstract class DataView
}
}
/**
* Retrieve default sorting rules for particular columns. These involve sort order and potential additional to sort
*
* @return array
*/
abstract public function getSortRules();
public function getMappedField($field)
{
return $this->query->getMappedField($field);
}
/**
* Return the query which was created in the constructor
*
@ -200,4 +213,9 @@ abstract class DataView
{
return $this->query;
}
public function getFilterDomain()
{
return null;
}
}

View File

@ -0,0 +1,101 @@
<?php
// {{{ICINGA_LICENSE_HEADER}}}
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\DataView;
use Icinga\Module\Monitoring\Filter\MonitoringFilter;
class HostStatus extends DataView
{
/**
* Retrieve columns provided by this view
*
* @return array
*/
public function getColumns()
{
return array(
'host_name',
'host_state',
'host_state_type',
'host_last_state_change',
'host_address',
'host_handled',
'host_icon_image',
'host_acknowledged',
'host_output',
'host_long_output',
'host_in_downtime',
'host_is_flapping',
'host_last_check',
'host_next_check',
'host_notifications_enabled',
'host_unhandled_service_count',
'host_action_url',
'host_notes_url',
'host_last_comment',
'host',
'host_display_name',
'host_alias',
'host_ipv4',
'host_severity',
'host_perfdata',
'host_does_active_checks',
'host_accepts_passive_checks',
'host_last_hard_state',
'host_last_hard_state_change',
'host_last_time_up',
'host_last_time_down',
'host_last_time_unreachable'
);
}
public static function getTableName()
{
return 'status';
}
public function getSortRules()
{
return array(
'host_name' => array(
'order' => self::SORT_ASC
),
'host_address' => array(
'columns' => array(
'host_ipv4',
'service_description'
),
'order' => self::SORT_ASC
),
'host_last_state_change' => array(
'order' => self::SORT_ASC
),
'host_severity' => array(
'columns' => array(
'host_severity',
'host_last_state_change',
),
'order' => self::SORT_ASC
)
);
}
public function getFilterColumns()
{
return array('hostgroups', 'servicegroups', 'service_problems');
}
public function isValidFilterTarget($column)
{
if ($column[0] === '_'
&& preg_match('/^_(?:host|service)_/', $column)
) {
return true;
}
return parent::isValidFilterTarget($column);
}
}

View File

@ -4,7 +4,7 @@
namespace Icinga\Module\Monitoring\DataView;
class HostAndServiceStatus extends DataView
class ServiceStatus extends DataView
{
/**
* Retrieve columns provided by this view
@ -54,7 +54,6 @@ class HostAndServiceStatus extends DataView
'host_display_name',
'host_alias',
'host_ipv4',
// 'host_problems',
'host_severity',
'host_perfdata',
'host_does_active_checks',
@ -65,7 +64,6 @@ class HostAndServiceStatus extends DataView
'host_last_time_down',
'host_last_time_unreachable',
'service',
// 'current_state',
'service_hard_state',
'service_perfdata',
'service_does_active_checks',
@ -78,13 +76,11 @@ class HostAndServiceStatus extends DataView
'service_last_time_unknown',
'service_current_check_attempt',
'service_max_check_attempts'
// 'object_type',
// 'problems',
// 'handled',
// 'severity'
);
}
public static function getTableName()
{
return 'status';
@ -121,13 +117,13 @@ class HostAndServiceStatus extends DataView
return array('hostgroups', 'servicegroups', 'service_problems');
}
public function isValidFilterColumn($column)
public function isValidFilterTarget($column)
{
if ($column[0] === '_'
&& preg_match('/^_(?:host|service)_/', $column)
) {
return true;
}
return parent::isValidFilterColumn($column);
return parent::isValidFilterTarget($column);
}
}

View File

@ -26,33 +26,50 @@
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\Filter\Backend;
use Icinga\Data\DatasourceInterface;
use Icinga\Data\Db\Query;
use Icinga\Filter\Query\Tree;
use Icinga\Filter\Query\Node;
use Icinga\Filter\Filterable;
use Icinga\Module\Monitoring\DataView\DataView;
use Icinga\Module\Monitoring\Backend\Ido\Query\AbstractQuery;
/**
* Converter class that takes a query tree and creates an SQL Query from it's state
*/
class IdoQueryConverter
{
private $view;
/**
* The query class to use as the base for converting
*
* @var AbstractQuery
*/
private $query;
private $params = array();
public function getParams()
/**
* The type of the filter (WHERE or HAVING, depending whether it's an aggregate query)
* @var string
*/
private $type = 'WHERE';
/**
* Create a new converter from this query
*
* @param AbstractQuery $query The query to use for conversion
*/
public function __construct(AbstractQuery $query)
{
return $this->params;
}
public function __construct(DataView $view, array $initialParams = array())
{
$this->view = $view;
$this->query = $this->view->getQuery();
$this->params = $initialParams;
$this->query = $query;
}
/**
* Return the SQL equivalent fo the given text operator
*
* @param String $operator The operator from the query node
* @return string The operator for the sql query part
*/
private function getSqlOperator($operator)
{
switch($operator) {
@ -65,6 +82,12 @@ class IdoQueryConverter
}
}
/**
* Convert a Query Tree node to an sql string
*
* @param Node $node The node to convert
* @return string The sql string representing the node's state
*/
private function nodeToSqlQuery(Node $node)
{
if ($node->type !== Node::TYPE_OPERATOR) {
@ -74,6 +97,12 @@ class IdoQueryConverter
}
}
/**
* Parse an AND or OR node to an sql string
*
* @param Node $node The AND/OR node to parse
* @return string The sql string representing this node
*/
private function parseConjunctionNode(Node $node)
{
$queryString = '';
@ -88,33 +117,78 @@ class IdoQueryConverter
return $queryString;
}
/**
* Parse an operator node to an sql string
*
* @param Node $node The operator node to parse
* @return string The sql string representing this node
*/
private function parseOperatorNode(Node $node)
{
if (!$this->view->isValidFilterColumn($node->left) && $this->query->getMappedColumn($node->left)) {
if (!$this->query->isValidFilterTarget($node->left) && $this->query->getMappedField($node->left)) {
return '';
}
$queryString = $this->query->getMappedColumn($node->left);
$queryString .= ' ' . (is_integer($node->right) ? $node->operator : $this->getSqlOperator($node->operator));
$queryString .= ' ? ';
$this->params[] = $this->getParameterValue($node);
$queryString = $this->query->getMappedField($node->left);
if ($this->query->isAggregateColumn($node->left)) {
$this->type = 'HAVING';
}
$queryString .= ' ' . (is_integer($node->right) ? $node->operator : $this->getSqlOperator($node->operator)) . ' ';
$queryString .= $this->getParameterValue($node);
return $queryString;
}
/**
* Convert a node value to it's sql equivalent
*
* This currently only detects if the node is in the timestring context and calls strtotime if so and it replaces
* '*' with '%'
*
* @param Node $node The node to retrieve the sql string value from
* @return String|int The converted and quoted value
*/
private function getParameterValue(Node $node) {
$value = $node->right;
if ($node->operator === Node::OPERATOR_EQUALS || $node->operator === Node::OPERATOR_EQUALS_NOT) {
$value = str_replace('*', '%', $value);
}
if ($this->query->isTimestamp($node->left)) {
$node->context = Node::CONTEXT_TIMESTRING;
}
switch($node->context) {
case Node::CONTEXT_TIMESTRING:
return strtotime($node->right);
$value = strtotime($value);
default:
return $node->right;
break;
}
return $this->query->getDatasource()->getConnection()->quote($value);
}
/**
* Apply the given tree to the query, either as where or as having clause
*
* @param Tree $tree The tree representing the filter
* @param \Zend_Db_Select $baseQuery The query to apply the filter on
*/
public function treeToSql(Tree $tree, $baseQuery)
{
if ($tree->root == null) {
return;
}
$sql = $this->nodeToSqlQuery($tree->root);
if ($this->filtersAggregate()) {
$baseQuery->having($sql);
} else {
$baseQuery->where($sql);
}
}
public function treeToSql(Tree $tree)
/**
* Return true if this is an filter that should be applied after aggregation
*
* @return bool True when having should be used, otherwise false
*/
private function filtersAggregate()
{
if ($tree->root == null) {
return '';
}
return $this->nodeToSqlQuery($tree->root);
return $this->type === 'HAVING';
}
}
}

View File

@ -26,7 +26,6 @@
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\Filter;
use Icinga\Filter\Domain;
@ -41,11 +40,9 @@ use Icinga\Module\Monitoring\Filter\Type\StatusFilter;
* Factory class to create filter for different monitoring objects
*
*/
class MonitoringFilter
class Registry
{
private static function getNextCheckFilterType()
public static function getNextCheckFilterType()
{
$type = new TimeRangeSpecifier();
$type->setOperator(
@ -57,7 +54,7 @@ class MonitoringFilter
return $type;
}
private static function getLastCheckFilterType()
public static function getLastCheckFilterType()
{
$type = new TimeRangeSpecifier();
$type->setOperator(
@ -79,30 +76,31 @@ class MonitoringFilter
FilterAttribute::create(new TextFilter())
->setHandledAttributes('Name', 'Hostname')
->setField('host_name')
)->registerAttribute(
FilterAttribute::create(StatusFilter::createForHost())
->setHandledAttributes('State', 'Status', 'Current Status')
->setField('host_state')
)->registerAttribute(
FilterAttribute::create(new BooleanFilter(array(
'host_is_flapping' => 'Flapping',
'host_problem' => 'In Problem State',
'host_notifications_enabled' => 'Sending Notifications',
'host_active_checks_enabled' => 'Active',
'host_passive_checks_enabled' => 'Accepting Passive Checks',
'host_handled' => 'Handled',
'host_in_downtime' => 'In Downtime',
)))
)->registerAttribute(
FilterAttribute::create(self::getLastCheckFilterType())
->setHandledAttributes('Last Check', 'Check')
->setField('host_last_check')
)->registerAttribute(
FilterAttribute::create(self::getNextCheckFilterType())
->setHandledAttributes('Next Check')
->setField('host_next_check')
);
)->registerAttribute(
FilterAttribute::create(StatusFilter::createForHost())
->setHandledAttributes('State', 'Status', 'Current Status')
->setField('host_state')
)->registerAttribute(
FilterAttribute::create(new BooleanFilter(
array(
'host_is_flapping' => 'Flapping',
'host_problem' => 'In Problem State',
'host_notifications_enabled' => 'Sending Notifications',
'host_active_checks_enabled' => 'Active',
'host_passive_checks_enabled' => 'Accepting Passive Checks',
'host_handled' => 'Handled',
'host_in_downtime' => 'In Downtime',
)
))
)->registerAttribute(
FilterAttribute::create(self::getLastCheckFilterType())
->setHandledAttributes('Last Check', 'Check')
->setField('host_last_check')
)->registerAttribute(
FilterAttribute::create(self::getNextCheckFilterType())
->setHandledAttributes('Next Check')
->setField('host_next_check')
);
return $domain;
}
}
}

View File

@ -318,4 +318,4 @@ class StatusFilter extends FilterType
{
$this->baseStates = $states;
}
}
}

View File

@ -30,11 +30,15 @@
namespace Icinga\Module\Monitoring\Filter;
use Icinga\Filter\Filterable;
use Icinga\Filter\Query\Tree;
use Icinga\Filter\Query\Node;
use Icinga\Web\Url;
use Icinga\Application\Logger;
/**
* Converter class that allows to create Query Trees from an request query and vice versa
*/
class UrlViewFilter
{
const FILTER_TARGET = 'target';
@ -42,27 +46,54 @@ class UrlViewFilter
const FILTER_VALUE = 'value';
const FILTER_ERROR = 'error';
private function evaluateNode(Node $node)
{
switch($node->type) {
/**
* An optional target filterable to use for validation and normalization
*
* @var Filterable
*/
private $target;
case Node::TYPE_OPERATOR:
return urlencode($node->left) . $node->operator . urlencode($node->right);
case Node::TYPE_AND:
return $this->evaluateNode($node->left) . '&' . $this->evaluateNode($node->right);
case Node::TYPE_OR:
return $this->evaluateNode($node->left) . '|' . $this->evaluateNode($node->right);
}
/**
* Create a new ViewFilter
*
* @param Filterable $target An optional Filterable to use for validation and normalization
*/
public function __construct(Filterable $target = null)
{
$this->target = $target;
}
/**
* Return an URL filter string for the given query tree
*
* @param Tree $filter The query tree to parse
* @return null|string The string representation of the query
*/
public function fromTree(Tree $filter)
{
return $this->evaluateNode($filter->root);
if ($filter->root === null) {
return '';
}
if ($this->target) {
$filter = $filter->getCopyForFilterable($this->target);
}
return $this->convertNodeToUrlString($filter->root);
}
public function parseUrl($query = "")
/**
* Parse the given given url and return a query tree
*
* @param string $query The query to parse, if not given $_SERVER['QUERY_STRING'] is used
* @return Tree A tree representing the valid parts of the filter
*/
public function parseUrl($query = '')
{
if (!isset($_SERVER['QUERY_STRING'])) {
$_SERVER['QUERY_STRING'] = $query;
}
$query = $query ? $query : $_SERVER['QUERY_STRING'];
$tokens = $this->tokenizeQuery($query);
$tree = new Tree();
foreach ($tokens as $token) {
@ -80,18 +111,70 @@ class UrlViewFilter
);
}
}
return $tree;
return $tree->getCopyForFilterable($this->target);
}
/**
* Convert a tree node and it's subnodes to a request string
*
* @param Node $node The node to convert
* @return null|string A string representing the node in the url form or null if it's invalid
* ( or if the Filterable doesn't support the attribute)
*/
private function convertNodeToUrlString(Node $node)
{
$left = null;
$right = null;
if ($node->type === Node::TYPE_OPERATOR) {
if ($this->target && !$this->target->isValidFilterTarget($node->left)) {
return null;
}
return urlencode($node->left) . $node->operator . urlencode($node->right);
}
if ($node->left) {
$left = $this->convertNodeToUrlString($node->left);
}
if ($node->right) {
$right = $this->convertNodeToUrlString($node->right);
}
if ($left && !$right) {
return null;
} elseif ($right && !$left) {
return $this->convertNodeToUrlString($node->right);
} elseif (!$left && !$right) {
return null;
}
$operator = ($node->type === Node::TYPE_AND) ? '&' : '|';
return $left . $operator . $right;
}
/**
* Split the query into seperate tokens that can be parsed seperately
*
* Tokens are associative arrays in the following form
*
* array(
* self::FILTER_TARGET => 'Attribute',
* self::FILTER_OPERATOR => '!=',
* self::FILTER_VALUE => 'Value'
* )
*
* @param String $query The query to tokenize
* @return array An array of tokens
*
* @see self::parseTarget() The tokenize function for target=value expressions
* @see self::parseValue() The tokenize function that only retrieves a value (e.g. target=value|value2)
*/
private function tokenizeQuery($query)
{
$tokens = array();
$state = self::FILTER_TARGET;
$query = urldecode($query);
for ($i = 0;$i <= strlen($query); $i++) {
for ($i = 0; $i <= strlen($query); $i++) {
switch ($state) {
case self::FILTER_TARGET:
list($i, $state) = $this->parseTarget($query, $i, $tokens);
@ -100,7 +183,7 @@ class UrlViewFilter
list($i, $state) = $this->parseValue($query, $i, $tokens);
break;
case self::FILTER_ERROR:
list($i, $state) = $this->skip($query, $i, $tokens);
list($i, $state) = $this->skip($query, $i);
break;
}
}
@ -108,6 +191,14 @@ class UrlViewFilter
return $tokens;
}
/**
* Return the operator matching the given query, or an empty string if none matches
*
* @param String $query The query to extract the operator from
* @param integer $i The offset to use in the query string
*
* @return string The operator string that matches best
*/
private function getMatchingOperator($query, $i)
{
$operatorToUse = '';
@ -118,13 +209,25 @@ class UrlViewFilter
}
}
}
return $operatorToUse;
}
/**
* Parse a new expression until the next conjunction or end and return the matching token for it
*
* @param String $query The query string to create a token from
* @param Integer $currentPos The offset to use in the query string
* @param array $tokenList The existing token list to add the token to
*
* @return array A two element array with the new offset in the beginning and the new
* parse state as the second parameter
*/
private function parseTarget($query, $currentPos, array &$tokenList)
{
$conjunctions = array('&', '|');
$i = $currentPos;
for ($i; $i < strlen($query); $i++) {
$currentChar = $query[$i];
// test if operator matches
@ -132,9 +235,9 @@ class UrlViewFilter
// Test if we're at an operator field right now, then add the current token
// without value to the tokenlist
if($operator !== '') {
if ($operator !== '') {
$tokenList[] = array(
self::FILTER_TARGET => urldecode(substr($query, $currentPos, $i - $currentPos)),
self::FILTER_TARGET => substr($query, $currentPos, $i - $currentPos),
self::FILTER_OPERATOR => $operator
);
// -1 because we're currently pointing at the first character of the operator
@ -153,7 +256,7 @@ class UrlViewFilter
if (is_array($lastState)) {
$tokenList[] = array(
self::FILTER_TARGET => urldecode($lastState[self::FILTER_TARGET]),
self::FILTER_TARGET => $lastState[self::FILTER_TARGET],
self::FILTER_OPERATOR => $lastState[self::FILTER_OPERATOR],
);
return $this->parseValue($query, $currentPos, $tokenList);
@ -165,7 +268,18 @@ class UrlViewFilter
return array($i, self::FILTER_TARGET);
}
/**
* Parse the value part of a query string, starting at current pos
*
* This expects an token without value to be placed in the tokenList stack
*
* @param String $query The query string to create a token from
* @param Integer $currentPos The offset to use in the query string
* @param array $tokenList The existing token list to add the token to
*
* @return array A two element array with the new offset in the beginning and the new
* parse state as the second parameter
*/
private function parseValue($query, $currentPos, array &$tokenList)
{
@ -190,7 +304,7 @@ class UrlViewFilter
array_pop($tokenList);
return array($currentPos, self::FILTER_TARGET);
}
$lastState[self::FILTER_VALUE] = urldecode(substr($query, $currentPos, $length));
$lastState[self::FILTER_VALUE] = substr($query, $currentPos, $length);
if (in_array($currentChar, $conjunctions)) {
$tokenList[] = $currentChar;
@ -198,7 +312,16 @@ class UrlViewFilter
return array($i, self::FILTER_TARGET);
}
private function skip($query, $currentPos, array &$tokenList)
/**
* Skip a query substring until the next conjunction appears
*
* @param String $query The query string to skip the next token
* @param Integer $currentPos The offset to use in the query string
*
* @return array A two element array with the new offset in the beginning and the new
* parse state as the second parameter
*/
private function skip($query, $currentPos)
{
$conjunctions = array('&', '|');
for ($i = $currentPos; strlen($query); $i++) {
@ -208,6 +331,4 @@ class UrlViewFilter
}
}
}
}
}

View File

@ -16,7 +16,6 @@ class HoststatusView extends AbstractView
'host_address',
'host_ipv4',
'host_icon_image',
// Hoststatus
'host_state',
'host_problem',
@ -39,7 +38,6 @@ class HoststatusView extends AbstractView
'host_last_time_unreachable',
'host_current_check_attempt',
'host_max_check_attempts',
// Services
'services_cnt',
'services_problem',
@ -65,8 +63,8 @@ class HoststatusView extends AbstractView
'columns' => array(
'host_ipv4',
'service_description'
),
'default_dir' => self::SORT_ASC
),
'default_dir' => self::SORT_ASC
),
'host_last_state_change' => array(
'default_dir' => self::SORT_DESC
@ -87,6 +85,6 @@ class HoststatusView extends AbstractView
) {
return true;
}
return parent::isValidFilterColumn($column);
return parent::isValidFilterColumn($column);
}
}

View File

@ -2,12 +2,17 @@
namespace Test\Monitoring\Application\Controllers\ListController;
require_once(dirname(__FILE__).'/../../testlib/MonitoringControllerTest.php');
require_once(dirname(__FILE__).'/../../../../library/Monitoring/DataView/DataView.php');
require_once(dirname(__FILE__).'/../../../../library/Monitoring/DataView/HostAndServiceStatus.php');
require_once(dirname(__FILE__).'/../../../../library/Monitoring/DataView/Notification.php');
require_once(dirname(__FILE__).'/../../../../library/Monitoring/DataView/Downtime.php');
require_once realpath(__DIR__ . '/../../../../../../library/Icinga/Test/BaseTestCase.php');
use Icinga\Test\BaseTestCase;
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/test/php/testlib/MonitoringControllerTest.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/Filter/Backend/IdoQueryConverter.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/DataView/DataView.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/DataView/HostStatus.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/DataView/Notification.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/DataView/Downtime.php'));
use Test\Monitoring\Testlib\MonitoringControllerTest;
use Test\Monitoring\Testlib\Datasource\TestFixture;

View File

@ -2,7 +2,16 @@
namespace Test\Monitoring\Application\Controllers\ListController;
require_once(dirname(__FILE__).'/../../testlib/MonitoringControllerTest.php');
use Icinga\Test\BaseTestCase;
require_once realpath(__DIR__ . '/../../../../../../library/Icinga/Test/BaseTestCase.php');
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/test/php/testlib/MonitoringControllerTest.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/Filter/Backend/IdoQueryConverter.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/DataView/DataView.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/DataView/ServiceStatus.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/DataView/Notification.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/DataView/Downtime.php'));
use Test\Monitoring\Testlib\MonitoringControllerTest;
use Test\Monitoring\Testlib\Datasource\TestFixture;

View File

@ -27,6 +27,8 @@
// {{{ICINGA_LICENSE_HEADER}}}
namespace Test\Modules\Monitoring\Library\Filter;
use Icinga\Filter\Filterable;
use Icinga\Filter\Query\Tree;
use Icinga\Module\Monitoring\Filter\Type\StatusFilter;
use Icinga\Filter\Type\TimeRangeSpecifier;
use Icinga\Filter\Query\Node;
@ -34,7 +36,6 @@ use Icinga\Filter\Filter;
use Icinga\Filter\Type\TextFilter;
use Icinga\Filter\FilterAttribute;
use Icinga\Module\Monitoring\Filter\UrlViewFilter;
use Icinga\Protocol\Ldap\Exception;
use Icinga\Test\BaseTestCase;
// @codingStandardsIgnoreStart
@ -51,6 +52,25 @@ require_once realpath(BaseTestCase::$libDir .'/Filter/Type/TimeRangeSpecifier.ph
require_once realpath(BaseTestCase::$moduleDir .'/monitoring/library/Monitoring/Filter/Type/StatusFilter.php');
require_once realpath(BaseTestCase::$moduleDir .'/monitoring/library/Monitoring/Filter/UrlViewFilter.php');
class FilterMock implements Filterable
{
public function isValidFilterTarget($field)
{
return true;
}
public function getMappedField($field)
{
return $field;
}
public function applyFilter(Tree $filter)
{
return true;
}
}
class UrlViewFilterTest extends BaseTestCase
{
@ -81,7 +101,7 @@ class UrlViewFilterTest extends BaseTestCase
. ' and attr5 is UP';
$tree = $searchEngine->createQueryTreeForFilter($query);
$filterFactory = new UrlViewFilter();
$filterFactory = new UrlViewFilter(new FilterMock());
$uri = $filterFactory->fromTree($tree);
$this->assertEquals(
'attr1!=Hans+wurst|attr2=%2Asomething%2A&attr3=%2Abla|attr4=1&host_last_state_change>=yesterday&attr5=0',
@ -92,7 +112,7 @@ class UrlViewFilterTest extends BaseTestCase
public function testTreeFromSimpleKeyValueUrlCreation()
{
$filterFactory = new UrlViewFilter();
$filterFactory = new UrlViewFilter(new FilterMock());
$tree = $filterFactory->parseUrl('attr1!=Hans+Wurst');
$this->assertEquals(
$tree->root->type,
@ -118,7 +138,7 @@ class UrlViewFilterTest extends BaseTestCase
public function testConjunctionFilterInUrl()
{
$filterFactory = new UrlViewFilter();
$filterFactory = new UrlViewFilter(new FilterMock());
$query = 'attr1!=Hans+Wurst&test=test123|bla=1';
$tree = $filterFactory->parseUrl($query);
$this->assertEquals($tree->root->type, Node::TYPE_AND, 'Assert the root of the filter tree to be an AND node');
@ -127,7 +147,7 @@ class UrlViewFilterTest extends BaseTestCase
public function testImplicitConjunctionInUrl()
{
$filterFactory = new UrlViewFilter();
$filterFactory = new UrlViewFilter(new FilterMock());
$query = 'attr1!=Hans+Wurst&test=test123|bla=1|2|3';
$tree = $filterFactory->parseUrl($query);
$this->assertEquals($tree->root->type, Node::TYPE_AND, 'Assert the root of the filter tree to be an AND node');
@ -140,7 +160,7 @@ class UrlViewFilterTest extends BaseTestCase
public function testMissingValuesInQueries()
{
$filterFactory = new UrlViewFilter();
$filterFactory = new UrlViewFilter(new FilterMock());
$queryStr = 'attr1!=Hans+Wurst&test=';
$tree = $filterFactory->parseUrl($queryStr);
$query = $filterFactory->fromTree($tree);
@ -149,7 +169,7 @@ class UrlViewFilterTest extends BaseTestCase
public function testErrorInQueries()
{
$filterFactory = new UrlViewFilter();
$filterFactory = new UrlViewFilter(new FilterMock());
$queryStr = 'test=&attr1!=Hans+Wurst';
$tree = $filterFactory->parseUrl($queryStr);
$query = $filterFactory->fromTree($tree);
@ -158,7 +178,7 @@ class UrlViewFilterTest extends BaseTestCase
public function testSenselessConjunctions()
{
$filterFactory = new UrlViewFilter();
$filterFactory = new UrlViewFilter(new FilterMock());
$queryStr = 'test=&|/5/|&attr1!=Hans+Wurst';
$tree = $filterFactory->parseUrl($queryStr);
$query = $filterFactory->fromTree($tree);
@ -168,7 +188,7 @@ class UrlViewFilterTest extends BaseTestCase
public function testRandomString()
{
$filter = '';
$filterFactory = new UrlViewFilter();
$filterFactory = new UrlViewFilter(new FilterMock());
for ($i=0; $i<10;$i++) {
$filter .= str_shuffle('&|ds& wra =!<>|dsgs=,-G');

View File

@ -162,6 +162,8 @@ abstract class MonitoringControllerTest extends Zend_Test_PHPUnit_ControllerTest
require_once('Data/Db/Query.php');
require_once('Exception/ProgrammingError.php');
require_once('Web/Widget/SortBox.php');
require_once('Web/Widget/FilterBox.php');
require_once('Web/Widget/FilterBadgeRenderer.php');
require_once('library/Monitoring/Backend/AbstractBackend.php');
require_once('library/Monitoring/Backend.php');

View File

@ -1,3 +1,4 @@
/*global Icinga:false, document: false, define:false require:false base_url:false console:false */
// {{{ICINGA_LICENSE_HEADER}}}
/**
* This file is part of Icinga 2 Web.
@ -24,50 +25,55 @@
* @author Icinga Development Team <info@icinga.org>
*/
// {{{ICINGA_LICENSE_HEADER}}}
/*global Icinga:false, document: false, define:false require:false base_url:false console:false */
/**
* Ensures that our date/time controls will work on every browser (natively or javascript based)
*/
define(['jquery', 'logging', 'URIjs/URI'], function($, log, URI) {
define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function($, log, URI, Container) {
'use strict';
return function(inputDOM) {
this.inputDom = $(inputDOM);
this.form = this.inputDom.parents('form').first();
this.domain = this.inputDom.attr('data-icinga-filter-domain');
this.module = this.inputDom.attr('data-icinga-filter-module');
this.form = $(this.inputDom.parents('form').first());
this.formUrl = URI(this.form.attr('action'));
this.lastTokens = [];
this.lastQueuedEvent = null;
this.pendingRequest = null;
/**
* Register the input listener
*/
this.construct = function() {
this.registerControlListener();
};
/**
* Request new proposals for the given input box
*/
this.getProposal = function() {
var text = this.inputDom.val().trim();
try {
if (this.pendingRequest) {
this.pendingRequest.abort();
}
this.pendingRequest = $.ajax({
data: {
'cache' : (new Date()).getTime(),
'query' : text
},
headers: {
'Accept': 'application/json'
},
url: this.formUrl
}).done(this.showProposals.bind(this)).fail(function() {});
} catch(exception) {
console.log(exception);
if (this.pendingRequest) {
this.pendingRequest.abort();
}
this.pendingRequest = $.ajax(this.getRequestParams(text))
.done(this.showProposals.bind(this))
.fail(this.showError.bind(this));
};
/**
* Apply a selected proposal to the text box
*
* String parts encapsulated in {} are parts that already exist in the input
*
* @param token The selected token
*/
this.applySelectedProposal = function(token) {
var currentText = $.trim(this.inputDom.val());
var substr = token.match(/^(\{.*\})/);
if (substr !== null) {
token = token.substr(substr[0].length);
@ -81,23 +87,63 @@ define(['jquery', 'logging', 'URIjs/URI'], function($, log, URI) {
this.inputDom.focus();
};
this.showProposals = function(tokens, state, args) {
var jsonRep = args.responseText;
if (tokens.length === 0) {
return this.inputDom.popover('destroy');
/**
* Display an error in the box if the request failed
*
* @param {Object} error The error response
* @param {String} state The HTTP state as a string
*/
this.showError = function(error, state) {
if (state === 'abort') {
return;
}
this.inputDom.popover('destroy').popover({
content: '<div class="alert alert-danger"> ' + error.message + ' </div>',
html: true,
trigger: 'manual'
}).popover('show');
};
/**
* Return an Object containing the request information for the given query
*
* @param query
* @returns {{data: {cache: number, query: *, filter_domain: (*|Function|Function), filter_module: Function}, headers: {Accept: string}, url: *}}
*/
this.getRequestParams = function(query) {
return {
data: {
'cache' : (new Date()).getTime(),
'query' : query,
'filter_domain' : this.domain,
'filter_module' : this.module
},
headers: {
'Accept': 'application/json'
},
url: this.formUrl
};
};
/**
* Callback that renders the proposal list after retrieving it from the server
*
* @param {Object} response The jquery response object inheritn XHttpResponse Attributes
*/
this.showProposals = function(response) {
if (response.proposals.length === 0) {
this.inputDom.popover('destroy');
return;
}
this.lastTokens = jsonRep;
var list = $('<ul>').addClass('nav nav-stacked nav-pills');
$.each(tokens, (function(idx, token) {
$.each(response.proposals, (function(idx, token) {
var displayToken = token.replace(/(\{|\})/g, '');
var proposal = $('<li>').
append($('<a href="#">').
text(displayToken)
).appendTo(list);
proposal.on('click', (function(ev) {
ev.preventDefault();
ev.stopPropagation();
@ -114,15 +160,58 @@ define(['jquery', 'logging', 'URIjs/URI'], function($, log, URI) {
}).popover('show');
};
/**
* Callback to update the current container with the entered url if it's valid
*/
this.updateFilter = function() {
var query = $.trim(this.inputDom.val());
this.pendingRequest = $.ajax(this.getRequestParams(query))
.done((function(response) {
var container = new Container($(this.inputDom));
var url = container.getContainerHref();
url += ( url.indexOf('?') === -1 ? '?' : '&' ) + response.urlParam;
container.replaceDomFromUrl(url);
}).bind(this));
};
/**
* Register listeners for the searchbox
*
* This means:
* - Activate/Deactivate the popover on focus and blur
* - Add Url tokens and submit on enter
*/
this.registerControlListener = function() {
this.inputDom.on('blur', (function() {
$(this).popover('hide');
}));
this.inputDom.on('focus', updateProposalList.bind(this));
this.inputDom.on('keyup', updateProposalList.bind(this));
this.inputDom.on('keydown', (function(keyEv) {
if ((keyEv.keyCode || keyEv.which) === 13) {
this.updateFilter();
keyEv.stopPropagation();
keyEv.preventDefault();
return false;
}
}).bind(this));
this.form.submit(function(ev) {
ev.stopPropagation();
ev.preventDefault();
return false;
});
};
var updateProposalList = function() {
/**
* Callback to update the proposal list if a slight delay on keyPress
*
* Needs to be bound to the object scope
*
* @param {jQuery.Event} keyEv The key Event to react on
*/
var updateProposalList = function(keyEv) {
if (this.lastQueuedEvent) {
window.clearTimeout(this.lastQueuedEvent);
}
@ -131,6 +220,4 @@ define(['jquery', 'logging', 'URIjs/URI'], function($, log, URI) {
this.construct();
};
});

View File

@ -294,4 +294,5 @@ class FilterTest extends BaseTestCase
'Assert the root->right->right->type node to be an OPERATOR (query :"' . $query . '")'
);
}
}

View File

@ -14,7 +14,7 @@ class StatusdatTestLoader extends LibraryLoader
require_once 'Zend/Log.php';
require_once($libPath."/Data/AbstractQuery.php");
require_once($libPath."/Application/Logger.php");
require_once($libPath."/Filter/Filterable.php");
require_once($libPath."/Data/DatasourceInterface.php");
$statusdat = realpath($libPath."/Protocol/Statusdat/");
require_once($statusdat."/View/AccessorStrategy.php");