Replace LoadMoreObjectList with HistoryObjectList

Rename and specialize the list for history views.
Insert a day-separator between entries of different days,
to support this when load-more is used the a URL parameter
with the date of the most recently added element is added to the `loadmoreUrl`.

The css for these new classes is added and `action-list.js` is adjusted so it
skips over the day-separator.
This commit is contained in:
Bastian Lederer 2026-04-23 09:47:06 +02:00
parent c927e7ef74
commit bf83c2637c
4 changed files with 120 additions and 46 deletions

View file

@ -0,0 +1,96 @@
<?php
// SPDX-FileCopyrightText: 2025 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Exception\NotImplementedError;
use Icinga\Module\Icingadb\Common\LoadMore;
use Icinga\Module\Icingadb\Model\History;
use Icinga\Module\Icingadb\Model\NotificationHistory;
use Icinga\Module\Icingadb\View\EventRenderer;
use Icinga\Module\Icingadb\View\NotificationRenderer;
use IntlDateFormatter;
use ipl\Html\Attributes;
use ipl\Html\HtmlDocument;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Orm\Model;
use ipl\Orm\ResultSet;
use ipl\Web\Widget\ItemList;
use Locale;
/**
* HistoryObjectList
*
* @template Item of NotificationHistory|History
*
* @extends ObjectList<Item>
*/
class HistoryObjectList extends ObjectList
{
use LoadMore;
/** @var ?int The timestamp of the recently added list item, used to detect day changes */
protected ?int $previousTimeStamp = null;
/**
* Create a list of History or NotificationHistory objects with a Load more link and add a separator between days.
*
* @param ResultSet $data
* @param ?int $previousTimeStamp The timestamp of the recently added list item, used to detect day changes
* @param bool $useRelativeTimestamps
*/
public function __construct(ResultSet $data, ?int $previousTimeStamp = null, bool $useRelativeTimestamps = false)
{
ItemList::__construct($data, function (Model $item) use ($useRelativeTimestamps) {
if ($item instanceof NotificationHistory) {
return new NotificationRenderer($useRelativeTimestamps);
} elseif ($item instanceof History) {
return new EventRenderer($useRelativeTimestamps);
}
throw new NotImplementedError('Not implemented');
});
$this->data = $this->getIterator($data);
$this->previousTimeStamp = $previousTimeStamp;
}
protected function init(): void
{
parent::init();
$formatter = new IntlDateFormatter(
Locale::getDefault(),
IntlDateFormatter::MEDIUM,
IntlDateFormatter::NONE
);
$this->on(ItemList::BEFORE_ITEM_ADD, function ($item, $data) use ($formatter) {
if ($data instanceof NotificationHistory) {
$timestamp = $data->send_time->getTimestamp();
} else {
$timestamp = $data->event_time->getTimestamp();
}
if (
$this->previousTimeStamp === null
|| $formatter->format($this->previousTimeStamp) !== $formatter->format($timestamp)
) {
$this->addHtml(new HtmlElement(
'li',
new Attributes(['class' => ['day-separator']]),
new Text($formatter->format($timestamp))
));
}
$this->previousTimeStamp = $timestamp;
});
$this->on(
HtmlDocument::ON_ASSEMBLED,
fn() => $this->loadMoreUrl->setParam('last-entry', $this->previousTimeStamp)
);
}
}

View file

@ -1,45 +0,0 @@
<?php
// SPDX-FileCopyrightText: 2025 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Exception\NotImplementedError;
use Icinga\Module\Icingadb\Common\LoadMore;
use Icinga\Module\Icingadb\Model\History;
use Icinga\Module\Icingadb\Model\NotificationHistory;
use Icinga\Module\Icingadb\View\EventRenderer;
use Icinga\Module\Icingadb\View\NotificationRenderer;
use ipl\Orm\Model;
use ipl\Orm\ResultSet;
use ipl\Web\Widget\ItemList;
/**
* LoadMoreObjectList
*
* Create a list of icingadb objects with Load more link
*
* @template Item of NotificationHistory|History
*
* @extends ObjectList<Item>
*/
class LoadMoreObjectList extends ObjectList
{
use LoadMore;
public function __construct(ResultSet $data)
{
ItemList::__construct($data, function (Model $item) {
if ($item instanceof NotificationHistory) {
return new NotificationRenderer();
} elseif ($item instanceof History) {
return new EventRenderer();
}
throw new NotImplementedError('Not implemented');
});
$this->data = $this->getIterator($data);
}
}

View file

@ -42,6 +42,21 @@
> .page-separator + .list-item .main {
border-top: none;
}
.interactive-time {
&:hover {
.rounded-corners(1em);
background: @gray-light;
}
.user-select(none);
padding-left: 0.5em;
padding-right: 0.5em;
}
.day-separator {
background-color: @gray-lighter;
}
}
// Layout
@ -80,4 +95,10 @@
}
}
}
.day-separator {
display: flex;
justify-content: end;
padding-right: 1.5em;
}
}

View file

@ -317,7 +317,9 @@
}
_this.clearSelection(activeItems);
if (toActiveItem.classList.contains('page-separator')) {
if (toActiveItem.classList.contains('page-separator')
|| toActiveItem.classList.contains('day-separator')
) {
toActiveItem = _this.getDirectionalNext(toActiveItem, event.key);
}
}