From e0520dfdb6956a5ca3f0950736e6f13c55656941 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 2 Dec 2021 15:07:41 +0100 Subject: [PATCH 1/4] Introduce new hook `Icingadb/CustomVarRenderer` --- .../Icingadb/Hook/CustomVarRendererHook.php | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 library/Icingadb/Hook/CustomVarRendererHook.php diff --git a/library/Icingadb/Hook/CustomVarRendererHook.php b/library/Icingadb/Hook/CustomVarRendererHook.php new file mode 100644 index 000000000..be3325b7c --- /dev/null +++ b/library/Icingadb/Hook/CustomVarRendererHook.php @@ -0,0 +1,102 @@ +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]; + }; + } +} From 3bc59e389730b5967863fd3c5bff092011a56225 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 2 Dec 2021 15:11:42 +0100 Subject: [PATCH 2/4] CustomVarTable: Utilize new hook `Icingadb/CustomVarRenderer` --- .../Icingadb/Widget/Detail/CustomVarTable.php | 187 +++++++++++++++++- public/css/widget/custom-var-table.less | 34 ++++ 2 files changed, 212 insertions(+), 9 deletions(-) diff --git a/library/Icingadb/Widget/Detail/CustomVarTable.php b/library/Icingadb/Widget/Detail/CustomVarTable.php index 25afb97ff..9d3e06b69 100644 --- a/library/Icingadb/Widget/Detail/CustomVarTable.php +++ b/library/Icingadb/Widget/Detail/CustomVarTable.php @@ -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)): @@ -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; @@ -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); @@ -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 === '') { @@ -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)); + } } } diff --git a/public/css/widget/custom-var-table.less b/public/css/widget/custom-var-table.less index 614c8263c..cc702dc43 100644 --- a/public/css/widget/custom-var-table.less +++ b/public/css/widget/custom-var-table.less @@ -22,4 +22,38 @@ .level-6 th { padding-left: 3em; } + + 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; + } + } } From 24970853df3ab681b94afb67467f6707dce442af Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 2 Dec 2021 15:22:32 +0100 Subject: [PATCH 3/4] Wrap widget `CustomVarTable` specifically for `collapsible.js` The widget itself isn't collapsible anymore by default. --- library/Icingadb/Widget/Detail/ObjectDetail.php | 8 +++++--- library/Icingadb/Widget/Detail/UserDetail.php | 9 ++++++--- library/Icingadb/Widget/Detail/UsergroupDetail.php | 9 ++++++--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/library/Icingadb/Widget/Detail/ObjectDetail.php b/library/Icingadb/Widget/Detail/ObjectDetail.php index 073a02628..709cba462 100644 --- a/library/Icingadb/Widget/Detail/ObjectDetail.php +++ b/library/Icingadb/Widget/Detail/ObjectDetail.php @@ -221,9 +221,11 @@ protected function createCustomVars(): array $this->fetchCustomVars(); $vars = (new CustomvarFlat())->unFlattenVars($this->object->customvar_flat); if (! empty($vars)) { - $customvarTable = new CustomVarTable($vars); - $customvarTable->setAttribute('id', $this->objectType . '-customvars'); - $content[] = $customvarTable; + $content[] = new HtmlElement('div', Attributes::create([ + 'id' => $this->objectType . '-customvars', + 'class' => 'collapsible', + 'data-visible-height' => 200 + ]), new CustomVarTable($vars, $this->object)); } else { $content[] = new EmptyState(t('No custom variables configured.')); } diff --git a/library/Icingadb/Widget/Detail/UserDetail.php b/library/Icingadb/Widget/Detail/UserDetail.php index 54adaab42..bfdfa46ec 100644 --- a/library/Icingadb/Widget/Detail/UserDetail.php +++ b/library/Icingadb/Widget/Detail/UserDetail.php @@ -10,6 +10,7 @@ use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; use Icinga\Module\Icingadb\Model\User; use Icinga\Module\Icingadb\Widget\EmptyState; +use ipl\Html\Attributes; use ipl\Web\Widget\HorizontalKeyValue; use Icinga\Module\Icingadb\Widget\ItemList\UsergroupList; use Icinga\Module\Icingadb\Widget\ShowMore; @@ -42,9 +43,11 @@ protected function createCustomVars(): array $vars = $this->user->customvar_flat->getModel()->unflattenVars($flattenedVars); if (! empty($vars)) { - $customvarTable = new CustomVarTable($vars); - $customvarTable->setAttribute('id', 'user-customvars'); - $content[] = $customvarTable; + $content[] = new HtmlElement('div', Attributes::create([ + 'id' => 'user-customvars', + 'class' => 'collapsible', + 'data-visible-height' => 200 + ]), new CustomVarTable($vars, $this->user)); } else { $content[] = new EmptyState(t('No custom variables configured.')); } diff --git a/library/Icingadb/Widget/Detail/UsergroupDetail.php b/library/Icingadb/Widget/Detail/UsergroupDetail.php index eba588a22..f2cdebe9e 100644 --- a/library/Icingadb/Widget/Detail/UsergroupDetail.php +++ b/library/Icingadb/Widget/Detail/UsergroupDetail.php @@ -12,6 +12,7 @@ use Icinga\Module\Icingadb\Widget\EmptyState; use Icinga\Module\Icingadb\Widget\ItemList\UserList; use Icinga\Module\Icingadb\Widget\ShowMore; +use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; use ipl\Html\Text; @@ -50,9 +51,11 @@ protected function createCustomVars(): array $vars = $this->usergroup->customvar_flat->getModel()->unflattenVars($flattenedVars); if (! empty($vars)) { - $customvarTable = new CustomVarTable($vars); - $customvarTable->setAttribute('id', 'usergroup-customvars'); - $content[] = $customvarTable; + $content[] = new HtmlElement('div', Attributes::create([ + 'id' => 'usergroup-customvars', + 'class' => 'collapsible', + 'data-visible-height' => 200 + ]), new CustomVarTable($vars, $this->usergroup)); } else { $content[] = new EmptyState(t('No custom variables configured.')); } From 50db54577750a492be426aa3e0ce9e76a1c9b6e4 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 2 Dec 2021 15:24:26 +0100 Subject: [PATCH 4/4] ObjectInspectionDetail: Show raw custom variables --- .../Common/ObjectInspectionDetail.php | 23 +++++++++++++++++++ .../Widget/Detail/HostInspectionDetail.php | 1 + .../Widget/Detail/ServiceInspectionDetail.php | 1 + 3 files changed, 25 insertions(+) diff --git a/library/Icingadb/Common/ObjectInspectionDetail.php b/library/Icingadb/Common/ObjectInspectionDetail.php index f2ebccb21..a310be7cc 100644 --- a/library/Icingadb/Common/ObjectInspectionDetail.php +++ b/library/Icingadb/Common/ObjectInspectionDetail.php @@ -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; @@ -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 * diff --git a/library/Icingadb/Widget/Detail/HostInspectionDetail.php b/library/Icingadb/Widget/Detail/HostInspectionDetail.php index 4603f0e7a..93b35b842 100644 --- a/library/Icingadb/Widget/Detail/HostInspectionDetail.php +++ b/library/Icingadb/Widget/Detail/HostInspectionDetail.php @@ -14,6 +14,7 @@ protected function assemble() $this->createSourceLocation(), $this->createLastCheckResult(), $this->createAttributes(), + $this->createCustomVariables(), $this->createRedisInfo() ]); } diff --git a/library/Icingadb/Widget/Detail/ServiceInspectionDetail.php b/library/Icingadb/Widget/Detail/ServiceInspectionDetail.php index 025def6a5..f29ee9bf0 100644 --- a/library/Icingadb/Widget/Detail/ServiceInspectionDetail.php +++ b/library/Icingadb/Widget/Detail/ServiceInspectionDetail.php @@ -14,6 +14,7 @@ protected function assemble() $this->createSourceLocation(), $this->createLastCheckResult(), $this->createAttributes(), + $this->createCustomVariables(), $this->createRedisInfo() ]); }