From c55f1dceb8900cafdfbb35eebc1e08179a1b2dd2 Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Fri, 21 Mar 2025 08:52:50 +0100 Subject: [PATCH] Introduce `TicketLinkObjectList` and `CommentRenderer` TicketLinkObjectList: This class creates object list with ticket links using TicketLinks trait CommentRenderer: Defines the rendering rules for Comment object Cleanup css and unused classes Adjust comment-popup.less --- application/controllers/CommentController.php | 10 +- .../controllers/CommentsController.php | 11 +- library/Icingadb/Common/TicketLinks.php | 7 +- .../View/BaseHostAndServiceRenderer.php | 13 +- library/Icingadb/View/CommentRenderer.php | 161 ++++++++++++++++++ .../Icingadb/Widget/Detail/ObjectDetail.php | 7 +- .../Icingadb/Widget/Detail/ObjectHeader.php | 6 + .../Widget/ItemList/BaseCommentListItem.php | 131 -------------- .../Icingadb/Widget/ItemList/CommentList.php | 49 ------ .../Widget/ItemList/CommentListItem.php | 12 -- .../ItemList/CommentListItemMinimal.php | 21 --- .../Icingadb/Widget/ItemList/ObjectList.php | 13 +- .../Widget/ItemList/TicketLinkObjectList.php | 32 ++++ public/css/common.less | 10 -- public/css/list/comment-list.less | 50 ------ public/css/widget/comment-popup.less | 28 ++- 16 files changed, 254 insertions(+), 307 deletions(-) create mode 100644 library/Icingadb/View/CommentRenderer.php delete mode 100644 library/Icingadb/Widget/ItemList/BaseCommentListItem.php delete mode 100644 library/Icingadb/Widget/ItemList/CommentList.php delete mode 100644 library/Icingadb/Widget/ItemList/CommentListItem.php delete mode 100644 library/Icingadb/Widget/ItemList/CommentListItemMinimal.php create mode 100644 library/Icingadb/Widget/ItemList/TicketLinkObjectList.php delete mode 100644 public/css/list/comment-list.less diff --git a/application/controllers/CommentController.php b/application/controllers/CommentController.php index b184d6b9..ab79ce23 100644 --- a/application/controllers/CommentController.php +++ b/application/controllers/CommentController.php @@ -10,7 +10,7 @@ use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\Comment; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\Detail\CommentDetail; -use Icinga\Module\Icingadb\Widget\ItemList\CommentList; +use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader; use ipl\Stdlib\Filter; use ipl\Web\Url; @@ -49,13 +49,9 @@ class CommentController extends Controller public function indexAction() { - $this->addControl((new CommentList([$this->comment])) - ->setViewMode('minimal') - ->setDetailActionsDisabled() - ->setCaptionDisabled() - ->setNoSubjectLink()); + $this->addControl(new ObjectHeader($this->comment)); - $this->addContent((new CommentDetail($this->comment))->setTicketLinkEnabled()); + $this->addContent(new CommentDetail($this->comment)); $this->setAutorefreshInterval(10); } diff --git a/application/controllers/CommentsController.php b/application/controllers/CommentsController.php index 2358423d..d84bf8ca 100644 --- a/application/controllers/CommentsController.php +++ b/application/controllers/CommentsController.php @@ -10,8 +10,8 @@ use Icinga\Module\Icingadb\Forms\Command\Object\DeleteCommentForm; use Icinga\Module\Icingadb\Model\Comment; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Controller; -use Icinga\Module\Icingadb\Widget\ItemList\CommentList; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; +use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use Icinga\Module\Icingadb\Widget\ShowMore; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; @@ -81,7 +81,12 @@ class CommentsController extends Controller $results = $comments->execute(); - $this->addContent((new CommentList($results))->setViewMode($viewModeSwitcher->getViewMode())); + $this->addContent( + (new ObjectList($results)) + ->setViewMode($viewModeSwitcher->getViewMode()) + ->setMultiselectUrl(Links::commentsDetails()) + ->setDetailUrl(Url::fromPath('icingadb/comment')) + ); if ($compact) { $this->addContent( @@ -156,7 +161,7 @@ class CommentsController extends Controller $rs = $comments->execute(); - $this->addControl((new CommentList($rs))->setViewMode('minimal')); + $this->addControl((new ObjectList($rs))->setViewMode('minimal')); $this->addControl(new ShowMore( $rs, diff --git a/library/Icingadb/Common/TicketLinks.php b/library/Icingadb/Common/TicketLinks.php index 3fb01c60..70bf48f9 100644 --- a/library/Icingadb/Common/TicketLinks.php +++ b/library/Icingadb/Common/TicketLinks.php @@ -5,11 +5,12 @@ namespace Icinga\Module\Icingadb\Common; use Icinga\Application\Hook; +use Icinga\Application\Hook\TicketHook; trait TicketLinks { /** @var bool */ - protected $ticketLinkEnabled = false; + protected $ticketLinkEnabled = false; // TODO: Remove once all usages are removed /** * Set whether list items should render host and service links @@ -43,11 +44,9 @@ trait TicketLinks public function createTicketLinks($text): string { if (Hook::has('ticket')) { + /** @var TicketHook $tickets */ $tickets = Hook::first('ticket'); - } - if ($this->getTicketLinkEnabled() && isset($tickets)) { - /** @var \Icinga\Application\Hook\TicketHook $tickets */ return $tickets->createLinks($text); } diff --git a/library/Icingadb/View/BaseHostAndServiceRenderer.php b/library/Icingadb/View/BaseHostAndServiceRenderer.php index 81de4aa9..2b4c87e9 100644 --- a/library/Icingadb/View/BaseHostAndServiceRenderer.php +++ b/library/Icingadb/View/BaseHostAndServiceRenderer.php @@ -13,7 +13,6 @@ use Icinga\Module\Icingadb\Util\PerfDataSet; use Icinga\Module\Icingadb\Util\PluginOutput; use Icinga\Module\Icingadb\Widget\CheckAttempt; use Icinga\Module\Icingadb\Widget\IconImage; -use Icinga\Module\Icingadb\Widget\ItemList\CommentList; use Icinga\Module\Icingadb\Widget\PluginOutputContainer; use Icinga\Module\Icingadb\Widget\StateChange; use ipl\Html\Attributes; @@ -25,6 +24,7 @@ use ipl\Html\Text; use ipl\Html\ValidHtml; use ipl\I18n\Translation; use ipl\Web\Common\ItemRenderer; +use ipl\Web\Layout\ItemLayout; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\Icon; use ipl\Web\Widget\StateBall; @@ -187,16 +187,15 @@ abstract class BaseHostAndServiceRenderer implements ItemRenderer $comment->host = $item; } - $comment = (new CommentList([$comment])) - ->setNoSubjectLink() - ->setObjectLinkDisabled() - ->setDetailActionsDisabled(); - $statusIcons->addHtml( new HtmlElement( 'div', Attributes::create(['class' => 'comment-wrapper']), - new HtmlElement('div', Attributes::create(['class' => 'comment-popup']), $comment), + new HtmlElement( + 'div', + Attributes::create(['class' => 'comment-popup']), + new ItemLayout($comment, (new CommentRenderer())->setIsDetailView()) + ), (new Icon('comments', ['class' => 'comment-icon'])) ) ); diff --git a/library/Icingadb/View/CommentRenderer.php b/library/Icingadb/View/CommentRenderer.php new file mode 100644 index 00000000..a258bc25 --- /dev/null +++ b/library/Icingadb/View/CommentRenderer.php @@ -0,0 +1,161 @@ + */ +class CommentRenderer implements ItemRenderer +{ + use Translation; + use TicketLinks; + use HostLink; + use ServiceLink; + + /** + * @var bool Whether the item is being rendered in the detail view of the associated object (host/service) + * + * When true: + * + * - Creation of the link of the associated object (host/service) is omitted from the title + * - The ticket link will be created + */ + protected $isDetailView = false; + + /** + * Set whether the item is being rendered in the detail view of the associated object (host/service) + * + * @param bool $state + * + * @return $this + */ + public function setIsDetailView(bool $state = true): self + { + $this->isDetailView = $state; + + return $this; + } + + public function assembleAttributes($item, Attributes $attributes, string $layout): void + { + $attributes->get('class')->addValue('comment'); + } + + public function assembleVisual($item, HtmlDocument $visual, string $layout): void + { + $visual->addHtml(new HtmlElement( + 'div', + Attributes::create(['class' => 'user-ball']), + Text::create($item->author[0]) + )); + } + + public function assembleTitle($item, HtmlDocument $title, string $layout): void + { + $isAck = $item->entry_type === 'ack'; + $expires = $item->expire_time; + + $subjectText = sprintf( + $isAck + ? $this->translate('%s acknowledged', '..') + : $this->translate('%s commented', '..'), + $item->author + ); + + $headerParts = [ + new Icon(Icons::USER), + $layout === 'header' + ? new HtmlElement('span', Attributes::create(['class' => 'subject']), Text::create($subjectText)) + : new Link($subjectText, Links::comment($item), ['class' => 'subject']) + ]; + + if ($isAck) { + $label = [Text::create('ack')]; + + if ($item->is_persistent) { + array_unshift($label, new Icon(Icons::IS_PERSISTENT)); + } + + $headerParts[] = Text::create(' '); + $headerParts[] = new HtmlElement('span', Attributes::create(['class' => 'ack-badge badge']), ...$label); + } + + if ($expires !== null) { + $headerParts[] = Text::create(' '); + $headerParts[] = new HtmlElement( + 'span', + Attributes::create(['class' => 'ack-badge badge']), + Text::create($this->translate('EXPIRES')) + ); + } + + if ($this->isDetailView) { + // pass + } elseif ($item->object_type === 'host') { + $headerParts[] = $this->createHostLink($item->host, true); + } else { + $headerParts[] = $this->createServiceLink($item->service, $item->service->host, true); + } + + $title->addHtml(...$headerParts); + } + + public function assembleCaption($item, HtmlDocument $caption, string $layout): void + { + $markdownLine = new MarkdownLine($this->isDetailView ? $this->createTicketLinks($item->text) : $item->text); + + $caption->getAttributes()->add($markdownLine->getAttributes()); + $caption->addFrom($markdownLine); + } + + public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void + { + if ($item->expire_time) { + $info->addHtml(new HtmlElement( + 'span', + null, + FormattedString::create( + $this->translate("expires %s"), + new TimeUntil($item->expire_time->getTimestamp()) + ) + )); + } else { + $info->addHtml(new HtmlElement( + 'span', + null, + FormattedString::create( + $this->translate("created %s"), + new TimeAgo($item->entry_time->getTimestamp()) + ) + )); + } + } + + public function assembleFooter($item, HtmlDocument $footer, string $layout): void + { + } + + public function assemble($item, string $name, HtmlDocument $element, string $layout): bool + { + return false; // no custom sections + } +} diff --git a/library/Icingadb/Widget/Detail/ObjectDetail.php b/library/Icingadb/Widget/Detail/ObjectDetail.php index 766df633..6661c7f5 100644 --- a/library/Icingadb/Widget/Detail/ObjectDetail.php +++ b/library/Icingadb/Widget/Detail/ObjectDetail.php @@ -29,6 +29,7 @@ use Icinga\Module\Icingadb\Model\UnreachableParent; use Icinga\Module\Icingadb\Redis\VolatileStateResults; use Icinga\Module\Icingadb\Web\Navigation\Action; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; +use Icinga\Module\Icingadb\Widget\ItemList\TicketLinkObjectList; use Icinga\Module\Icingadb\Widget\MarkdownText; use Icinga\Module\Icingadb\Common\ServiceLinks; use Icinga\Module\Icingadb\Forms\Command\Object\ToggleObjectFeaturesForm; @@ -42,11 +43,11 @@ use Icinga\Module\Icingadb\Widget\ItemList\DowntimeList; use Icinga\Module\Icingadb\Widget\ShowMore; use ipl\Sql\Expression; use ipl\Sql\Filter\Exists; +use ipl\Web\Url; use ipl\Web\Widget\CopyToClipboard; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\EmptyStateBar; use ipl\Web\Widget\HorizontalKeyValue; -use Icinga\Module\Icingadb\Widget\ItemList\CommentList; use Icinga\Module\Icingadb\Widget\PluginOutputContainer; use Icinga\Module\Icingadb\Widget\TagList; use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook; @@ -231,7 +232,9 @@ class ObjectDetail extends BaseHtmlElement $content = [Html::tag('h2', t('Comments'))]; if ($comments->hasResult()) { - $content[] = (new CommentList($comments))->setObjectLinkDisabled()->setTicketLinkEnabled(); + $content[] = (new TicketLinkObjectList($comments)) + ->setMultiselectUrl(Links::commentsDetails()) + ->setDetailUrl(Url::fromPath('icingadb/comment')); $content[] = (new ShowMore($comments, $link))->setBaseTarget('_next'); } else { $content[] = new EmptyState(t('No comments created.')); diff --git a/library/Icingadb/Widget/Detail/ObjectHeader.php b/library/Icingadb/Widget/Detail/ObjectHeader.php index 15a2978c..7f279c3f 100644 --- a/library/Icingadb/Widget/Detail/ObjectHeader.php +++ b/library/Icingadb/Widget/Detail/ObjectHeader.php @@ -5,11 +5,13 @@ namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Exception\NotImplementedError; +use Icinga\Module\Icingadb\Model\Comment; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\RedundancyGroup; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\User; use Icinga\Module\Icingadb\Model\Usergroup; +use Icinga\Module\Icingadb\View\CommentRenderer; use Icinga\Module\Icingadb\View\HostRenderer; use Icinga\Module\Icingadb\View\RedundancyGroupRenderer; use Icinga\Module\Icingadb\View\ServiceRenderer; @@ -53,6 +55,10 @@ class ObjectHeader extends BaseHtmlElement case $this->object instanceof User: $renderer = new UserRenderer(); + break; + case $this->object instanceof Comment: + $renderer = new CommentRenderer(); + break; default: throw new NotImplementedError('Not implemented'); diff --git a/library/Icingadb/Widget/ItemList/BaseCommentListItem.php b/library/Icingadb/Widget/ItemList/BaseCommentListItem.php deleted file mode 100644 index de11c0c9..00000000 --- a/library/Icingadb/Widget/ItemList/BaseCommentListItem.php +++ /dev/null @@ -1,131 +0,0 @@ -createTicketLinks($this->item->text)); - - $caption->getAttributes()->add($markdownLine->getAttributes()); - $caption->addFrom($markdownLine); - } - - protected function assembleTitle(BaseHtmlElement $title): void - { - $isAck = $this->item->entry_type === 'ack'; - $expires = $this->item->expire_time; - - $subjectText = sprintf( - $isAck ? t('%s acknowledged', '..') : t('%s commented', '..'), - $this->item->author - ); - - $headerParts = [ - new Icon(Icons::USER), - $this->getNoSubjectLink() - ? new HtmlElement('span', Attributes::create(['class' => 'subject']), Text::create($subjectText)) - : new Link($subjectText, Links::comment($this->item), ['class' => 'subject']) - ]; - - if ($isAck) { - $label = [Text::create('ack')]; - - if ($this->item->is_persistent) { - array_unshift($label, new Icon(Icons::IS_PERSISTENT)); - } - - $headerParts[] = Text::create(' '); - $headerParts[] = new HtmlElement('span', Attributes::create(['class' => 'ack-badge badge']), ...$label); - } - - if ($expires !== null) { - $headerParts[] = Text::create(' '); - $headerParts[] = new HtmlElement( - 'span', - Attributes::create(['class' => 'ack-badge badge']), - Text::create(t('EXPIRES')) - ); - } - - if ($this->getObjectLinkDisabled()) { - // pass - } elseif ($this->item->object_type === 'host') { - $headerParts[] = $this->createHostLink($this->item->host, true); - } else { - $headerParts[] = $this->createServiceLink($this->item->service, $this->item->service->host, true); - } - - $title->addHtml(...$headerParts); - } - - protected function assembleVisual(BaseHtmlElement $visual): void - { - $visual->addHtml(new HtmlElement( - 'div', - Attributes::create(['class' => 'user-ball']), - Text::create($this->item->author[0]) - )); - } - - protected function createTimestamp(): ?BaseHtmlElement - { - if ($this->item->expire_time) { - return Html::tag( - 'span', - FormattedString::create(t("expires %s"), new TimeUntil($this->item->expire_time->getTimestamp())) - ); - } - - return Html::tag( - 'span', - FormattedString::create(t("created %s"), new TimeAgo($this->item->entry_time->getTimestamp())) - ); - } - - protected function init(): void - { - $this->setTicketLinkEnabled($this->list->getTicketLinkEnabled()); - $this->list->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name)); - $this->list->addMultiselectFilterAttribute($this, Filter::equal('name', $this->item->name)); - $this->setObjectLinkDisabled($this->list->getObjectLinkDisabled()); - $this->setNoSubjectLink($this->list->getNoSubjectLink()); - } -} diff --git a/library/Icingadb/Widget/ItemList/CommentList.php b/library/Icingadb/Widget/ItemList/CommentList.php deleted file mode 100644 index 5cf65aeb..00000000 --- a/library/Icingadb/Widget/ItemList/CommentList.php +++ /dev/null @@ -1,49 +0,0 @@ - 'comment-list']; - - protected function getItemClass(): string - { - $viewMode = $this->getViewMode(); - - $this->addAttributes(['class' => $viewMode]); - - if ($viewMode === 'minimal') { - return CommentListItemMinimal::class; - } elseif ($viewMode === 'detailed') { - $this->removeAttribute('class', 'default-layout'); - } - - return CommentListItem::class; - } - - protected function init(): void - { - $this->initializeDetailActions(); - $this->setMultiselectUrl(Links::commentsDetails()); - $this->setDetailUrl(Url::fromPath('icingadb/comment')); - } -} diff --git a/library/Icingadb/Widget/ItemList/CommentListItem.php b/library/Icingadb/Widget/ItemList/CommentListItem.php deleted file mode 100644 index 3bbd0c2b..00000000 --- a/library/Icingadb/Widget/ItemList/CommentListItem.php +++ /dev/null @@ -1,12 +0,0 @@ -list->isCaptionDisabled()) { - $this->setCaptionDisabled(); - } - } -} diff --git a/library/Icingadb/Widget/ItemList/ObjectList.php b/library/Icingadb/Widget/ItemList/ObjectList.php index 959d4463..0787e7ba 100644 --- a/library/Icingadb/Widget/ItemList/ObjectList.php +++ b/library/Icingadb/Widget/ItemList/ObjectList.php @@ -6,6 +6,7 @@ namespace Icinga\Module\Icingadb\Widget\ItemList; use Icinga\Exception\NotImplementedError; use Icinga\Module\Icingadb\Common\DetailActions; +use Icinga\Module\Icingadb\Model\Comment; use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\RedundancyGroup; @@ -14,6 +15,7 @@ use Icinga\Module\Icingadb\Model\UnreachableParent; use Icinga\Module\Icingadb\Model\User; use Icinga\Module\Icingadb\Model\Usergroup; use Icinga\Module\Icingadb\Redis\VolatileStateResults; +use Icinga\Module\Icingadb\View\CommentRenderer; use Icinga\Module\Icingadb\View\HostRenderer; use Icinga\Module\Icingadb\View\RedundancyGroupRenderer; use Icinga\Module\Icingadb\View\ServiceRenderer; @@ -57,9 +59,11 @@ class ObjectList extends ItemList return new UsergroupRenderer(); } elseif ($item instanceof User) { return new UserRenderer(); - } else { - throw new NotImplementedError('Not implemented'); + } elseif ($item instanceof Comment) { + return new CommentRenderer(); } + + throw new NotImplementedError('Not implemented'); }); } @@ -190,6 +194,11 @@ class ObjectList extends ItemList case $object instanceof Usergroup || $data instanceof User: $this->addDetailFilterAttribute($item, Filter::equal('name', $object->name)); + break; + case $object instanceof Comment: + $this->addDetailFilterAttribute($item, Filter::equal('name', $object->name)); + $this->addMultiSelectFilterAttribute($item, Filter::equal('name', $object->name)); + break; } diff --git a/library/Icingadb/Widget/ItemList/TicketLinkObjectList.php b/library/Icingadb/Widget/ItemList/TicketLinkObjectList.php new file mode 100644 index 00000000..35ff7ab2 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/TicketLinkObjectList.php @@ -0,0 +1,32 @@ +setIsDetailView(); + } + + throw new NotImplementedError('Not implemented'); + }); + } +} diff --git a/public/css/common.less b/public/css/common.less index 14bd29f3..d314d46e 100644 --- a/public/css/common.less +++ b/public/css/common.less @@ -382,16 +382,6 @@ div.show-more { } } -.comment-popup { - .comment-list .main { - // This is necessary to limit the visible comment lines - // because the popup is shown in detailed list mode only - .caption { - height: 3em; - } - } -} - form[name="form_confirm_removal"] { text-align: center; } diff --git a/public/css/list/comment-list.less b/public/css/list/comment-list.less deleted file mode 100644 index 46b194de..00000000 --- a/public/css/list/comment-list.less +++ /dev/null @@ -1,50 +0,0 @@ -// Style - -// Layout - -.comment-list:not(.detailed) .list-item { - .title > i:first-child { - margin-right: 0; - } - - .title > .subject + .badge, - .title > .badge + .subject, - .title > .badge:last-of-type { - margin-left: 0; - } - - .title a { - &:not(.subject) { - .text-ellipsis(); - } - } - - .title .subject:not(:last-child) { - margin-left: 0; - } - - .title .subject:nth-child(3):last-child { - margin-left: 0; - } -} - -.comment-list.minimal .list-item { - .user-ball { - font-size: .857em; - height: 1.75em; - line-height: 1.5em; - width: 1.75em; - } -} - -.comment-list.detailed .list-item { - .title > .subject:nth-child(3), - .title > .badge + .subject:last-child { - margin-left: .3em; - } - - .caption { - max-height: 4.5em; - white-space: normal; - } -} diff --git a/public/css/widget/comment-popup.less b/public/css/widget/comment-popup.less index 40126974..42686a05 100644 --- a/public/css/widget/comment-popup.less +++ b/public/css/widget/comment-popup.less @@ -35,20 +35,24 @@ } } -ul.item-list li:last-child .comment-wrapper { - .comment-popup { - top: -7em; - } +ul.item-list li:nth-last-child(2):not(:first-child), +ul.item-list li:last-child:not(:first-child) { + .comment-wrapper { + .comment-popup { + top: -7em; + } - .comment-popup:before { - bottom: ~"calc(-0.75em - 1px)"; - top: unset; - transform: rotate(225deg); + .comment-popup:before { + bottom: ~"calc(-0.75em - 1px)"; + top: unset; + transform: rotate(225deg); + } } } .comment-wrapper:hover .comment-popup { - display: block + display: flex; + padding: 0 1em; } #layout { @@ -72,3 +76,9 @@ ul.item-list li:last-child .comment-wrapper { } } } + +.comment-popup .main .caption { + // This is necessary to limit the visible comment lines + // because the popup is shown in detailed list mode only + height: 3em; +}