Merge branch 'bugfix/service-grid-display_name-9538' into bugfix/service-grid-grouping-postgres-9333

Conflicts:
	library/Icinga/Data/PivotTable.php
	modules/monitoring/application/controllers/ListController.php
This commit is contained in:
Eric Lippmann 2015-08-25 15:51:20 +02:00
commit c8790fad93
19 changed files with 335 additions and 281 deletions

View File

@ -62,14 +62,14 @@ with Icinga Web 2 (e.g. an alias) no matter what the primary user id might actua
Directive | Description
------------------------|------------
**backend** | `ad`
**backend** | `msldap`
**resource** | The name of the LDAP resource defined in [resources.ini](resources.md#resources).
**Example:**
```
[auth_ad]
backend = ad
backend = msldap
resource = my_ad
```

View File

@ -31,6 +31,13 @@ class PivotTable implements Sortable
*/
protected $yAxisColumn;
/**
* Column for sorting the result set
*
* @var array
*/
protected $order = array();
/**
* The filter being applied on the query for the x axis
*
@ -60,11 +67,18 @@ class PivotTable implements Sortable
protected $yAxisQuery;
/**
* Column for sorting the result set
* X-axis header
*
* @var array
* @var string|null
*/
protected $order = array();
protected $xAxisHeader;
/**
* Y-axis header
*
* @var string|null
*/
protected $yAxisHeader;
/**
* Create a new pivot table
@ -131,6 +145,56 @@ class PivotTable implements Sortable
return $this;
}
/**
* Get the x-axis header
*
* Defaults to {@link $xAxisColumn} in case no x-axis header has been set using {@link setXAxisHeader()}
*
* @return string
*/
public function getXAxisHeader()
{
return $this->xAxisHeader !== null ? $this->xAxisHeader : $this->xAxisColumn;
}
/**
* Set the x-axis header
*
* @param string $xAxisHeader
*
* @return $this
*/
public function setXAxisHeader($xAxisHeader)
{
$this->xAxisHeader = (string) $xAxisHeader;
return $this;
}
/**
* Get the y-axis header
*
* Defaults to {@link $yAxisColumn} in case no x-axis header has been set using {@link setYAxisHeader()}
*
* @return string
*/
public function getYAxisHeader()
{
return $this->yAxisHeader !== null ? $this->yAxisHeader : $this->yAxisColumn;
}
/**
* Set the y-axis header
*
* @param string $yAxisHeader
*
* @return $this
*/
public function setYAxisHeader($yAxisHeader)
{
$this->yAxisHeader = (string) $yAxisHeader;
return $this;
}
/**
* Return the value for the given request parameter
*
@ -163,7 +227,7 @@ class PivotTable implements Sortable
if ($this->xAxisQuery === null) {
$this->xAxisQuery = clone $this->baseQuery;
$this->xAxisQuery->group($this->xAxisColumn);
$this->xAxisQuery->columns(array($this->xAxisColumn));
$this->xAxisQuery->columns(array($this->xAxisColumn, $this->getXAxisHeader()));
if ($this->xAxisFilter !== null) {
$this->xAxisQuery->addFilter($this->xAxisFilter);
@ -188,7 +252,7 @@ class PivotTable implements Sortable
if ($this->yAxisQuery === null) {
$this->yAxisQuery = clone $this->baseQuery;
$this->yAxisQuery->group($this->yAxisColumn);
$this->yAxisQuery->columns(array($this->yAxisColumn));
$this->yAxisQuery->columns(array($this->yAxisColumn, $this->getYAxisHeader()));
if ($this->yAxisFilter !== null) {
$this->yAxisQuery->addFilter($this->yAxisFilter);
@ -199,7 +263,6 @@ class PivotTable implements Sortable
isset($this->order[$this->yAxisColumn]) ? $this->order[$this->yAxisColumn] : self::SORT_ASC
);
}
return $this->yAxisQuery;
}
@ -276,33 +339,39 @@ class PivotTable implements Sortable
($this->xAxisFilter === null && $this->yAxisFilter === null)
|| ($this->xAxisFilter !== null && $this->yAxisFilter !== null)
) {
$xAxis = $this->queryXAxis()->fetchColumn();
$yAxis = $this->queryYAxis()->fetchColumn();
$xAxis = $this->queryXAxis()->fetchPairs();
$yAxis = $this->queryYAxis()->fetchPairs();
} else {
if ($this->xAxisFilter !== null) {
$xAxis = $this->queryXAxis()->fetchColumn();
$yAxis = $this->queryYAxis()->where($this->xAxisColumn, $xAxis)->fetchColumn();
$xAxis = $this->queryXAxis()->fetchPairs();
$yAxis = $this->queryYAxis()->where($this->xAxisColumn, $xAxis)->fetchPairs();
} else { // $this->yAxisFilter !== null
$yAxis = $this->queryYAxis()->fetchColumn();
$xAxis = $this->queryXAxis()->where($this->yAxisColumn, $yAxis)->fetchColumn();
$yAxis = $this->queryYAxis()->fetchPairs();
$xAxis = $this->queryXAxis()->where($this->yAxisColumn, $yAxis)->fetchPairs();
}
}
$pivotData = array();
$pivotHeader = array(
'cols' => $xAxis,
'rows' => $yAxis
);
if (! empty($xAxis) && ! empty($yAxis)) {
$xAxisKeys = array_keys($xAxis);
$yAxisKeys = array_keys($yAxis);
$this->baseQuery
->where($this->xAxisColumn, $xAxisKeys)
->where($this->yAxisColumn, $yAxisKeys);
$pivot = array();
if (!empty($xAxis) && !empty($yAxis)) {
$this->baseQuery->where($this->xAxisColumn, $xAxis)->where($this->yAxisColumn, $yAxis);
foreach ($yAxis as $yLabel) {
foreach ($xAxis as $xLabel) {
$pivot[$yLabel][$xLabel] = null;
foreach ($yAxisKeys as $yAxisKey) {
foreach ($xAxisKeys as $xAxisKey) {
$pivotData[$yAxisKey][$xAxisKey] = null;
}
}
foreach ($this->baseQuery as $row) {
$pivot[$row->{$this->yAxisColumn}][$row->{$this->xAxisColumn}] = $row;
$pivotData[$row->{$this->yAxisColumn}][$row->{$this->xAxisColumn}] = $row;
}
}
return $pivot;
return array($pivotData, $pivotHeader);
}
}

View File

@ -4,7 +4,7 @@
namespace Icinga\Util;
/**
* Common string helper
* Common string functions
*/
class String
{
@ -103,8 +103,8 @@ class String
/**
* Check if a string ends with a different string
*
* @param $haystack The string to search for matches
* @param $needle The string to match at the start of the haystack
* @param $haystack string The string to search for matches
* @param $needle string The string to match at the start of the haystack
*
* @return bool Whether or not needle is at the beginning of haystack
*/

View File

@ -12,6 +12,7 @@ class StyleSheet
protected static $lessFiles = array(
'../application/fonts/fontello-ifont/css/ifont-embedded.css',
'css/vendor/tipsy.css',
'css/icinga/mixins.less',
'css/icinga/defaults.less',
'css/icinga/animation.less',
'css/icinga/layout-colors.less',

View File

@ -581,29 +581,36 @@ class Monitoring_ListController extends Controller
{
$this->addTitleTab('servicegrid', $this->translate('Service Grid'), $this->translate('Show the Service Grid'));
$this->setAutorefreshInterval(15);
$problems = (bool) $this->params->shift('problems', 0);
$query = $this->backend->select()->from('servicestatus', array(
'host_display_name',
'host_name',
'service_description',
'service_state',
'service_display_name',
'service_handled',
'service_output',
'service_handled'
'service_state'
));
$this->filterQuery($query);
$this->applyRestriction('monitoring/filter/objects', $query);
$pivot = $query->pivot(
'service_description',
'host_name',
$problems ? Filter::where('service_problem', 1) : null,
$problems ? Filter::where('service_problem', 1) : null
);
$filter = (bool) $this->params->shift('problems', false) ? Filter::where('service_problem', 1) : null;
$pivot = $query
->pivot(
'service_description',
'host_name',
$filter,
$filter ? clone $filter : null
)
->setXAxisHeader('service_display_name')
->setYAxisHeader('host_display_name');
$this->setupSortControl(array(
'host_name' => $this->translate('Hostname'),
'service_description' => $this->translate('Service description')
'host_display_name' => $this->translate('Hostname'),
'service_display_name' => $this->translate('Service Name')
), $pivot);
$this->view->pivot = $pivot;
$this->view->horizontalPaginator = $pivot->paginateXAxis();
$this->view->verticalPaginator = $pivot->paginateYAxis();
$this->view->verticalPaginator = $pivot->paginateYAxis();
list($pivotData, $pivotHeader) = $pivot->toArray();
$this->view->pivotData = $pivotData;
$this->view->pivotHeader = $pivotHeader;
}
/**

View File

@ -27,8 +27,8 @@ class Zend_View_Helper_IconImage extends Zend_View_Helper_Abstract
public function host($object)
{
if ($object->host_icon_image && ! preg_match('/[\'"]/', $object->host_icon_image)) {
return $this->view->img(
'img/icons/' . Macro::resolveMacros($object->host_icon_image, $object),
return $this->view->icon(
Macro::resolveMacros($object->host_icon_image, $object),
null,
array(
'alt' => $object->host_icon_image_alt,
@ -49,8 +49,8 @@ class Zend_View_Helper_IconImage extends Zend_View_Helper_Abstract
public function service($object)
{
if ($object->service_icon_image && ! preg_match('/[\'"]/', $object->service_icon_image)) {
return $this->view->img(
'img/icons/' . Macro::resolveMacros($object->service_icon_image, $object),
return $this->view->icon(
Macro::resolveMacros($object->service_icon_image, $object),
null,
array(
'alt' => $object->service_icon_image_alt,

View File

@ -36,7 +36,8 @@ if (! $this->compact): ?>
$notification->service_description,
$notification->service_display_name,
$notification->host_name,
$notification->host_display_name
$notification->host_display_name,
'rowaction'
) ?>
<?php else: ?>
<?= $this->icon('host', $this->translate('Host')); ?>

View File

@ -12,97 +12,84 @@ if (! $this->compact): ?>
<?php endif ?>
<div class="content" data-base-target="_next">
<?php
$hasHeader = false;
$pivotData = $this->pivot->toArray();
if (count($pivotData) === 0) {
if (empty($pivotData)) {
echo $this->translate('No services found matching the filter') . '</div>';
return;
}
$hostFilter = '(host_name=' . implode('|host_name=', array_keys($pivotData)) . ')';
?>
<table class="pivot servicestates">
<?php foreach ($pivotData as $host_name => $serviceStates): ?>
<?php if (!$hasHeader): ?>
<thead>
<tr>
<th><?= $this->partial(
'joystickPagination.phtml',
'default',
array(
'xAxisPaginator' => $horizontalPaginator,
'yAxisPaginator' => $verticalPaginator
)
); ?></th>
<th colspan="<?= count($serviceStates); ?>">
<div>
<?php
$serviceDescriptions = array_keys($serviceStates);
$serviceFilter = '(service_description=' . implode('|service_description=', $serviceDescriptions) . ')';
foreach ($serviceDescriptions as $service_description): ?>
<span>
<?= $this->qlink(
'<abbr>' . (strlen($service_description) > 18 ? substr($service_description, 0, 18) . '...' : $service_description) . '</abbr>',
<table class="service-grid-table">
<thead>
<tr>
<th><?= $this->partial(
'joystickPagination.phtml',
'default',
array(
'xAxisPaginator' => $horizontalPaginator,
'yAxisPaginator' => $verticalPaginator
)
); ?></th>
<?php foreach ($pivotHeader['cols'] as $serviceDescription => $serviceDisplayName): ?>
<th class="rotate-45"><div><span><?= $this->qlink(
$this->ellipsis($serviceDisplayName, 18),
'monitoring/list/services?' . $hostFilter,
array(
'service_description' => $service_description
),
array(
'title' => sprintf($this->translate('List all services with the name "%s" on all reported hosts'), $service_description)
),
array('service_description' => $serviceDescription),
array('title' => sprintf(
$this->translate('List all services with the name "%s" on all reported hosts'),
$serviceDisplayName
)),
false
); ?>
</span>
<?php endforeach ?>
</div>
</th>
</tr>
) ?></span></div></th>
<?php endforeach ?>
</tr>
</thead>
<tbody>
<?php $hasHeader = true; ?>
<?php endif ?>
<tr>
<th>
<?= $this->qlink(
$host_name,
'monitoring/list/services?' . $serviceFilter,
array('host' => $host_name),
array('title' => sprintf($this->translate('List all reported services on host %s'), $host_name))
); ?>
</th>
<?php foreach (array_values($serviceStates) as $service): ?>
<?php if ($service !== null): ?>
<td>
<span class="sr-only" id="<?= $service->host_name . '_' . $service->service_description . '_desc'; ?>">
<?= $this->escape($service->service_output); ?>
</span>
<?= $this->qlink(
'',
'monitoring/show/service',
array(
'host' => $service->host_name,
'service' => $service->service_description
),
array(
'aria-describedby' => $service->host_name . '_' . $service->service_description . '_desc',
'class' => 'state_' . Service::getStateText($service->service_state). ($service->service_handled ? ' handled' : ''),
'title' => $this->escape($service->service_output),
'aria-label' => sprintf(
$this->translate('Show detailed information for service %s on host %s'),
$service->service_description,
$service->host_name
)
)
); ?>
</td>
<?php else: ?>
<td><span aria-hidden="true">&middot;</span></td>
<?php endif ?>
<?php endforeach ?>
</tr>
<?php endforeach ?>
<?php foreach ($pivotHeader['rows'] as $hostName => $hostDisplayName): ?>
<tr>
<th><?php
$services = $pivotData[$hostName];
$serviceFilter = '(service_description=' . implode('|service_description=', array_keys($services)) . ')';
echo $this->qlink(
$hostDisplayName,
'monitoring/list/services?' . $serviceFilter,
array('host_name' => $hostName),
array('title' => sprintf($this->translate('List all reported services on host %s'), $hostDisplayName))
);
?></th>
<?php foreach (array_keys($pivotHeader['cols']) as $serviceDescription): ?>
<td>
<?php
$service = $pivotData[$hostName][$serviceDescription];
if ($service === null): ?>
<span aria-hidden="true">&middot;</span>
<?php continue; endif ?>
<?php $ariaDescribedById = $this->protectId($service->host_name . '_' . $service->service_description . '_desc') ?>
<span class="sr-only" id="<?= $ariaDescribedById ?>">
<?= $this->escape($service->service_output) ?>
</span>
<?= $this->qlink(
'',
'monitoring/show/service',
array(
'host' => $hostName,
'service' => $serviceDescription
),
array(
'aria-describedby' => $ariaDescribedById,
'class' => 'bg-state-' . Service::getStateText($service->service_state) . ($service->service_handled ? ' handled' : ''),
'title' => $this->escape($service->service_output),
'aria-label' => sprintf(
$this->translate('Show detailed information for service %s on host %s'),
$service->service_display_name,
$service->host_display_name
)
)
); ?>
</td>
<?php endforeach ?>
</tr>
<?php endforeach ?>
</tbody>
</table>
</table>
</div>

View File

@ -223,3 +223,9 @@ $dashboard->add(
$this->translate('Host Problems'),
'monitoring/list/hosts?host_problem=1&sort=host_severity'
);
/*
* CSS
*/
$this->provideCssFile('colors.less');
$this->provideCssFile('service-grid.less');

View File

@ -9,7 +9,7 @@ use Icinga\Data\Filter\Filter;
/**
* Query for event history records
*/
class EventHistoryQuery extends IdoQuery
class EventhistoryQuery extends IdoQuery
{
/**
* {@inheritdoc}

View File

@ -8,7 +8,7 @@ use Zend_Db_Select;
/**
* Query for host and service group summaries
*/
class GroupSummaryQuery extends IdoQuery
class GroupsummaryQuery extends IdoQuery
{
/**
* {@inheritdoc}

View File

@ -10,7 +10,7 @@ use Icinga\Data\Filter\Filter;
/**
* Query for host and service status summary
*/
class StatusSummaryQuery extends IdoQuery
class StatussummaryQuery extends IdoQuery
{
/**
* {@inheritdoc}

View File

@ -29,8 +29,6 @@ abstract class DataView implements QueryInterface, SortRules, FilterColumns, Ite
*/
protected $query;
protected $filter;
protected $connection;
protected $isSorted = false;
@ -52,7 +50,6 @@ abstract class DataView implements QueryInterface, SortRules, FilterColumns, Ite
{
$this->connection = $connection;
$this->query = $connection->query($this->getQueryName(), $columns);
$this->filter = Filter::matchAll();
}
/**
@ -91,7 +88,6 @@ abstract class DataView implements QueryInterface, SortRules, FilterColumns, Ite
public function where($condition, $value = null)
{
$this->filter->addFilter(Filter::where($condition, $value));
$this->query->where($condition, $value);
return $this;
}
@ -268,9 +264,14 @@ abstract class DataView implements QueryInterface, SortRules, FilterColumns, Ite
return $columns;
}
/**
* Return the current filter
*
* @return Filter
*/
public function getFilter()
{
return $this->filter;
return $this->query->getFilter();
}
/**
@ -471,8 +472,7 @@ abstract class DataView implements QueryInterface, SortRules, FilterColumns, Ite
*/
public function addFilter(Filter $filter)
{
$this->query->addFilter(clone($filter));
$this->filter = $filter; // TODO: Hmmmm.... and?
$this->query->addFilter($filter);
return $this;
}
@ -496,7 +496,8 @@ abstract class DataView implements QueryInterface, SortRules, FilterColumns, Ite
*/
public function peekAhead($state = true)
{
return $this->query->peekAhead($state);
$this->query->peekAhead($state);
return $this;
}
/**

View File

@ -450,21 +450,29 @@ abstract class MonitoredObject implements Filterable
*/
public function fetchEventhistory()
{
$eventHistory = $this->backend->select()->from('eventhistory', array(
'object_type',
'host_name',
'host_display_name',
'service_description',
'service_display_name',
'timestamp',
'state',
'output',
'type'
))
$eventHistory = $this->backend
->select()
->from(
'eventhistory',
array(
'object_type',
'host_name',
'host_display_name',
'service_description',
'service_display_name',
'timestamp',
'state',
'output',
'type'
)
)
->where('object_type', $this->type)
->where('host_name', $this->host_name);
if ($this->type === self::TYPE_SERVICE) {
$eventHistory->where('service_description', $this->service_description);
}
$this->eventhistory = $eventHistory->applyFilter($this->getFilter());
return $this;
}

View File

@ -0,0 +1,43 @@
/*! Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
.bg-state-ok,
.bg-state-up {
background-color: @colorOk;
}
.bg-state-warning {
background-color: @colorWarning;
&.handled {
background-color: @colorWarningHandled;
}
}
.bg-state-critical,
.bg-state-down {
background-color: @colorCritical;
&.handled {
background-color: @colorCriticalHandled;
}
}
.bg-state-unreachable {
background-color: @colorUnreachable;
&.handled {
background-color: @colorUnreachableHandled;
}
}
.bg-state-unknown {
background-color: @colorUnknown;
&.handled {
background-color: @colorUnknownHandled;
}
}
.bg-state-pending {
background-color: @colorPending;
}

View File

@ -846,112 +846,6 @@ table.joystick-pagination {
}
}
table.pivot {
a {
text-decoration: none;
color: black;
&:hover {
color: @colorTextDefault;
}
}
& > thead {
th {
height: 6em;
div {
margin-right: -1.5em;
padding-left: 1.3em;
span {
width: 1.5em;
margin-right: 0.25em;
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 {
border: 0; // Remove highlighting in firefox
font-size: 0.8em;
}
}
}
}
}
& > tbody {
th {
padding: 0 14px 0 0;
white-space: nowrap;
a {
font-size: 0.8em;
}
}
td {
padding: 2px;
min-width: 1.5em;
line-height: 1.5em;
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 */
/* Monitoring timeline styles */

View File

@ -0,0 +1,38 @@
/*! Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
table.service-grid-table {
white-space: nowrap;
th {
a {
color: @colorMainLink;
font-weight: normal;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
td {
text-align: center;
width: 1.5em;
a {
.rounded-corners(0.4em);
display: block;
height: 1.5em;
width: 1.5em;
}
}
th.rotate-45 {
height: 6em;
div {
.transform(translate(0.4em, 2.1em) rotate(315deg));
width: 1.5em;
}
}
}

View File

@ -0,0 +1,19 @@
/*! Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
.transform(@transform) {
-webkit-transform: @transform;
-moz-transform: @transform;
-ms-transform: @transform;
-o-transform: @transform;
transform: @transform;
}
.rounded-corners(@border-radius: 0.4em) {
-webkit-border-radius: @border-radius;
-moz-border-radius: @border-radius;
border-radius: @border-radius;
-webkit-background-clip: padding-box;
-moz-background-clip: padding;
background-clip: padding-box;
}

View File

@ -424,7 +424,9 @@
this.icinga.ui.reloadCss();
}
if (req.getResponseHeader('X-Icinga-Redirect')) return;
if (req.getResponseHeader('X-Icinga-Redirect')) {
return;
}
// div helps getting an XML tree
var $resp = $('<div>' + req.responseText + '</div>');
@ -515,8 +517,6 @@
var $el = $(el);
if ($el.hasClass('dashboard')) {
return;
} else {
}
var url = $el.data('icingaUrl');
targets[i].data('icingaUrl', url);
@ -533,28 +533,9 @@
this.icinga.ui.initializeTriStates($resp);
/* Should we try to fiddle with responses containing full HTML? */
/*
if ($('body', $resp).length) {
req.responseText = $('script', $('body', $resp).html()).remove();
if (rendered) {
return;
}
*/
/*
var containers = [];
$('.dashboard .container').each(function(idx, el) {
urls.push($(el).data('icingaUrl'));
});
console.log(urls);
$('.container[data-icinga-refresh]').each(function(idx, el) {
var $el = $(el);
self.loadUrl($el.data('icingaUrl'), $el).autorefresh = true;
el = null;
});
*/
if (rendered) return;
// .html() removes outer div we added above
this.renderContentToContainer($resp.html(), req.$target, req.action, req.autorefresh);
@ -652,8 +633,7 @@
/*
* Test if a manual actions comes in and autorefresh is active: Stop refreshing
*/
if (req.addToHistory && ! req.autorefresh && req.$target.data('icingaRefresh') > 0
&& req.$target.data('icingaUrl') !== url) {
if (req.addToHistory && ! req.autorefresh && req.$target.data('icingaRefresh') > 0) {
req.$target.data('icingaRefresh', 0);
req.$target.data('icingaUrl', url);
}