summaryrefslogtreecommitdiffstats
path: root/library/Icingadb/Widget/ItemTable
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:36:40 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:36:40 +0000
commita0901c4b7f2db488cb4fb3be2dd921a0308f4659 (patch)
treefafb393cf330a60df129ff10d0059eb7b14052a7 /library/Icingadb/Widget/ItemTable
parentInitial commit. (diff)
downloadicingadb-web-a0901c4b7f2db488cb4fb3be2dd921a0308f4659.tar.xz
icingadb-web-a0901c4b7f2db488cb4fb3be2dd921a0308f4659.zip
Adding upstream version 1.0.2.upstream/1.0.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Icingadb/Widget/ItemTable')
-rw-r--r--library/Icingadb/Widget/ItemTable/BaseItemTable.php198
-rw-r--r--library/Icingadb/Widget/ItemTable/BaseRowItem.php106
-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/ServiceItemTable.php31
-rw-r--r--library/Icingadb/Widget/ItemTable/ServiceRowItem.php64
-rw-r--r--library/Icingadb/Widget/ItemTable/StateItemTable.php35
-rw-r--r--library/Icingadb/Widget/ItemTable/StateRowItem.php139
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 &nbsp; to have the height sized the same as the others
+ $label ?? HtmlString::create('&nbsp;')
+ ),
+ 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));
+ }
+ }
+}