Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom variable rendering hooks #467

Merged
merged 4 commits into from
May 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions library/Icingadb/Common/ObjectInspectionDetail.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Icinga\Exception\Json\JsonDecodeException;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Model\Service;
use Icinga\Module\Icingadb\Widget\Detail\CustomVarTable;
use Icinga\Module\Icingadb\Widget\EmptyState;
use Icinga\Util\Format;
use Icinga\Util\Json;
Expand Down Expand Up @@ -199,6 +200,28 @@ protected function createAttributes(): array
];
}

protected function createCustomVariables()
{
$query = $this->object->customvar
->columns(['name', 'value']);

$result = [];
foreach ($query as $row) {
$result[$row->name] = json_decode($row->value, true) ?? $row->value;
}

if (! empty($result)) {
$vars = new CustomVarTable($result);
} else {
$vars = new EmptyState(t('No custom variables configured.'));
}

return [
new HtmlElement('h2', null, Text::create(t('Custom Variables'))),
$vars
];
}

/**
* Format the given value as a json
*
Expand Down
102 changes: 102 additions & 0 deletions library/Icingadb/Hook/CustomVarRendererHook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */

namespace Icinga\Module\Icingadb\Hook;

use Closure;
use Exception;
use Icinga\Application\Hook;
use Icinga\Application\Logger;
use Icinga\Module\Icingadb\Hook\Common\HookUtils;
use ipl\Orm\Model;

abstract class CustomVarRendererHook
{
use HookUtils;

/**
* Prefetch the data the hook needs to render custom variables
*
* @param Model $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(Model $object): bool;

/**
* Render the given variable name
*
* @param string $key
*
* @return ?mixed
*/
abstract public function renderCustomVarKey(string $key);

/**
* Render the given variable value
*
* @param string $key
* @param mixed $value
*
* @return ?mixed
*/
abstract public function renderCustomVarValue(string $key, $value);

/**
* Return a group name for the given variable name
*
* @param string $key
*
* @return ?string
*/
abstract public function identifyCustomVarGroup(string $key): ?string;

/**
* Prepare available hooks to render custom variables of the given object
*
* @param Model $object
*
* @return Closure A callback ($key, $value) which returns an array [$newKey, $newValue, $group]
*/
final public static function prepareForObject(Model $object): Closure
{
$hooks = [];
foreach (Hook::all('Icingadb/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 (string $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 ?? $key;
$newValue = $renderedValue ?? $value;
break;
}
}

return [$newKey, $newValue, $group];
};
}
}
187 changes: 178 additions & 9 deletions library/Icingadb/Widget/Detail/CustomVarTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,109 @@

namespace Icinga\Module\Icingadb\Widget\Detail;

use Icinga\Module\Icingadb\Hook\CustomVarRendererHook;
use Icinga\Module\Icingadb\Widget\EmptyState;
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\Orm\Model;
use ipl\Web\Widget\Icon;

class CustomVarTable extends BaseHtmlElement
{
/** @var array The variables */
protected $data;

/** @var ?Model 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 = [
'data-visible-height' => 200,
'class' => 'custom-var-table name-value-table collapsible'
'class' => ['custom-var-table', 'name-value-table']
];

public function __construct($data)
/**
* Create a new CustomVarTable
*
* @param iterable $data
* @param ?Model $object
*/
public function __construct($data, Model $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(string $title): self
{
$this->headerTitle = $title;

return $this;
}

/**
* Add a new row to the body
*
* @param mixed $name
* @param mixed $value
*
* @return void
*/
protected function addRow($name, $value)
{
$this->add(Html::tag('tr', ['class' => "level-{$this->level}"], [
Html::tag('th', $name),
Html::tag('td', $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);
switch (true) {
case $isArray && is_int(key($value)):
Expand All @@ -49,10 +120,23 @@ protected function renderVar($name, $value)
}
}

/**
* Render an array
*
* @param string $name
* @param array $array
*
* @return void
*/
protected function renderArray($name, array $array)
{
$numItems = count($array);
$this->addRow("$name (Array)", sprintf(tp('%d item', '%d items', $numItems), $numItems));
$name = (new HtmlDocument())->addHtml(
Html::wantHtml($name),
Text::create(' (Array)')
);

$this->addRow($name, sprintf(tp('%d item', '%d items', $numItems), $numItems));

++$this->level;

Expand All @@ -64,6 +148,14 @@ protected function renderArray($name, array $array)
--$this->level;
}

/**
* Render an object (associative array)
*
* @param mixed $name
* @param array $object
*
* @return void
*/
protected function renderObject($name, array $object)
{
$numItems = count($object);
Expand All @@ -79,6 +171,14 @@ protected function renderObject($name, array $object)
--$this->level;
}

/**
* Render a scalar
*
* @param mixed $name
* @param mixed $value
*
* @return void
*/
protected function renderScalar($name, $value)
{
if ($value === '') {
Expand All @@ -88,11 +188,80 @@ protected function renderScalar($name, $value)
$this->addRow($name, $value);
}

/**
* Render a group
*
* @param string $name
* @param iterable $entries
*
* @return void
*/
protected function renderGroup(string $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()
{
ksort($this->data);
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));
}
}
}
Loading