diff --git a/.vagrant-puppet/files/etc/icingaweb/modules/monitoring/menu.ini b/.vagrant-puppet/files/etc/icingaweb/modules/monitoring/menu.ini index 81cf36324..7713dcd40 100644 --- a/.vagrant-puppet/files/etc/icingaweb/modules/monitoring/menu.ini +++ b/.vagrant-puppet/files/etc/icingaweb/modules/monitoring/menu.ini @@ -41,6 +41,11 @@ title = "Services" url = "monitoring/list/services" priority = 50 +[Overview.Servicematrix] +title = "Servicematrix" +url = "monitoring/list/servicematrix" +priority = 51 + [Overview.Servicegroups] title = "Servicegroups" url = "monitoring/list/servicegroups" diff --git a/application/views/scripts/pivottablePagination.phtml b/application/views/scripts/pivottablePagination.phtml new file mode 100644 index 000000000..0b4c7420e --- /dev/null +++ b/application/views/scripts/pivottablePagination.phtml @@ -0,0 +1,48 @@ +count() <= 1 && $yAxisPaginator->count() <= 1) { + return; // Display this pagination only if there are multiple pages +} + +$fromTo = $this->translate('%s: %d to %d of %d'); +$xAxisPages = $xAxisPaginator->getPages('all'); +$yAxisPages = $yAxisPaginator->getPages('all'); + +?> + +
+ translate('Navigation'); ?> + + +pagesInRange as $yAxisPage): ?> + +pagesInRange as $xAxisPage): ?> + current && $yAxisPage === $yAxisPages->current ? ' class="active"' : ''; ?>> +current || $yAxisPage !== $yAxisPages->current): ?> + + +   + + + + + + +
+
diff --git a/config/modules/monitoring/menu.ini b/config/modules/monitoring/menu.ini index 81cf36324..7713dcd40 100644 --- a/config/modules/monitoring/menu.ini +++ b/config/modules/monitoring/menu.ini @@ -41,6 +41,11 @@ title = "Services" url = "monitoring/list/services" priority = 50 +[Overview.Servicematrix] +title = "Servicematrix" +url = "monitoring/list/servicematrix" +priority = 51 + [Overview.Servicegroups] title = "Servicegroups" url = "monitoring/list/servicegroups" diff --git a/library/Icinga/Data/BaseQuery.php b/library/Icinga/Data/BaseQuery.php index 061ff0f46..6da7dbb13 100644 --- a/library/Icinga/Data/BaseQuery.php +++ b/library/Icinga/Data/BaseQuery.php @@ -59,6 +59,13 @@ abstract class BaseQuery implements Filterable */ private $limitOffset; + /** + * Whether its a distinct query or not + * + * @var bool + */ + private $distinct = false; + /** * The backend independent filter to use for this query * @@ -294,6 +301,30 @@ abstract class BaseQuery implements Filterable return $this; } + /** + * Return only distinct results + * + * @param bool $distinct Whether the query should be distinct or not + * + * @return BaseQuery + */ + public function distinct($distinct = true) + { + $this->distinct = $distinct; + + return $this; + } + + /** + * Determine whether this query returns only distinct results + * + * @return bool True in case its a distinct query otherwise false + */ + public function isDistinct() + { + return $this->distinct; + } + /** * Determine whether this query will be ordered explicitly * @@ -392,7 +423,7 @@ abstract class BaseQuery implements Filterable */ public function paginate($limit = null, $page = null) { - if ($page === null && $limit === null) { + if ($page === null || $limit === null) { $request = \Zend_Controller_Front::getInstance()->getRequest(); if ($page === null) { diff --git a/library/Icinga/Data/Db/Query.php b/library/Icinga/Data/Db/Query.php index 0031a9987..3ee381ff5 100644 --- a/library/Icinga/Data/Db/Query.php +++ b/library/Icinga/Data/Db/Query.php @@ -80,6 +80,21 @@ class Query extends BaseQuery $this->baseQuery = $this->db->select(); } + public function __clone() + { + if ($this->baseQuery !== null) { + $this->baseQuery = clone $this->baseQuery; + } + + if ($this->selectQuery !== null) { + $this->selectQuery = clone $this->selectQuery; + } + + if ($this->countQuery !== null) { + $this->countQuery = clone $this->countQuery; + } + } + /** * Return the raw base query * @@ -141,6 +156,7 @@ class Query extends BaseQuery { $this->selectQuery = clone($this->baseQuery); $this->selectQuery->columns($this->getColumns()); + $this->selectQuery->distinct($this->isDistinct()); if ($this->hasOrder()) { foreach ($this->getOrderColumns() as $col) { $this->selectQuery->order( @@ -193,8 +209,8 @@ class Query extends BaseQuery */ private function createCountQuery() { - if ($this->useSubqueryCount) { - $this->countQuery = $this->createCountAsSubquery(); + if ($this->isDistinct() || $this->useSubqueryCount) { + $this->countQuery = $this->createCountAsSubQuery(); } else { $this->countQuery = $this->createCustomCountQuery(); } diff --git a/library/Icinga/Data/Db/TreeToSqlParser.php b/library/Icinga/Data/Db/TreeToSqlParser.php index facdba9d4..b1d482ef7 100644 --- a/library/Icinga/Data/Db/TreeToSqlParser.php +++ b/library/Icinga/Data/Db/TreeToSqlParser.php @@ -112,15 +112,14 @@ class TreeToSqlParser private function parseConjunctionNode(Node $node) { $queryString = ''; - $leftQuery = $this->nodeToSqlQuery($node->left); - $rightQuery = $this->nodeToSqlQuery($node->right); + $leftQuery = $node->left !== null ? $this->nodeToSqlQuery($node->left) : ''; + $rightQuery = $node->right !== null ? $this->nodeToSqlQuery($node->right) : ''; if ($leftQuery != '') { $queryString .= $leftQuery . ' '; } if ($rightQuery != '') { - $queryString .= (($queryString !== '') ? $node->type . ' ' : ' ') . $rightQuery; } return $queryString; @@ -193,8 +192,7 @@ class TreeToSqlParser if ($tree->root == null) { return; } - $tree->root = $tree->normalizeTree($tree->root); - $sql = $this->nodeToSqlQuery($tree->root); + $sql = $this->nodeToSqlQuery($tree->normalizeTree($tree->root)); if ($this->filtersAggregate()) { $baseQuery->having($sql); diff --git a/library/Icinga/Data/PivotTable.php b/library/Icinga/Data/PivotTable.php index 92878a372..98c165206 100644 --- a/library/Icinga/Data/PivotTable.php +++ b/library/Icinga/Data/PivotTable.php @@ -1,135 +1,188 @@ query = $query; - $this->verticalColumn = $verticalColumn; - $this->horizontalColumn = $horizontalColumn; - } - - public function limit($limit = null, $offset = null) - { - $this->limit = $limit; - $this->offset = $offset; - return $this; - } - - public function getLimit() - { - if ($this->limit === null) { - return 20; - } - return $this->limit; - } - - public function getOffset() - { - if ($this->limit === null) { - return 20; - } - return $this->offset; - } - - public function verticalLimit($limit = null, $offset = null) - { - // TODO: Trigger limit by calling $this->limit()? - if ($limit === null) { - $limit = $this->getLimit(); - } - if ($offset === null) { - $offset = $this->getOffset(); - } - $this->verticalLimit = $limit; - $this->verticalOffset = $offset; - return $this; - } - - public function paginateVertical($limit = null, $offset = null) - { - $this->verticalLimit($limit, $offset); - return Paginator($this); - } - - public function getVerticalLimit() - { - if ($this->verticalLimit === null) { - return 20; - } - return $this->verticalLimit; - } - - public function getVerticalOffset() - { - if ($this->verticalLimit === null) { - return 20; - } - return $this->verticalOffset; + $this->baseQuery = $query; + $this->xAxisColumn = $xAxisColumn; + $this->yAxisColumn = $yAxisColumn; + $this->prepareQueries(); } /** - * Fetch all columns + * Prepare the queries used for the pre processing */ - public function fetchAll() + protected function prepareQueries() { - $xcol = $this->horizontalColumn; - $ycol = $this->verticalColumn; - $queryX = clone($this->query); - $queryX->columns($xcol); - if ($this->limit !== null) { - $queryX->limit($this->getLimit(), $this->getOffset()); - } - $queryX->limit(40); - $listX = $queryX->fetchColumn(); - $queryY = clone($this->query); + $this->xAxisQuery = clone $this->baseQuery; + $this->xAxisQuery->distinct(); + $this->xAxisQuery->setColumns(array($this->xAxisColumn)); + $this->yAxisQuery = clone $this->baseQuery; + $this->yAxisQuery->distinct(); + $this->yAxisQuery->setColumns(array($this->yAxisColumn)); + } - $queryY->columns($ycol); - if ($this->verticalLimit !== null) { - $queryY->limit($this->getVerticalLimit(), $this->getVerticalOffset()); - } - $queryY->limit(50); - $listY = $queryY->fetchColumn(); + /** + * Return the value for the given request parameter + * + * @param string $axis The axis for which to return the parameter ('x' or 'y') + * @param string $param The parameter name to return + * @param int $default The default value to return + * + * @return int + */ + protected function getPaginationParameter($axis, $param, $default = null) + { + $request = Icinga::app()->getFrontController()->getRequest(); - // TODO: resetOrder - $this->query - ->where($ycol, $listY) - ->where($xcol, $listX) - ->order($ycol) - ->order($xcol); - $pivot = array(); - $emptyrow = (object) array(); - foreach ($this->query->listColumns() as $col) { - $emptyrow->$col = null; + $value = $request->getParam($param, ''); + if (strpos($value, ',') > 0) { + $parts = explode(',', $value, 2); + return intval($parts[$axis === 'x' ? 0 : 1]); } - foreach ($listY as $y) { - foreach ($listX as $x) { - $row = clone($emptyrow); - $row->$xcol = $x; - $row->$ycol = $y; - $pivot[$y][$x] = $row; + + return $default !== null ? $default : 0; + } + + /** + * Return a pagination adapter for the x axis query + * + * $limit and $page are taken from the current request if not given. + * + * @param int $limit The maximum amount of entries to fetch + * @param int $page The page to set as current one + * + * @return Zend_Paginator + */ + public function paginateXAxis($limit = null, $page = null) + { + if ($limit === null || $page === null) { + if ($limit === null) { + $limit = $this->getPaginationParameter('x', 'limit', 20); + } + + if ($page === null) { + $page = $this->getPaginationParameter('x', 'page', 1); } } - foreach ($this->query->fetchAll() as $row) { - $pivot[$row->$ycol][$row->$xcol] = $row; + $this->xAxisQuery->limit($limit, $page > 0 ? ($page - 1) * $limit : 0); + + $paginator = new Zend_Paginator(new QueryAdapter($this->xAxisQuery)); + $paginator->setItemCountPerPage($limit); + $paginator->setCurrentPageNumber($page); + return $paginator; + } + + /** + * Return a pagination adapter for the y axis query + * + * $limit and $page are taken from the current request if not given. + * + * @param int $limit The maximum amount of entries to fetch + * @param int $page The page to set as current one + * + * @return Zend_Paginator + */ + public function paginateYAxis($limit = null, $page = null) + { + if ($limit === null || $page === null) { + if ($limit === null) { + $limit = $this->getPaginationParameter('y', 'limit', 20); + } + + if ($page === null) { + $page = $this->getPaginationParameter('y', 'page', 1); + } } + + $this->yAxisQuery->limit($limit, $page > 0 ? ($page - 1) * $limit : 0); + + $paginator = new Zend_Paginator(new QueryAdapter($this->yAxisQuery)); + $paginator->setItemCountPerPage($limit); + $paginator->setCurrentPageNumber($page); + return $paginator; + } + + /** + * Return the pivot table as array + * + * @return array + */ + public function toArray() + { + $xAxis = $this->xAxisQuery->fetchColumn(); + $yAxis = $this->yAxisQuery->fetchColumn(); + + $pivot = array(); + if (!empty($xAxis) && !empty($yAxis)) { + $this->baseQuery->where($this->xAxisColumn, $xAxis)->where($this->yAxisColumn, $yAxis); + + foreach ($this->baseQuery->fetchAll() as $row) { + if (!array_key_exists($row->{$this->yAxisColumn}, $pivot)) { + $defaults = array(); + foreach ($xAxis as $label) { + $defaults[$label] = null; + } + $pivot[$row->{$this->yAxisColumn}] = $defaults; + } + + $pivot[$row->{$this->yAxisColumn}][$row->{$this->xAxisColumn}] = $row; + } + } + return $pivot; } } diff --git a/library/Icinga/Filter/Query/Tree.php b/library/Icinga/Filter/Query/Tree.php index ced419679..eb2736f6f 100644 --- a/library/Icinga/Filter/Query/Tree.php +++ b/library/Icinga/Filter/Query/Tree.php @@ -232,12 +232,12 @@ class Tree $node->left = $this->removeInvalidFilter($node->left, $filter); $node->right = $this->removeInvalidFilter($node->right, $filter); - if ($node->left && $node->right) { + if ($node->left || $node->right) { + if (!$node->left) { + $node->left = $node->right; + $node->right = null; + } return $node; - } elseif ($node->left) { - return $node->left; - } elseif ($node->right) { - return $node->right; } return null; diff --git a/library/Icinga/Web/Widget/FilterBadgeRenderer.php b/library/Icinga/Web/Widget/FilterBadgeRenderer.php index 2a59fa689..8bf0bced9 100644 --- a/library/Icinga/Web/Widget/FilterBadgeRenderer.php +++ b/library/Icinga/Web/Widget/FilterBadgeRenderer.php @@ -182,6 +182,6 @@ EOT; return ''; } $this->buildBaseUrl(); - return $this->nodeToBadge($this->tree->root); + return $this->nodeToBadge(Tree::normalizeTree($this->tree->root)); } } diff --git a/modules/monitoring/application/controllers/ListController.php b/modules/monitoring/application/controllers/ListController.php index a6b5da9b3..f735e482c 100644 --- a/modules/monitoring/application/controllers/ListController.php +++ b/modules/monitoring/application/controllers/ListController.php @@ -458,6 +458,34 @@ class Monitoring_ListController extends Controller $this->view->history = $query->paginate(); } + public function servicematrixAction() + { + $this->view->title = 'Servicematrix'; + $this->addTitleTab('servicematrix'); + $dataview = ServiceStatusView::fromRequest( + $this->getRequest(), + array( + 'host_name', + 'service_description', + 'service_state', + 'service_output', + 'service_handled' + ) + ); + + $this->setupFilterControl($dataview, 'servicematrix'); + $this->setupSortControl( + array( + 'host_name' => 'Hostname', + 'service_description' => 'Service description' + ) + ); + + $this->view->pivot = $dataview->pivot('service_description', 'host_name'); + $this->view->horizontalPaginator = $this->view->pivot->paginateXAxis(); + $this->view->verticalPaginator = $this->view->pivot->paginateYAxis(); + } + /** * Apply current users monitoring/filter restrictions to the given query * diff --git a/modules/monitoring/application/views/scripts/list/servicematrix.phtml b/modules/monitoring/application/views/scripts/list/servicematrix.phtml new file mode 100644 index 000000000..dac296f46 --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/servicematrix.phtml @@ -0,0 +1,69 @@ +compact): ?> +
+ tabs; ?> +
+ Sort by sortControl->render($this); ?> +
+ partial( + 'pivottablePagination.phtml', + 'default', + array( + 'xAxisPaginator' => $this->horizontalPaginator, + 'yAxisPaginator' => $this->verticalPaginator + ) + ); ?> +
+ +
+ + +pivot->toArray() as $host_name => $serviceStates): ?> + + + + + + + + + + + + + + + + + + + + + +
  +
+ + + + 18 ? substr($service_description, 0, 18) . '...' : $service_description; ?> + + + +
+
+ + + + + + + + · + +
+
diff --git a/modules/monitoring/library/Monitoring/DataView/DataView.php b/modules/monitoring/library/Monitoring/DataView/DataView.php index b5152e4a1..3310a1d6b 100644 --- a/modules/monitoring/library/Monitoring/DataView/DataView.php +++ b/modules/monitoring/library/Monitoring/DataView/DataView.php @@ -30,6 +30,7 @@ namespace Icinga\Module\Monitoring\DataView; use Icinga\Data\BaseQuery; +use Icinga\Data\PivotTable; use Icinga\Filter\Filterable; use Icinga\Filter\Query\Tree; use Icinga\Module\Monitoring\Backend; @@ -167,6 +168,19 @@ abstract class DataView implements Filterable return array(); } + /** + * Return a pivot table for the given columns based on the current query + * + * @param string $xAxisColumn The column to use for the x axis + * @param string $yAxisColumn The column to use for the y axis + * + * @return PivotTable + */ + public function pivot($xAxisColumn, $yAxisColumn) + { + return new PivotTable($this->query, $xAxisColumn, $yAxisColumn); + } + /** * Sort the rows, according to the specified sort column and order * diff --git a/public/css/icinga/monitoring-colors.less b/public/css/icinga/monitoring-colors.less index 6c15489eb..b2a7b8f3a 100644 --- a/public/css/icinga/monitoring-colors.less +++ b/public/css/icinga/monitoring-colors.less @@ -501,6 +501,143 @@ div.box.contactgroup div.box.entry p { /* End of monitoring box element styles */ +/* Monitoring pivot table styles */ + +div.pivot-pagination { + margin: 1em; + + table { + border-spacing: 0.2em; + border-collapse: separate; + border: 1px solid LightGrey; + border-radius: 0.3em; + + td { + padding: 0; + line-height: 1em; + background-color: #fbfbfb; + + &:hover, &.active { + background-color: #e5e5e5; + } + + a { + width: 16px; + height: 16px; + display: block; + } + } + } +} + +table.pivot { + thead { + th { + height: 6em; + padding: 2px; + position: relative; + + div { + top: 2px; + left: 2px; + right: -1.5em; + position: absolute; + padding-left: 1.2em; + + span { + width: 1.5em; + margin-right: 4px; + margin-top: 4em; + line-height: 2em; + white-space: nowrap; + display: block; + float: left; + + transform: rotate(-45deg); + transform-origin: bottom left; + -o-transform: rotate(-45deg); + -o-transform-origin: bottom left; + -ms-transform: rotate(-45deg); + -ms-transform-origin: bottom left; + -moz-transform: rotate(-45deg); + -moz-transform-origin: bottom left; + -webkit-transform: rotate(-45deg); + -webkit-transform-origin: bottom left; + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + + abbr { + font-size: 0.8em; + } + } + } + } + } + + tbody { + th { + padding: 0 14px 0 0; + + a { + font-size: 0.8em; + text-decoration: none; + color: black; + + &:hover { + color: #666; + } + } + } + + td { + min-width: 1.5em; + min-height: 1.5em; + padding: 2px; + text-align: center; + + a { + width: 1.5em; + height: 1.5em; + display: block; + border-radius: 0.5em; + + &.state_ok { + background-color: @colorOk; + } + + &.state_pending { + background-color: @colorPending; + } + + &.state_warning { + background-color: @colorWarning; + + &.handled { + background-color: @colorWarningHandled; + } + } + + &.state_critical { + background-color: @colorCritical; + + &.handled { + background-color: @colorCriticalHandled; + } + } + + &.state_unknown { + background-color: @colorUnknown; + + &.handled { + background-color: @colorUnknownHandled; + } + } + } + } + } +} + +/* End of monitoring pivot table styles */ + .controls { font-size: 0.9em; }