Introduce class LoadMoreObjectList and NotificationRenderer

- Remove now obsolete ItemList classes
- Fix load-more element's css
- LoadMore: Replace `list-item` css class with new `item-layout` class, as this class is now responsible for list items
This commit is contained in:
Sukhwinder Dhillon 2025-03-24 13:02:27 +01:00 committed by Johannes Meyer
parent c15f32a43f
commit 3252ff8925
9 changed files with 190 additions and 254 deletions

View file

@ -8,13 +8,11 @@ use GuzzleHttp\Psr7\ServerRequest;
use Icinga\Module\Icingadb\Model\NotificationHistory;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\ItemList\NotificationList;
use Icinga\Module\Icingadb\Widget\ItemList\LoadMoreObjectList;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use ipl\Sql\Sql;
use ipl\Stdlib\Filter;
use ipl\Web\Control\LimitControl;
use ipl\Web\Control\SortControl;
use ipl\Web\Filter\QueryString;
use ipl\Web\Url;
class NotificationsController extends Controller
@ -94,7 +92,8 @@ class NotificationsController extends Controller
->onlyWith($preserveParams)
->setFilter($filter);
$notificationList = (new NotificationList($notifications->execute()))
$notificationList = (new LoadMoreObjectList($notifications->execute()))
->setDetailUrl(Url::fromPath('icingadb/event'))
->setPageSize($limitControl->getLimit())
->setViewMode($viewModeSwitcher->getViewMode())
->setLoadMoreUrl($url->setParam('before', $before));

View file

@ -1,43 +1,168 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
namespace Icinga\Module\Icingadb\View;
use Icinga\Module\Icingadb\Common\HostLink;
use Icinga\Module\Icingadb\Common\HostStates;
use Icinga\Module\Icingadb\Common\Icons;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Common\NoSubjectLink;
use Icinga\Module\Icingadb\Common\ServiceLink;
use Icinga\Module\Icingadb\Common\ServiceStates;
use Icinga\Module\Icingadb\Model\NotificationHistory;
use Icinga\Module\Icingadb\Util\PluginOutput;
use Icinga\Module\Icingadb\Widget\PluginOutputContainer;
use Icinga\Module\Icingadb\Widget\StateChange;
use ipl\Stdlib\Filter;
use ipl\Web\Common\BaseListItem;
use ipl\Web\Widget\EmptyState;
use ipl\Web\Widget\TimeAgo;
use InvalidArgumentException;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Attributes;
use ipl\Html\HtmlDocument;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\I18n\Translation;
use ipl\Web\Common\ItemRenderer;
use ipl\Web\Widget\EmptyState;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\Link;
use ipl\Web\Widget\StateBall;
use ipl\Web\Widget\TimeAgo;
abstract class BaseNotificationListItem extends BaseListItem
/** @implements ItemRenderer<NotificationHistory> */
class NotificationRenderer implements ItemRenderer
{
use Translation;
use HostLink;
use NoSubjectLink;
use ServiceLink;
/** @var NotificationList */
protected $list;
protected function init(): void
public function assembleAttributes($item, Attributes $attributes, string $layout): void
{
$this->setNoSubjectLink($this->list->getNoSubjectLink());
$this->list->addDetailFilterAttribute($this, Filter::equal('id', bin2hex($this->item->history->id)));
$attributes->get('class')->addValue('notification');
}
public function assembleVisual($item, HtmlDocument $visual, string $layout): void
{
$ballSize = StateBall::SIZE_LARGE;
if ($layout === 'minimal' || $layout === 'header') {
$ballSize = StateBall::SIZE_BIG;
}
switch ($item->type) {
case 'acknowledgement':
$visual->addHtml(HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $ballSize]],
new Icon(Icons::IS_ACKNOWLEDGED)
));
break;
case 'custom':
$visual->addHtml(HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $ballSize]],
new Icon(Icons::NOTIFICATION)
));
break;
case 'downtime_end':
case 'downtime_removed':
case 'downtime_start':
$visual->addHtml(HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $ballSize]],
new Icon(Icons::IN_DOWNTIME)
));
break;
case 'flapping_end':
case 'flapping_start':
$visual->addHtml(HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $ballSize]],
new Icon(Icons::IS_FLAPPING)
));
break;
case 'problem':
case 'recovery':
if ($item->object_type === 'host') {
$state = HostStates::text($item->state);
$previousHardState = HostStates::text($item->previous_hard_state);
} else {
$state = ServiceStates::text($item->state);
$previousHardState = ServiceStates::text($item->previous_hard_state);
}
$visual->addHtml(new StateChange($state, $previousHardState));
break;
}
}
public function assembleTitle($item, HtmlDocument $title, string $layout): void
{
if ($layout === 'header') {
$title->addHtml(HtmlElement::create(
'span',
['class' => 'subject'],
sprintf(self::phraseForType($item->type), ucfirst($item->object_type))
));
} else {
$title->addHtml(new Link(
sprintf(self::phraseForType($item->type), ucfirst($item->object_type)),
Links::event($item->history),
['class' => 'subject']
));
}
if ($item->object_type === 'host') {
$link = $this->createHostLink($item->host, true);
} else {
$link = $this->createServiceLink($item->service, $item->host, true);
}
$title->addHtml(Text::create(' '), $link);
}
public function assembleCaption($item, HtmlDocument $caption, string $layout): void
{
if (in_array($item->type, ['flapping_end', 'flapping_start', 'problem', 'recovery'])) {
$commandName = $item->object_type === 'host'
? $item->host->checkcommand_name
: $item->service->checkcommand_name;
if (isset($commandName)) {
if (empty($item->text)) {
$caption->addHtml(new EmptyState($this->translate('Output unavailable.')));
} else {
$caption->addHtml(new PluginOutputContainer(
(new PluginOutput($item->text))
->setCommandName($commandName)
));
}
} else {
$caption->addHtml(new EmptyState($this->translate('Waiting for Icinga DB to synchronize the config.')));
}
} else {
$caption->add([
new Icon(Icons::USER),
$item->author,
': ',
$item->text
]);
}
}
public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void
{
$info->addHtml(new TimeAgo($item->send_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
}
/**
@ -72,118 +197,4 @@ abstract class BaseNotificationListItem extends BaseListItem
throw new InvalidArgumentException(sprintf('Type %s is not a valid notification type', $type));
}
}
abstract protected function getStateBallSize();
protected function assembleCaption(BaseHtmlElement $caption): void
{
if (in_array($this->item->type, ['flapping_end', 'flapping_start', 'problem', 'recovery'])) {
$commandName = $this->item->object_type === 'host'
? $this->item->host->checkcommand_name
: $this->item->service->checkcommand_name;
if (isset($commandName)) {
if (empty($this->item->text)) {
$caption->addHtml(new EmptyState(t('Output unavailable.')));
} else {
$caption->addHtml(new PluginOutputContainer(
(new PluginOutput($this->item->text))
->setCommandName($commandName)
));
}
} else {
$caption->addHtml(new EmptyState(t('Waiting for Icinga DB to synchronize the config.')));
}
} else {
$caption->add([
new Icon(Icons::USER),
$this->item->author,
': ',
$this->item->text
]);
}
}
protected function assembleVisual(BaseHtmlElement $visual): void
{
switch ($this->item->type) {
case 'acknowledgement':
$visual->addHtml(HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]],
new Icon(Icons::IS_ACKNOWLEDGED)
));
break;
case 'custom':
$visual->addHtml(HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]],
new Icon(Icons::NOTIFICATION)
));
break;
case 'downtime_end':
case 'downtime_removed':
case 'downtime_start':
$visual->addHtml(HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]],
new Icon(Icons::IN_DOWNTIME)
));
break;
case 'flapping_end':
case 'flapping_start':
$visual->addHtml(HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]],
new Icon(Icons::IS_FLAPPING)
));
break;
case 'problem':
case 'recovery':
if ($this->item->object_type === 'host') {
$state = HostStates::text($this->item->state);
$previousHardState = HostStates::text($this->item->previous_hard_state);
} else {
$state = ServiceStates::text($this->item->state);
$previousHardState = ServiceStates::text($this->item->previous_hard_state);
}
$visual->addHtml(new StateChange($state, $previousHardState));
break;
}
}
protected function assembleTitle(BaseHtmlElement $title): void
{
if ($this->getNoSubjectLink()) {
$title->addHtml(HtmlElement::create(
'span',
['class' => 'subject'],
sprintf(self::phraseForType($this->item->type), ucfirst($this->item->object_type))
));
} else {
$title->addHtml(new Link(
sprintf(self::phraseForType($this->item->type), ucfirst($this->item->object_type)),
Links::event($this->item->history),
['class' => 'subject']
));
}
if ($this->item->object_type === 'host') {
$link = $this->createHostLink($this->item->host, true);
} else {
$link = $this->createServiceLink($this->item->service, $this->item->host, true);
}
$title->addHtml(Text::create(' '), $link);
}
protected function createTimestamp(): ?BaseHtmlElement
{
return new TimeAgo($this->item->send_time->getTimestamp());
}
}

View file

@ -0,0 +1,37 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Exception\NotImplementedError;
use Icinga\Module\Icingadb\Common\LoadMore;
use Icinga\Module\Icingadb\Model\NotificationHistory;
use Icinga\Module\Icingadb\View\NotificationRenderer;
use ipl\Orm\Model;
use ipl\Web\Widget\ItemList;
/**
* LoadMoreObjectList
*
* Create a list of icingadb objects with Load more link
*
* @extends ObjectList //TODO: define object type
*/
class LoadMoreObjectList extends ObjectList
{
use LoadMore;
public function __construct($data)
{
ItemList::__construct($data, function (Model $item) {
if ($item instanceof NotificationHistory) {
return new NotificationRenderer();
}
throw new NotImplementedError('Not implemented');
});
$this->data = $this->getIterator($data);
}
}

View file

@ -1,55 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\CaptionDisabled;
use Icinga\Module\Icingadb\Common\DetailActions;
use Icinga\Module\Icingadb\Common\LoadMore;
use Icinga\Module\Icingadb\Common\NoSubjectLink;
use Icinga\Module\Icingadb\Common\ViewMode;
use ipl\Orm\ResultSet;
use ipl\Web\Common\BaseItemList;
use ipl\Web\Url;
class NotificationList extends BaseItemList
{
use CaptionDisabled;
use NoSubjectLink;
use ViewMode;
use LoadMore;
use DetailActions;
protected $defaultAttributes = ['class' => 'notification-list'];
protected function init(): void
{
/** @var ResultSet $data */
$data = $this->data;
$this->data = $this->getIterator($data);
$this->initializeDetailActions();
$this->setDetailUrl(Url::fromPath('icingadb/event'));
}
protected function getItemClass(): string
{
switch ($this->getViewMode()) {
case 'minimal':
return NotificationListItemMinimal::class;
case 'detailed':
$this->removeAttribute('class', 'default-layout');
return NotificationListItemDetailed::class;
default:
return NotificationListItem::class;
}
}
protected function assemble(): void
{
$this->addAttributes(['class' => $this->getViewMode()]);
parent::assemble();
}
}

View file

@ -1,18 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\ListItemCommonLayout;
use ipl\Web\Widget\StateBall;
class NotificationListItem extends BaseNotificationListItem
{
use ListItemCommonLayout;
protected function getStateBallSize(): string
{
return StateBall::SIZE_LARGE;
}
}

View file

@ -1,18 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\ListItemDetailedLayout;
use ipl\Web\Widget\StateBall;
class NotificationListItemDetailed extends BaseNotificationListItem
{
use ListItemDetailedLayout;
protected function getStateBallSize(): string
{
return StateBall::SIZE_LARGE;
}
}

View file

@ -1,27 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\ListItemMinimalLayout;
use ipl\Web\Widget\StateBall;
class NotificationListItemMinimal extends BaseNotificationListItem
{
use ListItemMinimalLayout;
protected function init(): void
{
parent::init();
if ($this->list->isCaptionDisabled()) {
$this->setCaptionDisabled();
}
}
protected function getStateBallSize(): string
{
return StateBall::SIZE_BIG;
}
}

View file

@ -10,6 +10,7 @@ use Icinga\Module\Icingadb\Model\Comment;
use Icinga\Module\Icingadb\Model\DependencyNode;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Model\NotificationHistory;
use Icinga\Module\Icingadb\Model\RedundancyGroup;
use Icinga\Module\Icingadb\Model\Service;
use Icinga\Module\Icingadb\Model\UnreachableParent;
@ -207,6 +208,10 @@ class ObjectList extends ItemList
$this->addDetailFilterAttribute($item, Filter::equal('name', $object->name));
$this->addMultiSelectFilterAttribute($item, Filter::equal('name', $object->name));
break;
case $object instanceof NotificationHistory:
$this->addDetailFilterAttribute($item, Filter::equal('id', bin2hex($object->history->id)));
break;
}

View file

@ -43,8 +43,10 @@
// Layout
.item-list .list-item {
&.load-more a {
.item-list .load-more {
display: flex;
a {
flex: 1;
margin: 1.5em 0;
padding: .5em 0;