diff options
Diffstat (limited to '')
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 to have the height sized the same as the others + $label ?? HtmlString::create(' ') + ), + 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 + { + } +} |