From d33cec78de83b4874231c8f49c8998ff47b279bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannis=20Mo=C3=9Fhammer?= Date: Mon, 14 Oct 2013 13:25:25 +0200 Subject: [PATCH] Semantic search implementation - Only implemented for hosts as an example - URL behaviour still has to be normalized refs #4469 --- application/controllers/FilterController.php | 88 +-- doc/semantic_search.md | 11 - library/Icinga/Filter/Domain.php | 11 +- library/Icinga/Filter/Filter.php | 12 +- library/Icinga/Filter/FilterAttribute.php | 10 +- .../Icinga/{Data => Filter}/Filterable.php | 12 +- library/Icinga/Filter/Query/Node.php | 4 +- library/Icinga/Filter/Query/Tree.php | 220 +++++++- library/Icinga/Filter/QueryProposer.php | 4 +- library/Icinga/Filter/Type/BooleanFilter.php | 9 +- library/Icinga/Filter/Type/FilterType.php | 6 +- library/Icinga/Filter/Type/TextFilter.php | 10 +- .../Icinga/Filter/Type/TimeRangeSpecifier.php | 11 +- .../Icinga/Web/Widget/FilterBadgeRenderer.php | 56 +- library/Icinga/Web/Widget/FilterBox.php | 119 ++++ .../controllers/ListController.php | 31 +- .../views/scripts/list/hosts.phtml | 15 +- .../Backend/Ido/Query/AbstractQuery.php | 519 ++++++++---------- .../Backend/Ido/Query/HoststatusQuery.php | 361 ++++++------ .../Backend/Livestatus/Query/StatusQuery.php | 2 +- .../Backend/Statusdat/Query/Query.php | 23 +- .../library/Monitoring/DataView/DataView.php | 82 +-- .../Monitoring/DataView/HostStatus.php | 101 ++++ ...AndServiceStatus.php => ServiceStatus.php} | 14 +- .../Filter/Backend/IdoQueryConverter.php | 130 ++++- .../{MonitoringFilter.php => Registry.php} | 60 +- .../Monitoring/Filter/Type/StatusFilter.php | 2 +- .../Monitoring/Filter/UrlViewFilter.php | 173 +++++- .../Monitoring/View/HoststatusView.php | 8 +- .../controllers/ListControllerHostTest.php | 15 +- .../controllers/ListControllerServiceTest.php | 11 +- .../php/library/Filter/UrlViewFilterTest.php | 38 +- .../php/testlib/MonitoringControllerTest.php | 2 + public/js/icinga/components/semanticsearch.js | 151 +++-- test/php/library/Icinga/Filter/FilterTest.php | 1 + .../Statusdat/StatusdatTestLoader.php | 2 +- 36 files changed, 1540 insertions(+), 784 deletions(-) delete mode 100644 doc/semantic_search.md rename library/Icinga/{Data => Filter}/Filterable.php (85%) create mode 100644 library/Icinga/Web/Widget/FilterBox.php create mode 100644 modules/monitoring/library/Monitoring/DataView/HostStatus.php rename modules/monitoring/library/Monitoring/DataView/{HostAndServiceStatus.php => ServiceStatus.php} (91%) rename modules/monitoring/library/Monitoring/Filter/{MonitoringFilter.php => Registry.php} (62%) diff --git a/application/controllers/FilterController.php b/application/controllers/FilterController.php index 3754e4070..213e8eea2 100644 --- a/application/controllers/FilterController.php +++ b/application/controllers/FilterController.php @@ -25,6 +25,7 @@ * @author Icinga Development Team */ // {{{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 diff --git a/doc/semantic_search.md b/doc/semantic_search.md deleted file mode 100644 index df02a3559..000000000 --- a/doc/semantic_search.md +++ /dev/null @@ -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)] - diff --git a/library/Icinga/Filter/Domain.php b/library/Icinga/Filter/Domain.php index bbfea2b4f..ca961ee2a 100644 --- a/library/Icinga/Filter/Domain.php +++ b/library/Icinga/Filter/Domain.php @@ -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; } -} \ No newline at end of file +} diff --git a/library/Icinga/Filter/Filter.php b/library/Icinga/Filter/Filter.php index 77fee5035..4d324ce74 100644 --- a/library/Icinga/Filter/Filter.php +++ b/library/Icinga/Filter/Filter.php @@ -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; } -} \ No newline at end of file +} diff --git a/library/Icinga/Filter/FilterAttribute.php b/library/Icinga/Filter/FilterAttribute.php index 9927ddf13..7cf29eb6c 100644 --- a/library/Icinga/Filter/FilterAttribute.php +++ b/library/Icinga/Filter/FilterAttribute.php @@ -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); } - - } diff --git a/library/Icinga/Data/Filterable.php b/library/Icinga/Filter/Filterable.php similarity index 85% rename from library/Icinga/Data/Filterable.php rename to library/Icinga/Filter/Filterable.php index 376384ad2..417dcf8ec 100644 --- a/library/Icinga/Data/Filterable.php +++ b/library/Icinga/Filter/Filterable.php @@ -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); - -} \ No newline at end of file + public function isValidFilterTarget($field); + public function getMappedField($field); + public function applyFilter(Tree $filter); +} diff --git a/library/Icinga/Filter/Query/Node.php b/library/Icinga/Filter/Query/Node.php index ab32f478c..db82009c4 100644 --- a/library/Icinga/Filter/Query/Node.php +++ b/library/Icinga/Filter/Query/Node.php @@ -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 diff --git a/library/Icinga/Filter/Query/Tree.php b/library/Icinga/Filter/Query/Tree.php index e3c50717c..a069bb34b 100644 --- a/library/Icinga/Filter/Query/Tree.php +++ b/library/Icinga/Filter/Query/Tree.php @@ -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); + } + } } diff --git a/library/Icinga/Filter/QueryProposer.php b/library/Icinga/Filter/QueryProposer.php index ca198d927..9200d5e07 100644 --- a/library/Icinga/Filter/QueryProposer.php +++ b/library/Icinga/Filter/QueryProposer.php @@ -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); - -} \ No newline at end of file +} diff --git a/library/Icinga/Filter/Type/BooleanFilter.php b/library/Icinga/Filter/Type/BooleanFilter.php index ce6d816e5..436073cae 100644 --- a/library/Icinga/Filter/Type/BooleanFilter.php +++ b/library/Icinga/Filter/Type/BooleanFilter.php @@ -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; } - -} \ No newline at end of file +} diff --git a/library/Icinga/Filter/Type/FilterType.php b/library/Icinga/Filter/Type/FilterType.php index 6a498894f..74f23bb33 100644 --- a/library/Icinga/Filter/Type/FilterType.php +++ b/library/Icinga/Filter/Type/FilterType.php @@ -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; } -} \ No newline at end of file +} diff --git a/library/Icinga/Filter/Type/TextFilter.php b/library/Icinga/Filter/Type/TextFilter.php index e4e986665..14c9064e6 100644 --- a/library/Icinga/Filter/Type/TextFilter.php +++ b/library/Icinga/Filter/Type/TextFilter.php @@ -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...'; } } -} \ No newline at end of file +} diff --git a/library/Icinga/Filter/Type/TimeRangeSpecifier.php b/library/Icinga/Filter/Type/TimeRangeSpecifier.php index 1a0bbab22..6df936e02 100644 --- a/library/Icinga/Filter/Type/TimeRangeSpecifier.php +++ b/library/Icinga/Filter/Type/TimeRangeSpecifier.php @@ -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; } -} \ No newline at end of file +} diff --git a/library/Icinga/Web/Widget/FilterBadgeRenderer.php b/library/Icinga/Web/Widget/FilterBadgeRenderer.php index 02c27bdd5..d5bde0006 100644 --- a/library/Icinga/Web/Widget/FilterBadgeRenderer.php +++ b/library/Icinga/Web/Widget/FilterBadgeRenderer.php @@ -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 ' ' + + $newTree = $this->tree->withoutNode($node); + $url = $this->urlFilter->fromTree($newTree); + $url = $basePath . (empty($allParams) ? '?' : '&') . $url; + + return ' ' . $this->conjunctionCellar . ' ' . ucfirst($node->left) . ' ' . $node->operator . ' ' . $node->right . ''; } $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); } -} \ No newline at end of file +} diff --git a/library/Icinga/Web/Widget/FilterBox.php b/library/Icinga/Web/Widget/FilterBox.php new file mode 100644 index 000000000..784b987f0 --- /dev/null +++ b/library/Icinga/Web/Widget/FilterBox.php @@ -0,0 +1,119 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{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' +
+
{{FORM}}
+
{{BADGES}}
+
+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); + } +} diff --git a/modules/monitoring/application/controllers/ListController.php b/modules/monitoring/application/controllers/ListController.php index 9af60a85e..b0720ef19 100644 --- a/modules/monitoring/application/controllers/ListController.php +++ b/modules/monitoring/application/controllers/ListController.php @@ -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 '
'
                 . 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
      *
diff --git a/modules/monitoring/application/views/scripts/list/hosts.phtml b/modules/monitoring/application/views/scripts/list/hosts.phtml
index 652385191..333de23c3 100644
--- a/modules/monitoring/application/views/scripts/list/hosts.phtml
+++ b/modules/monitoring/application/views/scripts/list/hosts.phtml
@@ -5,8 +5,19 @@ $viewHelper = $this->getHelper('MonitoringState');
 tabs->render($this); ?>
 

Hosts Status

- sortControl->render($this); ?> - paginationControl($hosts, null, null, array('preserve' => $this->preserve)); ?> +
+
+
+ filterBox->render($this); ?> +
+
+ sortControl->render($this); ?> +
+
+
+ paginationControl($hosts, null, null, array('preserve' => $this->preserve)); ?> +
+
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/AbstractQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/AbstractQuery.php index 8bb47a051..53f7da2fe 100644 --- a/modules/monitoring/library/Monitoring/Backend/Ido/Query/AbstractQuery.php +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/AbstractQuery.php @@ -1,23 +1,50 @@ + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{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; } } diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php index 8738f3e3d..2a28e6888 100644 --- a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php @@ -1,49 +1,73 @@ + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{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; } diff --git a/modules/monitoring/library/Monitoring/Backend/Livestatus/Query/StatusQuery.php b/modules/monitoring/library/Monitoring/Backend/Livestatus/Query/StatusQuery.php index d3e259421..c3e0d5e7a 100644 --- a/modules/monitoring/library/Monitoring/Backend/Livestatus/Query/StatusQuery.php +++ b/modules/monitoring/library/Monitoring/Backend/Livestatus/Query/StatusQuery.php @@ -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', diff --git a/modules/monitoring/library/Monitoring/Backend/Statusdat/Query/Query.php b/modules/monitoring/library/Monitoring/Backend/Statusdat/Query/Query.php index d9aa4af63..c20c5a50b 100644 --- a/modules/monitoring/library/Monitoring/Backend/Statusdat/Query/Query.php +++ b/modules/monitoring/library/Monitoring/Backend/Statusdat/Query/Query.php @@ -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. + } } + diff --git a/modules/monitoring/library/Monitoring/DataView/DataView.php b/modules/monitoring/library/Monitoring/DataView/DataView.php index 718170ee1..b5d58463e 100644 --- a/modules/monitoring/library/Monitoring/DataView/DataView.php +++ b/modules/monitoring/library/Monitoring/DataView/DataView.php @@ -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; + } } diff --git a/modules/monitoring/library/Monitoring/DataView/HostStatus.php b/modules/monitoring/library/Monitoring/DataView/HostStatus.php new file mode 100644 index 000000000..2fefb4492 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/HostStatus.php @@ -0,0 +1,101 @@ + 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); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/HostAndServiceStatus.php b/modules/monitoring/library/Monitoring/DataView/ServiceStatus.php similarity index 91% rename from modules/monitoring/library/Monitoring/DataView/HostAndServiceStatus.php rename to modules/monitoring/library/Monitoring/DataView/ServiceStatus.php index bb6c0d6f5..fc16a9d4b 100644 --- a/modules/monitoring/library/Monitoring/DataView/HostAndServiceStatus.php +++ b/modules/monitoring/library/Monitoring/DataView/ServiceStatus.php @@ -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); } } diff --git a/modules/monitoring/library/Monitoring/Filter/Backend/IdoQueryConverter.php b/modules/monitoring/library/Monitoring/Filter/Backend/IdoQueryConverter.php index 36fa8319f..1f395f1d8 100644 --- a/modules/monitoring/library/Monitoring/Filter/Backend/IdoQueryConverter.php +++ b/modules/monitoring/library/Monitoring/Filter/Backend/IdoQueryConverter.php @@ -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'; } -} \ No newline at end of file +} diff --git a/modules/monitoring/library/Monitoring/Filter/MonitoringFilter.php b/modules/monitoring/library/Monitoring/Filter/Registry.php similarity index 62% rename from modules/monitoring/library/Monitoring/Filter/MonitoringFilter.php rename to modules/monitoring/library/Monitoring/Filter/Registry.php index 4a5b83921..08f54b783 100644 --- a/modules/monitoring/library/Monitoring/Filter/MonitoringFilter.php +++ b/modules/monitoring/library/Monitoring/Filter/Registry.php @@ -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; } - -} \ No newline at end of file +} diff --git a/modules/monitoring/library/Monitoring/Filter/Type/StatusFilter.php b/modules/monitoring/library/Monitoring/Filter/Type/StatusFilter.php index b94e858c1..fb93a2bea 100644 --- a/modules/monitoring/library/Monitoring/Filter/Type/StatusFilter.php +++ b/modules/monitoring/library/Monitoring/Filter/Type/StatusFilter.php @@ -318,4 +318,4 @@ class StatusFilter extends FilterType { $this->baseStates = $states; } -} \ No newline at end of file +} diff --git a/modules/monitoring/library/Monitoring/Filter/UrlViewFilter.php b/modules/monitoring/library/Monitoring/Filter/UrlViewFilter.php index 3767928a7..5434218d1 100644 --- a/modules/monitoring/library/Monitoring/Filter/UrlViewFilter.php +++ b/modules/monitoring/library/Monitoring/Filter/UrlViewFilter.php @@ -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 } } } - - -} \ No newline at end of file +} diff --git a/modules/monitoring/library/Monitoring/View/HoststatusView.php b/modules/monitoring/library/Monitoring/View/HoststatusView.php index ae499b455..09c810d0f 100644 --- a/modules/monitoring/library/Monitoring/View/HoststatusView.php +++ b/modules/monitoring/library/Monitoring/View/HoststatusView.php @@ -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); } } diff --git a/modules/monitoring/test/php/application/controllers/ListControllerHostTest.php b/modules/monitoring/test/php/application/controllers/ListControllerHostTest.php index 7c30c4485..33cc5db0f 100644 --- a/modules/monitoring/test/php/application/controllers/ListControllerHostTest.php +++ b/modules/monitoring/test/php/application/controllers/ListControllerHostTest.php @@ -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; diff --git a/modules/monitoring/test/php/application/controllers/ListControllerServiceTest.php b/modules/monitoring/test/php/application/controllers/ListControllerServiceTest.php index 240d034e0..02ebe134f 100644 --- a/modules/monitoring/test/php/application/controllers/ListControllerServiceTest.php +++ b/modules/monitoring/test/php/application/controllers/ListControllerServiceTest.php @@ -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; diff --git a/modules/monitoring/test/php/library/Filter/UrlViewFilterTest.php b/modules/monitoring/test/php/library/Filter/UrlViewFilterTest.php index 060d4fa27..e52c56ee2 100644 --- a/modules/monitoring/test/php/library/Filter/UrlViewFilterTest.php +++ b/modules/monitoring/test/php/library/Filter/UrlViewFilterTest.php @@ -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'); diff --git a/modules/monitoring/test/php/testlib/MonitoringControllerTest.php b/modules/monitoring/test/php/testlib/MonitoringControllerTest.php index 4e2ac46d0..63a28894d 100644 --- a/modules/monitoring/test/php/testlib/MonitoringControllerTest.php +++ b/modules/monitoring/test/php/testlib/MonitoringControllerTest.php @@ -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'); diff --git a/public/js/icinga/components/semanticsearch.js b/public/js/icinga/components/semanticsearch.js index 6593141f4..648bb80de 100644 --- a/public/js/icinga/components/semanticsearch.js +++ b/public/js/icinga/components/semanticsearch.js @@ -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 */ // {{{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: '
' + error.message + '
', + 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 = $('