Re-add perfdata pie charts in list views (#176)

This commit is contained in:
Florian Strohmaier 2021-04-22 10:34:32 +02:00 committed by GitHub
parent d4136d9d83
commit 351a5f4f35
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1273 additions and 10 deletions

View file

@ -0,0 +1,23 @@
<?php
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Common;
use ipl\Html\BaseHtmlElement;
trait ListItemDetailedLayout
{
protected function assembleHeader(BaseHtmlElement $header)
{
$header->add($this->createTitle());
$header->add($this->createTimestamp());
}
protected function assembleMain(BaseHtmlElement $main)
{
$main->add($this->createHeader());
$main->add($this->createCaption());
$main->add($this->createFooter());
}
}

View file

@ -0,0 +1,550 @@
<?php
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2+ */
namespace Icinga\Module\Icingadb\Util;
use Icinga\Exception\ProgrammingError;
use Icinga\Module\Monitoring\Object\Service;
use Icinga\Util\Format;
use Icinga\Web\Widget\Chart\InlinePie;
use InvalidArgumentException;
class PerfData
{
const PERFDATA_OK = 'ok';
const PERFDATA_WARNING = 'warning';
const PERFDATA_CRITICAL = 'critical';
/**
* The performance data value being parsed
*
* @var string
*/
protected $perfdataValue;
/**
* Unit of measurement (UOM)
*
* @var string
*/
protected $unit;
/**
* The label
*
* @var string
*/
protected $label;
/**
* The value
*
* @var float
*/
protected $value;
/**
* The minimum value
*
* @var float
*/
protected $minValue;
/**
* The maximum value
*
* @var float
*/
protected $maxValue;
/**
* The WARNING threshold
*
* @var ThresholdRange
*/
protected $warningThreshold;
/**
* The CRITICAL threshold
*
* @var ThresholdRange
*/
protected $criticalThreshold;
/**
* Create a new PerfData object based on the given performance data label and value
*
* @param string $label The perfdata label
* @param string $value The perfdata value
*/
public function __construct($label, $value)
{
$this->perfdataValue = $value;
$this->label = $label;
$this->parse();
if ($this->unit === '%') {
if ($this->minValue === null) {
$this->minValue = 0.0;
}
if ($this->maxValue === null) {
$this->maxValue = 100.0;
}
}
$warn = $this->warningThreshold->getMax();
if ($warn !== null) {
$crit = $this->criticalThreshold->getMax();
if ($crit !== null && $warn > $crit) {
$this->warningThreshold->setInverted();
$this->criticalThreshold->setInverted();
}
}
}
/**
* Return a new PerfData object based on the given performance data key=value pair
*
* @param string $perfdata The key=value pair to parse
*
* @return PerfData
*
* @throws InvalidArgumentException In case the given performance data has no content or a invalid format
*/
public static function fromString($perfdata)
{
if (empty($perfdata)) {
throw new InvalidArgumentException('PerfData::fromString expects a string with content');
} elseif (strpos($perfdata, '=') === false) {
throw new InvalidArgumentException(
'PerfData::fromString expects a key=value formatted string. Got "' . $perfdata . '" instead'
);
}
list($label, $value) = explode('=', $perfdata, 2);
return new static(trim($label), trim($value));
}
/**
* Return whether this performance data's value is a number
*
* @return bool True in case it's a number, otherwise False
*/
public function isNumber()
{
return $this->unit === null;
}
/**
* Return whether this performance data's value are seconds
*
* @return bool True in case it's seconds, otherwise False
*/
public function isSeconds()
{
return in_array($this->unit, array('s', 'ms', 'us'));
}
/**
* Return whether this performance data's value is a temperature
*
* @return bool True in case it's temperature, otherwise False
*/
public function isTemperature()
{
return in_array($this->unit, array('°c', '°f'));
}
/**
* Return whether this performance data's value is in percentage
*
* @return bool True in case it's in percentage, otherwise False
*/
public function isPercentage()
{
return $this->unit === '%';
}
/**
* Return whether this performance data's value is in bytes
*
* @return bool True in case it's in bytes, otherwise False
*/
public function isBytes()
{
return in_array($this->unit, array('b', 'kb', 'mb', 'gb', 'tb'));
}
/**
* Return whether this performance data's value is a counter
*
* @return bool True in case it's a counter, otherwise False
*/
public function isCounter()
{
return $this->unit === 'c';
}
/**
* Returns whether it is possible to display a visual representation
*
* @return bool True when the perfdata is visualizable
*/
public function isVisualizable()
{
return isset($this->minValue) && isset($this->maxValue) && isset($this->value);
}
/**
* Return this perfomance data's label
*/
public function getLabel()
{
return $this->label;
}
/**
* Return the value or null if it is unknown (U)
*
* @return null|float
*/
public function getValue()
{
return $this->value;
}
/**
* Return the unit as a string
*
* @return string
*/
public function getUnit()
{
return $this->unit;
}
/**
* Return the value as percentage (0-100)
*
* @return null|float
*/
public function getPercentage()
{
if ($this->isPercentage()) {
return $this->value;
}
if ($this->maxValue !== null) {
$minValue = $this->minValue !== null ? $this->minValue : 0.0;
if ($this->maxValue == $minValue) {
return null;
}
if ($this->value > $minValue) {
return (($this->value - $minValue) / ($this->maxValue - $minValue)) * 100;
}
}
}
/**
* Return this performance data's warning treshold
*
* @return ThresholdRange
*/
public function getWarningThreshold()
{
return $this->warningThreshold;
}
/**
* Return this performance data's critical treshold
*
* @return ThresholdRange
*/
public function getCriticalThreshold()
{
return $this->criticalThreshold;
}
/**
* Return the minimum value or null if it is not available
*
* @return null|string
*/
public function getMinimumValue()
{
return $this->minValue;
}
/**
* Return the maximum value or null if it is not available
*
* @return null|float
*/
public function getMaximumValue()
{
return $this->maxValue;
}
/**
* Return this performance data as string
*
* @return string
*/
public function __toString()
{
return $this->formatLabel();
}
/**
* Parse the current performance data value
*
* @todo Handle optional min/max if UOM == %
*/
protected function parse()
{
$parts = explode(';', $this->perfdataValue);
$matches = array();
if (preg_match('@^(-?\d+(\.\d+)?)([a-zA-Z%°]{1,2})$@u', $parts[0], $matches)) {
$this->unit = strtolower($matches[3]);
$this->value = self::convert($matches[1], $this->unit);
} else {
$this->value = self::convert($parts[0]);
}
switch (count($parts)) {
/* @noinspection PhpMissingBreakStatementInspection */
case 5:
if ($parts[4] !== '') {
$this->maxValue = self::convert($parts[4], $this->unit);
}
/* @noinspection PhpMissingBreakStatementInspection */
case 4:
if ($parts[3] !== '') {
$this->minValue = self::convert($parts[3], $this->unit);
}
/* @noinspection PhpMissingBreakStatementInspection */
case 3:
$this->criticalThreshold = self::convert(
ThresholdRange::fromString(trim($parts[2])),
$this->unit
);
// Fallthrough
case 2:
$this->warningThreshold = self::convert(
ThresholdRange::fromString(trim($parts[1])),
$this->unit
);
}
if ($this->warningThreshold === null) {
$this->warningThreshold = new ThresholdRange();
}
if ($this->criticalThreshold === null) {
$this->criticalThreshold = new ThresholdRange();
}
}
/**
* Return the given value converted to its smallest supported representation
*
* @param string $value The value to convert
* @param string $fromUnit The unit the value currently represents
*
* @return null|float Null in case the value is not a number
*/
protected static function convert($value, $fromUnit = null)
{
if ($value instanceof ThresholdRange) {
$value = clone $value;
$min = $value->getMin();
if ($min !== null) {
$value->setMin(self::convert($min, $fromUnit));
}
$max = $value->getMax();
if ($max !== null) {
$value->setMax(self::convert($max, $fromUnit));
}
return $value;
}
if (is_numeric($value)) {
switch ($fromUnit) {
case 'us':
return $value / pow(10, 6);
case 'ms':
return $value / pow(10, 3);
case 'tb':
return floatval($value) * pow(2, 40);
case 'gb':
return floatval($value) * pow(2, 30);
case 'mb':
return floatval($value) * pow(2, 20);
case 'kb':
return floatval($value) * pow(2, 10);
default:
return (float) $value;
}
}
}
protected function calculatePieChartData()
{
$rawValue = $this->getValue();
$minValue = $this->getMinimumValue() !== null ? $this->getMinimumValue() : 0;
$usedValue = ($rawValue - $minValue);
$green = $orange = $red = 0;
if ($this->criticalThreshold->contains($rawValue)) {
if ($this->warningThreshold->contains($rawValue)) {
$green = $usedValue;
} else {
$orange = $usedValue;
}
} else {
$red = $usedValue;
}
return array($green, $orange, $red, ($this->getMaximumValue() - $minValue) - $usedValue);
}
public function asInlinePie()
{
if (! $this->isVisualizable()) {
throw new ProgrammingError('Cannot calculate piechart data for unvisualizable perfdata entry.');
}
$data = $this->calculatePieChartData();
$pieChart = new InlinePie($data, $this);
$pieChart->setColors(array('#44bb77', '#ffaa44', '#ff5566', '#ddccdd'));
return $pieChart;
}
/**
* Format the given value depending on the currently used unit
*/
protected function format($value)
{
if ($value === null) {
return null;
}
if ($value instanceof ThresholdRange) {
if ($value->getMin()) {
return (string) $value;
}
$max = $value->getMax();
return $max === null ? '' : $this->format($max);
}
if ($this->isPercentage()) {
return (string)$value . '%';
}
if ($this->isBytes()) {
return Format::bytes($value);
}
if ($this->isSeconds()) {
return Format::seconds($value);
}
if ($this->isTemperature()) {
return (string)$value . strtoupper($this->unit);
}
return number_format($value, 2);
}
/**
* Format the title string that represents this perfdata set
*
* @param bool $html
*
* @return string
*/
public function formatLabel($html = false)
{
return sprintf(
$html ? '<b>%s %s</b> (%s%%)' : '%s %s (%s%%)',
htmlspecialchars($this->getLabel()),
$this->format($this->value),
number_format($this->getPercentage(), 2)
);
}
public function toArray()
{
return array(
'label' => $this->getLabel(),
'value' => $this->format($this->getvalue()),
'min' => isset($this->minValue) && !$this->isPercentage()
? $this->format($this->minValue)
: '',
'max' => isset($this->maxValue) && !$this->isPercentage()
? $this->format($this->maxValue)
: '',
'warn' => $this->format($this->warningThreshold),
'crit' => $this->format($this->criticalThreshold)
);
}
/**
* Return the state indicated by this perfdata
*
* @see Service
*
* @return int
*/
public function getState()
{
if ($this->value === null) {
return Service::STATE_UNKNOWN;
}
if (! $this->criticalThreshold->contains($this->value)) {
return Service::STATE_CRITICAL;
}
if (! $this->warningThreshold->contains($this->value)) {
return Service::STATE_WARNING;
}
return Service::STATE_OK;
}
/**
* Return whether the state indicated by this perfdata is worse than
* the state indicated by the other perfdata
* CRITICAL > UNKNOWN > WARNING > OK
*
* @param PerfData $rhs the other perfdata
*
* @return bool
*/
public function worseThan(PerfData $rhs)
{
if (($state = $this->getState()) === ($rhsState = $rhs->getState())) {
return $this->getPercentage() > $rhs->getPercentage();
}
if ($state === Service::STATE_CRITICAL) {
return true;
}
if ($state === Service::STATE_UNKNOWN) {
return $rhsState !== Service::STATE_CRITICAL;
}
if ($state === Service::STATE_WARNING) {
return $rhsState === Service::STATE_OK;
}
return false;
}
}

View file

@ -0,0 +1,144 @@
<?php
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2+ */
namespace Icinga\Module\Icingadb\Util;
use ArrayIterator;
use IteratorAggregate;
class PerfDataSet implements IteratorAggregate
{
/**
* The performance data being parsed
*
* @var string
*/
protected $perfdataStr;
/**
* The current parsing position
*
* @var int
*/
protected $parserPos = 0;
/**
* A list of PerfData objects
*
* @var array
*/
protected $perfdata = array();
/**
* Create a new set of performance data
*
* @param string $perfdataStr A space separated list of label/value pairs
*/
protected function __construct($perfdataStr)
{
if (($perfdataStr = trim($perfdataStr)) !== '') {
$this->perfdataStr = $perfdataStr;
$this->parse();
}
}
/**
* Return a iterator for this set of performance data
*
* @return ArrayIterator
*/
public function getIterator()
{
return new ArrayIterator($this->asArray());
}
/**
* Return a new set of performance data
*
* @param string $perfdataStr A space separated list of label/value pairs
*
* @return PerfDataSet
*/
public static function fromString($perfdataStr)
{
return new static($perfdataStr);
}
/**
* Return this set of performance data as array
*
* @return array
*/
public function asArray()
{
return $this->perfdata;
}
/**
* Parse the current performance data
*/
protected function parse()
{
while ($this->parserPos < strlen($this->perfdataStr)) {
$label = trim($this->readLabel());
$value = trim($this->readUntil(' '));
if ($label) {
$this->perfdata[] = new PerfData($label, $value);
}
}
}
/**
* Return the next label found in the performance data
*
* @return string The label found
*/
protected function readLabel()
{
$this->skipSpaces();
if (in_array($this->perfdataStr[$this->parserPos], array('"', "'"))) {
$quoteChar = $this->perfdataStr[$this->parserPos++];
$label = $this->readUntil('=');
$this->parserPos++;
if (($closingPos = strpos($label, $quoteChar)) > 0) {
$label = substr($label, 0, $closingPos);
}
} else {
$label = $this->readUntil('=');
$this->parserPos++;
}
$this->skipSpaces();
return $label;
}
/**
* Return all characters between the current parser position and the given character
*
* @param string $stopChar The character on which to stop
*
* @return string
*/
protected function readUntil($stopChar)
{
$start = $this->parserPos;
while ($this->parserPos < strlen($this->perfdataStr) && $this->perfdataStr[$this->parserPos] !== $stopChar) {
$this->parserPos++;
}
return substr($this->perfdataStr, $start, $this->parserPos - $start);
}
/**
* Advance the parser position to the next non-whitespace character
*/
protected function skipSpaces()
{
while ($this->parserPos < strlen($this->perfdataStr) && $this->perfdataStr[$this->parserPos] === ' ') {
$this->parserPos++;
}
}
}

View file

@ -0,0 +1,180 @@
<?php
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2+ */
namespace Icinga\Module\Icingadb\Util;
/**
* The warning/critical threshold of a measured value
*/
class ThresholdRange
{
/**
* The smallest value inside the range (null stands for -)
*
* @var float|null
*/
protected $min;
/**
* The biggest value inside the range (null stands for )
*
* @var float|null
*/
protected $max;
/**
* Whether to invert the result of contains()
*
* @var bool
*/
protected $inverted = false;
/**
* The unmodified range as passed to fromString()
*
* @var string
*/
protected $raw;
/**
* Create a new instance based on a threshold range conforming to <https://nagios-plugins.org/doc/guidelines.html>
*
* @param string $rawRange
*
* @return ThresholdRange
*/
public static function fromString($rawRange)
{
$range = new static();
$range->raw = $rawRange;
if ($rawRange == '') {
return $range;
}
$rawRange = ltrim($rawRange);
if (substr($rawRange, 0, 1) === '@') {
$range->setInverted();
$rawRange = substr($rawRange, 1);
}
if (strpos($rawRange, ':') === false) {
$min = 0.0;
$max = floatval(trim($rawRange));
} else {
list($min, $max) = explode(':', $rawRange, 2);
$min = trim($min);
$max = trim($max);
switch ($min) {
case '':
$min = 0.0;
break;
case '~':
$min = null;
break;
default:
$min = floatval($min);
}
$max = empty($max) ? null : floatval($max);
}
return $range->setMin($min)
->setMax($max);
}
/**
* Set the smallest value inside the range (null stands for -)
*
* @param float|null $min
*
* @return $this
*/
public function setMin($min)
{
$this->min = $min;
return $this;
}
/**
* Get the smallest value inside the range (null stands for -)
*
* @return float|null
*/
public function getMin()
{
return $this->min;
}
/**
* Set the biggest value inside the range (null stands for )
*
* @param float|null $max
*
* @return $this
*/
public function setMax($max)
{
$this->max = $max;
return $this;
}
/**
* Get the biggest value inside the range (null stands for )
*
* @return float|null
*/
public function getMax()
{
return $this->max;
}
/**
* Set whether to invert the result of contains()
*
* @param bool $inverted
*
* @return $this
*/
public function setInverted($inverted = true)
{
$this->inverted = $inverted;
return $this;
}
/**
* Get whether to invert the result of contains()
*
* @return bool
*/
public function isInverted()
{
return $this->inverted;
}
/**
* Return whether $value is inside $this
*
* @param float $value
*
* @return bool
*/
public function contains($value)
{
return (bool) ($this->inverted ^ (
($this->min === null || $this->min <= $value) && ($this->max === null || $this->max >= $value)
));
}
/**
* Return the textual representation of $this, suitable for fromString()
*
* @return string
*/
public function __toString()
{
return (string) $this->raw;
}
}

View file

@ -22,6 +22,7 @@ use Icinga\Module\Icingadb\Widget\DowntimeList;
use Icinga\Module\Icingadb\Widget\EmptyState;
use Icinga\Module\Icingadb\Widget\HorizontalKeyValue;
use Icinga\Module\Icingadb\Widget\ItemList\CommentList;
use Icinga\Module\Icingadb\Widget\PerfDataTable;
use Icinga\Module\Icingadb\Widget\ShowMore;
use Icinga\Module\Icingadb\Widget\TagList;
use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook;
@ -35,7 +36,6 @@ use ipl\Html\HtmlString;
use ipl\Orm\ResultSet;
use ipl\Stdlib\Filter;
use ipl\Web\Widget\Icon;
use Zend_View_Helper_Perfdata;
class ObjectDetail extends BaseHtmlElement
{
@ -300,12 +300,6 @@ class ObjectDetail extends BaseHtmlElement
protected function createPerformanceData()
{
require_once Icinga::app()->getModuleManager()->getModule('monitoring')->getBaseDir()
. '/application/views/helpers/Perfdata.php';
$helper = new Zend_View_Helper_Perfdata();
$helper->view = Icinga::app()->getViewRenderer()->view;
$content[] = Html::tag('h2', t('Performance Data'));
if (empty($this->object->state->performance_data)) {
@ -314,7 +308,7 @@ class ObjectDetail extends BaseHtmlElement
$content[] = new HtmlElement(
'div',
['id' => 'check-perfdata-' . $this->object->checkcommand],
new HtmlString($helper->perfdata($this->object->state->performance_data))
new PerfDataTable($this->object->state->performance_data)
);
}

View file

@ -4,11 +4,79 @@
namespace Icinga\Module\Icingadb\Widget;
use Icinga\Module\Icingadb\Common\ListItemDetailedLayout;
use Icinga\Module\Icingadb\Compat\CompatPluginOutput;
use Icinga\Module\Icingadb\Util\PerfDataSet;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\HtmlString;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\StateBall;
class HostListItemDetailed extends HostListItem
class HostListItemDetailed extends BaseHostListItem
{
use ListItemDetailedLayout;
/** @var int Max pie charts to be shown */
const PIE_CHART_LIMIT = 5;
protected function getStateBallSize()
{
return StateBall::SIZE_LARGE;
}
protected function assembleFooter(BaseHtmlElement $footer)
{
$statusIcons = new HtmlElement('div', ['class' => 'status-icons']);
// ToDo(fs): Get `has_comments` from database
if ($this->item->comment->limit(1)->execute()->hasResult()) {
$statusIcons->add(new Icon('comments', ['title' => t('This item has been commented')]));
}
if (! $this->item->notifications_enabled) {
$statusIcons->add(new Icon('bell-slash', ['title' => t('Notifications disabled')]));
}
if (! $this->item->active_checks_enabled) {
$statusIcons->add(new Icon('eye-slash', ['title' => t('Active checks disabled')]));
}
$performanceData = new HtmlElement('div', ['class' => 'performance-data']);
if ($this->item->state->performance_data) {
$pieChartData = PerfDataSet::fromString($this->item->state->performance_data)->asArray();
$pies = [];
foreach ($pieChartData as $i => $perfdata) {
if ($perfdata->isVisualizable()) {
$pies[] = $perfdata->asInlinePie()->render();
}
// Check if number of visualizable pie charts is larger than PIE_CHART_LIMIT
if (count($pies) > HostListItemDetailed::PIE_CHART_LIMIT) {
break;
}
}
$maxVisiblePies = HostListItemDetailed::PIE_CHART_LIMIT - 2;
$numOfPies = count($pies);
foreach ($pies as $i => $pie) {
if (
// Show max. 5 elements: if there are more than 5, show 4 + `…`
$i > $maxVisiblePies && $numOfPies > HostListItemDetailed::PIE_CHART_LIMIT
) {
$performanceData->add(new HtmlElement('span', [], '…'));
break;
}
$performanceData->add(HtmlString::create($pie));
}
}
$footer->add($statusIcons);
$footer->add($performanceData);
}
protected function assembleCaption(BaseHtmlElement $caption)
{
$caption->add(CompatPluginOutput::getInstance()->render(

View file

@ -0,0 +1,140 @@
<?php
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2+ */
namespace Icinga\Module\Icingadb\Widget;
use Icinga\Module\Icingadb\Util\PerfData;
use Icinga\Module\Icingadb\Util\PerfDataSet;
use ipl\Html\HtmlElement;
use ipl\Html\HtmlString;
use ipl\Html\Table;
class PerfDataTable extends Table
{
/** @var bool Whether the table contains a sparkline column */
protected $containsSparkline = false;
protected $defaultAttributes = [
'class' => 'performance-data-table collapsible',
'data-visible-rows' => 6
];
/** @var string The perfdata string */
protected $perfdataStr;
/** @var int Max labels to show; 0 for no limit */
protected $limit;
/** @var string The color indicating the perfdata state */
protected $color;
/**
* Display the given perfdata string to the user
*
* @param string $perfdataStr The perfdata string
* @param int $limit Max labels to show; 0 for no limit
* @param string $color The color indicating the perfdata state
*
* @return string
*/
public function __construct($perfdataStr, $limit = 0, $color = PerfData::PERFDATA_OK)
{
$this->perfdataStr = $perfdataStr;
$this->limit = $limit;
$this->color = $color;
}
public function assemble()
{
$pieChartData = PerfDataSet::fromString($this->perfdataStr)->asArray();
uasort(
$pieChartData,
function ($a, $b) {
return $a->worseThan($b) ? -1 : ($b->worseThan($a) ? 1 : 0);
}
);
$keys = ['', 'label', 'value', 'min', 'max', 'warn', 'crit'];
$columns = [];
$labels = array_combine(
$keys,
[
'',
t('Label'),
t('Value'),
t('Min'),
t('Max'),
t('Warning'),
t('Critical')
]
);
foreach ($pieChartData as $perfdata) {
if ($perfdata->isVisualizable()) {
$columns[''] = '';
$this->containsSparkline = true;
}
foreach ($perfdata->toArray() as $column => $value) {
if (
empty($value) ||
$column === 'min' && floatval($value) === 0.0 ||
$column === 'max' && $perfdata->isPercentage() && floatval($value) === 100
) {
continue;
}
$columns[$column] = $labels[$column];
}
}
$headerRow = new HtmlElement('tr');
foreach ($keys as $key => $col) {
if ((! $this->containsSparkline) && $col == '') {
unset($keys[$key]);
continue;
}
if (isset($col)) {
$headerRow->add(new HtmlElement('th', [
'class' => ($col == 'label' ? 'title' : null)
], $labels[$col]));
}
}
$this->getHeader()->add($headerRow);
foreach ($pieChartData as $count => $perfdata) {
if ($this->limit != 0 && $count > $this->limit) {
break;
} else {
$cols = [];
if ($this->containsSparkline) {
if ($perfdata->isVisualizable()) {
$cols[] = Table::td(
HtmlString::create($perfdata->asInlinePie($this->color)->render()),
[ 'class' => 'sparkline-col']
);
} else {
$cols[] = Table::td('');
}
}
foreach ($perfdata->toArray() as $column => $value) {
$text = htmlspecialchars(empty($value) ? '-' : $value);
$cols[] = Table::td(
new HtmlElement(
'span',
[
'title' => ($text == '-' ? t('no value given') : $text),
'class' => ($text != '-' ?: 'no-value')
],
$text
),
[ 'class' => ($column == 'label' ? 'title' : null) ]
);
}
$this->add(Table::tr([$cols]));
}
}
}
}

View file

@ -4,11 +4,80 @@
namespace Icinga\Module\Icingadb\Widget;
use Icinga\Module\Icingadb\Common\ListItemDetailedLayout;
use Icinga\Module\Icingadb\Compat\CompatPluginOutput;
use Icinga\Module\Icingadb\Util\PerfDataSet;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Html\HtmlElement;
use ipl\Html\HtmlString;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\StateBall;
class ServiceListItemDetailed extends ServiceListItem
class ServiceListItemDetailed extends BaseServiceListItem
{
use ListItemDetailedLayout;
/** @var int Max pie charts to be shown */
const PIE_CHART_LIMIT = 5;
protected function getStateBallSize()
{
return StateBall::SIZE_LARGE;
}
protected function assembleFooter(BaseHtmlElement $footer)
{
$statusIcons = new HtmlElement('div', ['class' => 'status-icons']);
// ToDo(fs): Get `has_comments` from database
if ($this->item->comment->limit(1)->execute()->hasResult()) {
$statusIcons->add(new Icon('comments', ['title' => t('This item has been commented')]));
}
if (! $this->item->notifications_enabled) {
$statusIcons->add(new Icon('bell-slash', ['title' => t('Notifications disabled')]));
}
if (! $this->item->active_checks_enabled) {
$statusIcons->add(new Icon('eye-slash', ['title' => t('Active checks disabled')]));
}
$performanceData = new HtmlElement('div', ['class' => 'performance-data']);
if ($this->item->state->performance_data) {
$pieChartData = PerfDataSet::fromString($this->item->state->performance_data)->asArray();
$pies = [];
foreach ($pieChartData as $i => $perfdata) {
if ($perfdata->isVisualizable()) {
$pies[] = $perfdata->asInlinePie()->render();
}
// Check if number of visualizable pie charts is larger than PIE_CHART_LIMIT
if (count($pies) > ServiceListItemDetailed::PIE_CHART_LIMIT) {
break;
}
}
$maxVisiblePies = ServiceListItemDetailed::PIE_CHART_LIMIT - 2;
$numOfPies = count($pies);
foreach ($pies as $i => $pie) {
if (
// Show max. 5 elements: if there are more than 5, show 4 + `…`
$i > $maxVisiblePies && $numOfPies > ServiceListItemDetailed::PIE_CHART_LIMIT
) {
$performanceData->add(new HtmlElement('span', [], '…'));
break;
}
$performanceData->add(HtmlString::create($pie));
}
}
$footer->add($statusIcons);
$footer->add($performanceData);
}
protected function assembleCaption(BaseHtmlElement $caption)
{
$caption->add(CompatPluginOutput::getInstance()->render(

View file

@ -9,6 +9,7 @@ use Icinga\Module\Icingadb\Compat\CompatPluginOutput;
use Icinga\Module\Icingadb\Model\State;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\StateBall;
@ -83,6 +84,15 @@ abstract class StateListItem extends BaseListItem
}
}
protected function createFooter()
{
$footer = new HtmlElement('div', ['class' => 'footer']);
$this->assembleFooter($footer);
return $footer;
}
protected function createTimestamp()
{
if ($this->state->is_overdue) {

View file

@ -124,6 +124,32 @@
margin: 0;
}
}
.footer {
display: flex;
justify-content: space-between;
> * {
font-size: .857em;
line-height: 1.5/.857em;
}
.status-icons {
color: @gray-light;
}
.performance-data {
.inline-pie {
display: inline-block;
line-height: 1.5/.857em;
width: 1em;
&:not(:last-child) {
margin-right: .209em;
}
}
}
}
}
.content.full-width .list-item {
@ -186,6 +212,8 @@
.caption {
display: block;
height: auto;
max-height: 7.5em; /* 5 lines */
position: relative;
}
}

View file

@ -0,0 +1,57 @@
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
.performance-data-table {
width: 100%;
overflow: auto;
display: block;
tr:not(:last-child) {
border-bottom: 1px solid @gray-lighter;
}
td {
text-align: right;
.text-ellipsis();
}
th {
font-size: .857em;
font-weight: normal;
text-transform: uppercase;
letter-spacing: .05em;
}
thead {
border-bottom: 1px solid @gray-light;
}
th:first-child,
td:first-child {
padding-left: 0;
}
.title {
text-align: left;
width: 100%;
}
td.title {
font-weight: bold;
}
.sparkline-col {
min-width: 1.75em;
width: 1.75em;
padding: 2/12em 0;
}
.inline-pie > svg {
vertical-align: middle;
}
.no-value {
opacity: .6;
text-align: center;
display: block;
}
}