Use new item list implementation (#1141)

This is the first step to support my proposal for new item layouts in
Icinga DB Web. I've only adjusted redundancy group items for now. The
remaining adjustments will be performed by @sukhwinder33445 in the
coming weeks.

blocked by https://github.com/Icinga/ipl-web/pull/252
This commit is contained in:
Johannes Meyer 2025-03-28 16:43:00 +01:00 committed by GitHub
commit 53374ab315
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
121 changed files with 5893 additions and 6378 deletions

View file

@ -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);
}

View file

@ -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,10 @@ class CommentsController extends Controller
$results = $comments->execute();
$this->addContent((new CommentList($results))->setViewMode($viewModeSwitcher->getViewMode()));
$this->addContent(
(new ObjectList($results))
->setViewMode($viewModeSwitcher->getViewMode())
);
if ($compact) {
$this->addContent(
@ -156,7 +159,11 @@ class CommentsController extends Controller
$rs = $comments->execute();
$this->addControl((new CommentList($rs))->setViewMode('minimal'));
$this->addControl(
(new ObjectList($rs))
->setViewMode('minimal')
->setDetailActionsDisabled()
);
$this->addControl(new ShowMore(
$rs,

View file

@ -10,7 +10,7 @@ use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\Detail\DowntimeDetail;
use Icinga\Module\Icingadb\Widget\ItemList\DowntimeList;
use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader;
use ipl\Stdlib\Filter;
use ipl\Web\Url;
@ -61,11 +61,7 @@ class DowntimeController extends Controller
{
$detail = new DowntimeDetail($this->downtime);
$this->addControl((new DowntimeList([$this->downtime]))
->setViewMode('minimal')
->setDetailActionsDisabled()
->setCaptionDisabled()
->setNoSubjectLink());
$this->addControl(new ObjectHeader($this->downtime));
$this->addContent($detail);

View file

@ -10,8 +10,8 @@ use Icinga\Module\Icingadb\Forms\Command\Object\DeleteDowntimeForm;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\ItemList\DowntimeList;
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;
@ -87,7 +87,10 @@ class DowntimesController extends Controller
$results = $downtimes->execute();
$this->addContent((new DowntimeList($results))->setViewMode($viewModeSwitcher->getViewMode()));
$this->addContent(
(new ObjectList($results))
->setViewMode($viewModeSwitcher->getViewMode())
);
if ($compact) {
$this->addContent(
@ -162,7 +165,11 @@ class DowntimesController extends Controller
$rs = $downtimes->execute();
$this->addControl((new DowntimeList($rs))->setViewMode('minimal'));
$this->addControl(
(new ObjectList($rs))
->setViewMode('minimal')
->setDetailActionsDisabled()
);
$this->addControl(new ShowMore(
$rs,

View file

@ -4,12 +4,10 @@
namespace Icinga\Module\Icingadb\Controllers;
use ArrayObject;
use Icinga\Module\Icingadb\Model\History;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\Detail\EventDetail;
use Icinga\Module\Icingadb\Widget\ItemList\HistoryList;
use ipl\Orm\ResultSet;
use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader;
use ipl\Stdlib\Filter;
class EventController extends Controller
@ -60,12 +58,7 @@ class EventController extends Controller
public function indexAction()
{
$this->addControl((new HistoryList(new ResultSet(new ArrayObject([$this->event]))))
->setViewMode('minimal')
->setPageSize(1)
->setCaptionDisabled()
->setNoSubjectLink()
->setDetailActionsDisabled());
$this->addContent((new EventDetail($this->event))->setTicketLinkEnabled());
$this->addControl(new ObjectHeader($this->event));
$this->addContent(new EventDetail($this->event));
}
}

View file

@ -8,8 +8,8 @@ use GuzzleHttp\Psr7\ServerRequest;
use Icinga\Module\Icingadb\Model\History;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\ItemList\HistoryList;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Widget\ItemList\LoadMoreObjectList;
use ipl\Stdlib\Filter;
use ipl\Web\Control\LimitControl;
use ipl\Web\Control\SortControl;
@ -98,7 +98,7 @@ class HistoryController extends Controller
->onlyWith($preserveParams)
->setFilter($filter);
$historyList = (new HistoryList($history->execute()))
$historyList = (new LoadMoreObjectList($history->execute()))
->setPageSize($limitControl->getLimit())
->setViewMode($viewModeSwitcher->getViewMode())
->setLoadMoreUrl($url->setParam('before', $before));

View file

@ -26,11 +26,10 @@ use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\Detail\HostDetail;
use Icinga\Module\Icingadb\Widget\Detail\HostInspectionDetail;
use Icinga\Module\Icingadb\Widget\Detail\HostMetaInfo;
use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader;
use Icinga\Module\Icingadb\Widget\Detail\QuickActions;
use Icinga\Module\Icingadb\Widget\ItemList\DependencyNodeList;
use Icinga\Module\Icingadb\Widget\ItemList\HostList;
use Icinga\Module\Icingadb\Widget\ItemList\HistoryList;
use Icinga\Module\Icingadb\Widget\ItemList\ServiceList;
use Icinga\Module\Icingadb\Widget\ItemList\LoadMoreObjectList;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use ipl\Orm\Query;
use ipl\Sql\Expression;
use ipl\Sql\Filter\Exists;
@ -69,10 +68,7 @@ class HostController extends Controller
$this->host = $host;
$this->loadTabsForObject($host);
$this->addControl((new HostList([$host]))
->setViewMode('objectHeader')
->setDetailActionsDisabled()
->setNoSubjectLink());
$this->addControl(new ObjectHeader($host));
$this->setTitleTab($this->getRequest()->getActionName());
$this->setTitle($host->display_name);
@ -171,7 +167,7 @@ class HostController extends Controller
$this->addControl($limitControl);
$this->addControl($viewModeSwitcher);
$historyList = (new HistoryList($history->execute()))
$historyList = (new LoadMoreObjectList($history->execute()))
->setViewMode($viewModeSwitcher->getViewMode())
->setPageSize($limitControl->getLimit())
->setLoadMoreUrl($url->setParam('before', $before));
@ -223,7 +219,7 @@ class HostController extends Controller
yield $this->export($services);
$serviceList = (new ServiceList($services))
$serviceList = (new ObjectList($services))
->setViewMode($viewModeSwitcher->getViewMode());
$this->addControl($paginationControl);
@ -288,7 +284,7 @@ class HostController extends Controller
$this->addControl($searchBar);
$this->addContent(
(new DependencyNodeList($nodesQuery))
(new ObjectList($nodesQuery))
->setViewMode($viewModeSwitcher->getViewMode())
);
@ -358,7 +354,7 @@ class HostController extends Controller
$this->addControl($searchBar);
$this->addContent(
(new DependencyNodeList($nodesQuery))
(new ObjectList($nodesQuery))
->setViewMode($viewModeSwitcher->getViewMode())
);
@ -537,7 +533,7 @@ class HostController extends Controller
protected function getDefaultTabControls(): array
{
return [(new HostList([$this->host]))->setDetailActionsDisabled()->setNoSubjectLink()];
return [new ObjectHeader($this->host)];
}
/**

View file

@ -12,7 +12,8 @@ use Icinga\Module\Icingadb\Redis\VolatileStateResults;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\ItemList\HostList;
use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Widget\ItemTable\HostgroupTableRow;
use ipl\Html\Html;
use ipl\Stdlib\Filter;
@ -109,15 +110,15 @@ class HostgroupController extends Controller
yield $this->export($hosts);
$hostList = (new HostList($hosts->execute()))
$hostList = (new ObjectList($hosts))
->setViewMode($viewModeSwitcher->getViewMode());
// ICINGAWEB_EXPORT_FORMAT is not set yet and $this->format is inaccessible, yeah...
if ($this->getRequest()->getParam('format') === 'pdf') {
$this->addContent(new HostgroupTableRow($hostgroup));
$this->addContent(new ObjectHeader($hostgroup));
$this->addContent(Html::tag('h2', null, t('Hosts')));
} else {
$this->addControl(new HostgroupTableRow($hostgroup));
$this->addControl(new ObjectHeader($hostgroup));
}
$this->addControl($paginationControl);

View file

@ -7,10 +7,13 @@ namespace Icinga\Module\Icingadb\Controllers;
use GuzzleHttp\Psr7\ServerRequest;
use Icinga\Module\Icingadb\Model\Hostgroup;
use Icinga\Module\Icingadb\Model\Hostgroupsummary;
use Icinga\Module\Icingadb\View\HostgroupGridRenderer;
use Icinga\Module\Icingadb\View\HostgroupRenderer;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\ItemTable\HostgroupTable;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Widget\ItemTable\ObjectGrid;
use Icinga\Module\Icingadb\Widget\ItemTable\ObjectTable;
use Icinga\Module\Icingadb\Widget\ShowMore;
use ipl\Web\Control\LimitControl;
use ipl\Web\Control\SortControl;
@ -99,11 +102,13 @@ class HostgroupsController extends Controller
$results = $hostgroups->execute();
$this->addContent(
(new HostgroupTable($results))
->setBaseFilter($filter)
->setViewMode($viewModeSwitcher->getViewMode())
);
if ($viewModeSwitcher->getViewMode() === 'grid') {
$content = new ObjectGrid($results, (new HostgroupGridRenderer())->setBaseFilter($filter));
} else {
$content = new ObjectTable($results, (new HostgroupRenderer())->setBaseFilter($filter));
}
$this->addContent($content);
if ($compact) {
$this->addContent(

View file

@ -15,8 +15,8 @@ use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\Detail\MultiselectQuickActions;
use Icinga\Module\Icingadb\Widget\Detail\ObjectsDetail;
use Icinga\Module\Icingadb\Widget\ItemList\HostList;
use Icinga\Module\Icingadb\Widget\HostStatusBar;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Widget\ItemTable\HostItemTable;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Widget\ShowMore;
@ -104,7 +104,7 @@ class HostsController extends Controller
$hostList = (new HostItemTable($results, HostItemTable::applyColumnMetaData($hosts, $columns)))
->setSort($sortControl->getSort());
} else {
$hostList = (new HostList($results))
$hostList = (new ObjectList($results))
->setViewMode($viewModeSwitcher->getViewMode());
}
@ -166,7 +166,7 @@ class HostsController extends Controller
$summary->comments_total = $comments->count();
$this->addControl(
(new HostList($results))
(new ObjectList($results))
->setViewMode('minimal')
->setDetailActionsDisabled()
);

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,7 @@ class NotificationsController extends Controller
->onlyWith($preserveParams)
->setFilter($filter);
$notificationList = (new NotificationList($notifications->execute()))
$notificationList = (new LoadMoreObjectList($notifications->execute()))
->setPageSize($limitControl->getLimit())
->setViewMode($viewModeSwitcher->getViewMode())
->setLoadMoreUrl($url->setParam('before', $before));

View file

@ -14,8 +14,8 @@ use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\Detail\MultiselectQuickActions;
use Icinga\Module\Icingadb\Widget\Detail\RedundancyGroupDetail;
use Icinga\Module\Icingadb\Widget\Detail\RedundancyGroupHeader;
use Icinga\Module\Icingadb\Widget\ItemList\DependencyNodeList;
use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Generator;
use ipl\Orm\Query;
use ipl\Stdlib\Filter;
@ -68,14 +68,9 @@ class RedundancygroupController extends Controller
$this->setTitleTab($this->getRequest()->getActionName());
$this->setTitle($this->group->display_name);
$summary = RedundancyGroupSummary::on($this->getDb())
->filter(Filter::equal('id', $this->groupId));
$this->groupSummary = $this->group->summary;
$this->applyRestrictions($summary);
$this->groupSummary = $summary->first();
$this->addControl(new RedundancyGroupHeader($this->group, $this->groupSummary));
$this->addControl(new ObjectHeader($this->group));
}
public function indexAction(): void
@ -145,7 +140,7 @@ class RedundancygroupController extends Controller
$this->addControl($searchBar);
$this->addContent(
(new DependencyNodeList($nodesQuery))
(new ObjectList($nodesQuery))
->setViewMode($viewModeSwitcher->getViewMode())
);
@ -214,7 +209,7 @@ class RedundancygroupController extends Controller
$this->addControl($searchBar);
$this->addContent(
(new DependencyNodeList($nodesQuery))
(new ObjectList($nodesQuery))
->setViewMode($viewModeSwitcher->getViewMode())
);

View file

@ -20,13 +20,13 @@ use Icinga\Module\Icingadb\Redis\VolatileStateResults;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader;
use Icinga\Module\Icingadb\Widget\Detail\QuickActions;
use Icinga\Module\Icingadb\Widget\Detail\ServiceDetail;
use Icinga\Module\Icingadb\Widget\Detail\ServiceInspectionDetail;
use Icinga\Module\Icingadb\Widget\Detail\ServiceMetaInfo;
use Icinga\Module\Icingadb\Widget\ItemList\DependencyNodeList;
use Icinga\Module\Icingadb\Widget\ItemList\HistoryList;
use Icinga\Module\Icingadb\Widget\ItemList\ServiceList;
use Icinga\Module\Icingadb\Widget\ItemList\LoadMoreObjectList;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use ipl\Orm\Query;
use ipl\Sql\Expression;
use ipl\Stdlib\Filter;
@ -79,10 +79,7 @@ class ServiceController extends Controller
$this->service = $service;
$this->loadTabsForObject($service);
$this->addControl((new ServiceList([$service]))
->setViewMode('objectHeader')
->setDetailActionsDisabled()
->setNoSubjectLink());
$this->addControl(new ObjectHeader($service));
$this->setTitleTab($this->getRequest()->getActionName());
$this->setTitle(
@ -159,7 +156,7 @@ class ServiceController extends Controller
$this->addControl($searchBar);
$this->addContent(
(new DependencyNodeList($nodesQuery))
(new ObjectList($nodesQuery))
->setViewMode($viewModeSwitcher->getViewMode())
);
@ -230,7 +227,7 @@ class ServiceController extends Controller
$this->addControl($searchBar);
$this->addContent(
(new DependencyNodeList($nodesQuery))
(new ObjectList($nodesQuery))
->setViewMode($viewModeSwitcher->getViewMode())
);
@ -316,7 +313,7 @@ class ServiceController extends Controller
$this->addControl($limitControl);
$this->addControl($viewModeSwitcher);
$historyList = (new HistoryList($history->execute()))
$historyList = (new LoadMoreObjectList($history->execute()))
->setViewMode($viewModeSwitcher->getViewMode())
->setPageSize($limitControl->getLimit())
->setLoadMoreUrl($url->setParam('before', $before));
@ -515,6 +512,6 @@ class ServiceController extends Controller
protected function getDefaultTabControls(): array
{
return [(new ServiceList([$this->service]))->setDetailActionsDisabled()->setNoSubjectLink()];
return [new ObjectHeader($this->service)];
}
}

View file

@ -12,8 +12,8 @@ use Icinga\Module\Icingadb\Redis\VolatileStateResults;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\ItemList\ServiceList;
use Icinga\Module\Icingadb\Widget\ItemTable\ServicegroupTableRow;
use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use ipl\Html\Html;
use ipl\Stdlib\Filter;
use ipl\Web\Control\LimitControl;
@ -117,15 +117,15 @@ class ServicegroupController extends Controller
yield $this->export($services);
$serviceList = (new ServiceList($services->execute()))
$serviceList = (new ObjectList($services))
->setViewMode($viewModeSwitcher->getViewMode());
// ICINGAWEB_EXPORT_FORMAT is not set yet and $this->format is inaccessible, yeah...
if ($this->getRequest()->getParam('format') === 'pdf') {
$this->addContent(new ServicegroupTableRow($servicegroup));
$this->addContent(new ObjectHeader($servicegroup));
$this->addContent(Html::tag('h2', null, t('Services')));
} else {
$this->addControl(new ServicegroupTableRow($servicegroup));
$this->addControl(new ObjectHeader($servicegroup));
}
$this->addControl($paginationControl);

View file

@ -7,14 +7,19 @@ namespace Icinga\Module\Icingadb\Controllers;
use GuzzleHttp\Psr7\ServerRequest;
use Icinga\Module\Icingadb\Model\Servicegroup;
use Icinga\Module\Icingadb\Model\ServicegroupSummary;
use Icinga\Module\Icingadb\View\ServicegroupGridRenderer;
use Icinga\Module\Icingadb\View\ServicegroupRenderer;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\ItemTable\ServicegroupTable;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Widget\ItemTable\ObjectGrid;
use Icinga\Module\Icingadb\Widget\ItemTable\ObjectTable;
use Icinga\Module\Icingadb\Widget\ShowMore;
use ipl\Html\Attributes;
use ipl\Web\Control\LimitControl;
use ipl\Web\Control\SortControl;
use ipl\Web\Url;
use ipl\Web\Widget\ItemList;
class ServicegroupsController extends Controller
{
@ -87,11 +92,13 @@ class ServicegroupsController extends Controller
$results = $servicegroups->execute();
$this->addContent(
(new ServicegroupTable($results))
->setBaseFilter($filter)
->setViewMode($viewModeSwitcher->getViewMode())
);
if ($viewModeSwitcher->getViewMode() === 'grid') {
$content = new ObjectGrid($results, (new ServicegroupGridRenderer())->setBaseFilter($filter));
} else {
$content = new ObjectTable($results, (new ServicegroupRenderer())->setBaseFilter($filter));
}
$this->addContent($content);
if ($compact) {
$this->addContent(

View file

@ -17,7 +17,7 @@ use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\Detail\MultiselectQuickActions;
use Icinga\Module\Icingadb\Widget\Detail\ObjectsDetail;
use Icinga\Module\Icingadb\Widget\ItemList\ServiceList;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Widget\ItemTable\ServiceItemTable;
use Icinga\Module\Icingadb\Widget\ServiceStatusBar;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
@ -115,7 +115,7 @@ class ServicesController extends Controller
$serviceList = (new ServiceItemTable($results, ServiceItemTable::applyColumnMetaData($services, $columns)))
->setSort($sortControl->getSort());
} else {
$serviceList = (new ServiceList($results))
$serviceList = (new ObjectList($results))
->setViewMode($viewModeSwitcher->getViewMode());
}
@ -182,7 +182,7 @@ class ServicesController extends Controller
$summary->comments_total = $comments->count();
$this->addControl(
(new ServiceList($results))
(new ObjectList($results))
->setViewMode('minimal')
->setDetailActionsDisabled()
);

View file

@ -7,8 +7,8 @@ namespace Icinga\Module\Icingadb\Controllers;
use Icinga\Exception\NotFoundError;
use Icinga\Module\Icingadb\Model\User;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader;
use Icinga\Module\Icingadb\Widget\Detail\UserDetail;
use Icinga\Module\Icingadb\Widget\ItemTable\UserTableRow;
use ipl\Stdlib\Filter;
class UserController extends Controller
@ -40,7 +40,7 @@ class UserController extends Controller
public function indexAction()
{
$this->addControl(new UserTableRow($this->user));
$this->addControl(new ObjectHeader($this->user));
$this->addContent(new UserDetail($this->user));
$this->setAutorefreshInterval(10);

View file

@ -7,8 +7,8 @@ namespace Icinga\Module\Icingadb\Controllers;
use Icinga\Exception\NotFoundError;
use Icinga\Module\Icingadb\Model\Usergroup;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader;
use Icinga\Module\Icingadb\Widget\Detail\UsergroupDetail;
use Icinga\Module\Icingadb\Widget\ItemTable\UsergroupTableRow;
use ipl\Stdlib\Filter;
class UsergroupController extends Controller
@ -40,7 +40,7 @@ class UsergroupController extends Controller
public function indexAction()
{
$this->addControl(new UsergroupTableRow($this->usergroup));
$this->addControl(new ObjectHeader($this->usergroup));
$this->addContent(new UsergroupDetail($this->usergroup));
$this->setAutorefreshInterval(10);

View file

@ -8,10 +8,11 @@ use GuzzleHttp\Psr7\ServerRequest;
use Icinga\Module\Icingadb\Model\Usergroup;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\ItemTable\UsergroupTable;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use ipl\Web\Control\LimitControl;
use ipl\Web\Control\SortControl;
use ipl\Web\Url;
class UsergroupsController extends Controller
{
@ -64,7 +65,7 @@ class UsergroupsController extends Controller
$this->addControl($limitControl);
$this->addControl($searchBar);
$this->addContent(new UsergroupTable($usergroups));
$this->addContent(new ObjectList($usergroups));
if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
$this->sendMultipartUpdate();

View file

@ -8,10 +8,11 @@ use GuzzleHttp\Psr7\ServerRequest;
use Icinga\Module\Icingadb\Model\User;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\ItemTable\UserTable;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use ipl\Web\Control\LimitControl;
use ipl\Web\Control\SortControl;
use ipl\Web\Url;
class UsersController extends Controller
{
@ -66,7 +67,7 @@ class UsersController extends Controller
$this->addControl($limitControl);
$this->addControl($searchBar);
$this->addContent(new UserTable($users));
$this->addContent(new ObjectList($users));
if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
$this->sendMultipartUpdate();

View file

@ -1,31 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Common;
trait CaptionDisabled
{
/** @var bool */
protected $captionDisabled = false;
/**
* @return bool
*/
public function isCaptionDisabled(): bool
{
return $this->captionDisabled;
}
/**
* @param bool $captionDisabled
*
* @return $this
*/
public function setCaptionDisabled(bool $captionDisabled = true): self
{
$this->captionDisabled = $captionDisabled;
return $this;
}
}

View file

@ -1,26 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Common;
use ipl\Html\BaseHtmlElement;
trait ListItemCommonLayout
{
use CaptionDisabled;
protected function assembleHeader(BaseHtmlElement $header): void
{
$header->addHtml($this->createTitle());
$header->add($this->createTimestamp());
}
protected function assembleMain(BaseHtmlElement $main): void
{
$main->addHtml($this->createHeader());
if (!$this->isCaptionDisabled()) {
$main->addHtml($this->createCaption());
}
}
}

View file

@ -1,23 +0,0 @@
<?php
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Common;
use ipl\Html\BaseHtmlElement;
trait ListItemDetailedLayout
{
protected function assembleHeader(BaseHtmlElement $header): void
{
$header->add($this->createTitle());
$header->add($this->createTimestamp());
}
protected function assembleMain(BaseHtmlElement $main): void
{
$main->add($this->createHeader());
$main->add($this->createCaption());
$main->add($this->createFooter());
}
}

View file

@ -1,26 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Common;
use ipl\Html\BaseHtmlElement;
trait ListItemMinimalLayout
{
use CaptionDisabled;
protected function assembleHeader(BaseHtmlElement $header): void
{
$header->add($this->createTitle());
if (! $this->isCaptionDisabled()) {
$header->add($this->createCaption());
}
$header->add($this->createTimestamp());
}
protected function assembleMain(BaseHtmlElement $main): void
{
$main->add($this->createHeader());
}
}

View file

@ -1,35 +0,0 @@
<?php
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Common;
trait NoSubjectLink
{
/** @var bool */
protected $noSubjectLink = false;
/**
* Set whether a list item's subject should be a link
*
* @param bool $state
*
* @return $this
*/
public function setNoSubjectLink(bool $state = true): self
{
$this->noSubjectLink = $state;
return $this;
}
/**
* Get whether a list item's subject should be a link
*
* @return bool
*/
public function getNoSubjectLink(): bool
{
return $this->noSubjectLink;
}
}

View file

@ -1,35 +0,0 @@
<?php
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Common;
trait ObjectLinkDisabled
{
/** @var bool */
protected $objectLinkDisabled = false;
/**
* Set whether list items should render host and service links
*
* @param bool $state
*
* @return $this
*/
public function setObjectLinkDisabled(bool $state = true): self
{
$this->objectLinkDisabled = $state;
return $this;
}
/**
* Get whether list items should render host and service links
*
* @return bool
*/
public function getObjectLinkDisabled(): bool
{
return $this->objectLinkDisabled;
}
}

View file

@ -5,34 +5,35 @@
namespace Icinga\Module\Icingadb\Common;
use Icinga\Application\Hook;
use Icinga\Application\Hook\TicketHook;
trait TicketLinks
{
/** @var bool */
protected $ticketLinkEnabled = false;
/** @var bool Whether the ticket link is disabled */
protected $ticketLinkDisabled = false;
/**
* Set whether list items should render host and service links
* Set whether the ticket link is disabled
*
* @param bool $state
*
* @return $this
*/
public function setTicketLinkEnabled(bool $state = true): self
public function setTicketLinkDisabled(bool $state = true): self
{
$this->ticketLinkEnabled = $state;
$this->ticketLinkDisabled = $state;
return $this;
}
/**
* Get whether list items should render host and service links
* Get whether the ticket link is disabled
*
* @return bool
*/
public function getTicketLinkEnabled(): bool
public function isTicketLinkDisabled(): bool
{
return $this->ticketLinkEnabled;
return $this->ticketLinkDisabled;
}
/**
@ -42,15 +43,13 @@ trait TicketLinks
*/
public function createTicketLinks($text): string
{
if (Hook::has('ticket')) {
$tickets = Hook::first('ticket');
if ($this->isTicketLinkDisabled() || ! Hook::has('ticket')) {
return $text ?? '';
}
if ($this->getTicketLinkEnabled() && isset($tickets)) {
/** @var \Icinga\Application\Hook\TicketHook $tickets */
return $tickets->createLinks($text);
}
/** @var TicketHook $tickets */
$tickets = Hook::first('ticket');
return $text ?? '';
return $tickets->createLinks($text);
}
}

View file

@ -1,35 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Common;
trait ViewMode
{
/** @var string */
protected $viewMode;
/**
* Get the view mode
*
* @return ?string
*/
public function getViewMode()
{
return $this->viewMode;
}
/**
* Set the view mode
*
* @param string $viewMode
*
* @return $this
*/
public function setViewMode(string $viewMode): self
{
$this->viewMode = $viewMode;
return $this;
}
}

View file

@ -4,13 +4,16 @@
namespace Icinga\Module\Icingadb\Model;
use Icinga\Module\Icingadb\Common\Auth;
use Icinga\Module\Icingadb\Common\Backend;
use Icinga\Module\Icingadb\Model\Behavior\ReRoute;
use ipl\Orm\Behavior\Binary;
use ipl\Orm\Behavior\BoolCast;
use ipl\Orm\Behaviors;
use ipl\Orm\Defaults;
use ipl\Orm\Model;
use ipl\Orm\Query;
use ipl\Orm\Relations;
use ipl\Stdlib\Filter;
/**
* Redundancy group model.
@ -22,9 +25,13 @@ use ipl\Orm\Relations;
* @property (?RedundancyGroupState)|Query $state
* @property DependencyEdge|Query $from
* @property DependencyEdge|Query $to
*
* @property RedundancyGroupSummary $summary
*/
class RedundancyGroup extends Model
{
use Auth;
public function getTableName(): string
{
return 'redundancy_group';
@ -78,4 +85,16 @@ class RedundancyGroup extends Model
->setTargetForeignKey('id')
->through(DependencyNode::class);
}
public function createDefaults(Defaults $defaults)
{
$defaults->add('summary', function (RedundancyGroup $group) {
$summary = RedundancyGroupSummary::on(Backend::getDb())
->filter(Filter::equal('id', $group->id));
$this->applyRestrictions($summary);
return $summary->first();
});
}
}

View file

@ -0,0 +1,304 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\View;
use Icinga\Module\Icingadb\Common\HostStates;
use Icinga\Module\Icingadb\Common\Icons;
use Icinga\Module\Icingadb\Common\ServiceStates;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Model\Service;
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\PluginOutputContainer;
use Icinga\Module\Icingadb\Widget\StateChange;
use ipl\Html\Attributes;
use ipl\Html\Html;
use ipl\Html\HtmlDocument;
use ipl\Html\HtmlElement;
use ipl\Html\HtmlString;
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;
use ipl\Web\Widget\TimeSince;
/**
* @template Item of Host|Service
*
* @implements ItemRenderer<Item>
*/
abstract class BaseHostAndServiceRenderer implements ItemRenderer
{
use Translation;
/**
* Create subject for the given item
*
* @param Item $item The item to create subject for
*
* @param string $layout The name of the layout
*
* @return ValidHtml
*/
abstract protected function createSubject($item, string $layout): ValidHtml;
public function assembleVisual($item, HtmlDocument $visual, string $layout): void
{
if ($layout === 'header') {
if ($item->state->state_type === 'soft') {
$stateType = 'soft_state';
$previousStateType = 'previous_soft_state';
if ($item->state->previous_soft_state === 0) {
$previousStateType = 'hard_state';
}
} else {
$stateType = 'hard_state';
$previousStateType = 'previous_hard_state';
if ($item->state->hard_state === $item->state->previous_hard_state) {
$previousStateType = 'previous_soft_state';
}
}
if ($item instanceof Host) {
$state = HostStates::text($item->state->$stateType);
$previousState = HostStates::text($item->state->$previousStateType);
} else {
$state = ServiceStates::text($item->state->$stateType);
$previousState = ServiceStates::text($item->state->$previousStateType);
}
$stateChange = new StateChange($state, $previousState);
if ($stateType === 'soft_state') {
$stateChange->setCurrentStateBallSize(StateBall::SIZE_MEDIUM_LARGE);
}
if ($previousStateType === 'previous_soft_state') {
$stateChange->setPreviousStateBallSize(StateBall::SIZE_MEDIUM_LARGE);
if ($stateType === 'soft_state') {
$visual->getAttributes()->add('class', 'small-state-change');
}
}
$stateChange->setIcon($item->state->getIcon());
$stateChange->setHandled(
$item->state->is_problem && ($item->state->is_handled || ! $item->state->is_reachable)
);
$visual->addHtml($stateChange);
return;
}
$ballSize = $layout === 'minimal' ? StateBall::SIZE_BIG : StateBall::SIZE_LARGE;
$stateBall = new StateBall($item->state->getStateText(), $ballSize);
$stateBall->add($item->state->getIcon());
if ($item->state->is_problem && ($item->state->is_handled || ! $item->state->is_reachable)) {
$stateBall->getAttributes()->add('class', 'handled');
}
$visual->addHtml($stateBall);
if ($layout !== 'minimal' && $item->state->state_type === 'soft') {
$visual->addHtml(
new CheckAttempt((int) $item->state->check_attempt, (int) $item->max_check_attempts)
);
}
}
public function assembleCaption($item, HtmlDocument $caption, string $layout): void
{
if ($item->state->soft_state === null && $item->state->output === null) {
$caption->addHtml(Text::create($this->translate('Waiting for Icinga DB to synchronize the state.')));
} else {
if (empty($item->state->output)) {
$pluginOutput = new EmptyState($this->translate('Output unavailable.'));
} else {
$pluginOutput = new PluginOutputContainer(PluginOutput::fromObject($item));
}
$caption->addHtml($pluginOutput);
}
}
public function assembleTitle($item, HtmlDocument $title, string $layout): void
{
$title->addHtml(Html::sprintf(
$this->translate('%s is %s', '<hostname> is <state-text>'),
$this->createSubject($item, $layout),
Html::tag('span', ['class' => 'state-text'], $item->state->getStateTextTranslated())
));
if (isset($item->state->affects_children) && $item->state->affects_children) {
$total = (int) $item->total_children;
if ($total > 1000) {
$total = '1000+';
$tooltip = $this->translate('Up to 1000+ affected objects');
} else {
$tooltip = sprintf(
$this->translatePlural(
'%d affected object',
'Up to %d affected objects',
$total
),
$total
);
}
$icon = new Icon(Icons::UNREACHABLE);
$title->addHtml(new HtmlElement(
'span',
Attributes::create([
'class' => 'affected-objects',
'title' => $tooltip
]),
$icon,
Text::create($total)
));
}
}
public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void
{
if ($item->state->is_overdue) {
$since = new TimeSince($item->state->next_update->getTimestamp());
$since->prepend($this->translate('Overdue') . ' ');
$since->prependHtml(new Icon(Icons::WARNING));
$info->addHtml($since);
} elseif ($item->state->last_state_change !== null && $item->state->last_state_change->getTimestamp() > 0) {
$info->addHtml(new TimeSince($item->state->last_state_change->getTimestamp()));
}
}
public function assembleFooter($item, HtmlDocument $footer, string $layout): void
{
$pieChartLimit = 5;
$statusIcons = new HtmlElement('div', Attributes::create(['class' => 'status-icons']));
$isService = $item instanceof Service;
if (
($isService && $item->state->last_comment->service_id === $item->id)
|| $item->state->last_comment->host_id === $item->id
) {
$comment = $item->state->last_comment;
if ($isService) {
$comment->service = $item;
} else {
$comment->host = $item;
}
$commentItem = new ItemLayout(
$comment,
(new CommentRenderer())
->setTicketLinkDisabled()
->setNoObjectLink()
->setNoSubjectLink()
);
$statusIcons->addHtml(
new HtmlElement(
'div',
Attributes::create(['class' => 'comment-wrapper']),
new HtmlElement(
'div',
$commentItem->getAttributes()->add('class', 'comment-popup'),
$commentItem
),
(new Icon('comments', ['class' => 'comment-icon']))
)
);
}
if ($item->state->is_flapping) {
$title = $isService
? sprintf(
$this->translate('Service "%s" on "%s" is in flapping state'),
$item->display_name,
$item->host->display_name
)
: sprintf(
$this->translate('Host "%s" is in flapping state'),
$item->display_name
);
$statusIcons->addHtml(new Icon('random', ['title' => $title]));
}
if (! $item->notifications_enabled) {
$statusIcons->addHtml(
new Icon('bell-slash', ['title' => $this->translate('Notifications disabled')])
);
}
if (! $item->active_checks_enabled) {
$statusIcons->addHtml(
new Icon('eye-slash', ['title' => $this->translate('Active checks disabled')])
);
}
$performanceData = new HtmlElement('div', Attributes::create(['class' => 'performance-data']));
if ($item->state->performance_data) {
$pieChartData = PerfDataSet::fromString($item->state->normalized_performance_data)->asArray();
$pies = [];
foreach ($pieChartData as $i => $perfdata) {
if ($perfdata->isVisualizable()) {
$pies[] = $perfdata->asInlinePie()->render();
}
// Check if number of visualizable pie charts is larger than $PIE_CHART_LIMIT
if (count($pies) > $pieChartLimit) {
break;
}
}
$maxVisiblePies = $pieChartLimit - 2;
$numOfPies = count($pies);
foreach ($pies as $i => $pie) {
if (
// Show max. 5 elements: if there are more than 5, show 4 + `…`
$i > $maxVisiblePies && $numOfPies > $pieChartLimit
) {
$performanceData->addHtml(new HtmlElement('span', null, Text::create('…')));
break;
}
$performanceData->addHtml(HtmlString::create($pie));
}
}
if (! $statusIcons->isEmpty()) {
$footer->addHtml($statusIcons);
}
if (! $performanceData->isEmpty()) {
$footer->addHtml($performanceData);
}
}
public function assemble($item, string $name, HtmlDocument $element, string $layout): bool
{
if ($name === 'icon-image') {
if (isset($item->icon_image->icon_image)) {
$element->addHtml(new IconImage($item->icon_image->icon_image, $item->icon_image_alt));
}
return true;
}
return false;
}
}

View file

@ -0,0 +1,167 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\View;
use Icinga\Module\Icingadb\Common\HostLink;
use Icinga\Module\Icingadb\Common\Icons;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Common\ServiceLink;
use Icinga\Module\Icingadb\Common\TicketLinks;
use Icinga\Module\Icingadb\Model\Comment;
use Icinga\Module\Icingadb\Widget\MarkdownLine;
use ipl\Html\Attributes;
use ipl\Html\FormattedString;
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\Icon;
use ipl\Web\Widget\Link;
use ipl\Web\Widget\TimeAgo;
use ipl\Web\Widget\TimeUntil;
/** @implements ItemRenderer<Comment> */
class CommentRenderer implements ItemRenderer
{
use Translation;
use TicketLinks;
use HostLink;
use ServiceLink;
/** @var bool Whether the object link for th item should be omitted */
protected $noObjectLink = false;
/** @var bool Whether item's subject should be a link */
protected $noSubjectLink = false;
/**
* Set whether the object link for th item should be omitted
*
* @param bool $state
*
* @return $this
*/
public function setNoObjectLink(bool $state = true): self
{
$this->noObjectLink = $state;
return $this;
}
/**
* Set whether item's subject should be a link
*
* @param bool $state
*
* @return $this
*/
public function setNoSubjectLink(bool $state = true): self
{
$this->noSubjectLink = $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', '<username>..')
: $this->translate('%s commented', '<username>..'),
$item->author
);
$headerParts = [
new Icon(Icons::USER),
$layout === 'header' || $this->noSubjectLink
? 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->noObjectLink) {
// 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->createTicketLinks($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(
FormattedString::create(
$this->translate("expires %s"),
new TimeUntil($item->expire_time->getTimestamp())
)
);
} else {
$info->addHtml(
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
}
}

View file

@ -0,0 +1,246 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\View;
use Icinga\Date\DateFormatter;
use Icinga\Module\Icingadb\Common\HostLink;
use Icinga\Module\Icingadb\Common\Icons;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Common\ServiceLink;
use Icinga\Module\Icingadb\Common\TicketLinks;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\Widget\MarkdownLine;
use ipl\Html\Attributes;
use ipl\Html\Html;
use ipl\Html\HtmlDocument;
use ipl\Html\HtmlElement;
use ipl\Html\TemplateString;
use ipl\Html\Text;
use ipl\I18n\Translation;
use ipl\Web\Common\ItemRenderer;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\Link;
/** @implements ItemRenderer<Downtime> */
class DowntimeRenderer implements ItemRenderer
{
use Translation;
use TicketLinks;
use HostLink;
use ServiceLink;
/** @var int Current Time */
protected $currentTime;
/** @var int Duration */
protected $duration;
/** @var int Downtime end time */
protected $endTime;
/** @var bool Whether the downtime is active */
protected $isActive;
/** @var int Downtime start time */
protected $startTime;
/** @var bool Whether the state has been loaded */
protected $stateLoaded = false;
/** @var bool Whether the object link for th item should be omitted */
protected $noObjectLink = false;
/**
* Set whether the object link for th item should be omitted
*
* @param bool $state
*
* @return $this
*/
public function setNoObjectLink(bool $state = true): self
{
$this->noObjectLink = $state;
return $this;
}
/**
* Load the state of the downtime
*
* @param Downtime $item
*
* @return void
*/
protected function loadState(Downtime $item): void
{
if ($this->stateLoaded) {
return;
}
if (
isset($item->start_time, $item->end_time)
&& $item->is_flexible
&& $item->is_in_effect
) {
$this->startTime = $item->start_time->getTimestamp();
$this->endTime = $item->end_time->getTimestamp();
} else {
$this->startTime = $item->scheduled_start_time->getTimestamp();
$this->endTime = $item->scheduled_end_time->getTimestamp();
}
$this->currentTime = time();
$this->isActive = $item->is_in_effect
|| ($item->is_flexible && $item->scheduled_start_time->getTimestamp() <= $this->currentTime);
$until = ($this->isActive ? $this->endTime : $this->startTime) - $this->currentTime;
$this->duration = explode(' ', DateFormatter::formatDuration(
$until <= 3600 ? $until : $until + (3600 - ((int) $until % 3600))
), 2)[0];
$this->stateLoaded = true;
}
public function assembleAttributes($item, Attributes $attributes, string $layout): void
{
$attributes->add(new Attributes(['class' => ['downtime', $item->is_in_effect ? 'in-effect' : '']]));
}
public function assembleVisual($item, HtmlDocument $visual, string $layout): void
{
$this->loadState($item);
$dateTime = DateFormatter::formatDateTime($this->endTime);
if ($this->isActive) {
$visual->addHtml(Html::sprintf(
$this->translate('%s left', '<timespan>..'),
Html::tag(
'strong',
Html::tag(
'time',
[
'datetime' => $dateTime,
'title' => $dateTime
],
$this->duration
)
)
));
} else {
$visual->addHtml(Html::sprintf(
$this->translate('in %s', '..<timespan>'),
Html::tag('strong', $this->duration)
));
}
}
public function assembleTitle($item, HtmlDocument $title, string $layout): void
{
if ($this->noObjectLink) {
$link = null;
} elseif ($item->object_type === 'host') {
$link = $this->createHostLink($item->host, true);
} else {
$link = $this->createServiceLink($item->service, $item->service->host, true);
}
if ($item->is_flexible) {
if ($link !== null) {
$template = $this->translate('{{#link}}Flexible Downtime{{/link}} for %s');
} else {
$template = $this->translate('Flexible Downtime');
}
} else {
if ($link !== null) {
$template = $this->translate('{{#link}}Fixed Downtime{{/link}} for %s');
} else {
$template = $this->translate('Fixed Downtime');
}
}
if ($layout === 'header') {
if ($link === null) {
$title->addHtml(HtmlElement::create('span', [ 'class' => 'subject'], $template));
} else {
$title->addHtml(TemplateString::create(
$template,
['link' => HtmlElement::create('span', [ 'class' => 'subject'])],
$link
));
}
} else {
if ($link === null) {
$title->addHtml(new Link($template, Links::downtime($item)));
} else {
$title->addHtml(TemplateString::create(
$template,
['link' => new Link('', Links::downtime($item))],
$link
));
}
}
}
public function assembleCaption($item, HtmlDocument $caption, string $layout): void
{
$markdownLine = new MarkdownLine($this->createTicketLinks($item->comment));
$caption->getAttributes()->add($markdownLine->getAttributes());
$caption->addHtml(
new HtmlElement(
'span',
null,
new Icon(Icons::USER),
Text::create($item->author)
),
Text::create(': ')
)->addFrom($markdownLine);
}
public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void
{
$this->loadState($item);
$dateTime = DateFormatter::formatDateTime($this->isActive ? $this->endTime : $this->startTime);
$info->addHtml(Html::tag(
'time',
[
'datetime' => $dateTime,
'title' => $dateTime
],
sprintf(
$this->isActive
? $this->translate('expires in %s', '..<timespan>')
: $this->translate('starts in %s', '..<timespan>'),
$this->duration
)
));
}
public function assembleFooter($item, HtmlDocument $footer, string $layout): void
{
}
public function assemble($item, string $name, HtmlDocument $element, string $layout): bool
{
if ($name === 'progress' && ($layout === 'detailed' || $layout === 'common')) {
$this->loadState($item);
$element
->addAttributes(Attributes::create([
'data-animate-progress' => true,
'data-start-time' => $this->startTime,
'data-end-time' => $this->endTime
]))
->addHtml(new HtmlElement('div', Attributes::create(['class' => 'bar'])));
return true;
}
return false;
}
}

View file

@ -0,0 +1,425 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\View;
use Icinga\Date\DateFormatter;
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\ServiceLink;
use Icinga\Module\Icingadb\Common\ServiceStates;
use Icinga\Module\Icingadb\Common\TicketLinks;
use Icinga\Module\Icingadb\Model\History;
use Icinga\Module\Icingadb\Util\PluginOutput;
use Icinga\Module\Icingadb\Widget\CheckAttempt;
use Icinga\Module\Icingadb\Widget\MarkdownLine;
use Icinga\Module\Icingadb\Widget\PluginOutputContainer;
use Icinga\Module\Icingadb\Widget\StateChange;
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;
/** @implements ItemRenderer<History> */
class EventRenderer implements ItemRenderer
{
use Translation;
use TicketLinks;
use HostLink;
use ServiceLink;
/** @var NotificationRenderer To render NotificationHistory event */
protected $notificationRenderer;
public function __construct()
{
$this->notificationRenderer = new NotificationRenderer();
}
public function assembleAttributes($item, Attributes $attributes, string $layout): void
{
$attributes->get('class')->addValue('history');
}
public function assembleVisual($item, HtmlDocument $visual, string $layout): void
{
$ballSize = StateBall::SIZE_LARGE;
if ($layout === 'minimal' || $layout === 'header') {
$ballSize = StateBall::SIZE_BIG;
}
switch ($item->event_type) {
case 'comment_add':
$visual->addHtml(
HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $ballSize]],
new Icon(Icons::COMMENT)
)
);
break;
case 'comment_remove':
case 'downtime_end':
case 'ack_clear':
$visual->addHtml(
HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $ballSize]],
new Icon(Icons::REMOVE)
)
);
break;
case 'downtime_start':
$visual->addHtml(
HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $ballSize]],
new Icon(Icons::IN_DOWNTIME)
)
);
break;
case 'ack_set':
$visual->addHtml(
HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $ballSize]],
new Icon(Icons::IS_ACKNOWLEDGED)
)
);
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 'notification':
$visual->addHtml(
HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $ballSize]],
new Icon(Icons::NOTIFICATION)
)
);
break;
case 'state_change':
if ($layout !== 'minimal' && $layout !== 'header' && $item->state->state_type === 'soft') {
$stateType = 'soft_state';
$previousStateType = 'previous_soft_state';
if ($item->state->previous_soft_state === 0) {
$previousStateType = 'hard_state';
}
$visual->addHtml(
new CheckAttempt(
(int) $item->state->check_attempt,
(int) $item->state->max_check_attempts
)
);
} else {
$stateType = 'hard_state';
$previousStateType = 'previous_hard_state';
if ($item->state->hard_state === $item->state->previous_hard_state) {
$previousStateType = 'previous_soft_state';
}
}
if ($item->object_type === 'host') {
$state = HostStates::text($item->state->$stateType);
$previousState = HostStates::text($item->state->$previousStateType);
} else {
$state = ServiceStates::text($item->state->$stateType);
$previousState = ServiceStates::text($item->state->$previousStateType);
}
$stateChange = new StateChange($state, $previousState);
if ($stateType === 'soft_state') {
$stateChange->setCurrentStateBallSize(StateBall::SIZE_MEDIUM_LARGE);
}
if ($previousStateType === 'previous_soft_state') {
$stateChange->setPreviousStateBallSize(StateBall::SIZE_MEDIUM_LARGE);
if ($stateType === 'soft_state') {
$visual->getAttributes()->add('class', 'small-state-change');
}
}
$visual->prependHtml($stateChange);
break;
}
}
public function assembleTitle($item, HtmlDocument $title, string $layout): void
{
if ($item->event_type === 'notification' && isset($item->notification->id)) {
$item->notification->history = $item;
$item->notification->host = $item->host;
$item->notification->service = $item->service;
$this->notificationRenderer->assembleTitle($item->notification, $title, $layout);
return;
}
switch ($item->event_type) {
case 'comment_add':
$subjectLabel = $this->translate('Comment added');
break;
case 'comment_remove':
if (! empty($item->comment->removed_by)) {
if ($item->comment->removed_by !== $item->comment->author) {
$subjectLabel = sprintf(
$this->translate('Comment removed by %s', '..<username>'),
$item->comment->removed_by
);
} else {
$subjectLabel = $this->translate('Comment removed by author');
}
} elseif (isset($item->comment->expire_time)) {
$subjectLabel = $this->translate('Comment expired');
} else {
$subjectLabel = $this->translate('Comment removed');
}
break;
case 'downtime_end':
if (! empty($item->downtime->cancelled_by)) {
if ($item->downtime->cancelled_by !== $item->downtime->author) {
$subjectLabel = sprintf(
$this->translate('Downtime cancelled by %s', '..<username>'),
$item->downtime->cancelled_by
);
} else {
$subjectLabel = $this->translate('Downtime cancelled by author');
}
} elseif ($item->downtime->has_been_cancelled === 'y') {
$subjectLabel = $this->translate('Downtime cancelled');
} else {
$subjectLabel = $this->translate('Downtime ended');
}
break;
case 'downtime_start':
$subjectLabel = $this->translate('Downtime started');
break;
case 'flapping_start':
$subjectLabel = $this->translate('Flapping started');
break;
case 'flapping_end':
$subjectLabel = $this->translate('Flapping stopped');
break;
case 'ack_set':
$subjectLabel = $this->translate('Acknowledgement set');
break;
case 'ack_clear':
if (! empty($item->acknowledgement->cleared_by)) {
if ($item->acknowledgement->cleared_by !== $item->acknowledgement->author) {
$subjectLabel = sprintf(
$this->translate('Acknowledgement cleared by %s', '..<username>'),
$item->acknowledgement->cleared_by
);
} else {
$subjectLabel = $this->translate('Acknowledgement cleared by author');
}
} elseif (isset($item->acknowledgement->expire_time)) {
$subjectLabel = $this->translate('Acknowledgement expired');
} else {
$subjectLabel = $this->translate('Acknowledgement cleared');
}
break;
case 'state_change':
$state = $item->state->state_type === 'hard'
? $item->state->hard_state
: $item->state->soft_state;
if ($state === 0) {
if ($item->object_type === 'service') {
$subjectLabel = $this->translate('Service recovered');
} else {
$subjectLabel = $this->translate('Host recovered');
}
} else {
if ($item->state->state_type === 'hard') {
$subjectLabel = $this->translate('Hard state changed');
} else {
$subjectLabel = $this->translate('Soft state changed');
}
}
break;
default:
$subjectLabel = $item->event_type;
break;
}
if ($layout === 'header') {
$title->addHtml(HtmlElement::create('span', ['class' => 'subject'], $subjectLabel));
} else {
$title->addHtml(new Link($subjectLabel, Links::event($item), ['class' => 'subject']));
}
if ($item->object_type === 'host' && isset($item->host->id)) {
$link = $this->createHostLink($item->host, true);
} elseif (isset($item->host->id, $item->service->id)) {
$link = $this->createServiceLink($item->service, $item->host, true);
}
$title->addHtml(Text::create(' '));
if (isset($link)) {
$title->addHtml($link);
}
}
public function assembleCaption($item, HtmlDocument $caption, string $layout): void
{
if ($item->event_type === 'notification') {
$item->notification->host = $item->host;
$item->notification->service = $item->service;
$this->notificationRenderer->assembleCaption($item->notification, $caption, $layout);
return;
}
switch ($item->event_type) {
case 'comment_add':
case 'comment_remove':
$markdownLine = new MarkdownLine($this->createTicketLinks($item->comment->comment));
$caption->getAttributes()->add($markdownLine->getAttributes());
$caption->add([
new Icon(Icons::USER),
$item->comment->author,
': '
])->addFrom($markdownLine);
break;
case 'downtime_end':
case 'downtime_start':
$markdownLine = new MarkdownLine($this->createTicketLinks($item->downtime->comment));
$caption->getAttributes()->add($markdownLine->getAttributes());
$caption->add([
new Icon(Icons::USER),
$item->downtime->author,
': '
])->addFrom($markdownLine);
break;
case 'flapping_start':
$caption
->add(
sprintf(
$this->translate('State Change Rate: %.2f%%; Start Threshold: %.2f%%'),
$item->flapping->percent_state_change_start,
$item->flapping->flapping_threshold_high
)
)
->getAttributes()
->add('class', 'plugin-output');
break;
case 'flapping_end':
$caption
->add(
sprintf(
$this->translate('State Change Rate: %.2f%%; End Threshold: %.2f%%; Flapping for %s'),
$item->flapping->percent_state_change_end,
$item->flapping->flapping_threshold_low,
isset($item->flapping->end_time)
? DateFormatter::formatDuration(
$item->flapping->end_time->getTimestamp()
- $item->flapping->start_time->getTimestamp()
)
: $this->translate('n. a.')
)
)
->getAttributes()
->add('class', 'plugin-output');
break;
case 'ack_clear':
case 'ack_set':
if (! isset($item->acknowledgement->comment) && ! isset($item->acknowledgement->author)) {
$caption->addHtml(
new EmptyState(
$this->translate('This acknowledgement was set before Icinga DB history recording')
)
);
} else {
$markdownLine = new MarkdownLine($this->createTicketLinks($item->acknowledgement->comment));
$caption->getAttributes()->add($markdownLine->getAttributes());
$caption->add([
new Icon(Icons::USER),
$item->acknowledgement->author,
': '
])->addFrom($markdownLine);
}
break;
case 'state_change':
$commandName = $item->object_type === 'host'
? $item->host->checkcommand_name
: $item->service->checkcommand_name;
if (isset($commandName)) {
if (empty($item->state->output)) {
$caption->addHtml(new EmptyState($this->translate('Output unavailable.')));
} else {
$caption->addHtml(
new PluginOutputContainer(
(new PluginOutput($item->state->output))
->setCommandName($commandName)
)
);
}
} else {
$caption->addHtml(
new EmptyState($this->translate('Waiting for Icinga DB to synchronize the config.'))
);
}
break;
}
}
public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void
{
$info->addHtml(new TimeAgo($item->event_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
}
}

View file

@ -0,0 +1,31 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\View;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Model\Host;
use ipl\Html\Attributes;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Html\ValidHtml;
use ipl\Web\Widget\Link;
/** @extends BaseHostAndServiceRenderer<Host> */
class HostRenderer extends BaseHostAndServiceRenderer
{
public function assembleAttributes($item, Attributes $attributes, string $layout): void
{
$attributes->get('class')->addValue('host');
}
protected function createSubject($item, string $layout): ValidHtml
{
if ($layout === 'header') {
return new HtmlElement('span', new Attributes(['class' => 'subject']), new Text($item->display_name));
}
return new Link($item->display_name, Links::host($item), ['class' => 'subject']);
}
}

View file

@ -0,0 +1,169 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\View;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Model\Hostgroupsummary;
use ipl\Html\Attributes;
use ipl\Html\HtmlDocument;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\I18n\Translation;
use ipl\Stdlib\BaseFilter;
use ipl\Stdlib\Filter;
use ipl\Web\Common\ItemRenderer;
use ipl\Web\Url;
use ipl\Web\Widget\Link;
use ipl\Web\Widget\StateBadge;
/** @implements ItemRenderer<Hostgroupsummary> */
class HostgroupGridRenderer implements ItemRenderer
{
use Translation;
use BaseFilter;
public function assembleAttributes($item, Attributes $attributes, string $layout): void
{
$attributes->get('class')->addValue(['object-grid-cell', 'hostgroup']);
}
public function assembleVisual($item, HtmlDocument $visual, string $layout): void
{
$url = Url::fromPath('icingadb/hosts');
$urlFilter = Filter::all(Filter::equal('hostgroup.name', $item->name));
if ($item->hosts_down_unhandled > 0) {
$urlFilter->add(Filter::equal('host.state.soft_state', 1))
->add(Filter::equal('host.state.is_handled', 'n'))
->add(Filter::equal('host.state.is_reachable', 'y'));
$link = new Link(
new StateBadge($item->hosts_down_unhandled, 'down'),
$url->setFilter($urlFilter),
[
'title' => sprintf(
$this->translatePlural(
'List %d host that is currently in DOWN state in host group "%s"',
'List %d hosts which are currently in DOWN state in host group "%s"',
$item->hosts_down_unhandled
),
$item->hosts_down_unhandled,
$item->display_name
)
]
);
} elseif ($item->hosts_down_handled > 0) {
$urlFilter->add(Filter::equal('host.state.soft_state', 1))
->add(Filter::any(
Filter::equal('host.state.is_handled', 'y'),
Filter::equal('host.state.is_reachable', 'n')
));
$link = new Link(
new StateBadge($item->hosts_down_handled, 'down', true),
$url->setFilter($urlFilter),
[
'title' => sprintf(
$this->translatePlural(
'List %d host that is currently in DOWN (Acknowledged) state in host group "%s"',
'List %d hosts which are currently in DOWN (Acknowledged) state in host group "%s"',
$item->hosts_down_handled
),
$item->hosts_down_handled,
$item->display_name
)
]
);
} elseif ($item->hosts_pending > 0) {
$urlFilter->add(Filter::equal('host.state.soft_state', 99));
$link = new Link(
new StateBadge($item->hosts_pending, 'pending'),
$url->setFilter($urlFilter),
[
'title' => sprintf(
$this->translatePlural(
'List %d host that is currently in PENDING state in host group "%s"',
'List %d hosts which are currently in PENDING state in host group "%s"',
$item->hosts_pending
),
$item->hosts_pending,
$item->display_name
)
]
);
} elseif ($item->hosts_up > 0) {
$urlFilter->add(Filter::equal('host.state.soft_state', 0));
$link = new Link(
new StateBadge($item->hosts_up, 'up'),
$url->setFilter($urlFilter),
[
'title' => sprintf(
$this->translatePlural(
'List %d host that is currently in UP state in host group "%s"',
'List %d hosts which are currently in UP state in host group "%s"',
$item->hosts_up
),
$item->hosts_up,
$item->display_name
)
]
);
} else {
$link = new Link(
new StateBadge(0, 'none'),
Links::hostgroup($item),
[
'title' => sprintf(
$this->translate('There are no hosts in host group "%s"'),
$item->display_name
)
]
);
}
$visual->addHtml($link);
}
public function assembleTitle($item, HtmlDocument $title, string $layout): void
{
$link = new Link(
$item->display_name,
Links::hostgroup($item),
[
'class' => 'subject',
'title' => sprintf(
$this->translate('List all hosts in the group "%s"'),
$item->display_name
)
]
);
if ($this->hasBaseFilter()) {
$link->getUrl()->setFilter($this->getBaseFilter());
}
$title->addHtml($link);
}
public function assembleCaption($item, HtmlDocument $caption, string $layout): void
{
$caption->addHtml(Text::create($item->name));
}
public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void
{
}
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
}
}

View file

@ -0,0 +1,115 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\View;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Model\Hostgroupsummary;
use Icinga\Module\Icingadb\Widget\Detail\HostStatistics;
use Icinga\Module\Icingadb\Widget\Detail\ServiceStatistics;
use ipl\Html\Attributes;
use ipl\Html\HtmlDocument;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\I18n\Translation;
use ipl\Stdlib\BaseFilter;
use ipl\Stdlib\Filter;
use ipl\Web\Widget\ItemTable\ItemTableRenderer;
use ipl\Web\Widget\Link;
/** @implements ItemTableRenderer<Hostgroupsummary> */
class HostgroupRenderer implements ItemTableRenderer
{
use Translation;
use BaseFilter;
public function assembleAttributes($item, Attributes $attributes, string $layout): void
{
$attributes->get('class')->addValue('hostgroup');
}
public function assembleVisual($item, HtmlDocument $visual, string $layout): void
{
}
public function assembleTitle($item, HtmlDocument $title, string $layout): void
{
if ($layout === 'header') {
$title->addHtml(new HtmlElement(
'span',
Attributes::create(['class' => 'subject']),
Text::create($item->display_name)
));
} else {
$link = new Link(
$item->display_name,
Links::hostgroup($item),
[
'class' => 'subject',
'title' => sprintf(
$this->translate('List all hosts in the group "%s"'),
$item->display_name
)
]
);
if ($this->hasBaseFilter()) {
$link->getUrl()->setFilter($this->getBaseFilter());
}
$title->addHtml($link);
}
}
public function assembleCaption($item, HtmlDocument $caption, string $layout): void
{
$caption->addHtml(Text::create($item->name));
}
public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void
{
// assembleExtendedInfo() is only called when $layout == header
$info->addHtml(...$this->createStatistics($item));
}
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
}
public function assembleColumns($item, HtmlDocument $columns, string $layout): void
{
[$hostStats, $serviceStats] = $this->createStatistics($item);
if ($this->hasBaseFilter()) {
$hostStats->setBaseFilter(Filter::all($hostStats->getBaseFilter(), $this->getBaseFilter()));
$serviceStats->setBaseFilter(Filter::all($serviceStats->getBaseFilter(), $this->getBaseFilter()));
}
$columns->addHtml($hostStats, $serviceStats);
}
/**
* Create statistics for the given item
*
* @param Hostgroupsummary $item
*
* @return array{0: HostStatistics, 1: ServiceStatistics}
*/
protected function createStatistics(Hostgroupsummary $item): array
{
$hostStats = (new HostStatistics($item))
->setBaseFilter(Filter::equal('hostgroup.name', $item->name));
$serviceStats = (new ServiceStatistics($item))
->setBaseFilter(Filter::equal('hostgroup.name', $item->name));
return [$hostStats, $serviceStats];
}
}

View file

@ -0,0 +1,200 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
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\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 InvalidArgumentException;
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;
/** @implements ItemRenderer<NotificationHistory> */
class NotificationRenderer implements ItemRenderer
{
use Translation;
use HostLink;
use ServiceLink;
public function assembleAttributes($item, Attributes $attributes, string $layout): void
{
$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($this->phraseForType($item->type), ucfirst($item->object_type))
));
} else {
$title->addHtml(new Link(
sprintf($this->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
}
/**
* Get a localized phrase for the given notification type
*
* @param string $type
*
* @return string
*/
protected function phraseForType(string $type): string
{
switch ($type) {
case 'acknowledgement':
return $this->translate('Problem acknowledged');
case 'custom':
return $this->translate('Custom Notification triggered');
case 'downtime_end':
return $this->translate('Downtime ended');
case 'downtime_removed':
return $this->translate('Downtime removed');
case 'downtime_start':
return $this->translate('Downtime started');
case 'flapping_end':
return $this->translate('Flapping stopped');
case 'flapping_start':
return $this->translate('Flapping started');
case 'problem':
return $this->translate('%s ran into a problem');
case 'recovery':
return $this->translate('%s recovered');
default:
throw new InvalidArgumentException(sprintf('Type %s is not a valid notification type', $type));
}
}
}

View file

@ -0,0 +1,91 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\View;
use Icinga\Module\Icingadb\Model\RedundancyGroup;
use Icinga\Module\Icingadb\Widget\DependencyNodeStatistics;
use ipl\Html\Attributes;
use ipl\Html\Html;
use ipl\Html\HtmlDocument;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\I18n\Translation;
use ipl\Web\Common\ItemRenderer;
use ipl\Web\Url;
use ipl\Web\Widget\Link;
use ipl\Web\Widget\StateBall;
use ipl\Web\Widget\TimeSince;
/** @implements ItemRenderer<RedundancyGroup> */
class RedundancyGroupRenderer implements ItemRenderer
{
use Translation;
public function assembleAttributes($item, Attributes $attributes, string $layout): void
{
$attributes->get('class')->addValue('redundancy-group');
}
public function assembleVisual($item, HtmlDocument $visual, string $layout): void
{
$ballSize = StateBall::SIZE_LARGE;
if ($layout === 'minimal' || $layout === 'header') {
$ballSize = StateBall::SIZE_BIG;
}
$stateBall = new StateBall($item->state->getStateText(), $ballSize);
$stateBall->add($item->state->getIcon());
$visual->addHtml($stateBall);
}
public function assembleCaption($item, HtmlDocument $caption, string $layout): void
{
$caption->addHtml(new DependencyNodeStatistics($item->summary));
}
public function assembleFooter($item, HtmlDocument $footer, string $layout): void
{
}
public function assembleTitle($item, HtmlDocument $title, string $layout): void
{
if ($layout === 'header') {
$subject = new HtmlElement(
'span',
Attributes::create(['class' => 'subject']),
Text::create($item->display_name)
);
} else {
$subject = new Link(
$item->display_name,
Url::fromPath('icingadb/redundancygroup', ['id' => bin2hex($item->id)]),
['class' => 'subject']
);
}
if ($item->state->failed) {
$title->addHtml(Html::sprintf(
$this->translate('%s has no working objects', '<groupname> has ...'),
$subject
));
} else {
$title->addHtml(Html::sprintf(
$this->translate('%s has working objects', '<groupname> has ...'),
$subject
));
}
}
public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void
{
$info->addHtml(new TimeSince($item->state->last_state_change->getTimestamp()));
}
public function assemble($item, string $name, HtmlDocument $element, string $layout): bool
{
return $name === 'icon-image'; // Always add the icon-image section
}
}

View file

@ -0,0 +1,43 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\View;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Model\Service;
use ipl\Html\Attributes;
use ipl\Html\Html;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Html\ValidHtml;
use ipl\Web\Widget\Link;
use ipl\Web\Widget\StateBall;
/** @extends BaseHostAndServiceRenderer<Service> */
class ServiceRenderer extends BaseHostAndServiceRenderer
{
public function assembleAttributes($item, Attributes $attributes, string $layout): void
{
$attributes->get('class')->addValue('service');
}
protected function createSubject($item, string $layout): ValidHtml
{
$service = $item->display_name;
$host = [
new StateBall($item->host->state->getStateText(), StateBall::SIZE_MEDIUM),
' ',
$item->host->display_name
];
$host = new Link($host, Links::host($item->host), ['class' => 'subject']);
if ($layout === 'header') {
$service = new HtmlElement('span', new Attributes(['class' => 'subject']), new Text($service));
} else {
$service = new Link($service, Links::service($item, $item->host), ['class' => 'subject']);
}
return Html::sprintf($this->translate('%s on %s', '<service> on <host>'), $service, $host);
}
}

View file

@ -1,54 +1,68 @@
<?php
/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemTable;
namespace Icinga\Module\Icingadb\View;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Model\ServicegroupSummary;
use ipl\Html\Attributes;
use ipl\Html\HtmlDocument;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\I18n\Translation;
use ipl\Stdlib\BaseFilter;
use ipl\Stdlib\Filter;
use ipl\Web\Common\ItemRenderer;
use ipl\Web\Url;
use ipl\Web\Widget\Link;
use ipl\Web\Widget\StateBadge;
class ServicegroupGridCell extends BaseServiceGroupItem
/** @implements ItemRenderer<ServicegroupSummary> */
class ServicegroupGridRenderer implements ItemRenderer
{
use GridCellLayout;
use Translation;
use BaseFilter;
protected $defaultAttributes = ['class' => ['group-grid-cell', 'servicegroup-grid-cell']];
public function assembleAttributes($item, Attributes $attributes, string $layout): void
{
$attributes->get('class')->addValue(['object-grid-cell', 'servicegroup']);
}
protected function createGroupBadge(): Link
public function assembleVisual($item, HtmlDocument $visual, string $layout): void
{
$url = Url::fromPath('icingadb/services/grid');
$urlFilter = Filter::all(Filter::equal('servicegroup.name', $this->item->name));
$urlFilter = Filter::all(Filter::equal('servicegroup.name', $item->name));
if ($this->item->services_critical_unhandled > 0) {
if ($item->services_critical_unhandled > 0) {
$urlFilter->add(Filter::equal('service.state.soft_state', 2))
->add(Filter::equal('service.state.is_handled', 'n'))
->add(Filter::equal('service.state.is_reachable', 'y'));
return new Link(
new StateBadge($this->item->services_critical_unhandled, 'critical'),
$link = new Link(
new StateBadge($item->services_critical_unhandled, 'critical'),
$url->setFilter($urlFilter),
[
'title' => sprintf(
$this->translatePlural(
'List %d service that is currently in CRITICAL state in service group "%s"',
'List %d services which are currently in CRITICAL state in service group "%s"',
$this->item->services_critical_unhandled
$item->services_critical_unhandled
),
$this->item->services_critical_unhandled,
$this->item->display_name
$item->services_critical_unhandled,
$item->display_name
)
]
);
} elseif ($this->item->services_critical_handled > 0) {
} elseif ($item->services_critical_handled > 0) {
$urlFilter->add(Filter::equal('service.state.soft_state', 2))
->add(Filter::any(
Filter::equal('service.state.is_handled', 'y'),
Filter::equal('service.state.is_reachable', 'n')
));
return new Link(
new StateBadge($this->item->services_critical_handled, 'critical', true),
$link = new Link(
new StateBadge($item->services_critical_handled, 'critical', true),
$url->setFilter($urlFilter),
[
'title' => sprintf(
@ -57,42 +71,42 @@ class ServicegroupGridCell extends BaseServiceGroupItem
. ' "%s"',
'List %d services which are currently in CRITICAL (Acknowledged) state in service group'
. ' "%s"',
$this->item->services_critical_handled
$item->services_critical_handled
),
$this->item->services_critical_handled,
$this->item->display_name
$item->services_critical_handled,
$item->display_name
)
]
);
} elseif ($this->item->services_warning_unhandled > 0) {
} elseif ($item->services_warning_unhandled > 0) {
$urlFilter->add(Filter::equal('service.state.soft_state', 1))
->add(Filter::equal('service.state.is_handled', 'n'))
->add(Filter::equal('service.state.is_reachable', 'y'));
return new Link(
new StateBadge($this->item->services_warning_unhandled, 'warning'),
$link = new Link(
new StateBadge($item->services_warning_unhandled, 'warning'),
$url->setFilter($urlFilter),
[
'title' => sprintf(
$this->translatePlural(
'List %d service that is currently in WARNING state in service group "%s"',
'List %d services which are currently in WARNING state in service group "%s"',
$this->item->services_warning_unhandled
$item->services_warning_unhandled
),
$this->item->services_warning_unhandled,
$this->item->display_name
$item->services_warning_unhandled,
$item->display_name
)
]
);
} elseif ($this->item->services_warning_handled > 0) {
} elseif ($item->services_warning_handled > 0) {
$urlFilter->add(Filter::equal('service.state.soft_state', 1))
->add(Filter::any(
Filter::equal('service.state.is_handled', 'y'),
Filter::equal('service.state.is_reachable', 'n')
));
return new Link(
new StateBadge($this->item->services_warning_handled, 'warning', true),
$link = new Link(
new StateBadge($item->services_warning_handled, 'warning', true),
$url->setFilter($urlFilter),
[
'title' => sprintf(
@ -101,42 +115,42 @@ class ServicegroupGridCell extends BaseServiceGroupItem
. ' "%s"',
'List %d services which are currently in WARNING (Acknowledged) state in service group'
. ' "%s"',
$this->item->services_warning_handled
$item->services_warning_handled
),
$this->item->services_warning_handled,
$this->item->display_name
$item->services_warning_handled,
$item->display_name
)
]
);
} elseif ($this->item->services_unknown_unhandled > 0) {
} elseif ($item->services_unknown_unhandled > 0) {
$urlFilter->add(Filter::equal('service.state.soft_state', 3))
->add(Filter::equal('service.state.is_handled', 'n'))
->add(Filter::equal('service.state.is_reachable', 'y'));
return new Link(
new StateBadge($this->item->services_unknown_unhandled, 'unknown'),
$link = new Link(
new StateBadge($item->services_unknown_unhandled, 'unknown'),
$url->setFilter($urlFilter),
[
'title' => sprintf(
$this->translatePlural(
'List %d service that is currently in UNKNOWN state in service group "%s"',
'List %d services which are currently in UNKNOWN state in service group "%s"',
$this->item->services_unknown_unhandled
$item->services_unknown_unhandled
),
$this->item->services_unknown_unhandled,
$this->item->display_name
$item->services_unknown_unhandled,
$item->display_name
)
]
);
} elseif ($this->item->services_unknown_handled > 0) {
} elseif ($item->services_unknown_handled > 0) {
$urlFilter->add(Filter::equal('service.state.soft_state', 3))
->add(Filter::any(
Filter::equal('service.state.is_handled', 'y'),
Filter::equal('service.state.is_reachable', 'n')
));
return new Link(
new StateBadge($this->item->services_unknown_handled, 'unknown', true),
$link = new Link(
new StateBadge($item->services_unknown_handled, 'unknown', true),
$url->setFilter($urlFilter),
[
'title' => sprintf(
@ -145,60 +159,101 @@ class ServicegroupGridCell extends BaseServiceGroupItem
. ' "%s"',
'List %d services which are currently in UNKNOWN (Acknowledged) state in service group'
. ' "%s"',
$this->item->services_unknown_handled
$item->services_unknown_handled
),
$this->item->services_unknown_handled,
$this->item->display_name
$item->services_unknown_handled,
$item->display_name
)
]
);
} elseif ($this->item->services_pending > 0) {
} elseif ($item->services_pending > 0) {
$urlFilter->add(Filter::equal('service.state.soft_state', 99));
return new Link(
new StateBadge($this->item->services_pending, 'pending'),
$link = new Link(
new StateBadge($item->services_pending, 'pending'),
$url->setFilter($urlFilter),
[
'title' => sprintf(
$this->translatePlural(
'List %d service that is currently in PENDING state in service group "%s"',
'List %d services which are currently in PENDING state in service group "%s"',
$this->item->services_pending
$item->services_pending
),
$this->item->services_pending,
$this->item->display_name
$item->services_pending,
$item->display_name
)
]
);
} elseif ($this->item->services_ok > 0) {
} elseif ($item->services_ok > 0) {
$urlFilter->add(Filter::equal('service.state.soft_state', 0));
return new Link(
new StateBadge($this->item->services_ok, 'ok'),
$link = new Link(
new StateBadge($item->services_ok, 'ok'),
$url->setFilter($urlFilter),
[
'title' => sprintf(
$this->translatePlural(
'List %d service that is currently in OK state in service group "%s"',
'List %d services which are currently in OK state in service group "%s"',
$this->item->services_ok
$item->services_ok
),
$this->item->services_ok,
$this->item->display_name
$item->services_ok,
$item->display_name
)
]
);
} else {
$link = new Link(
new StateBadge(0, 'none'),
Links::servicegroup($item),
[
'title' => sprintf(
$this->translate('There are no services in service group "%s"'),
$item->display_name
)
]
);
}
return new Link(
new StateBadge(0, 'none'),
$url,
$visual->addHtml($link);
}
public function assembleTitle($item, HtmlDocument $title, string $layout): void
{
$link = new Link(
$item->display_name,
Links::servicegroup($item),
[
'class' => 'subject',
'title' => sprintf(
$this->translate('There are no services in service group "%s"'),
$this->item->display_name
$this->translate('List all services in the group "%s"'),
$item->display_name
)
]
);
if ($this->hasBaseFilter()) {
$link->getUrl()->setFilter($this->getBaseFilter());
}
$title->addHtml($link);
}
public function assembleCaption($item, HtmlDocument $caption, string $layout): void
{
$caption->addHtml(Text::create($item->name));
}
public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void
{
}
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
}
}

View file

@ -0,0 +1,107 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\View;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Model\ServicegroupSummary;
use Icinga\Module\Icingadb\Widget\Detail\ServiceStatistics;
use ipl\Html\Attributes;
use ipl\Html\HtmlDocument;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\I18n\Translation;
use ipl\Stdlib\BaseFilter;
use ipl\Stdlib\Filter;
use ipl\Web\Widget\ItemTable\ItemTableRenderer;
use ipl\Web\Widget\Link;
/** @implements ItemTableRenderer<ServicegroupSummary> */
class ServicegroupRenderer implements ItemTableRenderer
{
use Translation;
use BaseFilter;
public function assembleAttributes($item, Attributes $attributes, string $layout): void
{
$attributes->get('class')->addValue('servicegroup');
}
public function assembleVisual($item, HtmlDocument $visual, string $layout): void
{
}
public function assembleTitle($item, HtmlDocument $title, string $layout): void
{
if ($layout === 'header') {
$title->addHtml(new HtmlElement(
'span',
Attributes::create(['class' => 'subject']),
Text::create($item->display_name)
));
} else {
$link = new Link(
$item->display_name,
Links::servicegroup($item),
[
'class' => 'subject',
'title' => sprintf(
$this->translate('List all services in the group "%s"'),
$item->display_name
)
]
);
if ($this->hasBaseFilter()) {
$link->getUrl()->setFilter($this->getBaseFilter());
}
$title->addHtml($link);
}
}
public function assembleCaption($item, HtmlDocument $caption, string $layout): void
{
$caption->addHtml(Text::create($item->name));
}
public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void
{
// assembleExtendedInfo() is only called when $layout == header
$info->addHtml($this->createStatistics($item));
}
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
}
public function assembleColumns($item, HtmlDocument $columns, string $layout): void
{
$serviceStats = $this->createStatistics($item);
if ($this->hasBaseFilter()) {
$serviceStats->setBaseFilter(Filter::all($serviceStats->getBaseFilter(), $this->getBaseFilter()));
}
$columns->addHtml($serviceStats);
}
/**
* Create statistics for the given item
*
* @param ServicegroupSummary $item
*
* @return ServiceStatistics
*/
protected function createStatistics(ServicegroupSummary $item): ServiceStatistics
{
return (new ServiceStatistics($item))
->setBaseFilter(Filter::equal('servicegroup.name', $item->name));
}
}

View file

@ -0,0 +1,63 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\View;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Model\User;
use ipl\Html\Attributes;
use ipl\Html\HtmlDocument;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Web\Common\ItemRenderer;
use ipl\Web\Widget\Link;
/** @implements ItemRenderer<User> */
class UserRenderer implements ItemRenderer
{
public function assembleAttributes($item, Attributes $attributes, string $layout): void
{
$attributes->get('class')->addValue('user');
}
public function assembleVisual($item, HtmlDocument $visual, string $layout): void
{
$visual->addHtml(new HtmlElement(
'div',
Attributes::create(['class' => 'user-ball']),
Text::create($item->display_name[0])
));
}
public function assembleTitle($item, HtmlDocument $title, string $layout): void
{
if ($layout === 'header') {
$title->addHtml(new HtmlElement(
'span',
Attributes::create(['class' => 'subject']),
Text::create($item->display_name)
));
} else {
$title->addHtml(new Link($item->display_name, Links::user($item), ['class' => 'subject']));
}
}
public function assembleCaption($item, HtmlDocument $caption, string $layout): void
{
$caption->addHtml(Text::create($item->name));
}
public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void
{
}
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
}
}

View file

@ -0,0 +1,63 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\View;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Model\Usergroup;
use ipl\Html\Attributes;
use ipl\Html\HtmlDocument;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Web\Common\ItemRenderer;
use ipl\Web\Widget\Link;
/** @implements ItemRenderer<Usergroup> */
class UsergroupRenderer implements ItemRenderer
{
public function assembleAttributes($item, Attributes $attributes, string $layout): void
{
$attributes->get('class')->addValue('user-group');
}
public function assembleVisual($item, HtmlDocument $visual, string $layout): void
{
$visual->addHtml(new HtmlElement(
'div',
Attributes::create(['class' => 'usergroup-ball']),
Text::create($item->display_name[0])
));
}
public function assembleTitle($item, HtmlDocument $title, string $layout): void
{
if ($layout === 'header') {
$title->addHtml(new HtmlElement(
'span',
Attributes::create(['class' => 'subject']),
Text::create($item->display_name)
));
} else {
$title->addHtml(new Link($item->display_name, Links::usergroup($item), ['class' => 'subject']));
}
}
public function assembleCaption($item, HtmlDocument $caption, string $layout): void
{
$caption->addHtml(Text::create($item->name));
}
public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void
{
}
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
}
}

View file

@ -37,8 +37,6 @@ use ipl\Html\ValidHtml;
use ipl\Orm\Query;
use ipl\Orm\UnionQuery;
use ipl\Stdlib\Filter;
use ipl\Web\Common\BaseItemList;
use ipl\Web\Common\BaseItemTable;
use ipl\Web\Compat\CompatController;
use ipl\Web\Control\LimitControl;
use ipl\Web\Control\PaginationControl;
@ -486,9 +484,7 @@ class Controller extends CompatController
protected function addContent(ValidHtml $content)
{
if ($content instanceof BaseItemList || $content instanceof BaseItemTable) {
$this->content->getAttributes()->add('class', 'full-width');
} elseif ($content instanceof StateItemTable) {
if ($content instanceof StateItemTable) {
$this->content->getAttributes()->add('class', 'full-height');
}

View file

@ -10,12 +10,13 @@ use Icinga\Module\Icingadb\Common\Auth;
use Icinga\Module\Icingadb\Common\Database;
use Icinga\Module\Icingadb\Common\HostLink;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Widget\MarkdownText;
use Icinga\Module\Icingadb\Common\ServiceLink;
use Icinga\Module\Icingadb\Forms\Command\Object\DeleteDowntimeForm;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\Widget\ItemList\DowntimeList;
use Icinga\Module\Icingadb\Widget\ShowMore;
use ipl\Web\Url;
use ipl\Web\Widget\EmptyState;
use ipl\Web\Widget\HorizontalKeyValue;
use ipl\Html\BaseHtmlElement;
@ -180,7 +181,7 @@ class DowntimeDetail extends BaseHtmlElement
if ($children->hasResult()) {
$this->addHtml(
new HtmlElement('h2', null, Text::create(t('Children'))),
new DowntimeList($children),
new ObjectList($children),
(new ShowMore($children, Links::downtimes()->setQueryString(
QueryString::render(Filter::any(
Filter::equal('downtime.parent.name', $this->downtime->name),

View file

@ -15,6 +15,7 @@ use Icinga\Module\Icingadb\Common\HostStates;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Common\TicketLinks;
use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Widget\MarkdownText;
use Icinga\Module\Icingadb\Common\ServiceLink;
use Icinga\Module\Icingadb\Common\ServiceStates;
@ -27,10 +28,10 @@ use Icinga\Module\Icingadb\Model\NotificationHistory;
use Icinga\Module\Icingadb\Model\StateHistory;
use Icinga\Module\Icingadb\Util\PluginOutput;
use Icinga\Module\Icingadb\Widget\ShowMore;
use ipl\Web\Url;
use ipl\Web\Widget\CopyToClipboard;
use ipl\Web\Widget\EmptyState;
use ipl\Web\Widget\HorizontalKeyValue;
use Icinga\Module\Icingadb\Widget\ItemTable\UserTable;
use Icinga\Module\Icingadb\Widget\PluginOutputContainer;
use ipl\Html\BaseHtmlElement;
use ipl\Html\FormattedString;
@ -169,7 +170,7 @@ class EventDetail extends BaseHtmlElement
$users = $users->execute();
/** @var ResultSet $users */
$notifiedUsers[] = new UserTable($users);
$notifiedUsers[] = new ObjectList($users);
$notifiedUsers[] = (new ShowMore(
$users,
Links::users()->addParams(['notification_history.id' => bin2hex($notification->id)]),

View file

@ -28,7 +28,8 @@ use Icinga\Module\Icingadb\Model\Service;
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\DependencyNodeList;
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,7 @@ class ObjectDetail extends BaseHtmlElement
$content = [Html::tag('h2', t('Comments'))];
if ($comments->hasResult()) {
$content[] = (new CommentList($comments))->setObjectLinkDisabled()->setTicketLinkEnabled();
$content[] = new TicketLinkObjectList($comments);
$content[] = (new ShowMore($comments, $link))->setBaseTarget('_next');
} else {
$content[] = new EmptyState(t('No comments created.'));
@ -282,7 +283,7 @@ class ObjectDetail extends BaseHtmlElement
$content = [Html::tag('h2', t('Downtimes'))];
if ($downtimes->hasResult()) {
$content[] = (new DowntimeList($downtimes))->setObjectLinkDisabled()->setTicketLinkEnabled();
$content[] = new TicketLinkObjectList($downtimes);
$content[] = (new ShowMore($downtimes, $link))->setBaseTarget('_next');
} else {
$content[] = new EmptyState(t('No downtimes scheduled.'));
@ -668,7 +669,7 @@ class ObjectDetail extends BaseHtmlElement
return [
HtmlElement::create('h2', null, Text::create(t('Root Problems'))),
(new DependencyNodeList($rootProblems))->setEmptyStateMessage(
(new ObjectList($rootProblems))->setEmptyStateMessage(
t('You are not authorized to view these objects.')
)
];
@ -744,7 +745,7 @@ class ObjectDetail extends BaseHtmlElement
return [
HtmlElement::create('h2', null, Text::create(t('Affected Objects'))),
(new DependencyNodeList($affectedObjects))->setEmptyStateMessage(
(new ObjectList($affectedObjects))->setEmptyStateMessage(
t('You are not authorized to view these objects.')
)
];

View file

@ -1,26 +1,48 @@
<?php
/* Icinga DB Web | (c) 2024 Icinga GmbH | GPLv2 */
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\Detail;
use ipl\Html\Attributes;
use Icinga\Exception\NotImplementedError;
use Icinga\Module\Icingadb\Model\Comment;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\Model\History;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Model\Hostgroupsummary;
use Icinga\Module\Icingadb\Model\RedundancyGroup;
use Icinga\Module\Icingadb\Model\Service;
use Icinga\Module\Icingadb\Model\ServicegroupSummary;
use Icinga\Module\Icingadb\Model\User;
use Icinga\Module\Icingadb\Model\Usergroup;
use Icinga\Module\Icingadb\View\CommentRenderer;
use Icinga\Module\Icingadb\View\DowntimeRenderer;
use Icinga\Module\Icingadb\View\EventRenderer;
use Icinga\Module\Icingadb\View\HostgroupRenderer;
use Icinga\Module\Icingadb\View\HostRenderer;
use Icinga\Module\Icingadb\View\RedundancyGroupRenderer;
use Icinga\Module\Icingadb\View\ServicegroupRenderer;
use Icinga\Module\Icingadb\View\ServiceRenderer;
use Icinga\Module\Icingadb\View\UsergroupRenderer;
use Icinga\Module\Icingadb\View\UserRenderer;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\I18n\Translation;
use ipl\Orm\Model;
use ipl\Web\Widget\StateBall;
use ipl\Web\Widget\TimeSince;
use ipl\Web\Layout\HeaderItemLayout;
use ipl\Web\Layout\ItemLayout;
abstract class ObjectHeader extends BaseHtmlElement
/**
* ObjectHeader
*
* Create a header for icingadb object
*
* @phpstan-type _PART1 = RedundancyGroup|Service|Host|Usergroup|User|Comment|Downtime|History
* @phpstan-type _PART2 = Hostgroupsummary|ServicegroupSummary
*
* @template Item of _PART1|_PART2
*/
class ObjectHeader extends BaseHtmlElement
{
use Translation;
/** @var array<string, mixed> */
protected $baseAttributes = ['class' => 'object-header'];
/** @var Model The associated object */
/** @var Item */
protected $object;
protected $tag = 'div';
@ -28,118 +50,70 @@ abstract class ObjectHeader extends BaseHtmlElement
/**
* Create a new object header
*
* @param Model $object
* @param Item $object
*/
public function __construct(Model $object)
{
$this->object = $object;
$this->addAttributes($this->baseAttributes);
$this->init();
}
abstract protected function assembleHeader(BaseHtmlElement $header): void;
abstract protected function assembleMain(BaseHtmlElement $main): void;
protected function assembleCaption(BaseHtmlElement $caption): void
{
}
protected function assembleTitle(BaseHtmlElement $title): void
{
}
protected function assembleVisual(BaseHtmlElement $visual): void
{
}
protected function getStateBallSize(): string
{
return StateBall::SIZE_BIG;
}
protected function createCaption(): BaseHtmlElement
{
$caption = new HtmlElement('section', Attributes::create(['class' => 'caption']));
$this->assembleCaption($caption);
return $caption;
}
protected function createHeader(): BaseHtmlElement
{
$header = new HtmlElement('header');
$this->assembleHeader($header);
return $header;
}
protected function createMain(): BaseHtmlElement
{
$main = new HtmlElement('div', Attributes::create(['class' => 'main']));
$this->assembleMain($main);
return $main;
}
protected function createTimestamp(): ?BaseHtmlElement
{
//TODO: add support for host/service
return new TimeSince($this->object->state->last_state_change->getTimestamp());
}
protected function createSubject(): BaseHtmlElement
{
return new HtmlElement(
'span',
Attributes::create(['class' => 'subject']),
Text::create($this->object->display_name)
);
}
protected function createTitle(): BaseHtmlElement
{
$title = new HtmlElement('div', Attributes::create(['class' => 'title']));
$this->assembleTitle($title);
return $title;
}
/**
* @return ?BaseHtmlElement
* @throws NotImplementedError When the object type is not supported
*/
protected function createVisual(): ?BaseHtmlElement
{
$visual = new HtmlElement('div', Attributes::create(['class' => 'visual']));
$this->assembleVisual($visual);
if ($visual->isEmpty()) {
return null;
}
return $visual;
}
/**
* Initialize the list item
*
* If you want to adjust the object header after construction, override this method.
*/
protected function init(): void
{
}
protected function assemble(): void
{
$this->add([
$this->createVisual(),
$this->createMain()
]);
switch (true) {
case $this->object instanceof RedundancyGroup:
$renderer = new RedundancyGroupRenderer();
break;
case $this->object instanceof Service:
$renderer = new ServiceRenderer();
break;
case $this->object instanceof Host:
$renderer = new HostRenderer();
break;
case $this->object instanceof Usergroup:
$renderer = new UsergroupRenderer();
break;
case $this->object instanceof User:
$renderer = new UserRenderer();
break;
case $this->object instanceof Comment:
$renderer = (new CommentRenderer())->setTicketLinkDisabled();
break;
case $this->object instanceof Downtime:
$renderer = (new DowntimeRenderer())->setTicketLinkDisabled();
break;
case $this->object instanceof History:
$renderer = new EventRenderer();
break;
case $this->object instanceof Hostgroupsummary:
$renderer = new HostgroupRenderer();
break;
case $this->object instanceof ServicegroupSummary:
$renderer = new ServicegroupRenderer();
break;
default:
throw new NotImplementedError('Not implemented');
}
$layout = new HeaderItemLayout($this->object, $renderer);
if (isset($this->object->icon_image->icon_image)) {
$layout->after(ItemLayout::VISUAL, 'icon-image');
}
$this->addAttributes($layout->getAttributes());
$this->addHtml($layout);
}
}

View file

@ -10,7 +10,7 @@ use Icinga\Module\Icingadb\Model\DependencyNode;
use Icinga\Module\Icingadb\Model\RedundancyGroup;
use Icinga\Module\Icingadb\Model\UnreachableParent;
use Icinga\Module\Icingadb\Redis\VolatileStateResults;
use Icinga\Module\Icingadb\Widget\ItemList\DependencyNodeList;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook;
use Icinga\Module\Icingadb\Widget\ShowMore;
use ipl\Html\BaseHtmlElement;
@ -96,7 +96,7 @@ class RedundancyGroupDetail extends BaseHtmlElement
return [
HtmlElement::create('h2', null, Text::create($this->translate('Root Problems'))),
(new DependencyNodeList($rootProblems))->setEmptyStateMessage(
(new ObjectList($rootProblems))->setEmptyStateMessage(
$this->translate('You are not authorized to view these objects.')
)
];
@ -129,7 +129,7 @@ class RedundancyGroupDetail extends BaseHtmlElement
return [
HtmlElement::create('h2', null, Text::create($this->translate('Group Members'))),
(new DependencyNodeList($members))
(new ObjectList($members))
->setEmptyStateMessage($this->translate('You are not authorized to view these objects.')),
(new ShowMore($members, Url::fromPath('icingadb/redundancygroup/members', ['id' => $this->group->id])))
->setBaseTarget('_self')

View file

@ -1,71 +0,0 @@
<?php
/* Icinga DB Web | (c) 2024 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\Detail;
use Icinga\Module\Icingadb\Model\RedundancyGroup;
use Icinga\Module\Icingadb\Model\RedundancyGroupSummary;
use Icinga\Module\Icingadb\Widget\DependencyNodeStatistics;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Web\Widget\StateBall;
/**
* @property RedundancyGroup $object
*/
class RedundancyGroupHeader extends ObjectHeader
{
/** @var RedundancyGroupSummary */
protected $summary;
public function __construct(RedundancyGroup $object, RedundancyGroupSummary $summary)
{
$this->summary = $summary;
parent::__construct($object);
}
protected function assembleVisual(BaseHtmlElement $visual): void
{
$stateBall = new StateBall($this->object->state->getStateText(), $this->getStateBallSize());
$stateBall->add($this->object->state->getIcon());
$visual->addHtml($stateBall);
}
protected function assembleTitle(BaseHtmlElement $title): void
{
$subject = $this->createSubject();
if ($this->object->state->failed) {
$title->addHtml(Html::sprintf(
$this->translate('%s has no working objects', '<groupname> has ...'),
$subject
));
} else {
$title->addHtml(Html::sprintf(
$this->translate('%s has working objects', '<groupname> has ...'),
$subject
));
}
}
protected function createStatistics(): BaseHtmlElement
{
return new DependencyNodeStatistics($this->summary);
}
protected function assembleHeader(BaseHtmlElement $header): void
{
$header->add($this->createTitle());
$header->add($this->createStatistics());
$header->add($this->createTimestamp());
}
protected function assembleMain(BaseHtmlElement $main): void
{
$main->add($this->createHeader());
}
}

View file

@ -9,11 +9,12 @@ use Icinga\Module\Icingadb\Common\Database;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook;
use Icinga\Module\Icingadb\Model\User;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Widget\ShowMore;
use ipl\Html\Attributes;
use ipl\Web\Url;
use ipl\Web\Widget\EmptyState;
use ipl\Web\Widget\HorizontalKeyValue;
use Icinga\Module\Icingadb\Widget\ItemTable\UsergroupTable;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
@ -89,7 +90,7 @@ class UserDetail extends BaseHtmlElement
return [
new HtmlElement('h2', null, Text::create(t('Groups'))),
new UsergroupTable($userGroups),
new ObjectList($userGroups),
$showMoreLink
];
}

View file

@ -9,12 +9,13 @@ use Icinga\Module\Icingadb\Common\Database;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook;
use Icinga\Module\Icingadb\Model\Usergroup;
use Icinga\Module\Icingadb\Widget\ItemTable\UserTable;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Widget\ShowMore;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Web\Url;
use ipl\Web\Widget\EmptyState;
use ipl\Web\Widget\HorizontalKeyValue;
@ -74,7 +75,7 @@ class UsergroupDetail extends BaseHtmlElement
return [
new HtmlElement('h2', null, Text::create(t('Users'))),
new UserTable($users),
new ObjectList($users),
$showMoreLink
];
}

View file

@ -1,131 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\NoSubjectLink;
use Icinga\Module\Icingadb\Common\ObjectLinkDisabled;
use Icinga\Module\Icingadb\Common\TicketLinks;
use ipl\Html\Html;
use Icinga\Module\Icingadb\Common\HostLink;
use Icinga\Module\Icingadb\Common\Icons;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Widget\MarkdownLine;
use Icinga\Module\Icingadb\Common\ServiceLink;
use Icinga\Module\Icingadb\Model\Comment;
use ipl\Html\FormattedString;
use ipl\Web\Common\BaseListItem;
use ipl\Web\Widget\TimeAgo;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Stdlib\Filter;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\Link;
use ipl\Web\Widget\TimeUntil;
/**
* Comment item of a comment list. Represents one database row.
*
* @property Comment $item
* @property CommentList $list
*/
abstract class BaseCommentListItem extends BaseListItem
{
use HostLink;
use ServiceLink;
use NoSubjectLink;
use ObjectLinkDisabled;
use TicketLinks;
protected function assembleCaption(BaseHtmlElement $caption): void
{
$markdownLine = new MarkdownLine($this->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', '<username>..') : t('%s commented', '<username>..'),
$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());
}
}

View file

@ -1,216 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Date\DateFormatter;
use Icinga\Module\Icingadb\Common\HostLink;
use Icinga\Module\Icingadb\Common\Icons;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Common\NoSubjectLink;
use Icinga\Module\Icingadb\Common\ObjectLinkDisabled;
use Icinga\Module\Icingadb\Common\ServiceLink;
use Icinga\Module\Icingadb\Common\TicketLinks;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\Widget\MarkdownLine;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Html\HtmlElement;
use ipl\Html\TemplateString;
use ipl\Html\Text;
use ipl\Stdlib\Filter;
use ipl\Web\Common\BaseListItem;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\Link;
/**
* Downtime item of a downtime list. Represents one database row.
*
* @property Downtime $item
* @property DowntimeList $list
*/
abstract class BaseDowntimeListItem extends BaseListItem
{
use HostLink;
use ServiceLink;
use NoSubjectLink;
use ObjectLinkDisabled;
use TicketLinks;
/** @var int Current Time */
protected $currentTime;
/** @var int Duration */
protected $duration;
/** @var int Downtime end time */
protected $endTime;
/** @var bool Whether the downtime is active */
protected $isActive;
/** @var int Downtime start time */
protected $startTime;
protected function init(): void
{
if (
isset($this->item->start_time, $this->item->end_time)
&& $this->item->is_flexible
&& $this->item->is_in_effect
) {
$this->startTime = $this->item->start_time->getTimestamp();
$this->endTime = $this->item->end_time->getTimestamp();
} else {
$this->startTime = $this->item->scheduled_start_time->getTimestamp();
$this->endTime = $this->item->scheduled_end_time->getTimestamp();
}
$this->currentTime = time();
$this->isActive = $this->item->is_in_effect
|| $this->item->is_flexible && $this->item->scheduled_start_time->getTimestamp() <= $this->currentTime;
$until = ($this->isActive ? $this->endTime : $this->startTime) - $this->currentTime;
$this->duration = explode(' ', DateFormatter::formatDuration(
$until <= 3600 ? $until : $until + (3600 - ((int) $until % 3600))
), 2)[0];
$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());
$this->setTicketLinkEnabled($this->list->getTicketLinkEnabled());
if ($this->item->is_in_effect) {
$this->getAttributes()->add('class', 'in-effect');
}
}
protected function createProgress(): BaseHtmlElement
{
return new HtmlElement(
'div',
Attributes::create([
'class' => 'progress',
'data-animate-progress' => true,
'data-start-time' => $this->startTime,
'data-end-time' => $this->endTime
]),
new HtmlElement(
'div',
Attributes::create(['class' => 'bar'])
)
);
}
protected function assembleCaption(BaseHtmlElement $caption): void
{
$markdownLine = new MarkdownLine($this->createTicketLinks($this->item->comment));
$caption->getAttributes()->add($markdownLine->getAttributes());
$caption->addHtml(
new HtmlElement(
'span',
null,
new Icon(Icons::USER),
Text::create($this->item->author)
),
Text::create(': ')
)->addFrom($markdownLine);
}
protected function assembleTitle(BaseHtmlElement $title): void
{
if ($this->getObjectLinkDisabled()) {
$link = null;
} elseif ($this->item->object_type === 'host') {
$link = $this->createHostLink($this->item->host, true);
} else {
$link = $this->createServiceLink($this->item->service, $this->item->service->host, true);
}
if ($this->item->is_flexible) {
if ($link !== null) {
$template = t('{{#link}}Flexible Downtime{{/link}} for %s');
} else {
$template = t('Flexible Downtime');
}
} else {
if ($link !== null) {
$template = t('{{#link}}Fixed Downtime{{/link}} for %s');
} else {
$template = t('Fixed Downtime');
}
}
if ($this->getNoSubjectLink()) {
if ($link === null) {
$title->addHtml(HtmlElement::create('span', [ 'class' => 'subject'], $template));
} else {
$title->addHtml(TemplateString::create(
$template,
['link' => HtmlElement::create('span', [ 'class' => 'subject'])],
$link
));
}
} else {
if ($link === null) {
$title->addHtml(new Link($template, Links::downtime($this->item)));
} else {
$title->addHtml(TemplateString::create(
$template,
['link' => new Link('', Links::downtime($this->item))],
$link
));
}
}
}
protected function assembleVisual(BaseHtmlElement $visual): void
{
$dateTime = DateFormatter::formatDateTime($this->endTime);
if ($this->isActive) {
$visual->addHtml(Html::sprintf(
t('%s left', '<timespan>..'),
Html::tag(
'strong',
Html::tag(
'time',
[
'datetime' => $dateTime,
'title' => $dateTime
],
$this->duration
)
)
));
} else {
$visual->addHtml(Html::sprintf(
t('in %s', '..<timespan>'),
Html::tag('strong', $this->duration)
));
}
}
protected function createTimestamp(): ?BaseHtmlElement
{
$dateTime = DateFormatter::formatDateTime($this->isActive ? $this->endTime : $this->startTime);
return Html::tag(
'time',
[
'datetime' => $dateTime,
'title' => $dateTime
],
sprintf(
$this->isActive
? t('expires in %s', '..<timespan>')
: t('starts in %s', '..<timespan>'),
$this->duration
)
);
}
}

View file

@ -1,407 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Date\DateFormatter;
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\TicketLinks;
use Icinga\Module\Icingadb\Widget\MarkdownLine;
use Icinga\Module\Icingadb\Common\ServiceLink;
use Icinga\Module\Icingadb\Common\ServiceStates;
use Icinga\Module\Icingadb\Model\History;
use Icinga\Module\Icingadb\Util\PluginOutput;
use Icinga\Module\Icingadb\Widget\CheckAttempt;
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\StateBall;
use ipl\Web\Widget\TimeAgo;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\Link;
abstract class BaseHistoryListItem extends BaseListItem
{
use HostLink;
use NoSubjectLink;
use ServiceLink;
use TicketLinks;
/** @var History */
protected $item;
/** @var HistoryList */
protected $list;
protected function init(): void
{
$this->setNoSubjectLink($this->list->getNoSubjectLink());
$this->setTicketLinkEnabled($this->list->getTicketLinkEnabled());
$this->list->addDetailFilterAttribute($this, Filter::equal('id', bin2hex($this->item->id)));
}
abstract protected function getStateBallSize(): string;
protected function assembleCaption(BaseHtmlElement $caption): void
{
switch ($this->item->event_type) {
case 'comment_add':
case 'comment_remove':
$markdownLine = new MarkdownLine($this->createTicketLinks($this->item->comment->comment));
$caption->getAttributes()->add($markdownLine->getAttributes());
$caption->add([
new Icon(Icons::USER),
$this->item->comment->author,
': '
])->addFrom($markdownLine);
break;
case 'downtime_end':
case 'downtime_start':
$markdownLine = new MarkdownLine($this->createTicketLinks($this->item->downtime->comment));
$caption->getAttributes()->add($markdownLine->getAttributes());
$caption->add([
new Icon(Icons::USER),
$this->item->downtime->author,
': '
])->addFrom($markdownLine);
break;
case 'flapping_start':
$caption
->add(sprintf(
t('State Change Rate: %.2f%%; Start Threshold: %.2f%%'),
$this->item->flapping->percent_state_change_start,
$this->item->flapping->flapping_threshold_high
))
->getAttributes()
->add('class', 'plugin-output');
break;
case 'flapping_end':
$caption
->add(sprintf(
t('State Change Rate: %.2f%%; End Threshold: %.2f%%; Flapping for %s'),
$this->item->flapping->percent_state_change_end,
$this->item->flapping->flapping_threshold_low,
isset($this->item->flapping->end_time)
? DateFormatter::formatDuration(
$this->item->flapping->end_time->getTimestamp()
- $this->item->flapping->start_time->getTimestamp()
)
: t('n. a.')
))
->getAttributes()
->add('class', 'plugin-output');
break;
case 'ack_clear':
case 'ack_set':
if (! isset($this->item->acknowledgement->comment) && ! isset($this->item->acknowledgement->author)) {
$caption->addHtml(new EmptyState(
t('This acknowledgement was set before Icinga DB history recording')
));
} else {
$markdownLine = new MarkdownLine($this->createTicketLinks($this->item->acknowledgement->comment));
$caption->getAttributes()->add($markdownLine->getAttributes());
$caption->add([
new Icon(Icons::USER),
$this->item->acknowledgement->author,
': '
])->addFrom($markdownLine);
}
break;
case 'notification':
if (! empty($this->item->notification->author)) {
$caption->add([
new Icon(Icons::USER),
$this->item->notification->author,
': ',
$this->item->notification->text
]);
} else {
$commandName = $this->item->object_type === 'host'
? $this->item->host->checkcommand_name
: $this->item->service->checkcommand_name;
if (isset($commandName)) {
if (empty($this->item->notification->text)) {
$caption->addHtml(new EmptyState(t('Output unavailable.')));
} else {
$caption->addHtml(new PluginOutputContainer(
(new PluginOutput($this->item->notification->text))
->setCommandName($commandName)
));
}
} else {
$caption->addHtml(new EmptyState(t('Waiting for Icinga DB to synchronize the config.')));
}
}
break;
case 'state_change':
$commandName = $this->item->object_type === 'host'
? $this->item->host->checkcommand_name
: $this->item->service->checkcommand_name;
if (isset($commandName)) {
if (empty($this->item->state->output)) {
$caption->addHtml(new EmptyState(t('Output unavailable.')));
} else {
$caption->addHtml(new PluginOutputContainer(
(new PluginOutput($this->item->state->output))
->setCommandName($commandName)
));
}
} else {
$caption->addHtml(new EmptyState(t('Waiting for Icinga DB to synchronize the config.')));
}
break;
}
}
protected function assembleVisual(BaseHtmlElement $visual): void
{
switch ($this->item->event_type) {
case 'comment_add':
$visual->addHtml(HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]],
new Icon(Icons::COMMENT)
));
break;
case 'comment_remove':
case 'downtime_end':
case 'ack_clear':
$visual->addHtml(HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]],
new Icon(Icons::REMOVE)
));
break;
case 'downtime_start':
$visual->addHtml(HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]],
new Icon(Icons::IN_DOWNTIME)
));
break;
case 'ack_set':
$visual->addHtml(HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]],
new Icon(Icons::IS_ACKNOWLEDGED)
));
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 'notification':
$visual->addHtml(HtmlElement::create(
'div',
['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]],
new Icon(Icons::NOTIFICATION)
));
break;
case 'state_change':
if ($this->item->state->state_type === 'soft') {
$stateType = 'soft_state';
$previousStateType = 'previous_soft_state';
if ($this->item->state->previous_soft_state === 0) {
$previousStateType = 'hard_state';
}
$visual->addHtml(new CheckAttempt(
(int) $this->item->state->check_attempt,
(int) $this->item->state->max_check_attempts
));
} else {
$stateType = 'hard_state';
$previousStateType = 'previous_hard_state';
if ($this->item->state->hard_state === $this->item->state->previous_hard_state) {
$previousStateType = 'previous_soft_state';
}
}
if ($this->item->object_type === 'host') {
$state = HostStates::text($this->item->state->$stateType);
$previousState = HostStates::text($this->item->state->$previousStateType);
} else {
$state = ServiceStates::text($this->item->state->$stateType);
$previousState = ServiceStates::text($this->item->state->$previousStateType);
}
$stateChange = new StateChange($state, $previousState);
if ($stateType === 'soft_state') {
$stateChange->setCurrentStateBallSize(StateBall::SIZE_MEDIUM_LARGE);
}
if ($previousStateType === 'previous_soft_state') {
$stateChange->setPreviousStateBallSize(StateBall::SIZE_MEDIUM_LARGE);
if ($stateType === 'soft_state') {
$visual->getAttributes()->add('class', 'small-state-change');
}
}
$visual->prependHtml($stateChange);
break;
}
}
protected function assembleTitle(BaseHtmlElement $title): void
{
switch ($this->item->event_type) {
case 'comment_add':
$subjectLabel = t('Comment added');
break;
case 'comment_remove':
if (! empty($this->item->comment->removed_by)) {
if ($this->item->comment->removed_by !== $this->item->comment->author) {
$subjectLabel = sprintf(
t('Comment removed by %s', '..<username>'),
$this->item->comment->removed_by
);
} else {
$subjectLabel = t('Comment removed by author');
}
} elseif (isset($this->item->comment->expire_time)) {
$subjectLabel = t('Comment expired');
} else {
$subjectLabel = t('Comment removed');
}
break;
case 'downtime_end':
if (! empty($this->item->downtime->cancelled_by)) {
if ($this->item->downtime->cancelled_by !== $this->item->downtime->author) {
$subjectLabel = sprintf(
t('Downtime cancelled by %s', '..<username>'),
$this->item->downtime->cancelled_by
);
} else {
$subjectLabel = t('Downtime cancelled by author');
}
} elseif ($this->item->downtime->has_been_cancelled === 'y') {
$subjectLabel = t('Downtime cancelled');
} else {
$subjectLabel = t('Downtime ended');
}
break;
case 'downtime_start':
$subjectLabel = t('Downtime started');
break;
case 'flapping_start':
$subjectLabel = t('Flapping started');
break;
case 'flapping_end':
$subjectLabel = t('Flapping stopped');
break;
case 'ack_set':
$subjectLabel = t('Acknowledgement set');
break;
case 'ack_clear':
if (! empty($this->item->acknowledgement->cleared_by)) {
if ($this->item->acknowledgement->cleared_by !== $this->item->acknowledgement->author) {
$subjectLabel = sprintf(
t('Acknowledgement cleared by %s', '..<username>'),
$this->item->acknowledgement->cleared_by
);
} else {
$subjectLabel = t('Acknowledgement cleared by author');
}
} elseif (isset($this->item->acknowledgement->expire_time)) {
$subjectLabel = t('Acknowledgement expired');
} else {
$subjectLabel = t('Acknowledgement cleared');
}
break;
case 'notification':
$subjectLabel = isset($this->item->notification->type) ? sprintf(
NotificationListItem::phraseForType($this->item->notification->type),
ucfirst($this->item->object_type)
) : $this->item->event_type;
break;
case 'state_change':
$state = $this->item->state->state_type === 'hard'
? $this->item->state->hard_state
: $this->item->state->soft_state;
if ($state === 0) {
if ($this->item->object_type === 'service') {
$subjectLabel = t('Service recovered');
} else {
$subjectLabel = t('Host recovered');
}
} else {
if ($this->item->state->state_type === 'hard') {
$subjectLabel = t('Hard state changed');
} else {
$subjectLabel = t('Soft state changed');
}
}
break;
default:
$subjectLabel = $this->item->event_type;
break;
}
if ($this->getNoSubjectLink()) {
$title->addHtml(HtmlElement::create('span', ['class' => 'subject'], $subjectLabel));
} else {
$title->addHtml(new Link($subjectLabel, Links::event($this->item), ['class' => 'subject']));
}
if ($this->item->object_type === 'host') {
if (isset($this->item->host->id)) {
$link = $this->createHostLink($this->item->host, true);
}
} else {
if (isset($this->item->host->id, $this->item->service->id)) {
$link = $this->createServiceLink($this->item->service, $this->item->host, true);
}
}
$title->addHtml(Text::create(' '));
if (isset($link)) {
$title->addHtml($link);
}
}
protected function createTimestamp(): ?BaseHtmlElement
{
return new TimeAgo($this->item->event_time->getTimestamp());
}
}

View file

@ -1,56 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Common\NoSubjectLink;
use Icinga\Module\Icingadb\Model\Host;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Stdlib\Filter;
use ipl\Web\Widget\Link;
/**
* Host item of a host list. Represents one database row.
*
* @property Host $item
* @property HostList $list
*/
abstract class BaseHostListItem extends StateListItem
{
use NoSubjectLink;
/**
* Create new subject link
*
* @return BaseHtmlElement
*/
protected function createSubject()
{
if ($this->getNoSubjectLink()) {
return new HtmlElement(
'span',
Attributes::create(['class' => 'subject']),
Text::create($this->item->display_name)
);
} else {
return new Link($this->item->display_name, Links::host($this->item), ['class' => 'subject']);
}
}
protected function init(): void
{
parent::init();
if ($this->list->getNoSubjectLink()) {
$this->setNoSubjectLink();
}
$this->list->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name))
->addMultiselectFilterAttribute($this, Filter::equal('host.name', $this->item->name));
}
}

View file

@ -1,189 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
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\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\HtmlElement;
use ipl\Html\Text;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\Link;
abstract class BaseNotificationListItem extends BaseListItem
{
use HostLink;
use NoSubjectLink;
use ServiceLink;
/** @var NotificationList */
protected $list;
protected function init(): void
{
$this->setNoSubjectLink($this->list->getNoSubjectLink());
$this->list->addDetailFilterAttribute($this, Filter::equal('id', bin2hex($this->item->history->id)));
}
/**
* Get a localized phrase for the given notification type
*
* @param string $type
*
* @return string
*/
public static function phraseForType(string $type): string
{
switch ($type) {
case 'acknowledgement':
return t('Problem acknowledged');
case 'custom':
return t('Custom Notification triggered');
case 'downtime_end':
return t('Downtime ended');
case 'downtime_removed':
return t('Downtime removed');
case 'downtime_start':
return t('Downtime started');
case 'flapping_end':
return t('Flapping stopped');
case 'flapping_start':
return t('Flapping started');
case 'problem':
return t('%s ran into a problem');
case 'recovery':
return t('%s recovered');
default:
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

@ -1,70 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Common\NoSubjectLink;
use Icinga\Module\Icingadb\Model\Service;
use ipl\Html\Attributes;
use ipl\Html\Html;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Stdlib\Filter;
use ipl\Web\Widget\Link;
use ipl\Web\Widget\StateBall;
/**
* Service item of a service list. Represents one database row.
*
* @property Service $item
* @property ServiceList $list
*/
abstract class BaseServiceListItem extends StateListItem
{
use NoSubjectLink;
protected function createSubject()
{
$service = $this->item->display_name;
$host = [
new StateBall($this->item->host->state->getStateText(), StateBall::SIZE_MEDIUM),
' ',
$this->item->host->display_name
];
$host = new Link($host, Links::host($this->item->host), ['class' => 'subject']);
if ($this->getNoSubjectLink()) {
$service = new HtmlElement('span', Attributes::create(['class' => 'subject']), Text::create($service));
} else {
$service = new Link($service, Links::service($this->item, $this->item->host), ['class' => 'subject']);
}
return [Html::sprintf(t('%s on %s', '<service> on <host>'), $service, $host)];
}
protected function init(): void
{
parent::init();
if ($this->list->getNoSubjectLink()) {
$this->setNoSubjectLink();
}
$this->list->addMultiselectFilterAttribute(
$this,
Filter::all(
Filter::equal('service.name', $this->item->name),
Filter::equal('host.name', $this->item->host->name)
)
);
$this->list->addDetailFilterAttribute(
$this,
Filter::all(
Filter::equal('name', $this->item->name),
Filter::equal('host.name', $this->item->host->name)
)
);
}
}

View file

@ -1,49 +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\Links;
use Icinga\Module\Icingadb\Common\NoSubjectLink;
use Icinga\Module\Icingadb\Common\ObjectLinkDisabled;
use Icinga\Module\Icingadb\Common\TicketLinks;
use Icinga\Module\Icingadb\Common\ViewMode;
use ipl\Web\Common\BaseItemList;
use ipl\Web\Url;
class CommentList extends BaseItemList
{
use CaptionDisabled;
use NoSubjectLink;
use ObjectLinkDisabled;
use ViewMode;
use TicketLinks;
use DetailActions;
protected $defaultAttributes = ['class' => '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'));
}
}

View file

@ -1,12 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\ListItemCommonLayout;
class CommentListItem extends BaseCommentListItem
{
use ListItemCommonLayout;
}

View file

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

View file

@ -1,62 +0,0 @@
<?php
/* Icinga DB Web | (c) 2024 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Model\DependencyNode;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Model\UnreachableParent;
use ipl\Web\Common\BaseListItem;
/**
* Dependency node list
*/
class DependencyNodeList extends StateList
{
protected $defaultAttributes = ['class' => ['dependency-node-list']];
protected function init(): void
{
$this->initializeDetailActions();
}
protected function getItemClass(): string
{
return '';
}
protected function createListItem(object $data): BaseListItem
{
$viewMode = $this->getViewMode();
/** @var UnreachableParent|DependencyNode $data */
if ($data->redundancy_group_id !== null) {
if ($viewMode === 'minimal') {
return new RedundancyGroupListItemMinimal($data->redundancy_group, $this);
}
if ($viewMode === 'detailed') {
$this->removeAttribute('class', 'default-layout');
}
return new RedundancyGroupListItem($data->redundancy_group, $this);
}
$object = $data->service_id !== null ? $data->service : $data->host;
switch ($viewMode) {
case 'minimal':
$class = $object instanceof Host ? HostListItemMinimal::class : ServiceListItemMinimal::class;
break;
case 'detailed':
$this->removeAttribute('class', 'default-layout');
$class = $object instanceof Host ? HostListItemDetailed::class : ServiceListItemDetailed::class;
break;
default:
$class = $object instanceof Host ? HostListItem::class : ServiceListItem::class;
}
return new $class($object, $this);
}
}

View file

@ -1,49 +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\Links;
use Icinga\Module\Icingadb\Common\NoSubjectLink;
use Icinga\Module\Icingadb\Common\ObjectLinkDisabled;
use Icinga\Module\Icingadb\Common\TicketLinks;
use Icinga\Module\Icingadb\Common\ViewMode;
use ipl\Web\Common\BaseItemList;
use ipl\Web\Url;
class DowntimeList extends BaseItemList
{
use CaptionDisabled;
use NoSubjectLink;
use ObjectLinkDisabled;
use ViewMode;
use TicketLinks;
use DetailActions;
protected $defaultAttributes = ['class' => 'downtime-list'];
protected function getItemClass(): string
{
$viewMode = $this->getViewMode();
$this->addAttributes(['class' => $viewMode]);
if ($viewMode === 'minimal') {
return DowntimeListItemMinimal::class;
} elseif ($viewMode === 'detailed') {
$this->removeAttribute('class', 'default-layout');
}
return DowntimeListItem::class;
}
protected function init(): void
{
$this->initializeDetailActions();
$this->setMultiselectUrl(Links::downtimesDetails());
$this->setDetailUrl(Url::fromPath('icingadb/downtime'));
}
}

View file

@ -1,23 +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\Html\BaseHtmlElement;
class DowntimeListItem extends BaseDowntimeListItem
{
use ListItemCommonLayout;
protected function assembleMain(BaseHtmlElement $main): void
{
if ($this->item->is_in_effect) {
$main->add($this->createProgress());
}
$main->add($this->createHeader());
$main->add($this->createCaption());
}
}

View file

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

View file

@ -1,57 +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\TicketLinks;
use Icinga\Module\Icingadb\Common\ViewMode;
use ipl\Orm\ResultSet;
use ipl\Web\Common\BaseItemList;
use ipl\Web\Url;
class HistoryList extends BaseItemList
{
use CaptionDisabled;
use NoSubjectLink;
use ViewMode;
use LoadMore;
use TicketLinks;
use DetailActions;
protected $defaultAttributes = ['class' => 'history-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 HistoryListItemMinimal::class;
case 'detailed':
$this->removeAttribute('class', 'default-layout');
return HistoryListItemDetailed::class;
default:
return HistoryListItem::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 HistoryListItem extends BaseHistoryListItem
{
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 HistoryListItemDetailed extends BaseHistoryListItem
{
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 HistoryListItemMinimal extends BaseHistoryListItem
{
use ListItemMinimalLayout;
protected function init(): void
{
parent::init();
if ($this->list->isCaptionDisabled()) {
$this->setCaptionDisabled();
}
}
protected function getStateBallSize(): string
{
return StateBall::SIZE_BIG;
}
}

View file

@ -1,69 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\HostStates;
use Icinga\Module\Icingadb\Widget\StateChange;
use ipl\Html\BaseHtmlElement;
use ipl\Web\Widget\StateBall;
class HostDetailHeader extends HostListItemMinimal
{
protected function getStateBallSize(): string
{
return '';
}
protected function assembleVisual(BaseHtmlElement $visual): void
{
if ($this->state->state_type === 'soft') {
$stateType = 'soft_state';
$previousStateType = 'previous_soft_state';
if ($this->state->previous_soft_state === 0) {
$previousStateType = 'hard_state';
}
} else {
$stateType = 'hard_state';
$previousStateType = 'previous_hard_state';
if ($this->state->hard_state === $this->state->previous_hard_state) {
$previousStateType = 'previous_soft_state';
}
}
$state = HostStates::text($this->state->$stateType);
$previousState = HostStates::text($this->state->$previousStateType);
$stateChange = new StateChange($state, $previousState);
if ($stateType === 'soft_state') {
$stateChange->setCurrentStateBallSize(StateBall::SIZE_MEDIUM_LARGE);
}
if ($previousStateType === 'previous_soft_state') {
$stateChange->setPreviousStateBallSize(StateBall::SIZE_MEDIUM_LARGE);
if ($stateType === 'soft_state') {
$visual->getAttributes()->add('class', 'small-state-change');
}
}
$stateChange->setIcon($this->state->getIcon());
$stateChange->setHandled(
$this->state->is_problem && ($this->state->is_handled || ! $this->state->is_reachable)
);
$visual->addHtml($stateChange);
}
protected function assemble(): void
{
$attributes = $this->list->getAttributes();
if (! in_array('minimal', $attributes->get('class')->getValue())) {
$attributes->add('class', 'minimal');
}
parent::assemble();
}
}

View file

@ -1,39 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\Links;
use ipl\Web\Url;
/**
* Host list
*/
class HostList extends StateList
{
protected $defaultAttributes = ['class' => 'host-list'];
protected function getItemClass(): string
{
switch ($this->getViewMode()) {
case 'minimal':
return HostListItemMinimal::class;
case 'detailed':
$this->removeAttribute('class', 'default-layout');
return HostListItemDetailed::class;
case 'objectHeader':
return HostDetailHeader::class;
default:
return HostListItem::class;
}
}
protected function init(): void
{
$this->initializeDetailActions();
$this->setMultiselectUrl(Links::hostsDetails());
$this->setDetailUrl(Url::fromPath('icingadb/host'));
}
}

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 HostListItem extends BaseHostListItem
{
use ListItemCommonLayout;
protected function getStateBallSize(): string
{
return StateBall::SIZE_LARGE;
}
}

View file

@ -1,108 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\ListItemDetailedLayout;
use Icinga\Module\Icingadb\Util\PerfDataSet;
use Icinga\Module\Icingadb\Widget\ItemList\CommentList;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\HtmlString;
use ipl\Html\Text;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\StateBall;
class HostListItemDetailed extends BaseHostListItem
{
use ListItemDetailedLayout;
/** @var int Max pie charts to be shown */
const PIE_CHART_LIMIT = 5;
protected function getStateBallSize(): string
{
return StateBall::SIZE_LARGE;
}
protected function assembleFooter(BaseHtmlElement $footer): void
{
$statusIcons = new HtmlElement('div', Attributes::create(['class' => 'status-icons']));
if ($this->item->state->last_comment->host_id === $this->item->id) {
$comment = $this->item->state->last_comment;
$comment->host = $this->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 Icon('comments', ['class' => 'comment-icon']))
)
);
}
if ($this->item->state->is_flapping) {
$statusIcons->addHtml(new Icon(
'random',
[
'title' => sprintf(t('Host "%s" is in flapping state'), $this->item->display_name),
]
));
}
if (! $this->item->notifications_enabled) {
$statusIcons->addHtml(new Icon('bell-slash', ['title' => t('Notifications disabled')]));
}
if (! $this->item->active_checks_enabled) {
$statusIcons->addHtml(new Icon('eye-slash', ['title' => t('Active checks disabled')]));
}
$performanceData = new HtmlElement('div', Attributes::create(['class' => 'performance-data']));
if ($this->item->state->performance_data) {
$pieChartData = PerfDataSet::fromString($this->item->state->normalized_performance_data)->asArray();
$pies = [];
foreach ($pieChartData as $i => $perfdata) {
if ($perfdata->isVisualizable()) {
$pies[] = $perfdata->asInlinePie()->render();
}
// Check if number of visualizable pie charts is larger than PIE_CHART_LIMIT
if (count($pies) > HostListItemDetailed::PIE_CHART_LIMIT) {
break;
}
}
$maxVisiblePies = HostListItemDetailed::PIE_CHART_LIMIT - 2;
$numOfPies = count($pies);
foreach ($pies as $i => $pie) {
if (
// Show max. 5 elements: if there are more than 5, show 4 + `…`
$i > $maxVisiblePies && $numOfPies > HostListItemDetailed::PIE_CHART_LIMIT
) {
$performanceData->addHtml(new HtmlElement('span', null, Text::create('…')));
break;
}
$performanceData->addHtml(HtmlString::create($pie));
}
}
if (! $statusIcons->isEmpty()) {
$footer->addHtml($statusIcons);
}
if (! $performanceData->isEmpty()) {
$footer->addHtml($performanceData);
}
}
}

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\ListItemMinimalLayout;
use ipl\Web\Widget\StateBall;
class HostListItemMinimal extends BaseHostListItem
{
use ListItemMinimalLayout;
protected function getStateBallSize(): string
{
return StateBall::SIZE_BIG;
}
}

View file

@ -0,0 +1,44 @@
<?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\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

@ -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

@ -0,0 +1,266 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Exception\NotImplementedError;
use Icinga\Module\Icingadb\Common\DetailActions;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Model\Comment;
use Icinga\Module\Icingadb\Model\DependencyNode;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\Model\History;
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\User;
use Icinga\Module\Icingadb\Model\Usergroup;
use Icinga\Module\Icingadb\Redis\VolatileStateResults;
use Icinga\Module\Icingadb\View\CommentRenderer;
use Icinga\Module\Icingadb\View\DowntimeRenderer;
use Icinga\Module\Icingadb\View\HostRenderer;
use Icinga\Module\Icingadb\View\RedundancyGroupRenderer;
use Icinga\Module\Icingadb\View\ServiceRenderer;
use Icinga\Module\Icingadb\View\UsergroupRenderer;
use Icinga\Module\Icingadb\View\UserRenderer;
use Icinga\Module\Icingadb\Widget\Notice;
use InvalidArgumentException;
use ipl\Html\HtmlDocument;
use ipl\I18n\Translation;
use ipl\Orm\Model;
use ipl\Stdlib\Filter;
use ipl\Web\Layout\DetailedItemLayout;
use ipl\Web\Layout\ItemLayout;
use ipl\Web\Layout\MinimalItemLayout;
use ipl\Web\Url;
use ipl\Web\Widget\ItemList;
/**
* ObjectList
*
* Create a list of icingadb objects
*
* @template Result of DependencyNode|Service|Host|Usergroup|User|Comment|Downtime
* @template Item of RedundancyGroup|Service|Host|Usergroup|User|Comment|Downtime = Result
*
* @extends ItemList<Result, Item>
*/
class ObjectList extends ItemList
{
use DetailActions;
use Translation;
/** @var bool Whether the list contains at least one item with an icon_image */
protected $hasIconImages = false;
public function __construct($data)
{
parent::__construct($data, function (Model $item) {
if ($item instanceof RedundancyGroup) {
return new RedundancyGroupRenderer();
} elseif ($item instanceof Service) {
return new ServiceRenderer();
} elseif ($item instanceof Host) {
return new HostRenderer();
} elseif ($item instanceof Usergroup) {
return new UsergroupRenderer();
} elseif ($item instanceof User) {
return new UserRenderer();
} elseif ($item instanceof Comment) {
return new CommentRenderer();
} elseif ($item instanceof Downtime) {
return new DowntimeRenderer();
}
throw new NotImplementedError('Not implemented');
});
}
protected function init(): void
{
$this->initializeDetailActions();
}
/**
* Get whether the list contains at least one item with an icon_image
*
* @return bool
*/
public function hasIconImages(): bool
{
return $this->hasIconImages;
}
/**
* Set whether the list contains at least one item with an icon_image
*
* @param bool $hasIconImages
*
* @return $this
*/
public function setHasIconImages(bool $hasIconImages): self
{
$this->hasIconImages = $hasIconImages;
return $this;
}
/**
* Set the view mode
*
* @param 'minimal'|'common'|'detailed' $mode
*
* @return $this
*/
public function setViewMode(string $mode): self
{
switch ($mode) {
case 'minimal':
$this->setItemLayoutClass(MinimalItemLayout::class);
break;
case 'detailed':
$this->setItemLayoutClass(DetailedItemLayout::class);
break;
case 'common':
$this->setItemLayoutClass(ItemLayout::class);
break;
default:
throw new InvalidArgumentException('Invalid view mode');
}
return $this;
}
public function getItemLayout($item): ItemLayout
{
$layout = parent::getItemLayout($item);
if ($this->hasIconImages()) {
$layout->after(ItemLayout::VISUAL, 'icon-image');
}
if ($item instanceof Downtime) {
$layout->before(ItemLayout::HEADER, 'progress');
}
return $layout;
}
protected function createListItem(object $data)
{
if ($data instanceof DependencyNode) {
if (isset($data->redundancy_group_id)) {
$object = $data->redundancy_group;
} else {
$object = isset($data->service_id) ? $data->service : $data->host;
}
} else {
$object = $data;
}
if (isset($object->icon_image->icon_image)) {
$this->setHasIconImages(true);
}
$item = parent::createListItem($object);
if ($this->getDetailActionsDisabled()) {
return $item;
}
switch (true) {
case $object instanceof RedundancyGroup:
$this->addDetailFilterAttribute($item, Filter::equal('id', bin2hex($object->id)));
break;
case $object instanceof Service:
$this
->setDetailUrl(Url::fromPath('icingadb/service'))
->setMultiselectUrl(Links::servicesDetails())
->addDetailFilterAttribute(
$item,
Filter::all(
Filter::equal('name', $object->name),
Filter::equal('host.name', $object->host->name)
)
);
$this->addMultiSelectFilterAttribute(
$item,
Filter::all(
Filter::equal('service.name', $object->name),
Filter::equal('host.name', $object->host->name)
)
);
break;
case $object instanceof Host:
$this
->setDetailUrl(Url::fromPath('icingadb/host'))
->setMultiselectUrl(Links::hostsDetails())
->addDetailFilterAttribute($item, Filter::equal('name', $object->name))
->addMultiSelectFilterAttribute($item, Filter::equal('host.name', $object->name));
break;
case $data instanceof User:
$this
->setDetailUrl(Url::fromPath('icingadb/user'))
->addDetailFilterAttribute($item, Filter::equal('name', $object->name));
break;
case $object instanceof Usergroup:
$this
->setDetailUrl(Url::fromPath('icingadb/usergroup'))
->addDetailFilterAttribute($item, Filter::equal('name', $object->name));
break;
case $object instanceof Comment:
$this
->setDetailUrl(Url::fromPath('icingadb/comment'))
->setMultiselectUrl(Links::commentsDetails())
->addDetailFilterAttribute($item, Filter::equal('name', $object->name))
->addMultiSelectFilterAttribute($item, Filter::equal('name', $object->name));
break;
case $object instanceof Downtime:
$this
->setDetailUrl(Url::fromPath('icingadb/downtime'))
->setMultiselectUrl(Links::downtimesDetails())
->addDetailFilterAttribute($item, Filter::equal('name', $object->name))
->addMultiSelectFilterAttribute($item, Filter::equal('name', $object->name));
break;
case $object instanceof NotificationHistory:
$this
->setDetailUrl(Url::fromPath('icingadb/event'))
->addDetailFilterAttribute($item, Filter::equal('id', bin2hex($object->history->id)));
break;
case $object instanceof History:
$this
->setDetailUrl(Url::fromPath('icingadb/event'))
->addDetailFilterAttribute($item, Filter::equal('id', bin2hex($object->id)));
break;
}
return $item;
}
protected function assemble(): void
{
parent::assemble();
if ($this->data instanceof VolatileStateResults && $this->data->isRedisUnavailable()) {
$this->prependWrapper((new HtmlDocument())->addHtml(new Notice(
$this->translate('Redis is currently unavailable. The shown information might be outdated.')
)));
}
}
}

View file

@ -1,106 +0,0 @@
<?php
/* Icinga DB Web | (c) 2024 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\Auth;
use Icinga\Module\Icingadb\Common\Database;
use Icinga\Module\Icingadb\Common\ListItemCommonLayout;
use Icinga\Module\Icingadb\Model\RedundancyGroup;
use Icinga\Module\Icingadb\Model\RedundancyGroupSummary;
use Icinga\Module\Icingadb\Model\RedundancyGroupState;
use Icinga\Module\Icingadb\Widget\DependencyNodeStatistics;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Stdlib\Filter;
use ipl\Web\Url;
use ipl\Web\Widget\Link;
use ipl\Web\Widget\StateBall;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Web\Widget\TimeSince;
/**
* Redundancy group list item. Represents one database row.
*
* @property RedundancyGroup $item
* @property RedundancyGroupState $state
*/
class RedundancyGroupListItem extends StateListItem
{
use ListItemCommonLayout;
use Database;
use Auth;
protected $defaultAttributes = ['class' => ['redundancy-group-list-item']];
protected function init(): void
{
parent::init();
$this->addAttributes(['data-action-item' => true]);
}
protected function getStateBallSize(): string
{
return StateBall::SIZE_LARGE;
}
protected function createTimestamp(): BaseHtmlElement
{
return new TimeSince($this->state->last_state_change->getTimestamp());
}
protected function createSubject(): Link
{
return new Link(
$this->item->display_name,
Url::fromPath('icingadb/redundancygroup', ['id' => bin2hex($this->item->id)]),
['class' => 'subject']
);
}
protected function assembleVisual(BaseHtmlElement $visual): void
{
$stateBall = new StateBall($this->state->getStateText(), $this->getStateBallSize());
$stateBall->add($this->state->getIcon());
$visual->addHtml($stateBall);
}
protected function assembleCaption(BaseHtmlElement $caption): void
{
$summary = RedundancyGroupSummary::on($this->getDb())
->filter(Filter::equal('id', $this->item->id));
$this->applyRestrictions($summary);
$caption->addHtml(new DependencyNodeStatistics($summary->first()));
}
protected function assembleTitle(BaseHtmlElement $title): void
{
$subject = $this->createSubject();
if ($this->state->failed) {
$title->addHtml(Html::sprintf(
$this->translate('%s has no working objects', '<groupname> has ...'),
$subject
));
} else {
$title->addHtml(Html::sprintf(
$this->translate('%s has working objects', '<groupname> has ...'),
$subject
));
}
}
protected function assemble(): void
{
$this->add([
$this->createVisual(),
$this->createIconImage(),
$this->createMain()
]);
}
}

View file

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

View file

@ -1,69 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\ServiceStates;
use Icinga\Module\Icingadb\Widget\StateChange;
use ipl\Html\BaseHtmlElement;
use ipl\Web\Widget\StateBall;
class ServiceDetailHeader extends ServiceListItemMinimal
{
protected function getStateBallSize(): string
{
return '';
}
protected function assembleVisual(BaseHtmlElement $visual): void
{
if ($this->state->state_type === 'soft') {
$stateType = 'soft_state';
$previousStateType = 'previous_soft_state';
if ($this->state->previous_soft_state === 0) {
$previousStateType = 'hard_state';
}
} else {
$stateType = 'hard_state';
$previousStateType = 'previous_hard_state';
if ($this->state->hard_state === $this->state->previous_hard_state) {
$previousStateType = 'previous_soft_state';
}
}
$state = ServiceStates::text($this->state->$stateType);
$previousState = ServiceStates::text($this->state->$previousStateType);
$stateChange = new StateChange($state, $previousState);
if ($stateType === 'soft_state') {
$stateChange->setCurrentStateBallSize(StateBall::SIZE_MEDIUM_LARGE);
}
if ($previousStateType === 'previous_soft_state') {
$stateChange->setPreviousStateBallSize(StateBall::SIZE_MEDIUM_LARGE);
if ($stateType === 'soft_state') {
$visual->getAttributes()->add('class', 'small-state-change');
}
}
$stateChange->setIcon($this->state->getIcon());
$stateChange->setHandled(
$this->state->is_problem && ($this->state->is_handled || ! $this->state->is_reachable)
);
$visual->addHtml($stateChange);
}
protected function assemble(): void
{
$attributes = $this->list->getAttributes();
if (! in_array('minimal', $attributes->get('class')->getValue())) {
$attributes->add('class', 'minimal');
}
parent::assemble();
}
}

View file

@ -1,36 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\Links;
use ipl\Web\Url;
class ServiceList extends StateList
{
protected $defaultAttributes = ['class' => 'service-list'];
protected function getItemClass(): string
{
switch ($this->getViewMode()) {
case 'minimal':
return ServiceListItemMinimal::class;
case 'detailed':
$this->removeAttribute('class', 'default-layout');
return ServiceListItemDetailed::class;
case 'objectHeader':
return ServiceDetailHeader::class;
default:
return ServiceListItem::class;
}
}
protected function init(): void
{
$this->initializeDetailActions();
$this->setMultiselectUrl(Links::servicesDetails());
$this->setDetailUrl(Url::fromPath('icingadb/service'));
}
}

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 ServiceListItem extends BaseServiceListItem
{
use ListItemCommonLayout;
protected function getStateBallSize(): string
{
return StateBall::SIZE_LARGE;
}
}

View file

@ -1,112 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\ListItemDetailedLayout;
use Icinga\Module\Icingadb\Util\PerfDataSet;
use Icinga\Module\Icingadb\Widget\ItemList\CommentList;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\HtmlString;
use ipl\Html\Text;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\StateBall;
class ServiceListItemDetailed extends BaseServiceListItem
{
use ListItemDetailedLayout;
/** @var int Max pie charts to be shown */
const PIE_CHART_LIMIT = 5;
protected function getStateBallSize(): string
{
return StateBall::SIZE_LARGE;
}
protected function assembleFooter(BaseHtmlElement $footer): void
{
$statusIcons = new HtmlElement('div', Attributes::create(['class' => 'status-icons']));
if ($this->item->state->last_comment->service_id === $this->item->id) {
$comment = $this->item->state->last_comment;
$comment->service = $this->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 Icon('comments', ['class' => 'comment-icon']))
)
);
}
if ($this->item->state->is_flapping) {
$statusIcons->addHtml(new Icon(
'random',
[
'title' => sprintf(
t('Service "%s" on "%s" is in flapping state'),
$this->item->display_name,
$this->item->host->display_name
),
]
));
}
if (! $this->item->notifications_enabled) {
$statusIcons->addHtml(new Icon('bell-slash', ['title' => t('Notifications disabled')]));
}
if (! $this->item->active_checks_enabled) {
$statusIcons->addHtml(new Icon('eye-slash', ['title' => t('Active checks disabled')]));
}
$performanceData = new HtmlElement('div', Attributes::create(['class' => 'performance-data']));
if ($this->item->state->performance_data) {
$pieChartData = PerfDataSet::fromString($this->item->state->normalized_performance_data)->asArray();
$pies = [];
foreach ($pieChartData as $i => $perfdata) {
if ($perfdata->isVisualizable()) {
$pies[] = $perfdata->asInlinePie()->render();
}
// Check if number of visualizable pie charts is larger than PIE_CHART_LIMIT
if (count($pies) > ServiceListItemDetailed::PIE_CHART_LIMIT) {
break;
}
}
$maxVisiblePies = ServiceListItemDetailed::PIE_CHART_LIMIT - 2;
$numOfPies = count($pies);
foreach ($pies as $i => $pie) {
if (
// Show max. 5 elements: if there are more than 5, show 4 + `…`
$i > $maxVisiblePies && $numOfPies > ServiceListItemDetailed::PIE_CHART_LIMIT
) {
$performanceData->addHtml(new HtmlElement('span', null, Text::create('…')));
break;
}
$performanceData->addHtml(HtmlString::create($pie));
}
}
if (! $statusIcons->isEmpty()) {
$footer->addHtml($statusIcons);
}
if (! $performanceData->isEmpty()) {
$footer->addHtml($performanceData);
}
}
}

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\ListItemMinimalLayout;
use ipl\Web\Widget\StateBall;
class ServiceListItemMinimal extends BaseServiceListItem
{
use ListItemMinimalLayout;
protected function getStateBallSize(): string
{
return StateBall::SIZE_BIG;
}
}

View file

@ -1,60 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\DetailActions;
use Icinga\Module\Icingadb\Common\NoSubjectLink;
use Icinga\Module\Icingadb\Common\ViewMode;
use Icinga\Module\Icingadb\Redis\VolatileStateResults;
use Icinga\Module\Icingadb\Widget\Notice;
use ipl\Html\HtmlDocument;
use ipl\Web\Common\BaseItemList;
abstract class StateList extends BaseItemList
{
use ViewMode;
use NoSubjectLink;
use DetailActions;
/** @var bool Whether the list contains at least one item with an icon_image */
protected $hasIconImages = false;
/**
* Get whether the list contains at least one item with an icon_image
*
* @return bool
*/
public function hasIconImages(): bool
{
return $this->hasIconImages;
}
/**
* Set whether the list contains at least one item with an icon_image
*
* @param bool $hasIconImages
*
* @return $this
*/
public function setHasIconImages(bool $hasIconImages): self
{
$this->hasIconImages = $hasIconImages;
return $this;
}
protected function assemble(): void
{
$this->addAttributes(['class' => $this->getViewMode()]);
parent::assemble();
if ($this->data instanceof VolatileStateResults && $this->data->isRedisUnavailable()) {
$this->prependWrapper((new HtmlDocument())->addHtml(new Notice(
t('Redis is currently unavailable. The shown information might be outdated.')
)));
}
}
}

View file

@ -1,174 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\Icons;
use Icinga\Module\Icingadb\Model\State;
use Icinga\Module\Icingadb\Util\PluginOutput;
use Icinga\Module\Icingadb\Widget\CheckAttempt;
use Icinga\Module\Icingadb\Widget\IconImage;
use Icinga\Module\Icingadb\Widget\PluginOutputContainer;
use ipl\Html\Attributes;
use ipl\Html\HtmlElement;
use ipl\I18n\Translation;
use ipl\Web\Common\BaseListItem;
use ipl\Web\Widget\EmptyState;
use ipl\Web\Widget\TimeSince;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Html\Text;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\StateBall;
/**
* Host or service item of a host or service list. Represents one database row.
*/
abstract class StateListItem extends BaseListItem
{
use Translation;
/** @var StateList The list where the item is part of */
protected $list;
/** @var State The state of the item */
protected $state;
protected function init(): void
{
$this->state = $this->item->state;
if (isset($this->item->icon_image->icon_image)) {
$this->list->setHasIconImages(true);
}
}
abstract protected function createSubject();
abstract protected function getStateBallSize(): string;
/**
* @return ?BaseHtmlElement
*/
protected function createIconImage(): ?BaseHtmlElement
{
if (! $this->list->hasIconImages()) {
return null;
}
$iconImage = HtmlElement::create('div', [
'class' => 'icon-image',
]);
$this->assembleIconImage($iconImage);
return $iconImage;
}
protected function assembleCaption(BaseHtmlElement $caption): void
{
if ($this->state->soft_state === null && $this->state->output === null) {
$caption->addHtml(Text::create($this->translate('Waiting for Icinga DB to synchronize the state.')));
} else {
if (empty($this->state->output)) {
$pluginOutput = new EmptyState($this->translate('Output unavailable.'));
} else {
$pluginOutput = new PluginOutputContainer(PluginOutput::fromObject($this->item));
}
$caption->addHtml($pluginOutput);
}
}
protected function assembleIconImage(BaseHtmlElement $iconImage): void
{
if (isset($this->item->icon_image->icon_image)) {
$iconImage->addHtml(new IconImage($this->item->icon_image->icon_image, $this->item->icon_image_alt));
} else {
$iconImage->addAttributes(['class' => 'placeholder']);
}
}
protected function assembleTitle(BaseHtmlElement $title): void
{
$title->addHtml(Html::sprintf(
$this->translate('%s is %s', '<hostname> is <state-text>'),
$this->createSubject(),
Html::tag('span', ['class' => 'state-text'], $this->state->getStateTextTranslated())
));
if (isset($this->state->affects_children) && $this->state->affects_children) {
$total = (int) $this->item->total_children;
if ($total > 1000) {
$total = '1000+';
$tooltip = $this->translate('Up to 1000+ affected objects');
} else {
$tooltip = sprintf(
$this->translatePlural(
'%d affected object',
'Up to %d affected objects',
$total
),
$total
);
}
$icon = new Icon(Icons::UNREACHABLE);
$title->addHtml(new HtmlElement(
'span',
Attributes::create([
'class' => 'affected-objects',
'title' => $tooltip
]),
$icon,
Text::create($total)
));
}
}
protected function assembleVisual(BaseHtmlElement $visual): void
{
$stateBall = new StateBall($this->state->getStateText(), $this->getStateBallSize());
$stateBall->add($this->state->getIcon());
if ($this->state->is_problem && ($this->state->is_handled || ! $this->state->is_reachable)) {
$stateBall->getAttributes()->add('class', 'handled');
}
$visual->addHtml($stateBall);
if ($this->state->state_type === 'soft') {
$visual->addHtml(
new CheckAttempt((int) $this->state->check_attempt, (int) $this->item->max_check_attempts)
);
}
}
protected function createTimestamp(): ?BaseHtmlElement
{
$since = null;
if ($this->state->is_overdue) {
$since = new TimeSince($this->state->next_update->getTimestamp());
$since->prepend($this->translate('Overdue') . ' ');
$since->prependHtml(new Icon(Icons::WARNING));
} elseif ($this->state->last_state_change !== null && $this->state->last_state_change->getTimestamp() > 0) {
$since = new TimeSince($this->state->last_state_change->getTimestamp());
}
return $since;
}
protected function assemble(): void
{
if ($this->state->is_overdue) {
$this->addAttributes(['class' => 'overdue']);
}
$this->add([
$this->createVisual(),
$this->createIconImage(),
$this->createMain()
]);
}
}

View file

@ -0,0 +1,38 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Exception\NotImplementedError;
use Icinga\Module\Icingadb\Model\Comment;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\View\CommentRenderer;
use Icinga\Module\Icingadb\View\DowntimeRenderer;
use ipl\Orm\Model;
use ipl\Web\Widget\ItemList;
/**
* TicketLinkObjectList
*
* Create a list of icingadb objects with ticket links
*
* @template Result of Comment|Downtime
*
* @extends ObjectList<Result>
*/
class TicketLinkObjectList extends ObjectList
{
public function __construct($data)
{
ItemList::__construct($data, function (Model $item) {
if ($item instanceof Comment) {
return (new CommentRenderer())->setNoObjectLink();
} elseif ($item instanceof Downtime) {
return (new DowntimeRenderer())->setNoObjectLink();
}
throw new NotImplementedError('Not implemented');
});
}
}

View file

@ -1,67 +0,0 @@
<?php
/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemTable;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Model\Hostgroupsummary;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\I18n\Translation;
use ipl\Stdlib\Filter;
use ipl\Web\Common\BaseTableRowItem;
use ipl\Web\Widget\Link;
/**
* Hostgroup item of a hostgroup list. Represents one database row.
*
* @property Hostgroupsummary $item
* @property HostgroupTable $table
*/
abstract class BaseHostGroupItem extends BaseTableRowItem
{
use Translation;
protected function init(): void
{
if (isset($this->table)) {
$this->table->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name));
}
}
protected function createSubject(): BaseHtmlElement
{
if (isset($this->table)) {
$link = new Link(
$this->item->display_name,
Links::hostgroup($this->item),
[
'class' => 'subject',
'title' => sprintf(
$this->translate('List all hosts in the group "%s"'),
$this->item->display_name
)
]
);
if ($this->table->hasBaseFilter()) {
$link->getUrl()->setFilter($this->table->getBaseFilter());
}
return $link;
}
return new HtmlElement(
'span',
Attributes::create(['class' => 'subject']),
Text::create($this->item->display_name)
);
}
protected function createCaption(): BaseHtmlElement
{
return new HtmlElement('span', null, Text::create($this->item->name));
}
}

View file

@ -1,67 +0,0 @@
<?php
/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemTable;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Model\ServicegroupSummary;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\I18n\Translation;
use ipl\Stdlib\Filter;
use ipl\Web\Common\BaseTableRowItem;
use ipl\Web\Widget\Link;
/**
* Servicegroup item of a servicegroup list. Represents one database row.
*
* @property ServicegroupSummary $item
* @property ServicegroupTable $table
*/
abstract class BaseServiceGroupItem extends BaseTableRowItem
{
use Translation;
protected function init(): void
{
if (isset($this->table)) {
$this->table->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name));
}
}
protected function createSubject(): BaseHtmlElement
{
if (isset($this->table)) {
$link = new Link(
$this->item->display_name,
Links::servicegroup($this->item),
[
'class' => 'subject',
'title' => sprintf(
$this->translate('List all services in the group "%s"'),
$this->item->display_name
)
]
);
if ($this->table->hasBaseFilter()) {
$link->getUrl()->setFilter($this->table->getBaseFilter());
}
return $link;
}
return new HtmlElement(
'span',
Attributes::create(['class' => 'subject']),
Text::create($this->item->display_name)
);
}
protected function createCaption(): BaseHtmlElement
{
return new HtmlElement('span', null, Text::create($this->item->name));
}
}

View file

@ -1,39 +0,0 @@
<?php
/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemTable;
use ipl\Html\BaseHtmlElement;
use ipl\Web\Widget\Link;
trait GridCellLayout
{
/**
* Creates a state badge for the Host / Service group with the highest severity that an object in the group has,
* along with the count of the objects with this severity belonging to the corresponding group.
*
* @return Link
*/
abstract public function createGroupBadge(): Link;
protected function assembleVisual(BaseHtmlElement $visual): void
{
$visual->add($this->createGroupBadge());
}
protected function assembleTitle(BaseHtmlElement $title): void
{
$title->addHtml(
$this->createSubject(),
$this->createCaption()
);
}
protected function assemble(): void
{
$this->add([
$this->createTitle()
]);
}
}

View file

@ -1,114 +0,0 @@
<?php
/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemTable;
use ipl\Stdlib\Filter;
use ipl\Web\Url;
use ipl\Web\Widget\Link;
use ipl\Web\Widget\StateBadge;
class HostgroupGridCell extends BaseHostGroupItem
{
use GridCellLayout;
protected $defaultAttributes = ['class' => ['group-grid-cell', 'hostgroup-grid-cell']];
protected function createGroupBadge(): Link
{
$url = Url::fromPath('icingadb/hosts');
$urlFilter = Filter::all(Filter::equal('hostgroup.name', $this->item->name));
if ($this->item->hosts_down_unhandled > 0) {
$urlFilter->add(Filter::equal('host.state.soft_state', 1))
->add(Filter::equal('host.state.is_handled', 'n'))
->add(Filter::equal('host.state.is_reachable', 'y'));
return new Link(
new StateBadge($this->item->hosts_down_unhandled, 'down'),
$url->setFilter($urlFilter),
[
'title' => sprintf(
$this->translatePlural(
'List %d host that is currently in DOWN state in host group "%s"',
'List %d hosts which are currently in DOWN state in host group "%s"',
$this->item->hosts_down_unhandled
),
$this->item->hosts_down_unhandled,
$this->item->display_name
)
]
);
} elseif ($this->item->hosts_down_handled > 0) {
$urlFilter->add(Filter::equal('host.state.soft_state', 1))
->add(Filter::any(
Filter::equal('host.state.is_handled', 'y'),
Filter::equal('host.state.is_reachable', 'n')
));
return new Link(
new StateBadge($this->item->hosts_down_handled, 'down', true),
$url->setFilter($urlFilter),
[
'title' => sprintf(
$this->translatePlural(
'List %d host that is currently in DOWN (Acknowledged) state in host group "%s"',
'List %d hosts which are currently in DOWN (Acknowledged) state in host group "%s"',
$this->item->hosts_down_handled
),
$this->item->hosts_down_handled,
$this->item->display_name
)
]
);
} elseif ($this->item->hosts_pending > 0) {
$urlFilter->add(Filter::equal('host.state.soft_state', 99));
return new Link(
new StateBadge($this->item->hosts_pending, 'pending'),
$url->setFilter($urlFilter),
[
'title' => sprintf(
$this->translatePlural(
'List %d host that is currently in PENDING state in host group "%s"',
'List %d hosts which are currently in PENDING state in host group "%s"',
$this->item->hosts_pending
),
$this->item->hosts_pending,
$this->item->display_name
)
]
);
} elseif ($this->item->hosts_up > 0) {
$urlFilter->add(Filter::equal('host.state.soft_state', 0));
return new Link(
new StateBadge($this->item->hosts_up, 'up'),
$url->setFilter($urlFilter),
[
'title' => sprintf(
$this->translatePlural(
'List %d host that is currently in UP state in host group "%s"',
'List %d hosts which are currently in UP state in host group "%s"',
$this->item->hosts_up
),
$this->item->hosts_up,
$this->item->display_name
)
]
);
}
return new Link(
new StateBadge(0, 'none'),
$url,
[
'title' => sprintf(
$this->translate('There are no hosts in host group "%s"'),
$this->item->display_name
)
]
);
}
}

View file

@ -1,38 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemTable;
use Icinga\Module\Icingadb\Common\DetailActions;
use Icinga\Module\Icingadb\Common\ViewMode;
use ipl\Web\Common\BaseItemTable;
use ipl\Web\Url;
class HostgroupTable extends BaseItemTable
{
use DetailActions;
use ViewMode;
protected $defaultAttributes = ['class' => 'hostgroup-table'];
protected function init(): void
{
$this->initializeDetailActions();
$this->setDetailUrl(Url::fromPath('icingadb/hostgroup'));
}
protected function getLayout(): string
{
return $this->getViewMode() === 'grid'
? 'group-grid'
: parent::getLayout();
}
protected function getItemClass(): string
{
return $this->getViewMode() === 'grid'
? HostgroupGridCell::class
: HostgroupTableRow::class;
}
}

View file

@ -1,55 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemTable;
use Icinga\Module\Icingadb\Model\Hostgroupsummary;
use Icinga\Module\Icingadb\Widget\Detail\HostStatistics;
use Icinga\Module\Icingadb\Widget\Detail\ServiceStatistics;
use ipl\Html\BaseHtmlElement;
use ipl\Stdlib\Filter;
/**
* Hostgroup table row of a hostgroup table. Represents one database row.
*
* @property Hostgroupsummary $item
* @property HostgroupTable $table
*/
class HostgroupTableRow extends BaseHostGroupItem
{
use TableRowLayout;
protected $defaultAttributes = ['class' => 'hostgroup-table-row'];
/**
* Create Host and service statistics columns
*
* @return BaseHtmlElement[]
*/
protected function createStatistics(): array
{
$hostStats = new HostStatistics($this->item);
$hostStats->setBaseFilter(Filter::equal('hostgroup.name', $this->item->name));
if (isset($this->table) && $this->table->hasBaseFilter()) {
$hostStats->setBaseFilter(
Filter::all($hostStats->getBaseFilter(), $this->table->getBaseFilter())
);
}
$serviceStats = new ServiceStatistics($this->item);
$serviceStats->setBaseFilter(Filter::equal('hostgroup.name', $this->item->name));
if (isset($this->table) && $this->table->hasBaseFilter()) {
$serviceStats->setBaseFilter(
Filter::all($serviceStats->getBaseFilter(), $this->table->getBaseFilter())
);
}
return [
$this->createColumn($hostStats),
$this->createColumn($serviceStats)
];
}
}

View file

@ -0,0 +1,113 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemTable;
use Icinga\Exception\NotImplementedError;
use Icinga\Module\Icingadb\Common\DetailActions;
use Icinga\Module\Icingadb\Model\Hostgroupsummary;
use Icinga\Module\Icingadb\Model\ServicegroupSummary;
use InvalidArgumentException;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\ValidHtml;
use ipl\Orm\ResultSet;
use ipl\Stdlib\Filter;
use ipl\Web\Common\ItemRenderer;
use ipl\Web\Layout\ItemLayout;
use ipl\Web\Url;
use ipl\Web\Widget\EmptyStateBar;
/**
* ObjectGrid
*
* @internal The only reason this class exists is due to the detail actions. In case those are part of the ipl
* some time, this class is obsolete, and we must be able to safely drop it.
*
* @template Item of Hostgroupsummary|ServicegroupSummary
*/
class ObjectGrid extends BaseHtmlElement
{
use DetailActions;
protected $defaultAttributes = [
'class' => 'object-grid',
'data-base-target' => '_next'
];
protected $tag = 'ul';
/** @var ItemRenderer<Item> */
protected $itemRenderer;
/** @var ResultSet|iterable<Item> */
protected $data;
/**
* Create a new object grid
*
* @param ResultSet|iterable<Item> $data
* @param ItemRenderer<Item> $renderer
*/
public function __construct($data, ItemRenderer $renderer)
{
if (! is_iterable($data)) {
throw new InvalidArgumentException('Data must be an array or an instance of Traversable');
}
$this->data = $data;
$this->itemRenderer = $renderer;
$this->initializeDetailActions();
}
/**
* Create a list item for the given data
*
* @param Item $data
*
* @return ValidHtml
*
* @throws NotImplementedError When the data is not of the expected type
*/
protected function createListItem(object $data): ValidHtml
{
$layout = new ItemLayout($data, $this->itemRenderer);
$item = new HtmlElement('li', $layout->getAttributes(), $layout);
if ($this->getDetailActionsDisabled()) {
return $item;
}
switch (true) {
case $data instanceof Hostgroupsummary:
$this->setDetailUrl(Url::fromPath('icingadb/hostgroup'));
break;
case $data instanceof ServicegroupSummary:
$this->setDetailUrl(Url::fromPath('icingadb/servicegroup'));
break;
default:
throw new NotImplementedError('Not implemented');
}
$this->addDetailFilterAttribute($item, Filter::equal('name', $data->name));
return $item;
}
protected function assemble(): void
{
/** @var Item $data */
foreach ($this->data as $data) {
$this->addHtml($this->createListItem($data));
}
if ($this->isEmpty()) {
$this->setTag('div');
$this->addHtml(new EmptyStateBar(t('No items found.')));
}
}
}

View file

@ -0,0 +1,69 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemTable;
use Icinga\Exception\NotImplementedError;
use Icinga\Module\Icingadb\Common\DetailActions;
use Icinga\Module\Icingadb\Model\Hostgroupsummary;
use Icinga\Module\Icingadb\Model\ServicegroupSummary;
use ipl\Html\ValidHtml;
use ipl\Stdlib\Filter;
use ipl\Web\Url;
use ipl\Web\Widget\ItemTable;
/**
* ObjectTable
*
* @internal The only reason this class exists is due to the detail actions. In case those are part of the ipl
* some time, this class is obsolete, and we must be able to safely drop it.
*
* @template Item of Hostgroupsummary|ServicegroupSummary
*
* @extends ItemTable<Item>
*/
class ObjectTable extends ItemTable
{
use DetailActions;
protected function init(): void
{
parent::init();
$this->initializeDetailActions();
}
/**
* @param Item $data
*
* @return ValidHtml
*
* @throws NotImplementedError When the data is not of the expected type
*/
protected function createListItem(object $data): ValidHtml
{
$item = parent::createListItem($data);
if ($this->getDetailActionsDisabled()) {
return $item;
}
switch (true) {
case $data instanceof Hostgroupsummary:
$this->setDetailUrl(Url::fromPath('icingadb/hostgroup'));
break;
case $data instanceof ServicegroupSummary:
$this->setDetailUrl(Url::fromPath('icingadb/servicegroup'));
break;
default:
throw new NotImplementedError('Not implemented');
}
$this->addDetailFilterAttribute($item, Filter::equal('name', $data->name));
return $item;
}
}

View file

@ -1,38 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemTable;
use Icinga\Module\Icingadb\Common\DetailActions;
use Icinga\Module\Icingadb\Common\ViewMode;
use ipl\Web\Common\BaseItemTable;
use ipl\Web\Url;
class ServicegroupTable extends BaseItemTable
{
use DetailActions;
use ViewMode;
protected $defaultAttributes = ['class' => 'servicegroup-table'];
protected function init(): void
{
$this->initializeDetailActions();
$this->setDetailUrl(Url::fromPath('icingadb/servicegroup'));
}
protected function getLayout(): string
{
return $this->getViewMode() === 'grid'
? 'group-grid'
: parent::getLayout();
}
protected function getItemClass(): string
{
return $this->getViewMode() === 'grid'
? ServicegroupGridCell::class
: ServicegroupTableRow::class;
}
}

View file

@ -1,42 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemTable;
use Icinga\Module\Icingadb\Model\ServicegroupSummary;
use Icinga\Module\Icingadb\Widget\Detail\ServiceStatistics;
use ipl\Html\BaseHtmlElement;
use ipl\Stdlib\Filter;
/**
* Servicegroup item of a servicegroup list. Represents one database row.
*
* @property ServicegroupSummary $item
* @property ServicegroupTable $table
*/
class ServicegroupTableRow extends BaseServiceGroupItem
{
use TableRowLayout;
protected $defaultAttributes = ['class' => 'servicegroup-table-row'];
/**
* Create Service statistics cell
*
* @return BaseHtmlElement[]
*/
protected function createStatistics(): array
{
$serviceStats = new ServiceStatistics($this->item);
$serviceStats->setBaseFilter(Filter::equal('servicegroup.name', $this->item->name));
if (isset($this->table) && $this->table->hasBaseFilter()) {
$serviceStats->setBaseFilter(
Filter::all($serviceStats->getBaseFilter(), $this->table->getBaseFilter())
);
}
return [$this->createColumn($serviceStats)];
}
}

Some files were not shown because too many files have changed in this diff Show more