Merge pull request #4614 from Icinga/feature/custom-variable-rendering-hooks-3479

Custom variable rendering hooks
This commit is contained in:
Johannes Meyer 2022-03-02 14:06:55 +01:00 committed by GitHub
commit e55422d376
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 436 additions and 12 deletions

View File

@ -45,12 +45,9 @@
<?php if (! empty($object->customvars)): ?>
<h2><?= $this->translate('Custom Variables') ?></h2>
<table id="<?= $object->type ?>-customvars" class="name-value-table collapsible" data-visible-height="200">
<tbody>
<?= $this->render('show/components/customvars.phtml') ?>
</tbody>
</table>
<div id="<?= $object->type ?>-customvars" data-visible-height="200" class="collapsible">
<?= (new \Icinga\Module\Monitoring\Web\Widget\CustomVarTable($object->customvarsWithOriginalNames, $object)) ?>
</div>
<?php endif ?>
<?= $this->render('show/components/flags.phtml') ?>
</div>

View File

@ -1,6 +0,0 @@
<?php foreach ($object->customvarsWithOriginalNames as $name => $value): ?>
<tr>
<th><?= $this->escape($name) ?></th>
<td class="custom-variables"><?= $this->customvar($value) ?></td>
</tr>
<?php endforeach ?>

View File

@ -0,0 +1,98 @@
<?php
/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
namespace Icinga\Module\Monitoring\Hook;
use Closure;
use Exception;
use Icinga\Application\Hook;
use Icinga\Application\Logger;
use Icinga\Module\Monitoring\Object\MonitoredObject;
abstract class CustomVarRendererHook
{
/**
* Prefetch the data the hook needs to render custom variables
*
* @param MonitoredObject $object The object for which they'll be rendered
*
* @return bool Return true if the hook can render variables for the given object, false otherwise
*/
abstract public function prefetchForObject(MonitoredObject $object);
/**
* Render the given variable name
*
* @param string $key
*
* @return ?mixed
*/
abstract public function renderCustomVarKey($key);
/**
* Render the given variable value
*
* @param string $key
* @param mixed $value
*
* @return ?mixed
*/
abstract public function renderCustomVarValue($key, $value);
/**
* Return a group name for the given variable name
*
* @param string $key
*
* @return ?string
*/
abstract public function identifyCustomVarGroup($key);
/**
* Prepare available hooks to render custom variables of the given object
*
* @param MonitoredObject $object
*
* @return Closure A callback ($key, $value) which returns an array [$newKey, $newValue, $group]
*/
final public static function prepareForObject(MonitoredObject $object)
{
$hooks = [];
foreach (Hook::all('Monitoring/CustomVarRenderer') as $hook) {
/** @var self $hook */
try {
if ($hook->prefetchForObject($object)) {
$hooks[] = $hook;
}
} catch (Exception $e) {
Logger::error('Failed to load hook %s:', get_class($hook), $e);
}
}
return function ($key, $value) use ($hooks, $object) {
$newKey = $key;
$newValue = $value;
$group = null;
foreach ($hooks as $hook) {
/** @var self $hook */
try {
$renderedKey = $hook->renderCustomVarKey($key);
$renderedValue = $hook->renderCustomVarValue($key, $value);
$group = $hook->identifyCustomVarGroup($key);
} catch (Exception $e) {
Logger::error('Failed to use hook %s:', get_class($hook), $e);
continue;
}
if ($renderedKey !== null || $renderedValue !== null) {
$newKey = $renderedKey !== null ? $renderedKey : $key;
$newValue = $renderedValue !== null ? $renderedValue : $value;
break;
}
}
return [$newKey, $newValue, $group];
};
}
}

View File

@ -0,0 +1,270 @@
<?php
/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
namespace Icinga\Module\Monitoring\Web\Widget;
use Icinga\Module\Monitoring\Hook\CustomVarRendererHook;
use Icinga\Module\Monitoring\Object\MonitoredObject;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Html\HtmlDocument;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Web\Widget\Icon;
class CustomVarTable extends BaseHtmlElement
{
/** @var iterable The variables */
protected $data;
/** @var ?MonitoredObject The object the variables are bound to */
protected $object;
/** @var Closure Callback to apply hooks */
protected $hookApplier;
/** @var array The groups as identified by hooks */
protected $groups = [];
/** @var string Header title */
protected $headerTitle;
/** @var int The nesting level */
protected $level = 0;
protected $tag = 'table';
/** @var HtmlElement The table body */
protected $body;
protected $defaultAttributes = [
'class' => ['custom-var-table', 'name-value-table']
];
/**
* Create a new CustomVarTable
*
* @param iterable $data
* @param ?MonitoredObject $object
*/
public function __construct($data, MonitoredObject $object = null)
{
$this->data = $data;
$this->object = $object;
$this->body = new HtmlElement('tbody');
}
/**
* Set the header to show
*
* @param string $title
*
* @return $this
*/
protected function setHeader($title)
{
$this->headerTitle = (string) $title;
return $this;
}
/**
* Add a new row to the body
*
* @param mixed $name
* @param mixed $value
*
* @return void
*/
protected function addRow($name, $value)
{
$this->body->addHtml(new HtmlElement(
'tr',
Attributes::create(['class' => "level-{$this->level}"]),
new HtmlElement('th', null, Html::wantHtml($name)),
new HtmlElement('td', null, Html::wantHtml($value))
));
}
/**
* Render a variable
*
* @param mixed $name
* @param mixed $value
*
* @return void
*/
protected function renderVar($name, $value)
{
if ($this->object !== null && $this->level === 0) {
list($name, $value, $group) = call_user_func($this->hookApplier, $name, $value);
if ($group !== null) {
$this->groups[$group][] = [$name, $value];
return;
}
}
$isArray = is_array($value);
if (! $isArray && $value instanceof \stdClass) {
$value = (array) $value;
$isArray = true;
}
switch (true) {
case $isArray && is_int(key($value)):
$this->renderArray($name, $value);
break;
case $isArray:
$this->renderObject($name, $value);
break;
default:
$this->renderScalar($name, $value);
}
}
/**
* Render an array
*
* @param mixed $name
* @param array $array
*
* @return void
*/
protected function renderArray($name, array $array)
{
$numItems = count($array);
$name = (new HtmlDocument())->addHtml(
Html::wantHtml($name),
Text::create(' (Array)')
);
$this->addRow($name, sprintf(tp('%d item', '%d items', $numItems), $numItems));
++$this->level;
ksort($array);
foreach ($array as $key => $value) {
$this->renderVar("[$key]", $value);
}
--$this->level;
}
/**
* Render an object (associative array)
*
* @param mixed $name
* @param array $object
*
* @return void
*/
protected function renderObject($name, array $object)
{
$numItems = count($object);
$this->addRow($name, sprintf(tp('%d item', '%d items', $numItems), $numItems));
++$this->level;
ksort($object);
foreach ($object as $key => $value) {
$this->renderVar($key, $value);
}
--$this->level;
}
/**
* Render a scalar
*
* @param mixed $name
* @param mixed $value
*
* @return void
*/
protected function renderScalar($name, $value)
{
if ($value === '') {
$value = new HtmlElement('span', Attributes::create(['class' => 'empty']), Text::create(t('empty string')));
}
$this->addRow($name, $value);
}
/**
* Render a group
*
* @param string $name
* @param iterable $entries
*
* @return void
*/
protected function renderGroup($name, $entries)
{
$table = new self($entries);
$wrapper = $this->getWrapper();
if ($wrapper === null) {
$wrapper = new HtmlDocument();
$wrapper->addHtml($this);
$this->prependWrapper($wrapper);
}
$wrapper->addHtml($table->setHeader($name));
}
protected function assemble()
{
if ($this->object !== null) {
$this->hookApplier = CustomVarRendererHook::prepareForObject($this->object);
}
if ($this->headerTitle !== null) {
$this->getAttributes()
->add('class', 'collapsible')
->add('data-visible-height', 100)
->add('data-toggle-element', 'thead')
->add(
'id',
preg_replace('/\s+/', '-', strtolower($this->headerTitle)) . '-customvars'
);
$this->addHtml(new HtmlElement('thead', null, new HtmlElement(
'tr',
null,
new HtmlElement(
'th',
Attributes::create(['colspan' => 2]),
new HtmlElement(
'span',
null,
new Icon('angle-right'),
new Icon('angle-down')
),
Text::create($this->headerTitle)
)
)));
}
if (is_array($this->data)) {
ksort($this->data);
}
foreach ($this->data as $name => $value) {
$this->renderVar($name, $value);
}
$this->addHtml($this->body);
// Hooks can return objects as replacement for keys, hence a generator is needed for group entries
$genGenerator = function ($entries) {
foreach ($entries as list($key, $value)) {
yield $key => $value;
}
};
foreach ($this->groups as $group => $entries) {
$this->renderGroup($group, $genGenerator($entries));
}
}
}

View File

@ -716,6 +716,71 @@ form.instance-features span.description, form.object-features span.description {
}
}
/* Object customvars */
.custom-var-table {
.level-1 th {
padding-left: .5em;
}
.level-2 th {
padding-left: 1em;
}
.level-3 th {
padding-left: 1.5em;
}
.level-4 th {
padding-left: 2em;
}
.level-5 th {
padding-left: 2.5em;
}
.level-6 th {
padding-left: 3em;
}
.empty {
color: @gray-semilight;
}
thead th {
padding-left: 0;
text-align: left;
font-weight: bold;
font-size: 1.167em;
> span {
:nth-child(1),
:nth-child(2) {
display: none;
}
}
}
&.can-collapse thead th > span {
:nth-child(1) {
display: none;
}
:nth-child(2) {
display: inline-block;
}
}
&.collapsed thead th > span {
:nth-child(1) {
display: inline-block;
}
:nth-child(2) {
display: none;
}
}
}
//p.pluginoutput {
// width: 100%;
// white-space: pre-wrap;