diff --git a/library/Notifications/Hook/ObjectsRendererHook.php b/library/Notifications/Hook/ObjectsRendererHook.php new file mode 100644 index 00000000..5d0af3ff --- /dev/null +++ b/library/Notifications/Hook/ObjectsRendererHook.php @@ -0,0 +1,310 @@ + ['object ID' => [object ID tags]]]. + * + * @var array>> + */ + private static $objectIdTags = []; + + /** + * Array of HTMLs for objects with their corresponding object IDs as keys + * + * It has the following structure : ['object ID' => registered HTML for the object name]. + * + * @var array + */ + private static $objectNameHtmls = []; + + /** + * Array of object names with their corresponding object IDs as keys + * + * It has the following structure : ['object ID' => 'object name']. + * + * @var array + */ + private static $objectNames = []; + + /** + * Get the object names for the objects using the object ID tags + * + * @param array> $objectIdTags Array of object ID tags of objects belonging to the source + * + * @return Generator, string> Generator for object names with their object ID tags as keys + */ + abstract public function getObjectNames(array $objectIdTags): Generator; + + /** + * Get the HTML for the object names for the objects using the object ID tags + * + * @param array> $objectIdTags Array of object ID tags of the objects belonging to the source + * + * @return Generator, ValidHtml> Generator for object name HTMLs with their object ID tags + * as keys + */ + abstract public function getHtmlForObjectNames(array $objectIdTags): Generator; + + /** + * Get the source type of the objects + * + * @return string + */ + abstract public function getSourceType(): string; + + /** + * Create the object link for the given object ID tag + * + * @param array $objectIdTag + * + * @return ?ValidHtml Returns null if no object with given tag found + */ + abstract public function createObjectLink(array $objectIdTag): ?ValidHtml; + + /** + * Register object ID tags to the cache + * + * @param Objects $obj + * + * @return void + */ + final public static function register(Objects $obj): void + { + self::$objectIdTags[$obj->source->type][$obj->id] = $obj->id_tags; + } + + /** + * Load HTMLs to be rendered for the object names to the cache using the cached objects + * + * @param bool $asHtml If true loads object names as HTMLs otherwise as string + * + * @return void + */ + final public static function load(bool $asHtml = true): void + { + self::prepare(self::$objectIdTags, $asHtml); // Prepare object names as HTML or string + + self::$objectIdTags = []; + } + + /** + * Prepare the objects to be rendered using the given object ID tags for each source + * + * The supplied object ID tags must have the following structure: + * ['object source type' => ['object ID' => [object ID tags]]]. + * + * @param array>> $objectIdTags Array of object ID tags for each source + * @param bool $asHtml When true, object names are prepared as HTML otherwise as string + * + * @return void + */ + private static function prepare(array $objectIdTags, bool $asHtml = true): void + { + $idTagToObjectIdMap = []; + $objectNames = $asHtml ? self::$objectNameHtmls : self::$objectNames; + foreach ($objectIdTags as $sourceType => $objects) { + foreach ($objects as $objectId => $tags) { + if (! isset($objectNames[$objectId])) { + $idTagToObjectIdMap[$sourceType][] = [$objectId, $tags]; + } + } + } + + $objectDisplayNames = []; + + /** @var self $hook */ + foreach (Hook::all('Notifications\\ObjectsRenderer') as $hook) { + $source = $hook->getSourceType(); + + if (isset($idTagToObjectIdMap[$source])) { + try { + $objectIDTagsForSource = array_map( + function ($object) { + return $object[1]; + }, + $idTagToObjectIdMap[$source] + ); + + $objectNamesFromSource = $asHtml + ? $hook->getHtmlForObjectNames($objectIDTagsForSource) + : $hook->getObjectNames($objectIDTagsForSource); + + /** @var array $objectIdTag */ + foreach ($objectNamesFromSource as $objectIdTag => $objectName) { + foreach ($idTagToObjectIdMap[$source] as $key => $val) { + $diff = array_intersect_assoc($val[1], $objectIdTag); + if (count($diff) === count($val[1]) && count($diff) === count($objectIdTag)) { + unset($idTagToObjectIdMap[$source][$key]); + + if ($asHtml) { + $objectName = HtmlElement::create( + 'div', + Attributes::create([ + 'class' => [ + 'icinga-module', + 'module-' . ($source === 'icinga2' ? 'icingadb' : $source) + ] + ]), + $objectName + ); + } + + $objectDisplayNames[$val[0]] = $objectName; + + continue 2; + } + } + } + } catch (Exception $e) { + Logger::error('Failed to load hook %s:', get_class($hook), $e); + } + } + } + + if ($asHtml) { + self::$objectNameHtmls += $objectDisplayNames; + } else { + self::$objectNames += $objectDisplayNames; + } + } + + /** + * Get the object name of the given object + * + * If an HTML for the object name is not loaded, it is prepared using object ID tags and the same is returned. + * + * @param Objects $obj + * + * @return BaseHtmlElement + */ + final public static function getObjectName(Objects $obj): BaseHtmlElement + { + $objId = $obj->id; + if (! isset(self::$objectNameHtmls[$objId])) { + self::prepare([$obj->source->type => [$objId => $obj->id_tags]]); + } + + if (isset(self::$objectNameHtmls[$objId])) { + return self::$objectNameHtmls[$objId]; + } + + self::$objectNameHtmls[$objId] = new HtmlElement( + 'div', + null, + Text::create(self::createObjectNameAsString($obj)) + ); + + return self::$objectNameHtmls[$objId]; + } + + /** + * Get the object name of the given object as string + * + * If the object name is not loaded, it is prepared using object ID tags and the same is returned. + * + * @param Objects $obj + * @param bool $prepare If true prepares the object name string from the hook implementation if it is not + * already present in the cache + * + * @return string + */ + final public static function getObjectNameAsString(Objects $obj): string + { + $objId = $obj->id; + if (! isset(self::$objectNames[$objId])) { + self::prepare([$obj->source->type => [$objId => $obj->id_tags]], false); + } + + if (isset(self::$objectNames[$objId])) { + return self::$objectNames[$objId]; + } + + return self::createObjectNameAsString($obj); + } + + /** + * Create object name string for the given object + * + * @param Objects $obj + * + * @return string + */ + private static function createObjectNameAsString(Objects $obj): string + { + $objectTags = []; + + foreach ($obj->id_tags as $tag => $value) { + $objectTags[] = sprintf('%s=%s', $tag, $value); + } + + return implode(', ', $objectTags); + } + + /** + * Render object link for the given object + * + * @param Objects $object + * + * @return ?ValidHtml + */ + final public static function renderObjectLink(Objects $object): ?ValidHtml + { + /** @var self $hook */ + foreach (Hook::all('Notifications\\ObjectsRenderer') as $hook) { + try { + $sourceType = $hook->getSourceType(); + if ($object->source->type === $sourceType) { + $objectLink = $hook->createObjectLink($object->id_tags); + if ($objectLink === null) { + break; + } + + return $objectLink->addAttributes([ + 'class' => [ + 'icinga-module', + 'module-' . ($sourceType === 'icinga2' ? 'icingadb' : $sourceType) + ] + ]); + } + } catch (Exception $e) { + Logger::error('Failed to load hook %s:', get_class($hook), $e); + } + } + + // Fallback, if the hook is not implemented + if (! $object->url) { + return null; + } + + $objUrl = Url::fromPath($object->url); + + return new Link( + Text::create(self::createObjectNameAsString($object)), + $objUrl->isExternal() ? $objUrl->getAbsoluteUrl() : $objUrl->getRelativeUrl(), + ['class' => 'subject', 'data-base-target' => '_next'] + ); + } +} diff --git a/library/Notifications/Model/Objects.php b/library/Notifications/Model/Objects.php index 8a2b08a4..006a82e1 100644 --- a/library/Notifications/Model/Objects.php +++ b/library/Notifications/Model/Objects.php @@ -4,8 +4,8 @@ namespace Icinga\Module\Notifications\Model; +use Icinga\Module\Notifications\Hook\ObjectsRendererHook; use Icinga\Module\Notifications\Model\Behavior\IdTagAggregator; -use ipl\Html\HtmlString; use ipl\Html\ValidHtml; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; @@ -91,13 +91,6 @@ public function createRelations(Relations $relations) public function getName(): ValidHtml { - //TODO: Once hooks are available, they should render the tags accordingly - $objectTags = []; - - foreach ($this->id_tags as $tag => $value) { - $objectTags[] = sprintf('%s=%s', $tag, $value); - } - - return new HtmlString(implode(', ', $objectTags)); + return ObjectsRendererHook::getObjectName($this); } } diff --git a/library/Notifications/Widget/Detail/EventDetail.php b/library/Notifications/Widget/Detail/EventDetail.php index 9f16a697..c3827e81 100644 --- a/library/Notifications/Widget/Detail/EventDetail.php +++ b/library/Notifications/Widget/Detail/EventDetail.php @@ -5,18 +5,18 @@ namespace Icinga\Module\Notifications\Widget\Detail; use Icinga\Date\DateFormatter; +use Icinga\Module\Notifications\Hook\ObjectsRendererHook; use Icinga\Module\Notifications\Model\Event; use Icinga\Module\Notifications\Model\Incident; -use Icinga\Module\Notifications\Model\Objects; use Icinga\Module\Notifications\Widget\EventSourceBadge; use Icinga\Module\Notifications\Widget\ItemList\IncidentList; use InvalidArgumentException; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; +use ipl\Html\HtmlElement; +use ipl\Html\Text; use ipl\Html\ValidHtml; use ipl\Web\Widget\HorizontalKeyValue; -use ipl\Web\Widget\Link; -use ipl\Web\Widget\StateBall; class EventDetail extends BaseHtmlElement { @@ -80,37 +80,15 @@ protected function createMessage(): array /** @return ValidHtml[] */ protected function createRelatedObject(): array { - //TODO(sd): This is just placeholder. Add hook implementation instead - $relatedObj = Html::tag('ul', ['class' => ['item-list', 'action-list'], 'data-base-target' => '_next']); + $objectUrl = ObjectsRendererHook::renderObjectLink($this->event->object); - /** @var Objects $obj */ - $obj = $this->event->object; - - /** @var string $objUrl */ - $objUrl = $obj->url; - $relatedObj->add( - Html::tag( - 'li', - ['class' => 'list-item', 'data-action-item' => true], - [ //TODO(sd): fix stateball - Html::tag('div', ['class' => 'visual'], new StateBall('down', StateBall::SIZE_LARGE)), - Html::tag( - 'div', - ['class' => 'main'], - Html::tag('header') - ->add(Html::tag( - 'div', - ['class' => 'title'], - new Link($obj->getName(), $objUrl, ['class' => 'subject']) - )) - ) - ] - ) - ); + if (! $objectUrl) { + return []; + } return [ - Html::tag('h2', t('Related Object')), - $relatedObj + new HtmlElement('h2', null, Text::create(t('Related Object'))), + $objectUrl ]; } diff --git a/library/Notifications/Widget/Detail/IncidentDetail.php b/library/Notifications/Widget/Detail/IncidentDetail.php index 21965067..a88036d8 100644 --- a/library/Notifications/Widget/Detail/IncidentDetail.php +++ b/library/Notifications/Widget/Detail/IncidentDetail.php @@ -4,8 +4,8 @@ namespace Icinga\Module\Notifications\Widget\Detail; +use Icinga\Module\Notifications\Hook\ObjectsRendererHook; use Icinga\Module\Notifications\Model\Incident; -use Icinga\Module\Notifications\Model\Objects; use Icinga\Module\Notifications\Widget\EventSourceBadge; use Icinga\Module\Notifications\Widget\ItemList\IncidentContactList; use Icinga\Module\Notifications\Widget\ItemList\IncidentHistoryList; @@ -14,8 +14,7 @@ use ipl\Html\Html; use ipl\Html\HtmlElement; use ipl\Html\Table; -use ipl\Web\Widget\Link; -use ipl\Web\Widget\StateBall; +use ipl\Html\Text; class IncidentDetail extends BaseHtmlElement { @@ -52,39 +51,15 @@ protected function createContacts() protected function createRelatedObject() { - //TODO(sd): Add hook implementation - $list = Html::tag('ul', ['class' => ['item-list', 'minimal', 'action-list'], 'data-base-target' => '_next']); - - /** @var Objects $obj */ - $obj = $this->incident->object; - - /** @var string $objUrl */ - $objUrl = $obj->url; - $list->add(Html::tag( - 'li', - ['class' => 'list-item', 'data-action-item' => true], - [ //TODO(sd): fix stateball - Html::tag( - 'div', - ['class' => 'visual'], - new StateBall('down', StateBall::SIZE_LARGE) - ), - Html::tag( - 'div', - ['class' => 'main'], - Html::tag('header') - ->add(Html::tag( - 'div', - ['class' => 'title'], - new Link($obj->getName(), $objUrl, ['class' => 'subject']) - )) - ) - ] - )); + $objectUrl = ObjectsRendererHook::renderObjectLink($this->incident->object); + + if (! $objectUrl) { + return []; + } return [ - Html::tag('h2', t('Object')), - $list + new HtmlElement('h2', null, Text::create(t('Related Object'))), + $objectUrl ]; } diff --git a/library/Notifications/Widget/ItemList/EventList.php b/library/Notifications/Widget/ItemList/EventList.php index 2ef47006..96d3b9a6 100644 --- a/library/Notifications/Widget/ItemList/EventList.php +++ b/library/Notifications/Widget/ItemList/EventList.php @@ -6,6 +6,8 @@ use Icinga\Module\Notifications\Common\LoadMore; use Icinga\Module\Notifications\Common\NoSubjectLink; +use Icinga\Module\Notifications\Hook\ObjectsRendererHook; +use Icinga\Module\Notifications\Model\Event; use ipl\Orm\ResultSet; use ipl\Web\Common\BaseItemList; @@ -27,6 +29,14 @@ public function __construct(ResultSet $data) protected function init(): void { $this->data = $this->getIterator($this->data); + + $this->on(self::ON_ITEM_ADD, function (EventListItem $item, Event $data) { + ObjectsRendererHook::register($data->object); + }); + + $this->on(self::ON_ASSEMBLED, function () { + ObjectsRendererHook::load(); + }); } protected function getItemClass(): string diff --git a/library/Notifications/Widget/ItemList/IncidentList.php b/library/Notifications/Widget/ItemList/IncidentList.php index 9992f1d5..ba66e37a 100644 --- a/library/Notifications/Widget/ItemList/IncidentList.php +++ b/library/Notifications/Widget/ItemList/IncidentList.php @@ -4,6 +4,8 @@ namespace Icinga\Module\Notifications\Widget\ItemList; +use Icinga\Module\Notifications\Hook\ObjectsRendererHook; +use Icinga\Module\Notifications\Model\Incident; use ipl\Web\Common\BaseItemList; use Icinga\Module\Notifications\Common\NoSubjectLink; @@ -13,6 +15,17 @@ class IncidentList extends BaseItemList protected $defaultAttributes = ['class' => ['action-list', 'incident-list']]; + protected function init(): void + { + $this->on(self::ON_ITEM_ADD, function (IncidentListItem $item, Incident $data) { + ObjectsRendererHook::register($data->object); + }); + + $this->on(self::ON_ASSEMBLED, function () { + ObjectsRendererHook::load(); + }); + } + protected function getItemClass(): string { return IncidentListItem::class; diff --git a/phpstan-baseline-standard.neon b/phpstan-baseline-standard.neon index 1a449e55..0a87a97e 100644 --- a/phpstan-baseline-standard.neon +++ b/phpstan-baseline-standard.neon @@ -740,11 +740,6 @@ parameters: count: 1 path: library/Notifications/Model/Event.php - - - message: "#^Parameter \\#1 \\$severity of static method Icinga\\\\Module\\\\Notifications\\\\Model\\\\Event\\:\\:mapSeverity\\(\\) expects string\\|null, mixed given\\.$#" - count: 1 - path: library/Notifications/Model/Event.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Model\\\\Incident\\:\\:createBehaviors\\(\\) has no return type specified\\.$#" count: 1 @@ -1250,36 +1245,11 @@ parameters: count: 1 path: library/Notifications/Widget/CheckboxIcon.php - - - message: "#^Cannot access property \\$source on mixed\\.$#" - count: 1 - path: library/Notifications/Widget/Detail/EventDetail.php - - - - message: "#^Cannot call method getTimestamp\\(\\) on mixed\\.$#" - count: 1 - path: library/Notifications/Widget/Detail/EventDetail.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Detail\\\\EventDetail\\:\\:assemble\\(\\) has no return type specified\\.$#" count: 1 path: library/Notifications/Widget/Detail/EventDetail.php - - - message: "#^Cannot access property \\$object_extra_tag on mixed\\.$#" - count: 1 - path: library/Notifications/Widget/Detail/IncidentDetail.php - - - - message: "#^Cannot access property \\$source on mixed\\.$#" - count: 1 - path: library/Notifications/Widget/Detail/IncidentDetail.php - - - - message: "#^Cannot call method with\\(\\) on mixed\\.$#" - count: 2 - path: library/Notifications/Widget/Detail/IncidentDetail.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Detail\\\\IncidentDetail\\:\\:assemble\\(\\) has no return type specified\\.$#" count: 1 @@ -1450,21 +1420,6 @@ parameters: count: 1 path: library/Notifications/Widget/ItemList/EventList.php - - - message: "#^Cannot access property \\$source on mixed\\.$#" - count: 1 - path: library/Notifications/Widget/ItemList/EventListItem.php - - - - message: "#^Cannot call method getTimestamp\\(\\) on mixed\\.$#" - count: 1 - path: library/Notifications/Widget/ItemList/EventListItem.php - - - - message: "#^Parameter \\#1 \\$id of static method Icinga\\\\Module\\\\Notifications\\\\Common\\\\Links\\:\\:event\\(\\) expects int, mixed given\\.$#" - count: 1 - path: library/Notifications/Widget/ItemList/EventListItem.php - - message: "#^Parameter \\#2 \\$value of method Icinga\\\\Web\\\\Url\\:\\:setParam\\(\\) expects array\\|bool\\|string, int given\\.$#" count: 1 @@ -1525,16 +1480,6 @@ parameters: count: 1 path: library/Notifications/Widget/ItemList/IncidentHistoryListItem.php - - - message: "#^Parameter \\#1 \\$severity of static method Icinga\\\\Module\\\\Notifications\\\\Model\\\\Event\\:\\:mapSeverity\\(\\) expects string\\|null, mixed given\\.$#" - count: 3 - path: library/Notifications/Widget/ItemList/IncidentHistoryListItem.php - - - - message: "#^Cannot call method getTimestamp\\(\\) on mixed\\.$#" - count: 2 - path: library/Notifications/Widget/ItemList/IncidentListItem.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\ItemList\\\\PageSeparatorItem\\:\\:assemble\\(\\) has no return type specified\\.$#" count: 1 @@ -1685,11 +1630,6 @@ parameters: count: 1 path: library/Notifications/Widget/Schedule.php - - - message: "#^Parameter \\#2 \\$value of static method ipl\\\\Stdlib\\\\Filter\\:\\:equal\\(\\) expects array\\|bool\\|float\\|int\\|string, mixed given\\.$#" - count: 1 - path: library/Notifications/Widget/Schedule.php - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Schedule\\:\\:\\$schedule \\(Icinga\\\\Module\\\\Notifications\\\\Model\\\\Schedule\\) does not accept Icinga\\\\Module\\\\Notifications\\\\Model\\\\Schedule\\|null\\.$#" count: 1