summaryrefslogtreecommitdiffstats
path: root/library/Icingadb/Widget/ItemTable
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icingadb/Widget/ItemTable')
-rw-r--r--library/Icingadb/Widget/ItemTable/BaseHostGroupItem.php60
-rw-r--r--library/Icingadb/Widget/ItemTable/BaseServiceGroupItem.php60
-rw-r--r--library/Icingadb/Widget/ItemTable/BaseStateRowItem.php107
-rw-r--r--library/Icingadb/Widget/ItemTable/GridCellLayout.php39
-rw-r--r--library/Icingadb/Widget/ItemTable/HostItemTable.php31
-rw-r--r--library/Icingadb/Widget/ItemTable/HostRowItem.php51
-rw-r--r--library/Icingadb/Widget/ItemTable/HostgroupGridCell.php114
-rw-r--r--library/Icingadb/Widget/ItemTable/HostgroupTable.php38
-rw-r--r--library/Icingadb/Widget/ItemTable/HostgroupTableRow.php55
-rw-r--r--library/Icingadb/Widget/ItemTable/ServiceItemTable.php31
-rw-r--r--library/Icingadb/Widget/ItemTable/ServiceRowItem.php64
-rw-r--r--library/Icingadb/Widget/ItemTable/ServicegroupGridCell.php204
-rw-r--r--library/Icingadb/Widget/ItemTable/ServicegroupTable.php38
-rw-r--r--library/Icingadb/Widget/ItemTable/ServicegroupTableRow.php42
-rw-r--r--library/Icingadb/Widget/ItemTable/StateItemTable.php216
-rw-r--r--library/Icingadb/Widget/ItemTable/StateRowItem.php124
-rw-r--r--library/Icingadb/Widget/ItemTable/TableRowLayout.php26
-rw-r--r--library/Icingadb/Widget/ItemTable/UserTable.php27
-rw-r--r--library/Icingadb/Widget/ItemTable/UserTableRow.php61
-rw-r--r--library/Icingadb/Widget/ItemTable/UsergroupTable.php27
-rw-r--r--library/Icingadb/Widget/ItemTable/UsergroupTableRow.php61
21 files changed, 1476 insertions, 0 deletions
diff --git a/library/Icingadb/Widget/ItemTable/BaseHostGroupItem.php b/library/Icingadb/Widget/ItemTable/BaseHostGroupItem.php
new file mode 100644
index 0000000..c56a1f8
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/BaseHostGroupItem.php
@@ -0,0 +1,60 @@
+<?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\Hostgroup;
+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 Hostgroup $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
+ {
+ return isset($this->table)
+ ? 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
+ )
+ ]
+ )
+ : 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));
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/BaseServiceGroupItem.php b/library/Icingadb/Widget/ItemTable/BaseServiceGroupItem.php
new file mode 100644
index 0000000..7bee532
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/BaseServiceGroupItem.php
@@ -0,0 +1,60 @@
+<?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\Servicegroup;
+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 Servicegroup $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
+ {
+ return isset($this->table)
+ ? 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
+ )
+ ]
+ )
+ : 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));
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/BaseStateRowItem.php b/library/Icingadb/Widget/ItemTable/BaseStateRowItem.php
new file mode 100644
index 0000000..642d6b3
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/BaseStateRowItem.php
@@ -0,0 +1,107 @@
+<?php
+
+/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Widget\ItemTable;
+
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\HtmlElement;
+use ipl\Orm\Model;
+
+/** @todo Figure out what this might (should) have in common with the new BaseTableRowItem implementation */
+abstract class BaseStateRowItem extends BaseHtmlElement
+{
+ protected $defaultAttributes = ['class' => 'row-item'];
+
+ /** @var Model */
+ protected $item;
+
+ /** @var StateItemTable */
+ protected $list;
+
+ protected $tag = 'tr';
+
+ /**
+ * Create a new row item
+ *
+ * @param Model $item
+ * @param StateItemTable $list
+ */
+ public function __construct(Model $item, StateItemTable $list)
+ {
+ $this->item = $item;
+ $this->list = $list;
+
+ $this->init();
+ }
+
+ /**
+ * Initialize the row item
+ *
+ * If you want to adjust the row item after construction, override this method.
+ */
+ protected function init()
+ {
+ }
+
+ abstract protected function assembleVisual(BaseHtmlElement $visual);
+
+ abstract protected function assembleCell(BaseHtmlElement $cell, string $path, $value);
+
+ protected function createVisual(): BaseHtmlElement
+ {
+ $visual = new HtmlElement('td', Attributes::create(['class' => 'visual']));
+
+ $this->assembleVisual($visual);
+
+ return $visual;
+ }
+
+ protected function assemble()
+ {
+ $this->addHtml($this->createVisual());
+
+ foreach ($this->list->getColumns() as $columnPath => $_) {
+ $steps = explode('.', $columnPath);
+ if ($steps[0] === $this->item->getTableName()) {
+ array_shift($steps);
+ $columnPath = implode('.', $steps);
+ }
+
+ $column = null;
+ $subject = $this->item;
+ foreach ($steps as $i => $step) {
+ if (isset($subject->$step)) {
+ if ($subject->$step instanceof Model) {
+ $subject = $subject->$step;
+ } else {
+ $column = $step;
+ }
+ } else {
+ $columnCandidate = implode('.', array_slice($steps, $i));
+ if (isset($subject->$columnCandidate)) {
+ $column = $columnCandidate;
+ } else {
+ break;
+ }
+ }
+ }
+
+ $value = null;
+ if ($column !== null) {
+ $value = $subject->$column;
+ if (is_array($value)) {
+ $value = empty($value) ? null : implode(',', $value);
+ }
+ }
+
+ $cell = new HtmlElement('td');
+ if ($value !== null) {
+ $this->assembleCell($cell, $columnPath, $value);
+ }
+
+ $this->addHtml($cell);
+ }
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/GridCellLayout.php b/library/Icingadb/Widget/ItemTable/GridCellLayout.php
new file mode 100644
index 0000000..95b1a0a
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/GridCellLayout.php
@@ -0,0 +1,39 @@
+<?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()
+ ]);
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/HostItemTable.php b/library/Icingadb/Widget/ItemTable/HostItemTable.php
new file mode 100644
index 0000000..e303746
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/HostItemTable.php
@@ -0,0 +1,31 @@
+<?php
+
+/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Widget\ItemTable;
+
+use Icinga\Module\Icingadb\Common\DetailActions;
+use Icinga\Module\Icingadb\Common\Links;
+use ipl\Web\Url;
+
+class HostItemTable extends StateItemTable
+{
+ use DetailActions;
+
+ protected function init()
+ {
+ $this->initializeDetailActions();
+ $this->setMultiselectUrl(Links::hostsDetails());
+ $this->setDetailUrl(Url::fromPath('icingadb/host'));
+ }
+
+ protected function getItemClass(): string
+ {
+ return HostRowItem::class;
+ }
+
+ protected function getVisualColumn(): string
+ {
+ return 'host.state.severity';
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/HostRowItem.php b/library/Icingadb/Widget/ItemTable/HostRowItem.php
new file mode 100644
index 0000000..cff70dd
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/HostRowItem.php
@@ -0,0 +1,51 @@
+<?php
+
+/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Widget\ItemTable;
+
+use Icinga\Module\Icingadb\Common\Links;
+use Icinga\Module\Icingadb\Model\Host;
+use ipl\Html\BaseHtmlElement;
+use ipl\Stdlib\Filter;
+use ipl\Web\Widget\Link;
+
+class HostRowItem extends StateRowItem
+{
+ /** @var HostItemTable */
+ protected $list;
+
+ /** @var Host */
+ protected $item;
+
+ protected function init()
+ {
+ parent::init();
+
+ $this->list->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name))
+ ->addMultiselectFilterAttribute($this, Filter::equal('host.name', $this->item->name));
+ }
+
+ protected function assembleCell(BaseHtmlElement $cell, string $path, $value)
+ {
+ switch ($path) {
+ case 'name':
+ case 'display_name':
+ $cell->addHtml(new Link($this->item->$path, Links::host($this->item), [
+ 'class' => 'subject',
+ 'title' => $this->item->$path
+ ]));
+ break;
+ case 'service.name':
+ case 'service.display_name':
+ $column = substr($path, 8);
+ $cell->addHtml(new Link(
+ $this->item->service->$column,
+ Links::service($this->item->service, $this->item)
+ ));
+ break;
+ default:
+ parent::assembleCell($cell, $path, $value);
+ }
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/HostgroupGridCell.php b/library/Icingadb/Widget/ItemTable/HostgroupGridCell.php
new file mode 100644
index 0000000..5396747
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/HostgroupGridCell.php
@@ -0,0 +1,114 @@
+<?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
+ )
+ ]
+ );
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/HostgroupTable.php b/library/Icingadb/Widget/ItemTable/HostgroupTable.php
new file mode 100644
index 0000000..6b40f76
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/HostgroupTable.php
@@ -0,0 +1,38 @@
+<?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;
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/HostgroupTableRow.php b/library/Icingadb/Widget/ItemTable/HostgroupTableRow.php
new file mode 100644
index 0000000..6aa61c2
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/HostgroupTableRow.php
@@ -0,0 +1,55 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Widget\ItemTable;
+
+use Icinga\Module\Icingadb\Model\Hostgroup;
+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 Hostgroup $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)
+ ];
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/ServiceItemTable.php b/library/Icingadb/Widget/ItemTable/ServiceItemTable.php
new file mode 100644
index 0000000..60872d8
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/ServiceItemTable.php
@@ -0,0 +1,31 @@
+<?php
+
+/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Widget\ItemTable;
+
+use Icinga\Module\Icingadb\Common\DetailActions;
+use Icinga\Module\Icingadb\Common\Links;
+use ipl\Web\Url;
+
+class ServiceItemTable extends StateItemTable
+{
+ use DetailActions;
+
+ protected function init()
+ {
+ $this->initializeDetailActions();
+ $this->setMultiselectUrl(Links::servicesDetails());
+ $this->setDetailUrl(Url::fromPath('icingadb/service'));
+ }
+
+ protected function getItemClass(): string
+ {
+ return ServiceRowItem::class;
+ }
+
+ protected function getVisualColumn(): string
+ {
+ return 'service.state.severity';
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/ServiceRowItem.php b/library/Icingadb/Widget/ItemTable/ServiceRowItem.php
new file mode 100644
index 0000000..0fb95d0
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/ServiceRowItem.php
@@ -0,0 +1,64 @@
+<?php
+
+/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Widget\ItemTable;
+
+use Icinga\Module\Icingadb\Common\Links;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\Html\BaseHtmlElement;
+use ipl\Stdlib\Filter;
+use ipl\Web\Widget\Link;
+
+class ServiceRowItem extends StateRowItem
+{
+ /** @var ServiceItemTable */
+ protected $list;
+
+ /** @var Service */
+ protected $item;
+
+ protected function init()
+ {
+ parent::init();
+
+ $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)
+ )
+ );
+ }
+
+ protected function assembleCell(BaseHtmlElement $cell, string $path, $value)
+ {
+ switch ($path) {
+ case 'name':
+ case 'display_name':
+ $cell->addHtml(new Link(
+ $this->item->$path,
+ Links::service($this->item, $this->item->host),
+ [
+ 'class' => 'subject',
+ 'title' => $this->item->$path
+ ]
+ ));
+ break;
+ case 'host.name':
+ case 'host.display_name':
+ $column = substr($path, 5);
+ $cell->addHtml(new Link($this->item->host->$column, Links::host($this->item->host)));
+ break;
+ default:
+ parent::assembleCell($cell, $path, $value);
+ }
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/ServicegroupGridCell.php b/library/Icingadb/Widget/ItemTable/ServicegroupGridCell.php
new file mode 100644
index 0000000..16e50e1
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/ServicegroupGridCell.php
@@ -0,0 +1,204 @@
+<?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 ServicegroupGridCell extends BaseServiceGroupItem
+{
+ use GridCellLayout;
+
+ protected $defaultAttributes = ['class' => ['group-grid-cell', 'servicegroup-grid-cell']];
+
+ protected function createGroupBadge(): Link
+ {
+ $url = Url::fromPath('icingadb/services/grid');
+ $urlFilter = Filter::all(Filter::equal('servicegroup.name', $this->item->name));
+
+ if ($this->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'),
+ $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
+ ),
+ $this->item->services_critical_unhandled,
+ $this->item->display_name
+ )
+ ]
+ );
+ } elseif ($this->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),
+ $url->setFilter($urlFilter),
+ [
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %d service that is currently in CRITICAL (Acknowledged) state in service group'
+ . ' "%s"',
+ 'List %d services which are currently in CRITICAL (Acknowledged) state in service group'
+ . ' "%s"',
+ $this->item->services_critical_handled
+ ),
+ $this->item->services_critical_handled,
+ $this->item->display_name
+ )
+ ]
+ );
+ } elseif ($this->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'),
+ $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
+ ),
+ $this->item->services_warning_unhandled,
+ $this->item->display_name
+ )
+ ]
+ );
+ } elseif ($this->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),
+ $url->setFilter($urlFilter),
+ [
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %d service that is currently in WARNING (Acknowledged) state in service group'
+ . ' "%s"',
+ 'List %d services which are currently in WARNING (Acknowledged) state in service group'
+ . ' "%s"',
+ $this->item->services_warning_handled
+ ),
+ $this->item->services_warning_handled,
+ $this->item->display_name
+ )
+ ]
+ );
+ } elseif ($this->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'),
+ $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
+ ),
+ $this->item->services_unknown_unhandled,
+ $this->item->display_name
+ )
+ ]
+ );
+ } elseif ($this->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),
+ $url->setFilter($urlFilter),
+ [
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %d service that is currently in UNKNOWN (Acknowledged) state in service group'
+ . ' "%s"',
+ 'List %d services which are currently in UNKNOWN (Acknowledged) state in service group'
+ . ' "%s"',
+ $this->item->services_unknown_handled
+ ),
+ $this->item->services_unknown_handled,
+ $this->item->display_name
+ )
+ ]
+ );
+ } elseif ($this->item->services_pending > 0) {
+ $urlFilter->add(Filter::equal('service.state.soft_state', 99));
+
+ return new Link(
+ new StateBadge($this->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
+ ),
+ $this->item->services_pending,
+ $this->item->display_name
+ )
+ ]
+ );
+ } elseif ($this->item->services_ok > 0) {
+ $urlFilter->add(Filter::equal('service.state.soft_state', 0));
+
+ return new Link(
+ new StateBadge($this->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
+ ),
+ $this->item->services_ok,
+ $this->item->display_name
+ )
+ ]
+ );
+ }
+
+ return new Link(
+ new StateBadge(0, 'none'),
+ $url,
+ [
+ 'title' => sprintf(
+ $this->translate('There are no services in service group "%s"'),
+ $this->item->display_name
+ )
+ ]
+ );
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/ServicegroupTable.php b/library/Icingadb/Widget/ItemTable/ServicegroupTable.php
new file mode 100644
index 0000000..2378a77
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/ServicegroupTable.php
@@ -0,0 +1,38 @@
+<?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;
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/ServicegroupTableRow.php b/library/Icingadb/Widget/ItemTable/ServicegroupTableRow.php
new file mode 100644
index 0000000..3dea4c1
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/ServicegroupTableRow.php
@@ -0,0 +1,42 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Widget\ItemTable;
+
+use Icinga\Module\Icingadb\Model\Servicegroup;
+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 Servicegroup $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)];
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/StateItemTable.php b/library/Icingadb/Widget/ItemTable/StateItemTable.php
new file mode 100644
index 0000000..f392322
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/StateItemTable.php
@@ -0,0 +1,216 @@
+<?php
+
+/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Widget\ItemTable;
+
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Form;
+use ipl\Html\Html;
+use ipl\Html\HtmlElement;
+use ipl\Html\HtmlString;
+use ipl\Orm\Common\SortUtil;
+use ipl\Orm\Query;
+use ipl\Web\Control\SortControl;
+use ipl\Web\Widget\EmptyStateBar;
+use ipl\Web\Widget\Icon;
+
+/** @todo Figure out what this might (should) have in common with the new BaseItemTable implementation */
+abstract class StateItemTable extends BaseHtmlElement
+{
+ protected $baseAttributes = [
+ 'class' => 'state-item-table'
+ ];
+
+ /** @var array<string, string> The columns to render */
+ protected $columns;
+
+ /** @var iterable The datasource */
+ protected $data;
+
+ /** @var string The sort rules */
+ protected $sort;
+
+ protected $tag = 'table';
+
+ /**
+ * Create a new item table
+ *
+ * @param iterable $data Datasource of the table
+ * @param array<string, string> $columns The columns to render, keys are labels
+ */
+ public function __construct(iterable $data, array $columns)
+ {
+ $this->data = $data;
+ $this->columns = array_flip($columns);
+
+ $this->addAttributes($this->baseAttributes);
+
+ $this->init();
+ }
+
+ /**
+ * Initialize the item table
+ *
+ * If you want to adjust the item table after construction, override this method.
+ */
+ protected function init()
+ {
+ }
+
+ /**
+ * Get the columns being rendered
+ *
+ * @return array<string, string>
+ */
+ public function getColumns(): array
+ {
+ return $this->columns;
+ }
+
+ /**
+ * Set sort rules (as returned by {@see SortControl::getSort()})
+ *
+ * @param ?string $sort
+ *
+ * @return $this
+ */
+ public function setSort(?string $sort): self
+ {
+ $this->sort = $sort;
+
+ return $this;
+ }
+
+ abstract protected function getItemClass(): string;
+
+ abstract protected function getVisualColumn(): string;
+
+ protected function getVisualLabel()
+ {
+ return new Icon('heartbeat', ['title' => t('Severity')]);
+ }
+
+ protected function assembleColumnHeader(BaseHtmlElement $header, string $name, $label): void
+ {
+ $sortRules = [];
+ if ($this->sort !== null) {
+ $sortRules = SortUtil::createOrderBy($this->sort);
+ }
+
+ $active = false;
+ $sortDirection = null;
+ foreach ($sortRules as $rule) {
+ if ($rule[0] === $name) {
+ $sortDirection = $rule[1];
+ $active = true;
+ break;
+ }
+ }
+
+ if ($sortDirection === 'desc') {
+ $value = "$name asc";
+ } else {
+ $value = "$name desc";
+ }
+
+ $icon = 'sort';
+ if ($active) {
+ $icon = $sortDirection === 'desc' ? 'sort-up' : 'sort-down';
+ }
+
+ $form = new Form();
+ $form->setAttribute('method', 'GET');
+
+ $button = $form->createElement('button', 'sort', [
+ 'value' => $value,
+ 'type' => 'submit',
+ 'title' => is_string($label) ? $label : null,
+ 'class' => $active ? 'active' : null
+ ]);
+ $button->addHtml(
+ Html::tag(
+ 'span',
+ null,
+ // With &nbsp; to have the height sized the same as the others
+ $label ?? HtmlString::create('&nbsp;')
+ ),
+ new Icon($icon)
+ );
+ $form->addElement($button);
+
+ $header->add($form);
+
+ switch (true) {
+ case substr($name, -7) === '.output':
+ case substr($name, -12) === '.long_output':
+ $header->getAttributes()->add('class', 'has-plugin-output');
+ break;
+ case substr($name, -22) === '.icon_image.icon_image':
+ $header->getAttributes()->add('class', 'has-icon-images');
+ break;
+ case substr($name, -17) === '.performance_data':
+ case substr($name, -28) === '.normalized_performance_data':
+ $header->getAttributes()->add('class', 'has-performance-data');
+ break;
+ }
+ }
+
+ protected function assemble()
+ {
+ $itemClass = $this->getItemClass();
+
+ $headerRow = new HtmlElement('tr');
+
+ $visualCell = new HtmlElement('th', Attributes::create(['class' => 'has-visual']));
+ $this->assembleColumnHeader($visualCell, $this->getVisualColumn(), $this->getVisualLabel());
+ $headerRow->addHtml($visualCell);
+
+ foreach ($this->columns as $name => $label) {
+ $headerCell = new HtmlElement('th');
+ $this->assembleColumnHeader($headerCell, $name, is_int($label) ? $name : $label);
+ $headerRow->addHtml($headerCell);
+ }
+
+ $this->addHtml(new HtmlElement('thead', null, $headerRow));
+
+ $body = new HtmlElement('tbody', Attributes::create(['data-base-target' => '_next']));
+ foreach ($this->data as $item) {
+ $body->addHtml(new $itemClass($item, $this));
+ }
+
+ if ($body->isEmpty()) {
+ $body->addHtml(new HtmlElement(
+ 'tr',
+ null,
+ new HtmlElement(
+ 'td',
+ Attributes::create(['colspan' => count($this->columns)]),
+ new EmptyStateBar(t('No items found.'))
+ )
+ ));
+ }
+
+ $this->addHtml($body);
+ }
+
+ /**
+ * Enrich the given list of column names with appropriate labels
+ *
+ * @param Query $query
+ * @param array $columns
+ *
+ * @return array
+ */
+ public static function applyColumnMetaData(Query $query, array $columns): array
+ {
+ $newColumns = [];
+ foreach ($columns as $columnPath) {
+ $label = $query->getResolver()->getColumnDefinition($columnPath)->getLabel();
+ $newColumns[$label ?? $columnPath] = $columnPath;
+ }
+
+ return $newColumns;
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/StateRowItem.php b/library/Icingadb/Widget/ItemTable/StateRowItem.php
new file mode 100644
index 0000000..f62286b
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/StateRowItem.php
@@ -0,0 +1,124 @@
+<?php
+
+/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Widget\ItemTable;
+
+use Icinga\Module\Icingadb\Common\HostStates;
+use Icinga\Module\Icingadb\Common\ServiceStates;
+use Icinga\Module\Icingadb\Model\Host;
+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 ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\HtmlElement;
+use ipl\Html\HtmlString;
+use ipl\Html\Text;
+use ipl\Web\Widget\EmptyState;
+use ipl\Web\Widget\Icon;
+use ipl\Web\Widget\StateBall;
+use ipl\Web\Widget\TimeSince;
+use ipl\Web\Widget\TimeUntil;
+
+abstract class StateRowItem extends BaseStateRowItem
+{
+ /** @var StateItemTable */
+ protected $list;
+
+ protected function assembleVisual(BaseHtmlElement $visual)
+ {
+ $stateBall = new StateBall($this->item->state->getStateText(), StateBall::SIZE_LARGE);
+ $stateBall->add($this->item->state->getIcon());
+
+ if ($this->item->state->is_handled) {
+ $stateBall->getAttributes()->add('class', 'handled');
+ }
+
+ $visual->addHtml($stateBall);
+ if ($this->item->state->state_type === 'soft') {
+ $visual->addHtml(new CheckAttempt(
+ (int) $this->item->state->check_attempt,
+ (int) $this->item->max_check_attempts
+ ));
+ }
+ }
+
+ protected function assembleCell(BaseHtmlElement $cell, string $path, $value)
+ {
+ switch (true) {
+ case $path === 'state.output':
+ case $path === 'state.long_output':
+ if (empty($value)) {
+ $pluginOutput = new EmptyState(t('Output unavailable.'));
+ } else {
+ $pluginOutput = new PluginOutputContainer(PluginOutput::fromObject($this->item));
+ }
+
+ $cell->addHtml($pluginOutput)
+ ->getAttributes()
+ ->add('class', 'has-plugin-output');
+ break;
+ case $path === 'state.soft_state':
+ case $path === 'state.hard_state':
+ case $path === 'state.previous_soft_state':
+ case $path === 'state.previous_hard_state':
+ $stateType = substr($path, 6);
+ if ($this->item instanceof Host) {
+ $stateName = HostStates::translated($this->item->state->$stateType);
+ } else {
+ $stateName = ServiceStates::translated($this->item->state->$stateType);
+ }
+
+ $cell->addHtml(Text::create($stateName));
+ break;
+ case $path === 'state.last_update':
+ case $path === 'state.last_state_change':
+ $column = substr($path, 6);
+ $cell->addHtml(new TimeSince($this->item->state->$column->getTimestamp()));
+ break;
+ case $path === 'state.next_check':
+ case $path === 'state.next_update':
+ $column = substr($path, 6);
+ $cell->addHtml(new TimeUntil($this->item->state->$column->getTimestamp()));
+ break;
+ case $path === 'state.performance_data':
+ case $path === 'state.normalized_performance_data':
+ $perfdataContainer = new HtmlElement('div', Attributes::create(['class' => 'performance-data']));
+
+ $pieChartData = PerfDataSet::fromString($this->item->state->normalized_performance_data)->asArray();
+ foreach ($pieChartData as $perfdata) {
+ if ($perfdata->isVisualizable()) {
+ $perfdataContainer->addHtml(new HtmlString($perfdata->asInlinePie()->render()));
+ }
+ }
+
+ $cell->addHtml($perfdataContainer)
+ ->getAttributes()
+ ->add('class', 'has-performance-data');
+ break;
+ case $path === 'is_volatile':
+ case $path === 'host.is_volatile':
+ case substr($path, -8) == '_enabled':
+ case (bool) preg_match('/state\.(is_|in_)/', $path):
+ if ($value) {
+ $cell->addHtml(new Icon('check'));
+ }
+
+ break;
+ case $path === 'icon_image.icon_image':
+ $cell->addHtml(new IconImage($value, $this->item->icon_image_alt))
+ ->getAttributes()
+ ->add('class', 'has-icon-images');
+ break;
+ default:
+ if (preg_match('/(^id|_id|.id|_checksum|_bin)$/', $path)) {
+ $value = bin2hex($value);
+ }
+
+ $cell->addHtml(Text::create($value));
+ }
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/TableRowLayout.php b/library/Icingadb/Widget/ItemTable/TableRowLayout.php
new file mode 100644
index 0000000..b9ce022
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/TableRowLayout.php
@@ -0,0 +1,26 @@
+<?php
+
+/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Widget\ItemTable;
+
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\HtmlDocument;
+
+trait TableRowLayout
+{
+ protected function assembleColumns(HtmlDocument $columns): void
+ {
+ foreach ($this->createStatistics() as $objectStatistic) {
+ $columns->addHtml($objectStatistic);
+ }
+ }
+
+ protected function assembleTitle(BaseHtmlElement $title): void
+ {
+ $title->addHtml(
+ $this->createSubject(),
+ $this->createCaption()
+ );
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/UserTable.php b/library/Icingadb/Widget/ItemTable/UserTable.php
new file mode 100644
index 0000000..432817b
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/UserTable.php
@@ -0,0 +1,27 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Widget\ItemTable;
+
+use Icinga\Module\Icingadb\Common\DetailActions;
+use ipl\Web\Common\BaseItemTable;
+use ipl\Web\Url;
+
+class UserTable extends BaseItemTable
+{
+ use DetailActions;
+
+ protected $defaultAttributes = ['class' => 'user-table'];
+
+ protected function init(): void
+ {
+ $this->initializeDetailActions();
+ $this->setDetailUrl(Url::fromPath('icingadb/user'));
+ }
+
+ protected function getItemClass(): string
+ {
+ return UserTableRow::class;
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/UserTableRow.php b/library/Icingadb/Widget/ItemTable/UserTableRow.php
new file mode 100644
index 0000000..c10851e
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/UserTableRow.php
@@ -0,0 +1,61 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Widget\ItemTable;
+
+use Icinga\Module\Icingadb\Common\Links;
+use Icinga\Module\Icingadb\Model\User;
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\HtmlDocument;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\Stdlib\Filter;
+use ipl\Web\Common\BaseTableRowItem;
+use ipl\Web\Widget\Link;
+
+/**
+ * User item of a user list. Represents one database row.
+ *
+ * @property User $item
+ * @property UserTable $table
+ */
+class UserTableRow extends BaseTableRowItem
+{
+ protected $defaultAttributes = ['class' => 'user-table-row'];
+
+ protected function init(): void
+ {
+ if (isset($this->table)) {
+ $this->table->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name));
+ }
+ }
+
+ protected function assembleVisual(BaseHtmlElement $visual): void
+ {
+ $visual->addHtml(new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'user-ball']),
+ Text::create($this->item->display_name[0])
+ ));
+ }
+
+ protected function assembleTitle(BaseHtmlElement $title): void
+ {
+ $title->addHtml(
+ isset($this->table)
+ ? new Link($this->item->display_name, Links::user($this->item), ['class' => 'subject'])
+ : new HtmlElement(
+ 'span',
+ Attributes::create(['class' => 'subject']),
+ Text::create($this->item->display_name)
+ ),
+ new HtmlElement('span', null, Text::create($this->item->name))
+ );
+ }
+
+ protected function assembleColumns(HtmlDocument $columns): void
+ {
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/UsergroupTable.php b/library/Icingadb/Widget/ItemTable/UsergroupTable.php
new file mode 100644
index 0000000..77d3ba9
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/UsergroupTable.php
@@ -0,0 +1,27 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Widget\ItemTable;
+
+use Icinga\Module\Icingadb\Common\DetailActions;
+use ipl\Web\Common\BaseItemTable;
+use ipl\Web\Url;
+
+class UsergroupTable extends BaseItemTable
+{
+ use DetailActions;
+
+ protected $defaultAttributes = ['class' => 'usergroup-table'];
+
+ protected function init(): void
+ {
+ $this->initializeDetailActions();
+ $this->setDetailUrl(Url::fromPath('icingadb/usergroup'));
+ }
+
+ protected function getItemClass(): string
+ {
+ return UsergroupTableRow::class;
+ }
+}
diff --git a/library/Icingadb/Widget/ItemTable/UsergroupTableRow.php b/library/Icingadb/Widget/ItemTable/UsergroupTableRow.php
new file mode 100644
index 0000000..c3cbf74
--- /dev/null
+++ b/library/Icingadb/Widget/ItemTable/UsergroupTableRow.php
@@ -0,0 +1,61 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Widget\ItemTable;
+
+use Icinga\Module\Icingadb\Common\Links;
+use Icinga\Module\Icingadb\Model\Usergroup;
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\HtmlDocument;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\Stdlib\Filter;
+use ipl\Web\Common\BaseTableRowItem;
+use ipl\Web\Widget\Link;
+
+/**
+ * Usergroup item of a usergroup list. Represents one database row.
+ *
+ * @property Usergroup $item
+ * @property UsergroupTable $table
+ */
+class UsergroupTableRow extends BaseTableRowItem
+{
+ protected $defaultAttributes = ['class' => 'usergroup-table-row'];
+
+ protected function init(): void
+ {
+ if (isset($this->table)) {
+ $this->table->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name));
+ }
+ }
+
+ protected function assembleVisual(BaseHtmlElement $visual): void
+ {
+ $visual->addHtml(new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'usergroup-ball']),
+ Text::create($this->item->display_name[0])
+ ));
+ }
+
+ protected function assembleTitle(BaseHtmlElement $title): void
+ {
+ $title->addHtml(
+ isset($this->table)
+ ? new Link($this->item->display_name, Links::usergroup($this->item), ['class' => 'subject'])
+ : new HtmlElement(
+ 'span',
+ Attributes::create(['class' => 'subject']),
+ Text::create($this->item->display_name)
+ ),
+ new HtmlElement('span', null, Text::create($this->item->name))
+ );
+ }
+
+ protected function assembleColumns(HtmlDocument $columns): void
+ {
+ }
+}