Semantic search implementation
- Only implemented for hosts as an example - URL behaviour still has to be normalized refs #4469
This commit is contained in:
parent
dac61eda19
commit
d33cec78de
|
@ -25,6 +25,7 @@
|
|||
* @author Icinga Development Team <info@icinga.org>
|
||||
*/
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
// @codingStandardsIgnoreStart
|
||||
|
||||
use Icinga\Web\Form;
|
||||
use Icinga\Web\Controller\ActionController;
|
||||
|
@ -34,69 +35,82 @@ use Icinga\Filter\Type\TextFilter;
|
|||
use Icinga\Application\Logger;
|
||||
use Icinga\Module\Monitoring\Filter\Type\StatusFilter;
|
||||
use Icinga\Module\Monitoring\Filter\UrlViewFilter;
|
||||
use Icinga\Module\Monitoring\DataView\HostStatus;
|
||||
use Icinga\Web\Url;
|
||||
|
||||
/**
|
||||
* Application wide interface for filtering
|
||||
*/
|
||||
class FilterController extends ActionController
|
||||
{
|
||||
/**
|
||||
* The current filter registry
|
||||
*
|
||||
* @var Filter
|
||||
*/
|
||||
private $registry;
|
||||
|
||||
/**
|
||||
* Entry point for filtering, uses the filter_domain and filter_module request parameter
|
||||
* to determine which filter registry should be used
|
||||
*/
|
||||
public function indexAction()
|
||||
{
|
||||
$this->registry = new Filter();
|
||||
$filter = new UrlViewFilter();
|
||||
|
||||
$this->view->form = new Form();
|
||||
$this->view->form->addElement(
|
||||
'text',
|
||||
'query',
|
||||
array(
|
||||
'name' => 'query',
|
||||
'label' => 'search',
|
||||
'type' => 'search',
|
||||
'data-icinga-component' => 'app/semanticsearch',
|
||||
'data-icinga-target' => 'host',
|
||||
'helptext' => 'Filter test'
|
||||
)
|
||||
);
|
||||
$this->view->form->addElement(
|
||||
'submit',
|
||||
'btn_submit',
|
||||
array(
|
||||
'name' => 'submit'
|
||||
)
|
||||
);
|
||||
$this->setupQueries();
|
||||
$this->view->form->setRequest($this->getRequest());
|
||||
|
||||
if ($this->view->form->isSubmittedAndValid()) {
|
||||
$tree = $this->registry->createQueryTreeForFilter($this->view->form->getValue('query'));
|
||||
$this->view->tree = new \Icinga\Web\Widget\FilterBadgeRenderer($tree);
|
||||
$view = \Icinga\Module\Monitoring\DataView\HostAndServiceStatus::fromRequest($this->getRequest());
|
||||
$cv = new \Icinga\Module\Monitoring\Filter\Backend\IdoQueryConverter($view);
|
||||
$this->view->sqlString = $cv->treeToSql($tree);
|
||||
$this->view->params = $cv->getParams();
|
||||
} else if ($this->getRequest()->getHeader('accept') == 'application/json') {
|
||||
if ($this->getRequest()->getHeader('accept') == 'application/json') {
|
||||
$this->getResponse()->setHeader('Content-Type', 'application/json');
|
||||
|
||||
$this->setupQueries(
|
||||
$this->getParam('filter_domain', ''),
|
||||
$this->getParam('filter_module', '')
|
||||
);
|
||||
|
||||
$this->_helper->json($this->parse($this->getRequest()->getParam('query', '')));
|
||||
} else {
|
||||
$this->redirect('index/welcome');
|
||||
}
|
||||
}
|
||||
|
||||
private function setupQueries()
|
||||
/**
|
||||
* Set up the query handler for the given domain and module
|
||||
*
|
||||
* @param string $domain The domain to use
|
||||
* @param string $module The module to use
|
||||
*/
|
||||
private function setupQueries($domain, $module = 'default')
|
||||
{
|
||||
$this->registry->addDomain(\Icinga\Module\Monitoring\Filter\MonitoringFilter::hostFilter());
|
||||
$class = '\\Icinga\\Module\\' . ucfirst($module) . '\\Filter\\Registry';
|
||||
$factory = strtolower($domain) . 'Filter';
|
||||
$this->registry->addDomain($class::$factory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given query text and returns the json as expected by the semantic search box
|
||||
*
|
||||
* @param String $text The query to parse
|
||||
* @return array The result structure to be returned in json format
|
||||
*/
|
||||
private function parse($text)
|
||||
{
|
||||
try {
|
||||
return $this->registry->getProposalsForQuery($text);
|
||||
$view = HostStatus::fromRequest($this->getRequest());
|
||||
$urlParser = new UrlViewFilter($view);
|
||||
$queryTree = $this->registry->createQueryTreeForFilter($text);
|
||||
|
||||
return array(
|
||||
'state' => 'success',
|
||||
'proposals' => $this->registry->getProposalsForQuery($text),
|
||||
'urlParam' => $urlParser->fromTree($queryTree)
|
||||
);
|
||||
} catch (\Exception $exc) {
|
||||
Logger::error($exc);
|
||||
$this->getResponse()->setHttpResponseCode(500);
|
||||
return array(
|
||||
'state' => 'error',
|
||||
'message' => 'Search service is currently not available'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
// @codingStandardsIgnoreEnd
|
||||
|
|
|
@ -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)]
|
||||
|
|
@ -26,7 +26,6 @@
|
|||
*/
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
|
||||
|
||||
namespace Icinga\Filter;
|
||||
|
||||
use Icinga\Filter\Query\Node;
|
||||
|
@ -132,10 +131,10 @@ 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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -27,12 +27,14 @@
|
|||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
|
||||
|
||||
namespace Icinga\Data;
|
||||
namespace Icinga\Filter;
|
||||
|
||||
|
||||
use Icinga\Filter\Query\Tree;
|
||||
|
||||
interface Filterable
|
||||
{
|
||||
public function isValidFilterTarget($targetOrColumn);
|
||||
public function resolveFilterTarget($targetOrColumn);
|
||||
|
||||
public function isValidFilterTarget($field);
|
||||
public function getMappedField($field);
|
||||
public function applyFilter(Tree $filter);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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,7 +90,7 @@ 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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 = '+';
|
||||
|
|
|
@ -26,43 +26,85 @@
|
|||
*/
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
|
||||
|
||||
namespace Icinga\Web\Widget;
|
||||
|
||||
|
||||
use Icinga\Filter\Query\Tree;
|
||||
use Icinga\Filter\Query\Node;
|
||||
use Icinga\Module\Monitoring\Filter\UrlViewFilter;
|
||||
use Icinga\Web\Url;
|
||||
|
||||
use Zend_View_Abstract;
|
||||
|
||||
/**
|
||||
* A renderer for filter badges that allow to disable specific filters
|
||||
*/
|
||||
class FilterBadgeRenderer implements Widget
|
||||
{
|
||||
private $tree;
|
||||
/**
|
||||
* @var Url
|
||||
*/
|
||||
private $baseUrl;
|
||||
private $conjunctionCellar = '';
|
||||
private $urlFilter;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new badge renderer for this tree
|
||||
*
|
||||
* @param Tree $tree
|
||||
*/
|
||||
public function __construct(Tree $tree)
|
||||
{
|
||||
$this->tree = $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a removable badge from a query tree node
|
||||
*
|
||||
* @param Node $node The node to create the badge for
|
||||
* @return string The html for the badge
|
||||
*/
|
||||
private function nodeToBadge(Node $node)
|
||||
{
|
||||
$basePath = $this->baseUrl->getAbsoluteUrl();
|
||||
$allParams = $this->baseUrl->getParams();
|
||||
|
||||
if ($node->type === Node::TYPE_OPERATOR) {
|
||||
return ' <a class="btn btn-default btn-xs">'
|
||||
|
||||
$newTree = $this->tree->withoutNode($node);
|
||||
$url = $this->urlFilter->fromTree($newTree);
|
||||
$url = $basePath . (empty($allParams) ? '?' : '&') . $url;
|
||||
|
||||
return ' <a class="btn btn-default btn-xs" href="' . $url . '">'
|
||||
. $this->conjunctionCellar . ' '
|
||||
. ucfirst($node->left) . ' '
|
||||
. $node->operator . ' '
|
||||
. $node->right . '</a>';
|
||||
}
|
||||
$result = '';
|
||||
|
||||
$result .= $this->nodeToBadge($node->left);
|
||||
$this->conjunctionCellar = $node->type;
|
||||
$result .= $this->nodeToBadge($node->right);
|
||||
|
||||
return $result;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize $this->baseUrl with an Url instance containing all non-filter parameter
|
||||
*/
|
||||
private function buildBaseUrl()
|
||||
{
|
||||
$baseUrl = Url::fromRequest();
|
||||
foreach ($baseUrl->getParams() as $key => $param) {
|
||||
$translated = preg_replace('/[^0-9A-Za-z_]{1,2}$/', '', $key);
|
||||
if ($this->tree->hasNodeWithAttribute($translated) === true) {
|
||||
$baseUrl->removeKey($key);
|
||||
}
|
||||
}
|
||||
$this->baseUrl = $baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders this widget via the given view and returns the
|
||||
|
@ -73,9 +115,11 @@ class FilterBadgeRenderer implements Widget
|
|||
*/
|
||||
public function render(Zend_View_Abstract $view)
|
||||
{
|
||||
$this->urlFilter = new UrlViewFilter();
|
||||
if ($this->tree->root == null) {
|
||||
return '';
|
||||
}
|
||||
$this->buildBaseUrl();
|
||||
return $this->nodeToBadge($this->tree->root);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
/**
|
||||
* This file is part of Icinga 2 Web.
|
||||
*
|
||||
* Icinga 2 Web - Head for multiple monitoring backends.
|
||||
* Copyright (C) 2013 Icinga Development Team
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* @copyright 2013 Icinga Development Team <info@icinga.org>
|
||||
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
|
||||
* @author Icinga Development Team <info@icinga.org>
|
||||
*/
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
|
||||
namespace Icinga\Web\Widget;
|
||||
|
||||
use Zend_View_Abstract;
|
||||
|
||||
use Icinga\Web\Form;
|
||||
use Icinga\Web\Url;
|
||||
use Icinga\Filter\Query\Tree;
|
||||
|
||||
/**
|
||||
* Widget that renders a filter input box together with an FilterBadgeRenderer widget
|
||||
*/
|
||||
class FilterBox implements Widget
|
||||
{
|
||||
/**
|
||||
* An optional initial filter to use
|
||||
*
|
||||
* @var \Icinga\Filter\Query\Tree
|
||||
*/
|
||||
private $initialFilter;
|
||||
|
||||
/**
|
||||
* The domain of the filter, set in the data-icinga-filter-domain attribute
|
||||
* @var string
|
||||
*/
|
||||
private $domain;
|
||||
|
||||
/**
|
||||
* The module of the filter, set in the data-icinga-filter-module attribute
|
||||
* @var string
|
||||
*/
|
||||
private $module;
|
||||
|
||||
/**
|
||||
* The template used for rendering the form and badges
|
||||
* @var string
|
||||
*/
|
||||
private static $TPL = <<<'EOT'
|
||||
<div class="row">
|
||||
<div class="col-md-12">{{FORM}}</div>
|
||||
<div class="col-md-12">{{BADGES}}</div>
|
||||
</div>
|
||||
EOT;
|
||||
|
||||
/**
|
||||
* Create a new FilterBox widget
|
||||
*
|
||||
* @param Tree $initialFilter The tree to use for initial population
|
||||
* @param String $domain The filter domain
|
||||
* @param String $module The filter module
|
||||
*/
|
||||
public function __construct(Tree $initialFilter, $domain, $module)
|
||||
{
|
||||
$this->initialFilter = $initialFilter;
|
||||
$this->domain = $domain;
|
||||
$this->module = $module;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render this widget
|
||||
*
|
||||
* @param Zend_View_Abstract $view The view to use for rendering the widget
|
||||
* @return string The HTML of the widget as a string
|
||||
*/
|
||||
public function render(Zend_View_Abstract $view)
|
||||
{
|
||||
|
||||
$form = new Form();
|
||||
$form->setAttrib('class', 'form-inline');
|
||||
$form->setMethod('GET');
|
||||
$form->setAction(Url::fromPath('/filter'));
|
||||
$form->setTokenDisabled();
|
||||
$form->addElement(
|
||||
'text',
|
||||
'filter',
|
||||
array(
|
||||
'label' => 'Filter Results',
|
||||
'name' => 'filter',
|
||||
'data-icinga-component' => 'app/semanticsearch',
|
||||
'data-icinga-filter-domain' => $this->domain,
|
||||
'data-icinga-filter-module' => $this->module
|
||||
)
|
||||
);
|
||||
$form->removeAttrib('data-icinga-component');
|
||||
|
||||
$form->setIgnoreChangeDiscarding(true);
|
||||
|
||||
$badges = new FilterBadgeRenderer($this->initialFilter);
|
||||
$html = str_replace('{{FORM}}', $form->render($view), self::$TPL);
|
||||
return str_replace('{{BADGES}}', $badges->render($view), $html);
|
||||
}
|
||||
}
|
|
@ -38,16 +38,21 @@ use Icinga\Web\Widget\Tabextension\OutputFormat;
|
|||
use Icinga\Web\Widget\Tabs;
|
||||
use Icinga\Module\Monitoring\Backend;
|
||||
use Icinga\Web\Widget\SortBox;
|
||||
use Icinga\Web\Widget\FilterBox;
|
||||
use Icinga\Application\Config as IcingaConfig;
|
||||
|
||||
use Icinga\Module\Monitoring\DataView\DataView;
|
||||
use Icinga\Module\Monitoring\DataView\Notification as NotificationView;
|
||||
use Icinga\Module\Monitoring\DataView\Downtime as DowntimeView;
|
||||
use Icinga\Module\Monitoring\DataView\Contact as ContactView;
|
||||
use Icinga\Module\Monitoring\DataView\Contactgroup as ContactgroupView;
|
||||
use Icinga\Module\Monitoring\DataView\HostAndServiceStatus as HostAndServiceStatusView;
|
||||
use Icinga\Module\Monitoring\DataView\HostStatus as HostStatusView;
|
||||
use Icinga\Module\Monitoring\DataView\ServiceStatus as ServiceStatusView;
|
||||
use Icinga\Module\Monitoring\DataView\Comment as CommentView;
|
||||
use Icinga\Module\Monitoring\DataView\Groupsummary as GroupsummaryView;
|
||||
use Icinga\Module\Monitoring\DataView\EventHistory as EventHistoryView;
|
||||
use Icinga\Module\Monitoring\Filter\UrlViewFilter;
|
||||
use Icinga\Filter\Filterable;
|
||||
|
||||
class Monitoring_ListController extends MonitoringController
|
||||
{
|
||||
|
@ -96,8 +101,9 @@ class Monitoring_ListController extends MonitoringController
|
|||
*/
|
||||
public function hostsAction()
|
||||
{
|
||||
|
||||
$this->compactView = 'hosts-compact';
|
||||
$query = HostAndServiceStatusView::fromRequest(
|
||||
$dataview = HostStatusView::fromRequest(
|
||||
$this->_request,
|
||||
array(
|
||||
'host_icon_image',
|
||||
|
@ -123,8 +129,9 @@ class Monitoring_ListController extends MonitoringController
|
|||
'host_current_check_attempt',
|
||||
'host_max_check_attempts'
|
||||
)
|
||||
)->getQuery();
|
||||
$this->view->hosts = $query->paginate();
|
||||
);
|
||||
$query = $dataview->getQuery();
|
||||
$this->setupFilterControl($dataview);
|
||||
$this->setupSortControl(array(
|
||||
'host_last_check' => 'Last Host Check',
|
||||
'host_severity' => 'Host Severity',
|
||||
|
@ -134,6 +141,8 @@ class Monitoring_ListController extends MonitoringController
|
|||
'host_state' => 'Hard State'
|
||||
));
|
||||
$this->handleFormatRequest($query);
|
||||
$this->view->hosts = $query->paginate();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -390,7 +399,8 @@ class Monitoring_ListController extends MonitoringController
|
|||
$this->_helper->viewRenderer($this->compactView);
|
||||
}
|
||||
|
||||
if ($this->_getParam('format') === 'sql'
|
||||
|
||||
if ($this->getParam('format') === 'sql'
|
||||
&& IcingaConfig::app()->global->get('environment', 'production') === 'development') {
|
||||
echo '<pre>'
|
||||
. htmlspecialchars(wordwrap($query->dump()))
|
||||
|
@ -426,6 +436,17 @@ class Monitoring_ListController extends MonitoringController
|
|||
$this->view->sortControl->applyRequest($this->getRequest());
|
||||
}
|
||||
|
||||
private function setupFilterControl(Filterable $dataview)
|
||||
{
|
||||
$parser = new UrlViewFilter($dataview);
|
||||
$this->view->filterBox = new FilterBox(
|
||||
$parser->parseUrl(),
|
||||
'host',
|
||||
'monitoring'
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all tabs for this controller
|
||||
*
|
||||
|
|
|
@ -5,8 +5,19 @@ $viewHelper = $this->getHelper('MonitoringState');
|
|||
<?= $this->tabs->render($this); ?>
|
||||
<h1>Hosts Status</h1>
|
||||
<div data-icinga-component="app/mainDetailGrid">
|
||||
<?= $this->sortControl->render($this); ?>
|
||||
<?= $this->paginationControl($hosts, null, null, array('preserve' => $this->preserve)); ?>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<?= $this->filterBox->render($this); ?>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<?= $this->sortControl->render($this); ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<?= $this->paginationControl($hosts, null, null, array('preserve' => $this->preserve)); ?>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-condensed">
|
||||
|
||||
<tbody>
|
||||
|
|
|
@ -1,23 +1,50 @@
|
|||
<?php
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
/**
|
||||
* This file is part of Icinga 2 Web.
|
||||
*
|
||||
* Icinga 2 Web - Head for multiple monitoring backends.
|
||||
* Copyright (C) 2013 Icinga Development Team
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* @copyright 2013 Icinga Development Team <info@icinga.org>
|
||||
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
|
||||
* @author Icinga Development Team <info@icinga.org>
|
||||
*/
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
|
||||
namespace Icinga\Module\Monitoring\Backend\Ido\Query;
|
||||
|
||||
use Icinga\Data\Db\Query;
|
||||
use Icinga\Application\Benchmark;
|
||||
use Icinga\Exception\ProgrammingError;
|
||||
use Icinga\Filter\Query\Tree;
|
||||
use Icinga\Filter\Filterable;
|
||||
use Icinga\Module\Monitoring\Filter\Backend\IdoQueryConverter;
|
||||
use Icinga\Module\Monitoring\Filter\UrlViewFilter;
|
||||
|
||||
abstract class AbstractQuery extends Query
|
||||
abstract class AbstractQuery extends Query implements Filterable
|
||||
{
|
||||
protected $prefix;
|
||||
|
||||
protected $idxAliasColumn;
|
||||
protected $idxAliasTable;
|
||||
protected $columnMap = array();
|
||||
|
||||
protected $query;
|
||||
protected $customVars = array();
|
||||
protected $joinedVirtualTables = array();
|
||||
|
||||
protected $object_id = 'object_id';
|
||||
protected $host_id = 'host_id';
|
||||
protected $hostgroup_id = 'hostgroup_id';
|
||||
|
@ -25,16 +52,62 @@ abstract class AbstractQuery extends Query
|
|||
protected $servicegroup_id = 'servicegroup_id';
|
||||
protected $contact_id = 'contact_id';
|
||||
protected $contactgroup_id = 'contactgroup_id';
|
||||
|
||||
protected $aggregateColumnIdx = array();
|
||||
|
||||
protected $allowCustomVars = false;
|
||||
|
||||
protected function isAggregateColumn($column)
|
||||
public function isAggregateColumn($column)
|
||||
{
|
||||
return array_key_exists($column, $this->aggregateColumnIdx);
|
||||
}
|
||||
|
||||
public function order($col, $dir = null)
|
||||
{
|
||||
$this->requireColumn($col);
|
||||
if ($this->isCustomvar($col)) {
|
||||
// TODO: Doesn't work right now. Does it?
|
||||
$col = $this->getCustomvarColumnName($col);
|
||||
} elseif ($this->hasAliasName($col)) {
|
||||
$col = $this->aliasToColumnName($col);
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Can\'t order by column '.$col);
|
||||
}
|
||||
$this->order_columns[] = array($col, $dir);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function applyFilter(Tree $filter)
|
||||
{
|
||||
foreach ($filter->getAttributes() as $target) {
|
||||
$this->requireColumn($target);
|
||||
}
|
||||
$converter = new IdoQueryConverter($this);
|
||||
$converter->treeToSql($filter, $this->baseQuery);
|
||||
}
|
||||
|
||||
public function isValidFilterTarget($field)
|
||||
{
|
||||
return $this->getMappedField($field) !== null;
|
||||
}
|
||||
|
||||
public function getMappedField($field)
|
||||
{
|
||||
foreach ($this->columnMap as $columnSource => $columnSet) {
|
||||
if (isset($columnSet[$field])) {
|
||||
return $columnSet[$field];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isTimestamp($field)
|
||||
{
|
||||
$mapped = $this->getMappedField($field);
|
||||
if ($mapped === null) {
|
||||
return false;
|
||||
}
|
||||
return stripos($mapped, 'UNIX_TIMESTAMP') !== false;
|
||||
}
|
||||
|
||||
protected function init()
|
||||
{
|
||||
parent::init();
|
||||
|
@ -65,11 +138,136 @@ abstract class AbstractQuery extends Query
|
|||
$this->prepareAliasIndexes();
|
||||
}
|
||||
|
||||
protected function joinBaseTables()
|
||||
{
|
||||
reset($this->columnMap);
|
||||
$table = key($this->columnMap);
|
||||
|
||||
$this->baseQuery = $this->db->select()->from(
|
||||
array($table => $this->prefix . $table),
|
||||
array()
|
||||
);
|
||||
|
||||
$this->joinedVirtualTables = array($table => true);
|
||||
}
|
||||
|
||||
protected function prepareAliasIndexes()
|
||||
{
|
||||
foreach ($this->columnMap as $tbl => & $cols) {
|
||||
foreach ($cols as $alias => $col) {
|
||||
$this->idxAliasTable[$alias] = $tbl;
|
||||
$this->idxAliasColumn[$alias] = preg_replace('~\n\s*~', ' ', $col);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function beforeCreatingCountQuery()
|
||||
{
|
||||
}
|
||||
|
||||
protected function beforeCreatingSelectQuery()
|
||||
{
|
||||
$this->setRealColumns();
|
||||
$classParts = explode('\\', get_class($this));
|
||||
Benchmark::measure(sprintf('%s ready to run', array_pop($classParts)));
|
||||
}
|
||||
|
||||
public function setRealColumns()
|
||||
{
|
||||
$columns = $this->columns;
|
||||
$this->columns = array();
|
||||
if (empty($columns)) {
|
||||
$columns = $this->getDefaultColumns();
|
||||
}
|
||||
|
||||
foreach ($columns as $alias => $col) {
|
||||
$this->requireColumn($col);
|
||||
if ($this->isCustomvar($col)) {
|
||||
$name = $this->getCustomvarColumnName($col);
|
||||
} else {
|
||||
$name = $this->aliasToColumnName($col);
|
||||
}
|
||||
if (is_int($alias)) {
|
||||
$alias = $col;
|
||||
}
|
||||
|
||||
$this->columns[$alias] = preg_replace('|\n|', ' ', $name);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function getDefaultColumns()
|
||||
{
|
||||
reset($this->columnMap);
|
||||
$table = key($this->columnMap);
|
||||
return array_keys($this->columnMap[$table]);
|
||||
}
|
||||
|
||||
protected function requireColumn($alias)
|
||||
{
|
||||
if ($this->hasAliasName($alias)) {
|
||||
$this->requireVirtualTable($this->aliasToTableName($alias));
|
||||
} elseif ($this->isCustomVar($alias)) {
|
||||
$this->requireCustomvar($alias);
|
||||
} else {
|
||||
throw new ProgrammingError(sprintf('Got invalid column: %s', $alias));
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function hasAliasName($alias)
|
||||
{
|
||||
return array_key_exists($alias, $this->idxAliasColumn);
|
||||
}
|
||||
|
||||
protected function requireVirtualTable($name)
|
||||
{
|
||||
if ($this->hasJoinedVirtualTable($name)) {
|
||||
return $this;
|
||||
}
|
||||
return $this->joinVirtualTable($name);
|
||||
}
|
||||
|
||||
protected function joinVirtualTable($table)
|
||||
{
|
||||
$func = 'join' . ucfirst($table);
|
||||
if (method_exists($this, $func)) {
|
||||
$this->$func();
|
||||
} else {
|
||||
throw new ProgrammingError(
|
||||
sprintf(
|
||||
'Cannot join "%s", no such table found',
|
||||
$table
|
||||
)
|
||||
);
|
||||
}
|
||||
$this->joinedVirtualTables[$table] = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function aliasToTableName($alias)
|
||||
{
|
||||
return $this->idxAliasTable[$alias];
|
||||
}
|
||||
|
||||
protected function isCustomVar($alias)
|
||||
{
|
||||
return $this->allowCustomVars && $alias[0] === '_';
|
||||
}
|
||||
|
||||
protected function requireCustomvar($customvar)
|
||||
{
|
||||
if (! $this->hasCustomvar($customvar)) {
|
||||
$this->joinCustomvar($customvar);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function hasCustomvar($customvar)
|
||||
{
|
||||
return array_key_exists($customvar, $this->customVars);
|
||||
}
|
||||
|
||||
protected function joinCustomvar($customvar)
|
||||
{
|
||||
// TODO: This is not generic enough yet
|
||||
|
@ -103,200 +301,6 @@ abstract class AbstractQuery extends Query
|
|||
return $this;
|
||||
}
|
||||
|
||||
protected function prepareAliasIndexes()
|
||||
{
|
||||
foreach ($this->columnMap as $tbl => & $cols) {
|
||||
foreach ($cols as $alias => $col) {
|
||||
$this->idxAliasTable[$alias] = $tbl;
|
||||
$this->idxAliasColumn[$alias] = preg_replace('~\n\s*~', ' ', $col);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getDefaultColumns()
|
||||
{
|
||||
reset($this->columnMap);
|
||||
$table = key($this->columnMap);
|
||||
return array_keys($this->columnMap[$table]);
|
||||
}
|
||||
|
||||
protected function joinBaseTables()
|
||||
{
|
||||
reset($this->columnMap);
|
||||
$table = key($this->columnMap);
|
||||
|
||||
$this->baseQuery = $this->db->select()->from(
|
||||
array($table => $this->prefix . $table),
|
||||
array()
|
||||
);
|
||||
|
||||
$this->joinedVirtualTables = array($table => true);
|
||||
}
|
||||
|
||||
protected function beforeCreatingCountQuery()
|
||||
{
|
||||
$this->applyAllFilters();
|
||||
}
|
||||
|
||||
protected function beforeCreatingSelectQuery()
|
||||
{
|
||||
$this->setRealColumns();
|
||||
$classParts = explode('\\', get_class($this));
|
||||
Benchmark::measure(sprintf('%s ready to run', array_pop($classParts)));
|
||||
}
|
||||
|
||||
protected function applyAllFilters()
|
||||
{
|
||||
$filters = array();
|
||||
foreach ($this->filters as $f) {
|
||||
$alias = $f[0];
|
||||
$value = $f[1];
|
||||
$this->requireColumn($alias);
|
||||
|
||||
if ($this->isCustomvar($alias)) {
|
||||
$col = $this->getCustomvarColumnName($alias);
|
||||
} elseif ($this->hasAliasName($alias)) {
|
||||
$col = $this->aliasToColumnName($alias);
|
||||
} else {
|
||||
throw new ProgrammingError(
|
||||
'If you finished here, code has been messed up'
|
||||
);
|
||||
}
|
||||
|
||||
$func = 'filter' . ucfirst($alias);
|
||||
if (method_exists($this, $func)) {
|
||||
$this->$func($value);
|
||||
return;
|
||||
}
|
||||
if ($this->isAggregateColumn($alias)) {
|
||||
$this->baseQuery->having($this->prepareFilterStringForColumn($col, $value));
|
||||
} else {
|
||||
$this->baseQuery->where($this->prepareFilterStringForColumn($col, $value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function order($col, $dir = null)
|
||||
{
|
||||
$this->requireColumn($col);
|
||||
if ($this->isCustomvar($col)) {
|
||||
// TODO: Doesn't work right now. Does it?
|
||||
$col = $this->getCustomvarColumnName($col);
|
||||
} elseif ($this->hasAliasName($col)) {
|
||||
$col = $this->aliasToColumnName($col);
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Can\'t order by column '.$col);
|
||||
}
|
||||
$this->order_columns[] = array($col, $dir);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setRealColumns()
|
||||
{
|
||||
$columns = $this->columns;
|
||||
$this->columns = array();
|
||||
if (empty($columns)) {
|
||||
$colums = $this->getDefaultColumns();
|
||||
}
|
||||
|
||||
foreach ($columns as $alias => $col) {
|
||||
$this->requireColumn($col);
|
||||
if ($this->isCustomvar($col)) {
|
||||
$name = $this->getCustomvarColumnName($col);
|
||||
} else {
|
||||
$name = $this->aliasToColumnName($col);
|
||||
}
|
||||
if (is_int($alias)) {
|
||||
$alias = $col;
|
||||
}
|
||||
|
||||
$this->columns[$alias] = preg_replace('|\n|', ' ' , $name);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function requireColumn($alias)
|
||||
{
|
||||
if ($this->hasAliasName($alias)) {
|
||||
$this->requireVirtualTable($this->aliasToTableName($alias));
|
||||
} elseif ($this->isCustomVar($alias)) {
|
||||
$this->requireCustomvar($alias);
|
||||
} else {
|
||||
throw new ProgrammingError(sprintf('Got invalid column: %s', $alias));
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function hasAliasName($alias)
|
||||
{
|
||||
return array_key_exists($alias, $this->idxAliasColumn);
|
||||
}
|
||||
|
||||
public function aliasToColumnName($alias)
|
||||
{
|
||||
return $this->idxAliasColumn[$alias];
|
||||
}
|
||||
|
||||
protected function aliasToTableName($alias)
|
||||
{
|
||||
return $this->idxAliasTable[$alias];
|
||||
}
|
||||
|
||||
protected function hasJoinedVirtualTable($name)
|
||||
{
|
||||
return array_key_exists($name, $this->joinedVirtualTables);
|
||||
}
|
||||
|
||||
protected function requireVirtualTable($name)
|
||||
{
|
||||
if ($this->hasJoinedVirtualTable($name)) {
|
||||
return $this;
|
||||
}
|
||||
return $this->joinVirtualTable($name);
|
||||
}
|
||||
|
||||
protected function joinVirtualTable($table)
|
||||
{
|
||||
$func = 'join' . ucfirst($table);
|
||||
if (method_exists($this, $func)) {
|
||||
$this->$func();
|
||||
} else {
|
||||
throw new ProgrammingError(sprintf(
|
||||
'Cannot join "%s", no such table found',
|
||||
$table
|
||||
));
|
||||
}
|
||||
$this->joinedVirtualTables[$table] = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function requireCustomvar($customvar)
|
||||
{
|
||||
if (! $this->hasCustomvar($customvar)) {
|
||||
$this->joinCustomvar($customvar);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function hasCustomvar($customvar)
|
||||
{
|
||||
return array_key_exists($customvar, $this->customVars);
|
||||
}
|
||||
|
||||
protected function getCustomvarColumnName($customvar)
|
||||
{
|
||||
return $this->customVars[$customvar] . '.varvalue';
|
||||
}
|
||||
|
||||
protected function createSubQuery($queryName, $columns = array())
|
||||
{
|
||||
$class = '\\'
|
||||
. substr(__CLASS__, 0, strrpos(__CLASS__, '\\') + 1)
|
||||
. ucfirst($queryName) . 'Query';
|
||||
$query = new $class($this->ds, $columns);
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function customvarNameToTypeName($customvar)
|
||||
{
|
||||
// TODO: Improve this:
|
||||
|
@ -311,106 +315,27 @@ abstract class AbstractQuery extends Query
|
|||
return array($m[1], $m[2]);
|
||||
}
|
||||
|
||||
protected function prepareFilterStringForColumn($column, $value)
|
||||
protected function hasJoinedVirtualTable($name)
|
||||
{
|
||||
$filter = '';
|
||||
$filters = array();
|
||||
|
||||
$or = array();
|
||||
$and = array();
|
||||
|
||||
if (
|
||||
! is_array($value) &&
|
||||
(strpos($value, ',') !== false || strpos($value, '|') !== false)
|
||||
) {
|
||||
$value = preg_split('~[,|]~', $value, -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
if (! is_array($value)) {
|
||||
$value = array($value);
|
||||
}
|
||||
|
||||
// Go through all given values
|
||||
foreach ($value as $val) {
|
||||
if ($val === '') {
|
||||
// TODO: REALLY??
|
||||
continue;
|
||||
}
|
||||
$not = false;
|
||||
$force = false;
|
||||
$op = '=';
|
||||
$wildcard = false;
|
||||
|
||||
if ($val[0] === '-' || $val[0] === '!') {
|
||||
// Value starting with minus or !: negation
|
||||
$val = substr($val, 1);
|
||||
$not = true;
|
||||
}
|
||||
|
||||
if ($val[0] === '+') {
|
||||
// Value starting with +: enforces AND
|
||||
// TODO: depends on correct URL handling, not given in all
|
||||
// ZF versions.
|
||||
$val = substr($val, 1);
|
||||
$force = true;
|
||||
}
|
||||
if ($val[0] === '<' || $val[0] === '>') {
|
||||
$op = $val[0];
|
||||
$val = substr($val, 1);
|
||||
}
|
||||
if (strpos($val, '*') !== false) {
|
||||
$wildcard = true;
|
||||
$val = str_replace('*', '%', $val);
|
||||
}
|
||||
|
||||
$operator = null;
|
||||
switch ($op) {
|
||||
case '=':
|
||||
if ($not) {
|
||||
$operator = $wildcard ? 'NOT LIKE' : '!=';
|
||||
} else {
|
||||
$operator = $wildcard ? 'LIKE' : '=';
|
||||
}
|
||||
break;
|
||||
case '>':
|
||||
$operator = $not ? '<=' : '>';
|
||||
break;
|
||||
case '<':
|
||||
$operator = $not ? '>=' : '<';
|
||||
break;
|
||||
default:
|
||||
throw new ProgrammingError("'$op' is not a valid operator");
|
||||
}
|
||||
|
||||
if ($not || $force) {
|
||||
$and[] = $this->db->quoteInto($column . ' ' . $operator . ' ?', $val);
|
||||
} else {
|
||||
$or[] = $this->db->quoteInto($column . ' ' . $operator . ' ?', $val);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($or)) {
|
||||
$filters[] = implode(' OR ', $or);
|
||||
}
|
||||
|
||||
if (! empty($and)) {
|
||||
$filters[] = implode(' AND ', $and);
|
||||
}
|
||||
|
||||
if (! empty($filters)) {
|
||||
$filter = '(' . implode(') AND (', $filters) . ')';
|
||||
}
|
||||
|
||||
return $filter;
|
||||
return array_key_exists($name, $this->joinedVirtualTables);
|
||||
}
|
||||
|
||||
public function getMappedColumn($name)
|
||||
protected function getCustomvarColumnName($customvar)
|
||||
{
|
||||
foreach ($this->columnMap as $column => $results) {
|
||||
if (isset($results[$name])) {
|
||||
return $results[$name];
|
||||
}
|
||||
}
|
||||
return $this->customVars[$customvar] . '.varvalue';
|
||||
}
|
||||
|
||||
return null;
|
||||
public function aliasToColumnName($alias)
|
||||
{
|
||||
return $this->idxAliasColumn[$alias];
|
||||
}
|
||||
|
||||
protected function createSubQuery($queryName, $columns = array())
|
||||
{
|
||||
$class = '\\'
|
||||
. substr(__CLASS__, 0, strrpos(__CLASS__, '\\') + 1)
|
||||
. ucfirst($queryName) . 'Query';
|
||||
$query = new $class($this->ds, $columns);
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,49 +1,73 @@
|
|||
<?php
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
/**
|
||||
* This file is part of Icinga 2 Web.
|
||||
*
|
||||
* Icinga 2 Web - Head for multiple monitoring backends.
|
||||
* Copyright (C) 2013 Icinga Development Team
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* @copyright 2013 Icinga Development Team <info@icinga.org>
|
||||
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
|
||||
* @author Icinga Development Team <info@icinga.org>
|
||||
*/
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
|
||||
namespace Icinga\Module\Monitoring\Backend\Ido\Query;
|
||||
|
||||
class HoststatusQuery extends AbstractQuery
|
||||
{
|
||||
protected $allowCustomVars = true;
|
||||
|
||||
protected $columnMap = array(
|
||||
'hosts' => array(
|
||||
'host' => 'ho.name1 COLLATE latin1_general_ci',
|
||||
'host_name' => 'ho.name1 COLLATE latin1_general_ci',
|
||||
'host_display_name' => 'h.display_name',
|
||||
'host_alias' => 'h.alias',
|
||||
'host_address' => 'h.address',
|
||||
'host_ipv4' => 'INET_ATON(h.address)',
|
||||
'host_icon_image' => 'h.icon_image',
|
||||
'host' => 'ho.name1 COLLATE latin1_general_ci',
|
||||
'host_name' => 'ho.name1 COLLATE latin1_general_ci',
|
||||
'host_display_name' => 'h.display_name',
|
||||
'host_alias' => 'h.alias',
|
||||
'host_address' => 'h.address',
|
||||
'host_ipv4' => 'INET_ATON(h.address)',
|
||||
'host_icon_image' => 'h.icon_image',
|
||||
),
|
||||
'hoststatus' => array(
|
||||
'problems' => 'CASE WHEN hs.current_state = 0 THEN 0 ELSE 1 END',
|
||||
'handled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) > 0 THEN 1 ELSE 0 END',
|
||||
'unhandled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) = 0 THEN 1 ELSE 0 END',
|
||||
'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END',
|
||||
'host_output' => 'hs.output',
|
||||
'host_long_output' => 'hs.long_output',
|
||||
'host_perfdata' => 'hs.perfdata',
|
||||
'host_problem' => 'CASE WHEN hs.current_state = 0 THEN 0 ELSE 1 END',
|
||||
'host_acknowledged' => 'hs.problem_has_been_acknowledged',
|
||||
'host_in_downtime' => 'CASE WHEN (hs.scheduled_downtime_depth = 0) THEN 0 ELSE 1 END',
|
||||
'host_handled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) > 0 THEN 1 ELSE 0 END',
|
||||
'host_does_active_checks' => 'hs.active_checks_enabled',
|
||||
'problems' => 'CASE WHEN hs.current_state = 0 THEN 0 ELSE 1 END',
|
||||
'handled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) > 0 THEN 1 ELSE 0 END',
|
||||
'unhandled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) = 0 THEN 1 ELSE 0 END',
|
||||
'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END',
|
||||
'host_output' => 'hs.output',
|
||||
'host_long_output' => 'hs.long_output',
|
||||
'host_perfdata' => 'hs.perfdata',
|
||||
'host_problem' => 'CASE WHEN hs.current_state = 0 THEN 0 ELSE 1 END',
|
||||
'host_acknowledged' => 'hs.problem_has_been_acknowledged',
|
||||
'host_in_downtime' => 'CASE WHEN (hs.scheduled_downtime_depth = 0) THEN 0 ELSE 1 END',
|
||||
'host_handled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) > 0 THEN 1 ELSE 0 END',
|
||||
'host_does_active_checks' => 'hs.active_checks_enabled',
|
||||
'host_accepts_passive_checks' => 'hs.passive_checks_enabled',
|
||||
'host_last_state_change' => 'UNIX_TIMESTAMP(hs.last_state_change)',
|
||||
'host_last_hard_state' => 'hs.last_hard_state',
|
||||
'host_check_command' => 'hs.check_command',
|
||||
'host_last_check' => 'UNIX_TIMESTAMP(hs.last_check)',
|
||||
'host_next_check' => 'CASE WHEN hs.should_be_scheduled THEN UNIX_TIMESTAMP(hs.next_check) ELSE NULL END',
|
||||
'host_check_execution_time' => 'hs.execution_time',
|
||||
'host_check_latency' => 'hs.latency',
|
||||
'host_notifications_enabled' => 'hs.notifications_enabled',
|
||||
'host_last_time_up' => 'hs.last_time_up',
|
||||
'host_last_time_down' => 'hs.last_time_down',
|
||||
'host_last_time_unreachable' => 'hs.last_time_unreachable',
|
||||
'host_current_check_attempt' => 'hs.current_check_attempt',
|
||||
'host_max_check_attempts' => 'hs.max_check_attempts',
|
||||
|
||||
'host_last_state_change' => 'UNIX_TIMESTAMP(hs.last_state_change)',
|
||||
'host_last_hard_state' => 'hs.last_hard_state',
|
||||
'host_check_command' => 'hs.check_command',
|
||||
'host_last_check' => 'UNIX_TIMESTAMP(hs.last_check)',
|
||||
'host_next_check' => 'CASE WHEN hs.should_be_scheduled THEN UNIX_TIMESTAMP(hs.next_check) ELSE NULL END',
|
||||
'host_check_execution_time' => 'hs.execution_time',
|
||||
'host_check_latency' => 'hs.latency',
|
||||
'host_notifications_enabled' => 'hs.notifications_enabled',
|
||||
'host_last_time_up' => 'hs.last_time_up',
|
||||
'host_last_time_down' => 'hs.last_time_down',
|
||||
'host_last_time_unreachable' => 'hs.last_time_unreachable',
|
||||
'host_current_check_attempt' => 'hs.current_check_attempt',
|
||||
'host_max_check_attempts' => 'hs.max_check_attempts',
|
||||
'host_severity' => 'CASE WHEN hs.current_state = 0
|
||||
THEN
|
||||
CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL
|
||||
|
@ -86,37 +110,35 @@ class HoststatusQuery extends AbstractQuery
|
|||
'contact' => 'hco.name1 COLLATE latin1_general_ci',
|
||||
),
|
||||
'services' => array(
|
||||
'services_cnt' => 'SUM(1)',
|
||||
'services_ok' => 'SUM(CASE WHEN ss.current_state = 0 THEN 1 ELSE 0 END)',
|
||||
'services_warning' => 'SUM(CASE WHEN ss.current_state = 1 THEN 1 ELSE 0 END)',
|
||||
'services_cnt' => 'SUM(1)',
|
||||
'services_ok' => 'SUM(CASE WHEN ss.current_state = 0 THEN 1 ELSE 0 END)',
|
||||
'services_warning' => 'SUM(CASE WHEN ss.current_state = 1 THEN 1 ELSE 0 END)',
|
||||
'services_critical' => 'SUM(CASE WHEN ss.current_state = 2 THEN 1 ELSE 0 END)',
|
||||
'services_unknown' => 'SUM(CASE WHEN ss.current_state = 3 THEN 1 ELSE 0 END)',
|
||||
'services_pending' => 'SUM(CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 1 ELSE 0 END)',
|
||||
'services_problem' => 'SUM(CASE WHEN ss.current_state > 0 THEN 1 ELSE 0 END)',
|
||||
'services_problem_handled' => 'SUM(CASE WHEN ss.current_state > 0 AND (ss.problem_has_been_acknowledged = 1 OR ss.scheduled_downtime_depth > 0) THEN 1 ELSE 0 END)',
|
||||
'services_problem_unhandled' => 'SUM(CASE WHEN ss.current_state > 0 AND (ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0) THEN 1 ELSE 0 END)',
|
||||
'services_warning_handled' => 'SUM(CASE WHEN ss.current_state = 1 AND (ss.problem_has_been_acknowledged = 1 OR ss.scheduled_downtime_depth > 0) THEN 1 ELSE 0 END)',
|
||||
'services_unknown' => 'SUM(CASE WHEN ss.current_state = 3 THEN 1 ELSE 0 END)',
|
||||
'services_pending' => 'SUM(CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 1 ELSE 0 END)',
|
||||
'services_problem' => 'SUM(CASE WHEN ss.current_state > 0 THEN 1 ELSE 0 END)',
|
||||
'services_problem_handled' => 'SUM(CASE WHEN ss.current_state > 0 AND (ss.problem_has_been_acknowledged = 1 OR ss.scheduled_downtime_depth > 0) THEN 1 ELSE 0 END)',
|
||||
'services_problem_unhandled' => 'SUM(CASE WHEN ss.current_state > 0 AND (ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0) THEN 1 ELSE 0 END)',
|
||||
'services_warning_handled' => 'SUM(CASE WHEN ss.current_state = 1 AND (ss.problem_has_been_acknowledged = 1 OR ss.scheduled_downtime_depth > 0) THEN 1 ELSE 0 END)',
|
||||
'services_critical_handled' => 'SUM(CASE WHEN ss.current_state = 2 AND (ss.problem_has_been_acknowledged = 1 OR ss.scheduled_downtime_depth > 0) THEN 1 ELSE 0 END)',
|
||||
'services_unknown_handled' => 'SUM(CASE WHEN ss.current_state = 3 AND (ss.problem_has_been_acknowledged = 1 OR ss.scheduled_downtime_depth > 0) THEN 1 ELSE 0 END)',
|
||||
'services_warning_unhandled' => 'SUM(CASE WHEN ss.current_state = 1 AND (ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0) THEN 1 ELSE 0 END)',
|
||||
'services_unknown_handled' => 'SUM(CASE WHEN ss.current_state = 3 AND (ss.problem_has_been_acknowledged = 1 OR ss.scheduled_downtime_depth > 0) THEN 1 ELSE 0 END)',
|
||||
'services_warning_unhandled' => 'SUM(CASE WHEN ss.current_state = 1 AND (ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0) THEN 1 ELSE 0 END)',
|
||||
'services_critical_unhandled' => 'SUM(CASE WHEN ss.current_state = 2 AND (ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0) THEN 1 ELSE 0 END)',
|
||||
'services_unknown_unhandled' => 'SUM(CASE WHEN ss.current_state = 3 AND (ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0) THEN 1 ELSE 0 END)',
|
||||
'services_unknown_unhandled' => 'SUM(CASE WHEN ss.current_state = 3 AND (ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0) THEN 1 ELSE 0 END)',
|
||||
),
|
||||
);
|
||||
|
||||
protected $aggregateColumnIdx = array(
|
||||
'services_cnt' => true,
|
||||
'services_problem' => true,
|
||||
'services_problem_handled' => true,
|
||||
'services_cnt' => true,
|
||||
'services_problem' => true,
|
||||
'services_problem_handled' => true,
|
||||
'services_problem_unhandled' => true,
|
||||
);
|
||||
|
||||
protected $hcgSub;
|
||||
|
||||
protected function getDefaultColumns()
|
||||
{
|
||||
return $this->columnMap['hosts']
|
||||
+ $this->columnMap['hoststatus'];
|
||||
+ $this->columnMap['hoststatus'];
|
||||
}
|
||||
|
||||
protected function joinBaseTables()
|
||||
|
@ -126,16 +148,16 @@ class HoststatusQuery extends AbstractQuery
|
|||
array('ho' => $this->prefix . 'objects'),
|
||||
array()
|
||||
)->join(
|
||||
array('hs' => $this->prefix . 'hoststatus'),
|
||||
'ho.' . $this->object_id . ' = hs.host_object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
|
||||
array()
|
||||
)->join(
|
||||
array('h' => $this->prefix . 'hosts'),
|
||||
'hs.host_object_id = h.host_object_id',
|
||||
array()
|
||||
);
|
||||
array('hs' => $this->prefix . 'hoststatus'),
|
||||
'ho.' . $this->object_id . ' = hs.host_object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
|
||||
array()
|
||||
)->join(
|
||||
array('h' => $this->prefix . 'hosts'),
|
||||
'hs.host_object_id = h.host_object_id',
|
||||
array()
|
||||
);
|
||||
$this->joinedVirtualTables = array(
|
||||
'hosts' => true,
|
||||
'hosts' => true,
|
||||
'hoststatus' => true,
|
||||
);
|
||||
}
|
||||
|
@ -157,14 +179,14 @@ class HoststatusQuery extends AbstractQuery
|
|||
's.host_object_id = h.host_object_id',
|
||||
array()
|
||||
)->join(
|
||||
array('so' => $this->prefix . 'objects'),
|
||||
"so.$this->object_id = s.service_object_id AND so.is_active = 1",
|
||||
array()
|
||||
)->joinLeft(
|
||||
array('ss' => $this->prefix . 'servicestatus'),
|
||||
"so.$this->object_id = ss.service_object_id",
|
||||
array()
|
||||
);
|
||||
array('so' => $this->prefix . 'objects'),
|
||||
"so.$this->object_id = s.service_object_id AND so.is_active = 1",
|
||||
array()
|
||||
)->joinLeft(
|
||||
array('ss' => $this->prefix . 'servicestatus'),
|
||||
"so.$this->object_id = ss.service_object_id",
|
||||
array()
|
||||
);
|
||||
foreach ($this->columns as $col) {
|
||||
$real = $this->aliasToColumnName($col);
|
||||
if (substr($real, 0, 4) === 'SUM(') {
|
||||
|
@ -184,25 +206,65 @@ class HoststatusQuery extends AbstractQuery
|
|||
}
|
||||
}
|
||||
|
||||
protected function joinServiceHostgroups()
|
||||
{
|
||||
$this->baseQuery->join(
|
||||
array('hgm' => $this->prefix . 'hostgroup_members'),
|
||||
'hgm.host_object_id = s.host_object_id',
|
||||
array()
|
||||
)->join(
|
||||
array('hg' => $this->prefix . 'hostgroups'),
|
||||
'hgm.hostgroup_id = hg.' . $this->hostgroup_id,
|
||||
array()
|
||||
)->join(
|
||||
array('hgo' => $this->prefix . 'objects'),
|
||||
'hgo.' . $this->object_id . ' = hg.hostgroup_object_id'
|
||||
. ' AND hgo.is_active = 1',
|
||||
array()
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function joinHostHostgroups()
|
||||
{
|
||||
$this->baseQuery->join(
|
||||
array('hgm' => $this->prefix . 'hostgroup_members'),
|
||||
'hgm.host_object_id = h.host_object_id',
|
||||
array()
|
||||
)->join(
|
||||
array('hg' => $this->prefix . 'hostgroups'),
|
||||
"hgm.hostgroup_id = hg.$this->hostgroup_id",
|
||||
array()
|
||||
)->join(
|
||||
array('hgo' => $this->prefix . 'objects'),
|
||||
'hgo.' . $this->object_id . ' = hg.hostgroup_object_id'
|
||||
. ' AND hgo.is_active = 1',
|
||||
array()
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function joinContacts()
|
||||
{
|
||||
$this->hcgcSub = $this->db->select()->distinct()->from(
|
||||
array('hcgc' => $this->prefix . 'host_contactgroups'),
|
||||
array('host_name' => 'ho.name1')
|
||||
)->join(
|
||||
array('cgo' => $this->prefix . 'objects'),
|
||||
'hcg.contactgroup_object_id = cgo.' . $this->object_id
|
||||
. ' AND cgo.is_active = 1',
|
||||
array()
|
||||
)->join(
|
||||
array('h' => $this->prefix . 'hosts'),
|
||||
'hcg.host_id = h.host_id',
|
||||
array()
|
||||
)->join(
|
||||
array('ho' => $this->prefix . 'objects'),
|
||||
'h.host_object_id = ho.' . $this->object_id . ' AND ho.is_active = 1',
|
||||
array()
|
||||
);
|
||||
array('cgo' => $this->prefix . 'objects'),
|
||||
'hcg.contactgroup_object_id = cgo.' . $this->object_id
|
||||
. ' AND cgo.is_active = 1',
|
||||
array()
|
||||
)->join(
|
||||
array('h' => $this->prefix . 'hosts'),
|
||||
'hcg.host_id = h.host_id',
|
||||
array()
|
||||
)->join(
|
||||
array('ho' => $this->prefix . 'objects'),
|
||||
'h.host_object_id = ho.' . $this->object_id . ' AND ho.is_active = 1',
|
||||
array()
|
||||
);
|
||||
$this->baseQuery->join(
|
||||
array('hcg' => $this->hcgSub),
|
||||
'hcg.host_name = ho.name1',
|
||||
|
@ -212,48 +274,6 @@ class HoststatusQuery extends AbstractQuery
|
|||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
protected function joinContacts()
|
||||
{
|
||||
|
||||
|
||||
$this->baseQuery->join(
|
||||
array('hc' => $this->prefix . 'host_contacts'),
|
||||
'hc.host_id = h.host_id',
|
||||
array()
|
||||
)->join(
|
||||
array('hco' => $this->prefix . 'objects'),
|
||||
'hco.' . $this->object_id. ' = hc.contact_object_id'
|
||||
. ' AND hco.is_active = 1',
|
||||
array()
|
||||
);
|
||||
|
||||
$this->baseQuery->join(
|
||||
array('hcg' => $this->prefix . 'host_contactgroups'),
|
||||
'hcg.host_id = h.host_id',
|
||||
array()
|
||||
)->join(
|
||||
array('hcgo' => $this->prefix . 'objects'),
|
||||
'hcgo.' . $this->object_id. ' = hcg.contactgroup_object_id'
|
||||
. ' AND hcgo.is_active = 1',
|
||||
array()
|
||||
);
|
||||
$this->baseQuery->join(
|
||||
array('cgm' => $this->prefix . 'contactgroup_members'),
|
||||
'cgm.contactgroup_id = cg.contactgroup_id',
|
||||
array()
|
||||
)->join(
|
||||
array('co' => $this->prefix . 'objects'),
|
||||
'cgm.contact_object_id = co.object_id AND co.is_active = 1',
|
||||
array()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return $this;
|
||||
}
|
||||
*/
|
||||
protected function filterContactgroup($value)
|
||||
{
|
||||
$this->hcgSub->where(
|
||||
|
@ -265,28 +285,6 @@ class HoststatusQuery extends AbstractQuery
|
|||
return $this;
|
||||
}
|
||||
|
||||
|
||||
protected function createContactgroupFilterSubselect()
|
||||
{
|
||||
die((string) $this->db->select()->distinct()->from(
|
||||
array('hcg' => $this->prefix . 'host_contactgroups'),
|
||||
array('object_id' => 'ho.object_id')
|
||||
)->join(
|
||||
array('cgo' => $this->prefix . 'objects'),
|
||||
'hcg.contactgroup_object_id = cgo.' . $this->object_id
|
||||
. ' AND cgo.is_active = 1',
|
||||
array()
|
||||
)->join(
|
||||
array('h' => $this->prefix . 'hosts'),
|
||||
'hcg.host_id = h.host_id',
|
||||
array()
|
||||
)->join(
|
||||
array('ho' => $this->prefix . 'objects'),
|
||||
'h.host_object_id = ho.' . $this->object_id . ' AND ho.is_active = 1',
|
||||
array()
|
||||
));
|
||||
}
|
||||
|
||||
protected function joinContactgroups()
|
||||
{
|
||||
$this->hcgSub = $this->createContactgroupFilterSubselect();
|
||||
|
@ -299,44 +297,25 @@ class HoststatusQuery extends AbstractQuery
|
|||
return $this;
|
||||
}
|
||||
|
||||
protected function joinHostHostgroups()
|
||||
protected function createContactgroupFilterSubselect()
|
||||
{
|
||||
$this->baseQuery->join(
|
||||
array('hgm' => $this->prefix . 'hostgroup_members'),
|
||||
'hgm.host_object_id = h.host_object_id',
|
||||
array()
|
||||
die((string)$this->db->select()->distinct()->from(
|
||||
array('hcg' => $this->prefix . 'host_contactgroups'),
|
||||
array('object_id' => 'ho.object_id')
|
||||
)->join(
|
||||
array('hg' => $this->prefix . 'hostgroups'),
|
||||
"hgm.hostgroup_id = hg.$this->hostgroup_id",
|
||||
array()
|
||||
)->join(
|
||||
array('hgo' => $this->prefix . 'objects'),
|
||||
'hgo.' . $this->object_id. ' = hg.hostgroup_object_id'
|
||||
. ' AND hgo.is_active = 1',
|
||||
array()
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function joinServiceHostgroups()
|
||||
{
|
||||
$this->baseQuery->join(
|
||||
array('hgm' => $this->prefix . 'hostgroup_members'),
|
||||
'hgm.host_object_id = s.host_object_id',
|
||||
array()
|
||||
)->join(
|
||||
array('hg' => $this->prefix . 'hostgroups'),
|
||||
'hgm.hostgroup_id = hg.' . $this->hostgroup_id,
|
||||
array()
|
||||
)->join(
|
||||
array('hgo' => $this->prefix . 'objects'),
|
||||
'hgo.' . $this->object_id. ' = hg.hostgroup_object_id'
|
||||
. ' AND hgo.is_active = 1',
|
||||
array()
|
||||
);
|
||||
|
||||
return $this;
|
||||
array('cgo' => $this->prefix . 'objects'),
|
||||
'hcg.contactgroup_object_id = cgo.' . $this->object_id
|
||||
. ' AND cgo.is_active = 1',
|
||||
array()
|
||||
)->join(
|
||||
array('h' => $this->prefix . 'hosts'),
|
||||
'hcg.host_id = h.host_id',
|
||||
array()
|
||||
)->join(
|
||||
array('ho' => $this->prefix . 'objects'),
|
||||
'h.host_object_id = ho.' . $this->object_id . ' AND ho.is_active = 1',
|
||||
array()
|
||||
));
|
||||
}
|
||||
|
||||
protected function joinServicegroups()
|
||||
|
@ -348,15 +327,15 @@ class HoststatusQuery extends AbstractQuery
|
|||
'sgm.service_object_id = s.service_object_id',
|
||||
array()
|
||||
)->join(
|
||||
array('sg' => $this->prefix . 'servicegroups'),
|
||||
'sgm.servicegroup_id = sg.' . $this->servicegroup_id,
|
||||
array()
|
||||
)->join(
|
||||
array('sgo' => $this->prefix . 'objects'),
|
||||
'sgo.' . $this->object_id. ' = sg.servicegroup_object_id'
|
||||
. ' AND sgo.is_active = 1',
|
||||
array()
|
||||
);
|
||||
array('sg' => $this->prefix . 'servicegroups'),
|
||||
'sgm.servicegroup_id = sg.' . $this->servicegroup_id,
|
||||
array()
|
||||
)->join(
|
||||
array('sgo' => $this->prefix . 'objects'),
|
||||
'sgo.' . $this->object_id . ' = sg.servicegroup_object_id'
|
||||
. ' AND sgo.is_active = 1',
|
||||
array()
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
|
||||
namespace Icinga\Module\Monitoring\DataView;
|
||||
|
||||
use Icinga\Module\Monitoring\Filter\MonitoringFilter;
|
||||
|
||||
class HostStatus extends DataView
|
||||
{
|
||||
/**
|
||||
* Retrieve columns provided by this view
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getColumns()
|
||||
{
|
||||
return array(
|
||||
'host_name',
|
||||
'host_state',
|
||||
'host_state_type',
|
||||
'host_last_state_change',
|
||||
'host_address',
|
||||
'host_handled',
|
||||
'host_icon_image',
|
||||
'host_acknowledged',
|
||||
'host_output',
|
||||
'host_long_output',
|
||||
'host_in_downtime',
|
||||
'host_is_flapping',
|
||||
'host_last_check',
|
||||
'host_next_check',
|
||||
'host_notifications_enabled',
|
||||
'host_unhandled_service_count',
|
||||
'host_action_url',
|
||||
'host_notes_url',
|
||||
'host_last_comment',
|
||||
'host',
|
||||
'host_display_name',
|
||||
'host_alias',
|
||||
'host_ipv4',
|
||||
'host_severity',
|
||||
'host_perfdata',
|
||||
'host_does_active_checks',
|
||||
'host_accepts_passive_checks',
|
||||
'host_last_hard_state',
|
||||
'host_last_hard_state_change',
|
||||
'host_last_time_up',
|
||||
'host_last_time_down',
|
||||
'host_last_time_unreachable'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function getTableName()
|
||||
{
|
||||
return 'status';
|
||||
}
|
||||
|
||||
public function getSortRules()
|
||||
{
|
||||
return array(
|
||||
'host_name' => array(
|
||||
'order' => self::SORT_ASC
|
||||
),
|
||||
'host_address' => array(
|
||||
'columns' => array(
|
||||
'host_ipv4',
|
||||
'service_description'
|
||||
),
|
||||
'order' => self::SORT_ASC
|
||||
),
|
||||
'host_last_state_change' => array(
|
||||
'order' => self::SORT_ASC
|
||||
),
|
||||
'host_severity' => array(
|
||||
'columns' => array(
|
||||
'host_severity',
|
||||
'host_last_state_change',
|
||||
),
|
||||
'order' => self::SORT_ASC
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function getFilterColumns()
|
||||
{
|
||||
return array('hostgroups', 'servicegroups', 'service_problems');
|
||||
}
|
||||
|
||||
public function isValidFilterTarget($column)
|
||||
{
|
||||
if ($column[0] === '_'
|
||||
&& preg_match('/^_(?:host|service)_/', $column)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return parent::isValidFilterTarget($column);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/*global Icinga:false, document: false, define:false require:false base_url:false console:false */
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
/**
|
||||
* This file is part of Icinga 2 Web.
|
||||
|
@ -24,50 +25,55 @@
|
|||
* @author Icinga Development Team <info@icinga.org>
|
||||
*/
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
/*global Icinga:false, document: false, define:false require:false base_url:false console:false */
|
||||
|
||||
/**
|
||||
* Ensures that our date/time controls will work on every browser (natively or javascript based)
|
||||
*/
|
||||
define(['jquery', 'logging', 'URIjs/URI'], function($, log, URI) {
|
||||
define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function($, log, URI, Container) {
|
||||
'use strict';
|
||||
|
||||
return function(inputDOM) {
|
||||
this.inputDom = $(inputDOM);
|
||||
this.form = this.inputDom.parents('form').first();
|
||||
this.domain = this.inputDom.attr('data-icinga-filter-domain');
|
||||
this.module = this.inputDom.attr('data-icinga-filter-module');
|
||||
this.form = $(this.inputDom.parents('form').first());
|
||||
this.formUrl = URI(this.form.attr('action'));
|
||||
this.lastTokens = [];
|
||||
|
||||
this.lastQueuedEvent = null;
|
||||
this.pendingRequest = null;
|
||||
|
||||
/**
|
||||
* Register the input listener
|
||||
*/
|
||||
this.construct = function() {
|
||||
this.registerControlListener();
|
||||
};
|
||||
|
||||
/**
|
||||
* Request new proposals for the given input box
|
||||
*/
|
||||
this.getProposal = function() {
|
||||
var text = this.inputDom.val().trim();
|
||||
|
||||
try {
|
||||
if (this.pendingRequest) {
|
||||
this.pendingRequest.abort();
|
||||
}
|
||||
this.pendingRequest = $.ajax({
|
||||
data: {
|
||||
'cache' : (new Date()).getTime(),
|
||||
'query' : text
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
url: this.formUrl
|
||||
}).done(this.showProposals.bind(this)).fail(function() {});
|
||||
} catch(exception) {
|
||||
console.log(exception);
|
||||
|
||||
if (this.pendingRequest) {
|
||||
this.pendingRequest.abort();
|
||||
}
|
||||
this.pendingRequest = $.ajax(this.getRequestParams(text))
|
||||
.done(this.showProposals.bind(this))
|
||||
.fail(this.showError.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply a selected proposal to the text box
|
||||
*
|
||||
* String parts encapsulated in {} are parts that already exist in the input
|
||||
*
|
||||
* @param token The selected token
|
||||
*/
|
||||
this.applySelectedProposal = function(token) {
|
||||
var currentText = $.trim(this.inputDom.val());
|
||||
|
||||
var substr = token.match(/^(\{.*\})/);
|
||||
if (substr !== null) {
|
||||
token = token.substr(substr[0].length);
|
||||
|
@ -81,23 +87,63 @@ define(['jquery', 'logging', 'URIjs/URI'], function($, log, URI) {
|
|||
this.inputDom.focus();
|
||||
};
|
||||
|
||||
this.showProposals = function(tokens, state, args) {
|
||||
|
||||
var jsonRep = args.responseText;
|
||||
|
||||
|
||||
if (tokens.length === 0) {
|
||||
return this.inputDom.popover('destroy');
|
||||
/**
|
||||
* Display an error in the box if the request failed
|
||||
*
|
||||
* @param {Object} error The error response
|
||||
* @param {String} state The HTTP state as a string
|
||||
*/
|
||||
this.showError = function(error, state) {
|
||||
if (state === 'abort') {
|
||||
return;
|
||||
}
|
||||
this.inputDom.popover('destroy').popover({
|
||||
content: '<div class="alert alert-danger"> ' + error.message + ' </div>',
|
||||
html: true,
|
||||
trigger: 'manual'
|
||||
}).popover('show');
|
||||
};
|
||||
|
||||
/**
|
||||
* Return an Object containing the request information for the given query
|
||||
*
|
||||
* @param query
|
||||
* @returns {{data: {cache: number, query: *, filter_domain: (*|Function|Function), filter_module: Function}, headers: {Accept: string}, url: *}}
|
||||
*/
|
||||
this.getRequestParams = function(query) {
|
||||
return {
|
||||
data: {
|
||||
'cache' : (new Date()).getTime(),
|
||||
'query' : query,
|
||||
'filter_domain' : this.domain,
|
||||
'filter_module' : this.module
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
url: this.formUrl
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback that renders the proposal list after retrieving it from the server
|
||||
*
|
||||
* @param {Object} response The jquery response object inheritn XHttpResponse Attributes
|
||||
*/
|
||||
this.showProposals = function(response) {
|
||||
if (response.proposals.length === 0) {
|
||||
this.inputDom.popover('destroy');
|
||||
return;
|
||||
}
|
||||
this.lastTokens = jsonRep;
|
||||
|
||||
var list = $('<ul>').addClass('nav nav-stacked nav-pills');
|
||||
$.each(tokens, (function(idx, token) {
|
||||
$.each(response.proposals, (function(idx, token) {
|
||||
var displayToken = token.replace(/(\{|\})/g, '');
|
||||
var proposal = $('<li>').
|
||||
append($('<a href="#">').
|
||||
text(displayToken)
|
||||
).appendTo(list);
|
||||
|
||||
proposal.on('click', (function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -114,15 +160,58 @@ define(['jquery', 'logging', 'URIjs/URI'], function($, log, URI) {
|
|||
}).popover('show');
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback to update the current container with the entered url if it's valid
|
||||
*/
|
||||
this.updateFilter = function() {
|
||||
var query = $.trim(this.inputDom.val());
|
||||
this.pendingRequest = $.ajax(this.getRequestParams(query))
|
||||
.done((function(response) {
|
||||
var container = new Container($(this.inputDom));
|
||||
var url = container.getContainerHref();
|
||||
url += ( url.indexOf('?') === -1 ? '?' : '&' ) + response.urlParam;
|
||||
container.replaceDomFromUrl(url);
|
||||
}).bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* Register listeners for the searchbox
|
||||
*
|
||||
* This means:
|
||||
* - Activate/Deactivate the popover on focus and blur
|
||||
* - Add Url tokens and submit on enter
|
||||
*/
|
||||
this.registerControlListener = function() {
|
||||
this.inputDom.on('blur', (function() {
|
||||
$(this).popover('hide');
|
||||
}));
|
||||
this.inputDom.on('focus', updateProposalList.bind(this));
|
||||
this.inputDom.on('keyup', updateProposalList.bind(this));
|
||||
this.inputDom.on('keydown', (function(keyEv) {
|
||||
if ((keyEv.keyCode || keyEv.which) === 13) {
|
||||
this.updateFilter();
|
||||
keyEv.stopPropagation();
|
||||
keyEv.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}).bind(this));
|
||||
|
||||
this.form.submit(function(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
var updateProposalList = function() {
|
||||
/**
|
||||
* Callback to update the proposal list if a slight delay on keyPress
|
||||
*
|
||||
* Needs to be bound to the object scope
|
||||
*
|
||||
* @param {jQuery.Event} keyEv The key Event to react on
|
||||
*/
|
||||
var updateProposalList = function(keyEv) {
|
||||
|
||||
if (this.lastQueuedEvent) {
|
||||
window.clearTimeout(this.lastQueuedEvent);
|
||||
}
|
||||
|
@ -131,6 +220,4 @@ define(['jquery', 'logging', 'URIjs/URI'], function($, log, URI) {
|
|||
|
||||
this.construct();
|
||||
};
|
||||
|
||||
|
||||
});
|
|
@ -294,4 +294,5 @@ class FilterTest extends BaseTestCase
|
|||
'Assert the root->right->right->type node to be an OPERATOR (query :"' . $query . '")'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -14,7 +14,7 @@ class StatusdatTestLoader extends LibraryLoader
|
|||
require_once 'Zend/Log.php';
|
||||
require_once($libPath."/Data/AbstractQuery.php");
|
||||
require_once($libPath."/Application/Logger.php");
|
||||
|
||||
require_once($libPath."/Filter/Filterable.php");
|
||||
require_once($libPath."/Data/DatasourceInterface.php");
|
||||
$statusdat = realpath($libPath."/Protocol/Statusdat/");
|
||||
require_once($statusdat."/View/AccessorStrategy.php");
|
||||
|
|
Loading…
Reference in New Issue