diff options
Diffstat (limited to 'library/Icingadb/Widget/ItemTable')
-rw-r--r-- | library/Icingadb/Widget/ItemTable/BaseItemTable.php | 198 | ||||
-rw-r--r-- | library/Icingadb/Widget/ItemTable/BaseRowItem.php | 106 | ||||
-rw-r--r-- | library/Icingadb/Widget/ItemTable/HostItemTable.php | 31 | ||||
-rw-r--r-- | library/Icingadb/Widget/ItemTable/HostRowItem.php | 51 | ||||
-rw-r--r-- | library/Icingadb/Widget/ItemTable/ServiceItemTable.php | 31 | ||||
-rw-r--r-- | library/Icingadb/Widget/ItemTable/ServiceRowItem.php | 64 | ||||
-rw-r--r-- | library/Icingadb/Widget/ItemTable/StateItemTable.php | 35 | ||||
-rw-r--r-- | library/Icingadb/Widget/ItemTable/StateRowItem.php | 139 |
8 files changed, 655 insertions, 0 deletions
diff --git a/library/Icingadb/Widget/ItemTable/BaseItemTable.php b/library/Icingadb/Widget/ItemTable/BaseItemTable.php new file mode 100644 index 0000000..d8fd85b --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/BaseItemTable.php @@ -0,0 +1,198 @@ +<?php + +/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use Icinga\Module\Icingadb\Widget\EmptyState; +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\Icon; + +abstract class BaseItemTable extends BaseHtmlElement +{ + protected $baseAttributes = [ + 'class' => '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; + + abstract protected function getVisualLabel(); + + 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); + } + + 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 EmptyState(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/BaseRowItem.php b/library/Icingadb/Widget/ItemTable/BaseRowItem.php new file mode 100644 index 0000000..0189619 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/BaseRowItem.php @@ -0,0 +1,106 @@ +<?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; + +abstract class BaseRowItem extends BaseHtmlElement +{ + protected $defaultAttributes = ['class' => 'row-item']; + + /** @var Model */ + protected $item; + + /** @var BaseItemTable */ + protected $list; + + protected $tag = 'tr'; + + /** + * Create a new row item + * + * @param Model $item + * @param BaseItemTable $list + */ + public function __construct(Model $item, BaseItemTable $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/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/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/StateItemTable.php b/library/Icingadb/Widget/ItemTable/StateItemTable.php new file mode 100644 index 0000000..5f9b38a --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/StateItemTable.php @@ -0,0 +1,35 @@ +<?php + +/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use ipl\Html\BaseHtmlElement; +use ipl\Web\Widget\Icon; + +abstract class StateItemTable extends BaseItemTable +{ + protected function getVisualLabel() + { + return new Icon('heartbeat', ['title' => t('Severity')]); + } + + protected function assembleColumnHeader(BaseHtmlElement $header, string $name, $label): void + { + parent::assembleColumnHeader($header, $name, $label); + + 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; + } + } +} diff --git a/library/Icingadb/Widget/ItemTable/StateRowItem.php b/library/Icingadb/Widget/ItemTable/StateRowItem.php new file mode 100644 index 0000000..6e2cb5c --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/StateRowItem.php @@ -0,0 +1,139 @@ +<?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\Icons; +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\EmptyState; +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\Icon; +use ipl\Web\Widget\StateBall; +use ipl\Web\Widget\TimeSince; +use ipl\Web\Widget\TimeUntil; + +abstract class StateRowItem extends BaseRowItem +{ + /** @var StateItemTable */ + protected $list; + + protected function getHandledIcon(): string + { + switch (true) { + case $this->item->state->in_downtime: + return Icons::IN_DOWNTIME; + case $this->item->state->is_acknowledged: + return Icons::IS_ACKNOWLEDGED; + case $this->item->state->is_flapping: + return Icons::IS_FLAPPING; + default: + return Icons::HOST_DOWN; + } + } + + protected function assembleVisual(BaseHtmlElement $visual) + { + $stateBall = new StateBall($this->item->state->getStateText(), StateBall::SIZE_LARGE); + + if ($this->item->state->is_handled) { + $stateBall->addHtml(new Icon($this->getHandledIcon())); + $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)); + break; + case $path === 'state.next_check': + case $path === 'state.next_update': + $column = substr($path, 6); + $cell->addHtml(new TimeUntil($this->item->state->$column)); + 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)); + } + } +} |