summaryrefslogtreecommitdiffstats
path: root/library
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:42:52 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:42:52 +0000
commitda0f8427204ad57aad08059906df0ea10a7ccf31 (patch)
treeccda92692e296a50dac104945f62b4402d5e4447 /library
parentInitial commit. (diff)
downloadicingaweb2-module-cube-da0f8427204ad57aad08059906df0ea10a7ccf31.tar.xz
icingaweb2-module-cube-da0f8427204ad57aad08059906df0ea10a7ccf31.zip
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library')
-rw-r--r--library/Cube/Cube.php323
-rw-r--r--library/Cube/CubeRenderer.php406
-rw-r--r--library/Cube/CubeRenderer/HostStatusCubeRenderer.php105
-rw-r--r--library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php126
-rw-r--r--library/Cube/Dimension.php52
-rw-r--r--library/Cube/DimensionParams.php87
-rw-r--r--library/Cube/Hook/ActionsHook.php99
-rw-r--r--library/Cube/Hook/IcingaDbActionsHook.php125
-rw-r--r--library/Cube/IcingaDb/CustomVariableDimension.php119
-rw-r--r--library/Cube/IcingaDb/IcingaDbCube.php196
-rw-r--r--library/Cube/IcingaDb/IcingaDbHostStatusCube.php100
-rw-r--r--library/Cube/IcingaDb/IcingaDbServiceStatusCube.php109
-rw-r--r--library/Cube/Ido/CustomVarDimension.php158
-rw-r--r--library/Cube/Ido/DataView/Hoststatus.php17
-rw-r--r--library/Cube/Ido/DbCube.php298
-rw-r--r--library/Cube/Ido/IdoCube.php198
-rw-r--r--library/Cube/Ido/IdoHostStatusCube.php112
-rw-r--r--library/Cube/Ido/IdoServiceStatusCube.php113
-rw-r--r--library/Cube/Ido/Query/HoststatusQuery.php47
-rw-r--r--library/Cube/Ido/ZfSelectWrapper.php77
-rw-r--r--library/Cube/ProvidedHook/Cube/IcingaDbActions.php41
-rw-r--r--library/Cube/ProvidedHook/Cube/MonitoringActions.php53
-rw-r--r--library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php11
-rw-r--r--library/Cube/Web/ActionLink.php103
-rw-r--r--library/Cube/Web/ActionLinks.php115
-rw-r--r--library/Cube/Web/Controller.php115
-rw-r--r--library/Cube/Web/IdoController.php24
27 files changed, 3329 insertions, 0 deletions
diff --git a/library/Cube/Cube.php b/library/Cube/Cube.php
new file mode 100644
index 0000000..b307869
--- /dev/null
+++ b/library/Cube/Cube.php
@@ -0,0 +1,323 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+use Icinga\Exception\IcingaException;
+use Icinga\Web\View;
+
+abstract class Cube
+{
+ /** @var array<string, Dimension> Available dimensions */
+ protected $availableDimensions;
+
+ /** @var array Fact names */
+ protected $chosenFacts;
+
+ /** @var Dimension[] */
+ protected $dimensions = array();
+
+ protected $slices = array();
+
+ protected $renderer;
+
+ abstract public function fetchAll();
+
+ public function removeDimension($name)
+ {
+ unset($this->dimensions[$name]);
+ unset($this->slices[$name]);
+ return $this;
+ }
+
+ /**
+ * @return CubeRenderer
+ * @throws IcingaException
+ */
+ public function getRenderer()
+ {
+ throw new IcingaException('Got no cube renderer');
+ }
+
+ public function getPathLabel()
+ {
+ $dimensions = $this->getDimensionsLabel();
+ $slices = $this->getSlicesLabel();
+ $parts = array();
+ if ($dimensions !== null) {
+ $parts[] = $dimensions;
+ }
+
+ if ($slices !== null) {
+ $parts[] = $slices;
+ }
+
+ return implode(', ', $parts);
+ }
+
+ public function getDimensionsLabel()
+ {
+ $dimensions = $this->listDimensions();
+ if (empty($dimensions)) {
+ return null;
+ }
+
+ return implode(' -> ', array_map(function ($d) {
+ return $d->getLabel();
+ }, $dimensions));
+ }
+
+ public function getSlicesLabel()
+ {
+ $parts = array();
+
+ $slices = $this->getSlices();
+ if (empty($slices)) {
+ return null;
+ }
+ foreach ($slices as $key => $value) {
+ $parts[] = sprintf('%s = %s', $this->getDimension($key)->getLabel(), $value);
+ }
+
+ return implode(', ', $parts);
+ }
+
+ /**
+ * Create a new dimension
+ *
+ * @param string $name
+ * @return Dimension
+ */
+ abstract public function createDimension($name);
+
+ protected function registerAvailableDimensions()
+ {
+ if ($this->availableDimensions !== null) {
+ return;
+ }
+
+ $this->availableDimensions = [];
+ foreach ($this->listAvailableDimensions() as $label) {
+ $name = strtolower($label);
+ if (! isset($this->availableDimensions[$name])) {
+ $this->availableDimensions[$name] = $this->createDimension($name)->setLabel($label);
+ } else {
+ $this->availableDimensions[$name]->addLabel($label);
+ }
+ }
+ }
+
+ public function listAdditionalDimensions()
+ {
+ $this->registerAvailableDimensions();
+
+ $list = [];
+ foreach ($this->availableDimensions as $name => $dimension) {
+ if (! $this->hasDimension($name)) {
+ $list[$name] = $dimension->getLabel();
+ }
+ }
+
+ return $list;
+ }
+
+ abstract public function listAvailableDimensions();
+
+ public function getDimensionAfter($name)
+ {
+ $found = false;
+ $after = null;
+
+ foreach ($this->listDimensions() as $k => $d) {
+ if ($found) {
+ $after = $d;
+ break;
+ }
+
+ if ($k === $name) {
+ $found = true;
+ }
+ }
+
+ return $after;
+ }
+
+ public function listDimensionsUpTo($name)
+ {
+ $res = array();
+ foreach ($this->listDimensions() as $d => $_) {
+ $res[] = $d;
+ if ($d === $name) {
+ break;
+ }
+ }
+
+ return $res;
+ }
+
+ public function moveDimensionUp($name)
+ {
+ $last = $found = null;
+ $positions = array_keys($this->dimensions);
+
+ foreach ($positions as $k => $v) {
+ if ($v === $name) {
+ $found = $k;
+ break;
+ }
+
+ $last = $k;
+ }
+
+ if ($found !== null) {
+ $this->flipPositions($positions, $last, $found);
+ }
+
+ $this->reOrderDimensions($positions);
+ return $this;
+ }
+
+ public function moveDimensionDown($name)
+ {
+ $next = $found = null;
+ $positions = array_keys($this->dimensions);
+
+ foreach ($positions as $k => $v) {
+ if ($found !== null) {
+ $next = $k;
+ break;
+ }
+
+ if ($v === $name) {
+ $found = $k;
+ }
+ }
+
+ if ($next !== null) {
+ $this->flipPositions($positions, $next, $found);
+ }
+
+ $this->reOrderDimensions($positions);
+ return $this;
+ }
+
+ protected function flipPositions(&$array, $pos1, $pos2)
+ {
+ list(
+ $array[$pos1],
+ $array[$pos2]
+ ) = array(
+ $array[$pos2],
+ $array[$pos1]
+ );
+ }
+
+ protected function reOrderDimensions($positions)
+ {
+ $dimensions = array();
+ foreach ($positions as $pos => $key) {
+ $dimensions[$key] = $this->dimensions[$key];
+ }
+
+ $this->dimensions = $dimensions;
+ }
+
+ public function addDimension(Dimension $dimension)
+ {
+ $name = $dimension->getName();
+ if (array_key_exists($name, $this->dimensions)) {
+ throw new IcingaException('Cannot add dimension "%s" twice', $name);
+ }
+
+ $this->dimensions[$name] = $dimension;
+ return $this;
+ }
+
+ public function slice($key, $value)
+ {
+ if ($this->hasDimension($key)) {
+ $this->slices[$key] = $value;
+ } else {
+ throw new IcingaException('Got no such dimension: "%s"', $key);
+ }
+
+ return $this;
+ }
+
+ public function hasDimension($name)
+ {
+ return array_key_exists($name, $this->dimensions);
+ }
+
+ public function hasSlice($name)
+ {
+ return array_key_exists($name, $this->slices);
+ }
+
+ public function listSlices()
+ {
+ return array_keys($this->slices);
+ }
+
+ public function getSlices()
+ {
+ return $this->slices;
+ }
+
+ public function hasFact($name)
+ {
+ return array_key_exists($name, $this->chosenFacts);
+ }
+
+ public function getDimension($name)
+ {
+ return $this->dimensions[$name];
+ }
+
+ /**
+ * Return a list of chosen facts
+ *
+ * @return array
+ */
+ public function listFacts()
+ {
+ return $this->chosenFacts;
+ }
+
+ /**
+ * Choose a list of facts
+ *
+ * @param array $facts
+ * @return $this
+ */
+ public function chooseFacts(array $facts)
+ {
+ $this->chosenFacts = $facts;
+ return $this;
+ }
+
+ public function listDimensions()
+ {
+ return array_diff_key($this->dimensions, $this->slices);
+ }
+
+ public function listColumns()
+ {
+ return array_merge(array_keys($this->listDimensions()), $this->listFacts());
+ }
+
+ /**
+ * @param View $view
+ * @param CubeRenderer $renderer
+ * @return string
+ */
+ public function render(View $view, CubeRenderer $renderer = null)
+ {
+ if ($renderer === null) {
+ $renderer = $this->getRenderer();
+ }
+
+ return $renderer->render($view);
+ }
+}
diff --git a/library/Cube/CubeRenderer.php b/library/Cube/CubeRenderer.php
new file mode 100644
index 0000000..3f8c80d
--- /dev/null
+++ b/library/Cube/CubeRenderer.php
@@ -0,0 +1,406 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+use Icinga\Web\View;
+
+/**
+ * CubeRenderer base class
+ *
+ * Every Cube Renderer must extend this class.
+ *
+ * TODO: Should we introduce DimensionRenderer, FactRenderer and SummaryHelper
+ * instead?
+ *
+ * @package Icinga\Module\Cube
+ */
+abstract class CubeRenderer
+{
+ /** @var View */
+ protected $view;
+
+ /** @var Cube */
+ protected $cube;
+
+ /** @var array Our dimensions */
+ protected $dimensions;
+
+ /** @var array Our dimensions in regular order */
+ protected $dimensionOrder;
+
+ /** @var array Our dimensions in reversed order as a quick lookup source */
+ protected $reversedDimensions;
+
+ /** @var array Level (deepness) for each dimension (0, 1, 2...) */
+ protected $dimensionLevels;
+
+ protected $facts;
+
+ /** @var object The row before the current one */
+ protected $lastRow;
+
+ /**
+ * Current summaries
+ *
+ * This is an object of objects, with dimension names being the keys and
+ * a facts row containing current (rollup) summaries for that dimension
+ * being it's value
+ *
+ * @var object
+ */
+ protected $summaries;
+
+ protected $started;
+
+ /**
+ * CubeRenderer constructor.
+ *
+ * @param Cube $cube
+ */
+ public function __construct(Cube $cube)
+ {
+ $this->cube = $cube;
+ }
+
+ /**
+ * Render the given facts
+ *
+ * @param $facts
+ * @return string
+ */
+ abstract public function renderFacts($facts);
+
+ /**
+ * Returns the base url for the details action
+ *
+ * @return string
+ */
+ abstract protected function getDetailsBaseUrl();
+
+ /**
+ * Initialize all we need
+ */
+ protected function initialize()
+ {
+ $this->started = false;
+ $this->initializeDimensions()
+ ->initializeFacts()
+ ->initializeLastRow()
+ ->initializeSummaries();
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeLastRow()
+ {
+ $object = (object) array();
+ foreach ($this->dimensions as $dimension) {
+ $object->{$dimension->getName()} = null;
+ }
+
+ $this->lastRow = $object;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeDimensions()
+ {
+ $this->dimensions = $this->cube->listDimensions();
+
+ $min = 3;
+ $cnt = count($this->dimensions);
+ if ($cnt < $min) {
+ $pos = 0;
+ $diff = $min - $cnt;
+ $this->dimensionOrder = [];
+ foreach ($this->dimensions as $name => $_) {
+ $this->dimensionOrder[$pos++ + $diff] = $name;
+ }
+ } else {
+ $this->dimensionOrder = array_keys($this->dimensions);
+ }
+
+ $this->reversedDimensions = array_reverse($this->dimensionOrder);
+ $this->dimensionLevels = array_flip($this->dimensionOrder);
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeFacts()
+ {
+ $this->facts = $this->cube->listFacts();
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeSummaries()
+ {
+ $this->summaries = (object) array();
+ return $this;
+ }
+
+ /**
+ * @param object $row
+ * @return bool
+ */
+ protected function startsDimension($row)
+ {
+ foreach ($this->dimensionOrder as $name) {
+ if ($row->$name === null) {
+ $this->summaries->$name = $this->extractFacts($row);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param $row
+ * @return object
+ */
+ protected function extractFacts($row)
+ {
+ $res = (object) array();
+
+ foreach ($this->facts as $fact) {
+ $res->$fact = $row->$fact;
+ }
+
+ return $res;
+ }
+
+ public function render(View $view)
+ {
+ $this->view = $view;
+ $this->initialize();
+ $htm = $this->beginContainer();
+ foreach ($this->cube->fetchAll() as $row) {
+ $htm .= $this->renderRow($row);
+ }
+
+ return $htm . $this->closeDimensions() . $this->endContainer();
+ }
+
+ protected function renderRow($row)
+ {
+ $htm = '';
+ if ($dimension = $this->startsDimension($row)) {
+ return $htm;
+ }
+
+ $htm .= $this->closeDimensionsForRow($row);
+ $htm .= $this->beginDimensionsForRow($row);
+ $htm .= $this->renderFacts($row);
+ $this->lastRow = $row;
+ return $htm;
+ }
+
+ protected function beginDimensionsForRow($row)
+ {
+ $last = $this->lastRow;
+ foreach ($this->dimensionOrder as $name) {
+ if ($last->$name !== $row->$name) {
+ return $this->beginDimensionsUpFrom($name, $row);
+ }
+ }
+
+ return '';
+ }
+
+ protected function beginDimensionsUpFrom($dimension, $row)
+ {
+ $htm = '';
+ $found = false;
+
+ foreach ($this->dimensionOrder as $name) {
+ if ($name === $dimension) {
+ $found = true;
+ }
+
+ if ($found) {
+ $htm .= $this->beginDimension($name, $row);
+ }
+ }
+
+ return $htm;
+ }
+
+ protected function closeDimensionsForRow($row)
+ {
+ $last = $this->lastRow;
+ foreach ($this->dimensionOrder as $name) {
+ if ($last->$name !== $row->$name) {
+ return $this->closeDimensionsDownTo($name);
+ }
+ }
+
+ return '';
+ }
+
+ protected function closeDimensionsDownTo($name)
+ {
+ $htm = '';
+
+ foreach ($this->reversedDimensions as $dimension) {
+ $htm .= $this->closeDimension($dimension);
+
+ if ($name === $dimension) {
+ break;
+ }
+ }
+
+ return $htm;
+ }
+
+ protected function closeDimensions()
+ {
+ $htm = '';
+ foreach ($this->reversedDimensions as $name) {
+ $htm .= $this->closeDimension($name);
+ }
+
+ return $htm;
+ }
+
+ protected function closeDimension($name)
+ {
+ if (! $this->started) {
+ return '';
+ }
+
+ $indent = $this->getIndent($name);
+ return $indent . ' </div>' . "\n" . $indent . "</div><!-- $name -->\n";
+ }
+
+ protected function getIndent($name)
+ {
+ return str_repeat(' ', $this->getLevel($name));
+ }
+
+ protected function beginDimension($name, $row)
+ {
+ $indent = $this->getIndent($name);
+ if (! $this->started) {
+ $this->started = true;
+ }
+ $view = $this->view;
+ $dimension = $this->cube->getDimension($name);
+
+ return
+ $indent . '<div class="'
+ . $this->getDimensionClassString($name, $row)
+ . '">' . "\n"
+ . $indent . ' <div class="header"><a href="'
+ . $this->getDetailsUrl($name, $row)
+ . '" title="' . $view->escape(sprintf('Show details for %s: %s', $dimension->getLabel(), $row->$name)) . '"'
+ . ' data-base-target="_next">'
+ . $this->renderDimensionLabel($name, $row)
+ . '</a><a class="icon-filter" href="'
+ . $this->getSliceUrl($name, $row)
+ . '" title="' . $view->escape('Slice this cube') . '"></a></div>' . "\n"
+ . $indent . ' <div class="body">' . "\n";
+ }
+
+ /**
+ * Render the label for a given dimension name
+ *
+ * To have some context available, also
+ *
+ * @param $name
+ * @param $row
+ * @return string
+ */
+ protected function renderDimensionLabel($name, $row)
+ {
+ $caption = $row->$name;
+ if (empty($caption)) {
+ $caption = '_';
+ }
+
+ return $this->view->escape($caption);
+ }
+
+ protected function getDetailsUrl($name, $row)
+ {
+ $cube = $this->cube;
+
+ $dimensions = array_merge(array_keys($cube->listDimensions()), $cube->listSlices());
+ $params = [
+ 'dimensions' => DimensionParams::update($dimensions)->getParams()
+ ];
+
+ foreach ($this->cube->listDimensionsUpTo($name) as $dimensionName) {
+ $params[$dimensionName] = $row->$dimensionName;
+ }
+
+ foreach ($this->cube->getSlices() as $key => $val) {
+ $params[$key] = $val;
+ }
+
+ return $this->view->url(
+ $this->getDetailsBaseUrl(),
+ $params
+ );
+ }
+
+ protected function getSliceUrl($name, $row)
+ {
+ return $this->view->url()
+ ->setParam($name, $row->$name);
+ }
+
+ protected function isOuterDimension($name)
+ {
+ return $this->reversedDimensions[0] !== $name;
+ }
+
+ protected function getDimensionClassString($name, $row)
+ {
+ return implode(' ', $this->getDimensionClasses($name, $row));
+ }
+
+ protected function getDimensionClasses($name, $row)
+ {
+ return array('cube-dimension' . $this->getLevel($name));
+ }
+
+ protected function getLevel($name)
+ {
+ return $this->dimensionLevels[$name];
+ }
+
+ /**
+ * @return string
+ */
+ protected function beginContainer()
+ {
+ return '<div class="cube">' . "\n";
+ }
+
+ /**
+ * @return string
+ */
+ protected function endContainer()
+ {
+ return '</div>' . "\n";
+ }
+
+ /**
+ * Well... just to be on the safe side
+ */
+ public function __destruct()
+ {
+ unset($this->cube);
+ }
+}
diff --git a/library/Cube/CubeRenderer/HostStatusCubeRenderer.php b/library/Cube/CubeRenderer/HostStatusCubeRenderer.php
new file mode 100644
index 0000000..89322cb
--- /dev/null
+++ b/library/Cube/CubeRenderer/HostStatusCubeRenderer.php
@@ -0,0 +1,105 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\CubeRenderer;
+
+use Icinga\Module\Cube\CubeRenderer;
+
+class HostStatusCubeRenderer extends CubeRenderer
+{
+ protected function renderDimensionLabel($name, $row)
+ {
+ $htm = parent::renderDimensionLabel($name, $row);
+
+ if (($next = $this->cube->getDimensionAfter($name)) && isset($this->summaries->{$next->getName()})) {
+ $htm .= ' <span class="sum">(' . $this->summaries->{$next->getName()}->hosts_cnt . ')</span>';
+ }
+
+ return $htm;
+ }
+
+ protected function getDimensionClasses($name, $row)
+ {
+ $classes = parent::getDimensionClasses($name, $row);
+
+ $sums = $row;
+ if ($sums->hosts_down > 0) {
+ $classes[] = 'critical';
+ if ((int) $sums->hosts_unhandled_down === 0) {
+ $classes[] = 'handled';
+ }
+ } elseif ($sums->hosts_unreachable > 0) {
+ $classes[] = 'unreachable';
+ if ((int) $sums->hosts_unhandled_unreachable === 0) {
+ $classes[] = 'handled';
+ }
+ } else {
+ $classes[] = 'ok';
+ }
+
+ return $classes;
+ }
+
+ public function renderFacts($facts)
+ {
+ $indent = str_repeat(' ', 3);
+ $parts = array();
+
+ if ($facts->hosts_unhandled_down > 0) {
+ $parts['critical'] = $facts->hosts_unhandled_down;
+ }
+
+ if ($facts->hosts_down > 0 && $facts->hosts_down > $facts->hosts_unhandled_down) {
+ $parts['critical handled'] = $facts->hosts_down - $facts->hosts_unhandled_down;
+ }
+
+ if ($facts->hosts_unhandled_unreachable > 0) {
+ $parts['unreachable'] = $facts->hosts_unhandled_unreachable;
+ }
+
+ if ($facts->hosts_unreachable > 0 && $facts->hosts_unreachable > $facts->hosts_unhandled_unreachable) {
+ $parts['unreachable handled'] = $facts->hosts_unreachable - $facts->hosts_unhandled_unreachable;
+ }
+
+ if ($facts->hosts_cnt > $facts->hosts_down && $facts->hosts_cnt > $facts->hosts_unreachable) {
+ $parts['ok'] = $facts->hosts_cnt - $facts->hosts_down - $facts->hosts_unreachable;
+ }
+
+ $main = '';
+ $sub = '';
+ foreach ($parts as $class => $count) {
+ if ($main === '') {
+ $main = $this->makeBadgeHtml($class, $count);
+ } else {
+ $sub .= $this->makeBadgeHtml($class, $count);
+ }
+ }
+ if ($sub !== '') {
+ $sub = $indent
+ . '<span class="others">'
+ . "\n "
+ . $sub
+ . $indent
+ . "</span>\n";
+ }
+
+ return $main . $sub;
+ }
+
+ protected function makeBadgeHtml($class, $count)
+ {
+ $indent = str_repeat(' ', 3);
+ return sprintf(
+ '%s<span class="%s">%s</span>',
+ $indent,
+ $class,
+ $count
+ ) . "\n";
+ }
+
+ protected function getDetailsBaseUrl()
+ {
+ return 'cube/hosts/details';
+ }
+}
diff --git a/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
new file mode 100644
index 0000000..33caa84
--- /dev/null
+++ b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
@@ -0,0 +1,126 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\CubeRenderer;
+
+use Icinga\Module\Cube\CubeRenderer;
+
+class ServiceStatusCubeRenderer extends CubeRenderer
+{
+ public function renderFacts($facts)
+ {
+ $indent = str_repeat(' ', 3);
+ $parts = [];
+
+ if ($facts->services_unhandled_critical > 0) {
+ $parts['critical'] = $facts->services_unhandled_critical;
+ }
+
+ if ($facts->services_critical > 0 && $facts->services_critical > $facts->services_unhandled_critical) {
+ $parts['critical handled'] = $facts->services_critical - $facts->services_unhandled_critical;
+ }
+
+ if ($facts->services_unhandled_warning > 0) {
+ $parts['warning'] = $facts->services_unhandled_warning;
+ }
+
+ if ($facts->services_warning > 0 && $facts->services_warning > $facts->services_unhandled_warning) {
+ $parts['warning handled'] = $facts->services_warning - $facts->services_unhandled_warning;
+ }
+
+ if ($facts->services_unhandled_unknown > 0) {
+ $parts['unknown'] = $facts->services_unhandled_unknown;
+ }
+
+ if ($facts->services_unknown > 0 && $facts->services_unknown > $facts->services_unhandled_unknown) {
+ $parts['unknown handled'] = $facts->services_unknown - $facts->services_unhandled_unknown;
+ }
+
+ if (
+ $facts->services_cnt > $facts->services_critical && $facts->services_cnt > $facts->services_warning
+ && $facts->services_cnt > $facts->services_unknown
+ ) {
+ $parts['ok'] = $facts->services_cnt - $facts->services_critical - $facts->services_warning -
+ $facts->services_unknown;
+ }
+
+ $main = '';
+ $sub = '';
+ foreach ($parts as $class => $count) {
+ if ($main === '') {
+ $main = $this->makeBadgeHtml($class, $count);
+ } else {
+ $sub .= $this->makeBadgeHtml($class, $count);
+ }
+ }
+ if ($sub !== '') {
+ $sub = $indent
+ . '<span class="others">'
+ . "\n "
+ . $sub
+ . $indent
+ . "</span>\n";
+ }
+
+ return $main . $sub;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function renderDimensionLabel($name, $row)
+ {
+ $htm = parent::renderDimensionLabel($name, $row);
+
+ if (($next = $this->cube->getDimensionAfter($name)) && isset($this->summaries->{$next->getName()})) {
+ $htm .= ' <span class="sum">(' . $this->summaries->{$next->getName()}->services_cnt . ')</span>';
+ }
+
+ return $htm;
+ }
+
+ protected function getDimensionClasses($name, $row)
+ {
+ $classes = parent::getDimensionClasses($name, $row);
+
+ $sums = $row;
+ if ($sums->services_critical > 0) {
+ $classes[] = 'critical';
+ if ((int) $sums->services_unhandled_critical === 0) {
+ $classes[] = 'handled';
+ }
+ } elseif ($sums->services_warning > 0) {
+ $classes[] = 'warning';
+ if ((int) $sums->services_unhandled_warning === 0) {
+ $classes[] = 'handled';
+ }
+ } elseif ($sums->services_unknown > 0) {
+ $classes[] = 'unknown';
+ if ((int) $sums->services_unhandled_unknown === 0) {
+ $classes[] = 'handled';
+ }
+ } else {
+ $classes[] = 'ok';
+ }
+
+ return $classes;
+ }
+
+ protected function makeBadgeHtml($class, $count)
+ {
+ $indent = str_repeat(' ', 3);
+
+ return sprintf(
+ '%s<span class="%s">%s</span>',
+ $indent,
+ $class,
+ $count
+ ) . "\n";
+ }
+
+ protected function getDetailsBaseUrl()
+ {
+ return 'cube/services/details';
+ }
+}
diff --git a/library/Cube/Dimension.php b/library/Cube/Dimension.php
new file mode 100644
index 0000000..7519da4
--- /dev/null
+++ b/library/Cube/Dimension.php
@@ -0,0 +1,52 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+/**
+ * Dimension interface
+ *
+ * All available dimensions must implement this interface
+ *
+ * @package Icinga\Module\Cube
+ */
+interface Dimension
+{
+ /**
+ * The name of this dimension
+ *
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * The label of this dimension
+ *
+ * @return string
+ */
+ public function getLabel();
+
+ /**
+ * Column expression
+ *
+ * This is the expression used to fetch the related column. Usually an SQL
+ * snippet when a relational database is involved
+ *
+ * @param Cube $cube
+ *
+ * @return string
+ */
+ public function getColumnExpression(Cube $cube);
+
+ /**
+ * Add this dimension to a cube
+ *
+ * This allows your dimension to apply itself to the Cube. That way your
+ * dimension is able to join optional tables and more
+ *
+ * @param Cube $cube
+ * @return void
+ */
+ public function addToCube(Cube $cube);
+}
diff --git a/library/Cube/DimensionParams.php b/library/Cube/DimensionParams.php
new file mode 100644
index 0000000..02b7ea1
--- /dev/null
+++ b/library/Cube/DimensionParams.php
@@ -0,0 +1,87 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2020 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+use Icinga\Web\Url;
+
+class DimensionParams
+{
+ /**
+ * @var array Raw dimensions
+ */
+ protected $dimensions = [];
+
+ /**
+ * @var string encoded dimensions separated by coma
+ */
+ protected $params;
+
+ // For the form: DimensionsParam::fromUrl($url)
+ public static function fromUrl(Url $url)
+ {
+ return static::fromString($url->getParam('dimensions'));
+ }
+
+ public static function fromArray(array $dimensions = [])
+ {
+ $self = new static();
+
+ $self->dimensions = array_filter($dimensions);
+
+ return $self;
+ }
+
+ // For the controller: DimensionsParam::fromArray($this->params->shift('dimensions'))
+ public static function fromString($dimensions)
+ {
+ return static::fromArray(explode(',', $dimensions));
+ }
+
+
+ /**
+ * @param $dimension
+ *
+ * @return $this
+ */
+ public function add($dimension)
+ {
+ if (! empty($dimension)) {
+ $this->dimensions[] = $dimension;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Overwrite dimensions
+ *
+ * @param $dimensions
+ *
+ * @return $this
+ */
+ public static function update($dimensions)
+ {
+ $self = new static();
+ $self->dimensions = $dimensions;
+
+ return $self;
+ }
+
+ /**
+ * @return string encoded dimensions separated by coma
+ */
+ public function getParams()
+ {
+ return implode(',', $this->dimensions);
+ }
+
+ /**
+ * @return array
+ */
+ public function getDimensions()
+ {
+ return $this->dimensions;
+ }
+}
diff --git a/library/Cube/Hook/ActionsHook.php b/library/Cube/Hook/ActionsHook.php
new file mode 100644
index 0000000..8ba8a7c
--- /dev/null
+++ b/library/Cube/Hook/ActionsHook.php
@@ -0,0 +1,99 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Hook;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Web\ActionLink;
+use Icinga\Module\Cube\Web\ActionLinks;
+use Icinga\Web\Url;
+use Icinga\Web\View;
+
+/**
+ * ActionsHook
+ *
+ * Implement this hook in case your module wants to add links to the detail
+ * page shown for a slice.
+ *
+ * @package Icinga\Module\Cube\Hook
+ */
+abstract class ActionsHook
+{
+ /** @var ActionLinks */
+ private $actionLinks;
+
+ /**
+ * Your implementation should extend this method
+ *
+ * Then use the addActionLink() method, eventually combined with the
+ * createUrl() helper like this:
+ *
+ * <code>
+ * $this->addActionLink(
+ * $this->makeUrl('mymodule/controller/action', array('some' => 'param')),
+ * 'A shown title',
+ * 'A longer description text, should fit into the available square field',
+ * 'icon-name'
+ * );
+ * </code>
+ *
+ * For a list of available icon names please enable the Icinga Web 2 'doc'
+ * module and go to "Documentation" -> "Developer - Style" -> "Icons"
+ *
+ * @param Cube $cube
+ * @param View $view
+ *
+ * @return void
+ */
+ abstract public function prepareActionLinks(Cube $cube, View $view);
+
+ /**
+ * Lazy access to an ActionLinks object
+ *
+ * @return ActionLinks
+ */
+ public function getActionLinks()
+ {
+ if ($this->actionLinks === null) {
+ $this->actionLinks = new ActionLinks();
+ }
+ return $this->actionLinks;
+ }
+
+ /**
+ * Helper method instantiating an ActionLink object
+ *
+ * @param Url $url
+ * @param string $title
+ * @param string $description
+ * @param string $icon
+ *
+ * @return $this
+ */
+ public function addActionLink(Url $url, $title, $description, $icon)
+ {
+ $this->getActionLinks()->add(
+ new ActionLink($url, $title, $description, $icon)
+ );
+
+ return $this;
+ }
+
+ /**
+ * Helper method instantiating an Url object
+ *
+ * @param string $path
+ * @param array $params
+ * @return Url
+ */
+ public function makeUrl($path, $params = null)
+ {
+ $url = Url::fromPath($path);
+ if ($params !== null) {
+ $url->getParams()->mergeValues($params);
+ }
+
+ return $url;
+ }
+}
diff --git a/library/Cube/Hook/IcingaDbActionsHook.php b/library/Cube/Hook/IcingaDbActionsHook.php
new file mode 100644
index 0000000..63c24fe
--- /dev/null
+++ b/library/Cube/Hook/IcingaDbActionsHook.php
@@ -0,0 +1,125 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Hook;
+
+use Exception;
+use Icinga\Application\Hook;
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use ipl\Web\Url;
+use ipl\Html\HtmlDocument;
+use ipl\Html\HtmlElement;
+use ipl\Web\Widget\Icon;
+use ipl\Web\Widget\Link;
+
+/**
+ * ActionsHook
+ *
+ * Implement this hook in case your module wants to add links to the detail
+ * page shown for a slice.
+ *
+ * @package Icinga\Module\Cube\Hook
+ */
+abstract class IcingaDbActionsHook
+{
+ /** @var Link[] */
+ private $actionLinks = [];
+
+ /**
+ * Create additional action links for the given cube
+ *
+ * @param IcingaDbCube $cube
+ * @return void
+ */
+ abstract public function createActionLinks(IcingaDbCube $cube);
+
+ /**
+ * Return the action links for the cube
+ *
+ * @return Link[]
+ */
+ final protected function getActionLinks(): array
+ {
+ return $this->actionLinks;
+ }
+
+ /**
+ * Helper method to populate action links array
+ *
+ * @param Url $url
+ * @param string $title
+ * @param string $description
+ * @param string $icon
+ *
+ * @return $this
+ */
+ final protected function addActionLink(Url $url, string $title, string $description, string $icon): self
+ {
+ $linkContent = (new HtmlDocument());
+ $linkContent->addHtml(new Icon($icon));
+ $linkContent->addHtml(HtmlElement::create('span', ['class' => 'title'], $title));
+ $linkContent->addHtml(HtmlElement::create('p', null, $description));
+
+ $this->actionLinks[] = new Link($linkContent, $url);
+
+ return $this;
+ }
+
+ /**
+ * Helper method instantiating an Url object
+ *
+ * @param string $path
+ * @param array $params
+ * @return Url
+ */
+ final protected function makeUrl(string $path, array $params = null): Url
+ {
+ $url = Url::fromPath($path);
+ if ($params !== null) {
+ $url->getParams()->mergeValues($params);
+ }
+
+ return $url;
+ }
+
+ /**
+ * Render all links for all Hook implementations
+ *
+ * This is what the Cube calls when rendering details
+ *
+ * @param IcingaDbCube $cube
+ *
+ * @return string
+ */
+ public static function renderAll(Cube $cube)
+ {
+ $html = new HtmlDocument();
+
+ /** @var IcingaDbActionsHook $hook */
+ foreach (Hook::all('Cube/IcingaDbActions') as $hook) {
+ try {
+ $hook->createActionLinks($cube);
+ } catch (Exception $e) {
+ $html->addHtml(HtmlElement::create('li', ['class' => 'error'], $e->getMessage()));
+ }
+
+ foreach ($hook->getActionLinks() as $link) {
+ $html->addHtml(HtmlElement::create('li', null, $link));
+ }
+ }
+
+ if ($html->isEmpty()) {
+ $html->addHtml(
+ HtmlElement::create(
+ 'li',
+ ['class' => 'error'],
+ t('No action links have been provided for this cube')
+ )
+ );
+ }
+
+ return $html->render();
+ }
+}
diff --git a/library/Cube/IcingaDb/CustomVariableDimension.php b/library/Cube/IcingaDb/CustomVariableDimension.php
new file mode 100644
index 0000000..2266395
--- /dev/null
+++ b/library/Cube/IcingaDb/CustomVariableDimension.php
@@ -0,0 +1,119 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Dimension;
+use Icinga\Module\Icingadb\Model\CustomvarFlat;
+use ipl\Sql\Expression;
+use ipl\Stdlib\Filter;
+
+class CustomVariableDimension implements Dimension
+{
+ protected $name;
+
+ protected $label;
+
+ protected $wantNull = false;
+
+ public function __construct($name)
+ {
+ $this->name = $name;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getLabel()
+ {
+ return $this->label ?: $this->getName();
+ }
+
+ /**
+ * Set the label
+ *
+ * @param string $label
+ * @return $this
+ */
+ public function setLabel($label)
+ {
+ $this->label = $label;
+
+ return $this;
+ }
+
+ /**
+ * Add a label
+ *
+ * @param string $label
+ * @return $this
+ */
+ public function addLabel($label)
+ {
+ if ($this->label === null) {
+ $this->setLabel($label);
+ } else {
+ $this->label .= ' & ' . $label;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Define whether null values should be shown
+ *
+ * @param bool $wantNull
+ * @return $this
+ */
+ public function wantNull($wantNull = true)
+ {
+ $this->wantNull = $wantNull;
+
+ return $this;
+ }
+
+ /**
+ * @param IcingaDbCube $cube
+ * @return Expression|string
+ */
+ public function getColumnExpression(Cube $cube)
+ {
+ $expression = $cube->getDb()->quoteIdentifier(['c_' . $this->getName(), 'flatvalue']);
+
+ if ($this->wantNull) {
+ return new Expression("COALESCE($expression, '-')");
+ }
+
+ return $expression;
+ }
+
+ public function addToCube(Cube $cube)
+ {
+ /** @var IcingaDbCube $cube */
+ $name = $this->getName();
+ $innerQuery = $cube->innerQuery();
+ $sourceTable = $innerQuery->getModel()->getTableName();
+
+ $subQuery = $innerQuery->createSubQuery(new CustomvarFlat(), $sourceTable . '.vars');
+ $subQuery->getSelectBase()->resetWhere(); // The link to the outer query is the ON condition
+ $subQuery->columns(['flatvalue', 'object_id' => $sourceTable . '.id']);
+ $subQuery->filter(Filter::like('flatname', $name));
+
+ // Values might not be unique (wildcard dimensions)
+ $subQuery->getSelectBase()->groupBy([
+ $subQuery->getResolver()->getAlias($subQuery->getModel()) . '.flatvalue',
+ 'object_id'
+ ]);
+
+ $subQueryAlias = $cube->getDb()->quoteIdentifier(['c_' . $name]);
+ $innerQuery->getSelectBase()->groupBy($subQueryAlias . '.flatvalue');
+ $innerQuery->getSelectBase()->join(
+ [$subQueryAlias => $subQuery->assembleSelect()],
+ [$subQueryAlias . '.object_id = ' . $innerQuery->getResolver()->getAlias($innerQuery->getModel()) . '.id']
+ );
+ }
+}
diff --git a/library/Cube/IcingaDb/IcingaDbCube.php b/library/Cube/IcingaDb/IcingaDbCube.php
new file mode 100644
index 0000000..3f38d9b
--- /dev/null
+++ b/library/Cube/IcingaDb/IcingaDbCube.php
@@ -0,0 +1,196 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Common\Database;
+use ipl\Orm\Query;
+use ipl\Sql\Adapter\Pgsql;
+use ipl\Sql\Expression;
+use ipl\Sql\Select;
+
+abstract class IcingaDbCube extends Cube
+{
+ use Auth;
+ use Database;
+
+ /** @var Query The inner query fetching all required data */
+ protected $innerQuery;
+
+ /** @var Select The rollup query, creating grouped sums over innerQuery */
+ protected $rollupQuery;
+
+ /** @var Select The outer query, orders respecting NULL values, rollup first */
+ protected $fullQuery;
+
+ protected $objectsFilter;
+
+ abstract public function getObjectsFilter();
+ /**
+ * An IcingaDbCube must provide a list of all available columns
+ *
+ * This is a key/value array with the key being the fact name / column alias
+ * and
+ *
+ * @return array
+ */
+ abstract public function getAvailableFactColumns();
+
+ /**
+ * @return Query
+ */
+ abstract public function prepareInnerQuery();
+
+ /**
+ * Get our inner query
+ *
+ * Hint: mostly used to get rid of NULL values
+ *
+ * @return Query
+ */
+ public function innerQuery()
+ {
+ if ($this->innerQuery === null) {
+ $this->innerQuery = $this->prepareInnerQuery();
+ }
+
+ return $this->innerQuery;
+ }
+
+ /**
+ * Get our rollup query
+ *
+ * @return Select
+ */
+ protected function rollupQuery()
+ {
+ if ($this->rollupQuery === null) {
+ $this->rollupQuery = $this->prepareRollupQuery();
+ }
+
+ return $this->rollupQuery;
+ }
+
+ /**
+ * Add a specific named dimension
+ *
+ * @param string $name
+ * @return $this
+ */
+ public function addDimensionByName($name)
+ {
+ $this->addDimension($this->createDimension($name));
+
+ return $this;
+ }
+
+ /**
+ * We first prepare the queries and to finalize it later on
+ *
+ * This way dimensions can be added one by one, they will be allowed to
+ * optionally join additional tables or apply other modifications late
+ * in the process
+ *
+ * @return void
+ */
+ protected function finalizeInnerQuery()
+ {
+ $query = $this->innerQuery()->getSelectBase();
+ $columns = [];
+ foreach ($this->dimensions as $name => $dimension) {
+ $quotedDimension = $this->getDb()->quoteIdentifier([$name]);
+ $dimension->addToCube($this);
+ $columns[$quotedDimension] = $dimension->getColumnExpression($this);
+
+ if ($this->hasSlice($name)) {
+ $query->where(
+ $dimension->getColumnExpression($this) . ' = ?',
+ $this->slices[$name]
+ );
+ } else {
+ $columns[$quotedDimension] = $dimension->getColumnExpression($this);
+ }
+ }
+
+ $query->columns($columns);
+ }
+
+ protected function prepareRollupQuery()
+ {
+ $dimensions = $this->listDimensions();
+ $this->finalizeInnerQuery();
+
+ $columns = [];
+ $groupBy = [];
+ foreach ($dimensions as $name => $dimension) {
+ $quotedDimension = $this->getDb()->quoteIdentifier([$name]);
+
+ $columns[$quotedDimension] = 'f.' . $quotedDimension;
+ $groupBy[] = $quotedDimension;
+ }
+
+ $availableFacts = $this->getAvailableFactColumns();
+
+ foreach ($this->chosenFacts as $alias) {
+ $columns[$alias] = new Expression('SUM(f.' . $availableFacts[$alias] . ')');
+ }
+
+ if (! empty($groupBy)) {
+ if ($this->getDb()->getAdapter() instanceof Pgsql) {
+ $groupBy = 'ROLLUP(' . implode(', ', $groupBy) . ')';
+ } else {
+ $groupBy[count($groupBy) - 1] .= ' WITH ROLLUP';
+ }
+ }
+
+ $rollupQuery = new Select();
+ $rollupQuery->from(['f' => $this->innerQuery()->assembleSelect()])
+ ->columns($columns)
+ ->groupBy($groupBy);
+
+ return $rollupQuery;
+ }
+
+ protected function prepareFullQuery()
+ {
+ $rollupQuery = $this->rollupQuery();
+ $columns = [];
+ foreach ($this->listColumns() as $column) {
+ $quotedColumn = $this->getDb()->quoteIdentifier([$column]);
+ $columns[$quotedColumn] = 'rollup.' . $this->getDb()->quoteIdentifier([$column]);
+ }
+
+ $fullQuery = new Select();
+ $fullQuery->from(['rollup' => $rollupQuery])->columns($columns);
+
+ foreach ($columns as $quotedColumn => $_) {
+ $fullQuery->orderBy("($quotedColumn IS NOT NULL)");
+ $fullQuery->orderBy($quotedColumn);
+ }
+
+ return $fullQuery;
+ }
+
+ /**
+ * Lazy-load our full query
+ *
+ * @return Select
+ */
+ protected function fullQuery()
+ {
+ if ($this->fullQuery === null) {
+ $this->fullQuery = $this->prepareFullQuery();
+ }
+
+ return $this->fullQuery;
+ }
+
+ public function fetchAll()
+ {
+ $query = $this->fullQuery();
+ return $this->getDb()->fetchAll($query);
+ }
+}
diff --git a/library/Cube/IcingaDb/IcingaDbHostStatusCube.php b/library/Cube/IcingaDb/IcingaDbHostStatusCube.php
new file mode 100644
index 0000000..028d4d7
--- /dev/null
+++ b/library/Cube/IcingaDb/IcingaDbHostStatusCube.php
@@ -0,0 +1,100 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\CubeRenderer\HostStatusCubeRenderer;
+use Icinga\Module\Icingadb\Model\CustomvarFlat;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\HoststateSummary;
+use ipl\Stdlib\Filter;
+
+class IcingaDbHostStatusCube extends IcingaDbCube
+{
+ public function getRenderer()
+ {
+ return new HostStatusCubeRenderer($this);
+ }
+
+ public function getAvailableFactColumns()
+ {
+ return [
+ 'hosts_cnt' => 'hosts_total',
+ 'hosts_down' => 'hosts_down_handled + f.hosts_down_unhandled',
+ 'hosts_unhandled_down' => 'hosts_down_unhandled',
+ 'hosts_unreachable' => 'hosts_unreachable',
+ 'hosts_unhandled_unreachable' => 'hosts_unreachable_unhandled'
+ ];
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVariableDimension($name);
+ }
+
+ public function listAvailableDimensions()
+ {
+ $db = $this->getDb();
+
+ $query = CustomvarFlat::on($db);
+ $this->applyRestrictions($query);
+
+ $query
+ ->columns('flatname')
+ ->orderBy('flatname')
+ ->filter(Filter::like('host.id', '*'));
+ $query->getSelectBase()->groupBy('flatname');
+
+ $dimensions = [];
+ foreach ($query as $row) {
+ // Replaces array index notations with [*] to get results for arbitrary indexes
+ $name = preg_replace('/\\[\d+](?=\\.|$)/', '[*]', $row->flatname);
+ $dimensions[$name] = $name;
+ }
+
+ return $dimensions;
+ }
+
+ public function prepareInnerQuery()
+ {
+ $query = HoststateSummary::on($this->getDb());
+ $query->columns(array_diff_key($query->getModel()->getColumns(), (new Host())->getColumns()));
+ $query->disableDefaultSort();
+ $this->applyRestrictions($query);
+
+ $this->innerQuery = $query;
+ return $this->innerQuery;
+ }
+
+ /**
+ * Return Filter for Hosts cube.
+ *
+ * @return Filter\Any|Filter\Chain
+ */
+ public function getObjectsFilter()
+ {
+ if ($this->objectsFilter === null) {
+ $this->finalizeInnerQuery();
+
+ $hosts = $this->innerQuery()->columns(['host' => 'host.name']);
+ $hosts->getSelectBase()->resetGroupBy();
+
+ $filter = Filter::any();
+
+ foreach ($hosts as $object) {
+ $filter->add(Filter::equal('host.name', $object->host));
+ }
+
+ $this->objectsFilter = $filter;
+ }
+
+ return $this->objectsFilter;
+ }
+}
diff --git a/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php b/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php
new file mode 100644
index 0000000..9336cdf
--- /dev/null
+++ b/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php
@@ -0,0 +1,109 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\CubeRenderer\ServiceStatusCubeRenderer;
+use Icinga\Module\Icingadb\Model\CustomvarFlat;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Icingadb\Model\ServicestateSummary;
+use ipl\Stdlib\Filter;
+
+class IcingaDbServiceStatusCube extends IcingaDbCube
+{
+ public function getRenderer()
+ {
+ return new ServiceStatusCubeRenderer($this);
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVariableDimension($name);
+ }
+
+ public function getAvailableFactColumns()
+ {
+ return [
+ 'services_cnt' => 'services_total',
+ 'services_critical' => 'services_critical_handled + f.services_critical_unhandled',
+ 'services_unhandled_critical' => 'services_critical_unhandled',
+ 'services_warning' => 'services_warning_handled + f.services_warning_unhandled',
+ 'services_unhandled_warning' => 'services_warning_unhandled',
+ 'services_unknown' => 'services_unknown_handled + f.services_unknown_unhandled',
+ 'services_unhandled_unknown' => 'services_unknown_unhandled',
+ ];
+ }
+
+ public function listAvailableDimensions()
+ {
+ $db = $this->getDb();
+
+ $query = CustomvarFlat::on($db);
+ $this->applyRestrictions($query);
+
+ $query
+ ->columns('flatname')
+ ->orderBy('flatname')
+ ->filter(Filter::like('service.id', '*'));
+ $query->getSelectBase()->groupBy('flatname');
+
+ $dimensions = [];
+ foreach ($query as $row) {
+ // Replaces array index notations with [*] to get results for arbitrary indexes
+ $name = preg_replace('/\\[\d+](?=\\.|$)/', '[*]', $row->flatname);
+ $dimensions[$name] = $name;
+ }
+
+ return $dimensions;
+ }
+
+ public function prepareInnerQuery()
+ {
+ $query = ServicestateSummary::on($this->getDb());
+ $query->columns(array_diff_key($query->getModel()->getColumns(), (new Service())->getColumns()));
+ $query->disableDefaultSort();
+ $this->applyRestrictions($query);
+
+ return $query;
+ }
+
+ /**
+ * Return Filter for Services cube.
+ *
+ * @return Filter\Any|Filter\Chain
+ */
+ public function getObjectsFilter()
+ {
+ if ($this->objectsFilter === null) {
+ $this->finalizeInnerQuery();
+
+ $services = $this->innerQuery()->columns([
+ 'host_name' => 'host.name',
+ 'service_name' => 'service.name'
+ ]);
+
+ $services->getSelectBase()->resetGroupBy();
+ $filter = Filter::any();
+
+ foreach ($services as $service) {
+ $filter->add(
+ Filter::all(
+ Filter::equal('service.name', $service->service_name),
+ Filter::equal('host.name', $service->host_name)
+ )
+ );
+ }
+
+ $this->objectsFilter = $filter;
+ }
+
+ return $this->objectsFilter;
+ }
+}
diff --git a/library/Cube/Ido/CustomVarDimension.php b/library/Cube/Ido/CustomVarDimension.php
new file mode 100644
index 0000000..0c21282
--- /dev/null
+++ b/library/Cube/Ido/CustomVarDimension.php
@@ -0,0 +1,158 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Dimension;
+
+/**
+ * CustomVarDimension
+ *
+ * This provides dimenstions for custom variables available in the IDO
+ *
+ * TODO: create safe aliases for special characters
+ *
+ * @package Icinga\Module\Cube\Ido
+ */
+class CustomVarDimension implements Dimension
+{
+ public const TYPE_HOST = 'host';
+
+ public const TYPE_SERVICE = 'service';
+
+ /**
+ * @var string custom variable name
+ */
+ protected $varName;
+
+ /**
+ * @var string custom variable label
+ */
+ protected $varLabel;
+
+ /**
+ * @var bool Whether null values should be shown
+ */
+ protected $wantNull = false;
+
+ /** @var string Type of the custom var */
+ protected $type;
+
+ /**
+ * CustomVarDimension constructor.
+ *
+ * @param $varName
+ * @param string $type Type of the custom var
+ */
+ public function __construct($varName, $type = null)
+ {
+ $this->varName = $varName;
+ $this->type = $type;
+ }
+
+ /**
+ * Define whether null values should be shown
+ *
+ * @param bool $wantNull
+ * @return $this
+ */
+ public function wantNull($wantNull = true)
+ {
+ $this->wantNull = $wantNull;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->varName;
+ }
+
+ /**
+ * @return string
+ */
+ public function getLabel()
+ {
+ return $this->varLabel ?: $this->getName();
+ }
+
+ /**
+ * Set the label
+ *
+ * @param string $label
+ * @return $this
+ */
+ public function setLabel($label)
+ {
+ $this->varLabel = $label;
+
+ return $this;
+ }
+
+ /**
+ * Add a label
+ *
+ * @param string $label
+ * @return $this
+ */
+ public function addLabel($label)
+ {
+ if ($this->varLabel === null) {
+ $this->setLabel($label);
+ } else {
+ $this->varLabel .= ' & ' . $label;
+ }
+
+ return $this;
+ }
+
+ public function getColumnExpression(Cube $cube)
+ {
+ /** @var IdoCube $cube */
+ if ($this->wantNull) {
+ return 'COALESCE(' . $cube->db()->quoteIdentifier(['c_' . $this->varName, 'varvalue']) . ", '-')";
+ } else {
+ return $cube->db()->quoteIdentifier(['c_' . $this->varName, 'varvalue']);
+ }
+ }
+
+ protected function safeVarname($name)
+ {
+ return $name;
+ }
+
+ public function addToCube(Cube $cube)
+ {
+ switch ($this->type) {
+ case self::TYPE_HOST:
+ $objectId = 'ho.object_id';
+ break;
+ case self::TYPE_SERVICE:
+ $objectId = 'so.object_id';
+ break;
+ default:
+ $objectId = 'o.object_id';
+ }
+ $name = $this->safeVarname($this->varName);
+ /** @var $cube IdoCube */
+ $alias = $cube->db()->quoteIdentifier(['c_' . $name]);
+
+ if ($cube->isPgsql()) {
+ $on = "LOWER($alias.varname) = ?";
+ $name = strtolower($name);
+ } else {
+ $on = $alias . '.varname = ? COLLATE latin1_general_ci';
+ }
+
+ $cube->innerQuery()->joinLeft(
+ array($alias => $cube->tableName('icinga_customvariablestatus')),
+ $cube->db()->quoteInto($on, $name)
+ . ' AND ' . $alias . '.object_id = ' . $objectId,
+ array()
+ )->group($alias . '.varvalue');
+ }
+}
diff --git a/library/Cube/Ido/DataView/Hoststatus.php b/library/Cube/Ido/DataView/Hoststatus.php
new file mode 100644
index 0000000..32ee44b
--- /dev/null
+++ b/library/Cube/Ido/DataView/Hoststatus.php
@@ -0,0 +1,17 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2021 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido\DataView;
+
+use Icinga\Data\ConnectionInterface;
+use Icinga\Module\Cube\Ido\Query\HoststatusQuery;
+
+class Hoststatus extends \Icinga\Module\Monitoring\DataView\Hoststatus
+{
+ public function __construct(ConnectionInterface $connection, array $columns = null)
+ {
+ $this->connection = $connection;
+ $this->query = new HoststatusQuery($connection->getResource(), $columns);
+ }
+}
diff --git a/library/Cube/Ido/DbCube.php b/library/Cube/Ido/DbCube.php
new file mode 100644
index 0000000..5fc5b47
--- /dev/null
+++ b/library/Cube/Ido/DbCube.php
@@ -0,0 +1,298 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Data\Db\DbConnection;
+use Icinga\Module\Cube\Cube;
+
+abstract class DbCube extends Cube
+{
+ /** @var DbConnection */
+ protected $connection;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var ZfSelectWrapper The inner query fetching all required data */
+ protected $innerQuery;
+
+ /** @var \Zend_Db_Select The rollup query, creating grouped sums over innerQuery */
+ protected $rollupQuery;
+
+ /** @var \Zend_Db_Select The outer query, orders respecting NULL values, rollup first */
+ protected $fullQuery;
+
+ /** @var string Database name. Allows to eventually join over multiple dbs */
+ protected $dbName;
+
+ /** @var array Key/value array containing our chosen facts and the corresponding SQL expression */
+ protected $factColumns = array();
+
+ /**
+ * A DbCube must provide a list of all available columns
+ *
+ * This is a key/value array with the key being the fact name / column alias
+ * and
+ *
+ * @return array
+ */
+ abstract public function getAvailableFactColumns();
+
+ /**
+ * @return \Zend_Db_Select
+ */
+ abstract public function prepareInnerQuery();
+
+ /**
+ * Set a database connection
+ *
+ * @param DbConnection $connection
+ * @return $this
+ */
+ public function setConnection(DbConnection $connection)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ return $this;
+ }
+
+ /**
+ * Prepare the query and fetch all data
+ *
+ * @return array
+ */
+ public function fetchAll()
+ {
+ $query = $this->fullQuery();
+ return $this->db()->fetchAll($query);
+ }
+
+ /**
+ * Choose a one or more facts
+ *
+ * This also initializes a fact column lookup array
+ *
+ * @param array $facts
+ * @return $this
+ */
+ public function chooseFacts(array $facts)
+ {
+ parent::chooseFacts($facts);
+
+ $this->factColumns = array();
+ $columns = $this->getAvailableFactColumns();
+ foreach ($this->chosenFacts as $name) {
+ $this->factColumns[$name] = $columns[$name];
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param $name
+ * @return $this
+ */
+ public function setDbName($name)
+ {
+ $this->dbName = $name;
+ return $this;
+ }
+
+ /**
+ * Gives back the table name, eventually prefixed with a defined DB name
+ *
+ * @param string $name
+ * @return string
+ */
+ public function tableName($name)
+ {
+ if ($this->dbName === null) {
+ return $name;
+ } else {
+ return $this->dbName . '.' . $name;
+ }
+ }
+
+ /**
+ * Returns an eventually defined DB name
+ *
+ * @return string|null
+ */
+ public function getDbName()
+ {
+ return $this->dbName;
+ }
+
+ /**
+ * Get our inner query
+ *
+ * Hint: mostly used to get rid of NULL values
+ *
+ * @return ZfSelectWrapper
+ */
+ public function innerQuery()
+ {
+ if ($this->innerQuery === null) {
+ $this->innerQuery = new ZfSelectWrapper($this->prepareInnerQuery());
+ }
+
+ return $this->innerQuery;
+ }
+
+ /**
+ * We first prepare the queries and to finalize it later on
+ *
+ * This way dimensions can be added one by one, they will be allowed to
+ * optionally join additional tables or apply other modifications late
+ * in the process
+ *
+ * @return void
+ */
+ public function finalizeInnerQuery()
+ {
+ $query = $this->innerQuery()->unwrap();
+ $columns = array();
+ foreach ($this->dimensions as $name => $dimension) {
+ $dimension->addToCube($this);
+ if ($this->hasSlice($name)) {
+ $query->where(
+ $dimension->getColumnExpression($this) . ' = ?',
+ $this->slices[$name]
+ );
+ } else {
+ $columns[$name] = $dimension->getColumnExpression($this);
+ }
+ }
+
+ $c = [];
+
+ foreach ($columns + $this->factColumns as $k => $v) {
+ $c[$this->db()->quoteIdentifier([$k])] = $v;
+ }
+
+ $query->columns($c);
+ }
+
+ /**
+ * Lazy-load our full query
+ *
+ * @return \Zend_Db_Select
+ */
+ protected function fullQuery()
+ {
+ if ($this->fullQuery === null) {
+ $this->fullQuery = $this->prepareFullQuery();
+ }
+
+ return $this->fullQuery;
+ }
+
+ /**
+ * Lazy-load our full query
+ *
+ * @return \Zend_Db_Select
+ */
+ protected function rollupQuery()
+ {
+ if ($this->rollupQuery === null) {
+ $this->rollupQuery = $this->prepareRollupQuery();
+ }
+
+ return $this->rollupQuery;
+ }
+
+ /**
+ * The full query wraps the rollup query in a sub-query to work around
+ * MySQL limitations. This is required to not get into trouble when ordering,
+ * especially combined with the need to keep control over (eventually desired)
+ * NULL value fact columns
+ *
+ * @return \Zend_Db_Select
+ */
+ protected function prepareFullQuery()
+ {
+ $alias = 'rollup';
+ $cols = $this->listColumns();
+
+ $columns = array();
+
+ foreach ($cols as $col) {
+ $columns[$this->db()->quoteIdentifier([$col])] = $alias . '.' . $this->db()->quoteIdentifier([$col]);
+ }
+
+ $select = $this->db()->select()->from(
+ array($alias => $this->rollupQuery()),
+ $columns
+ );
+
+ foreach ($columns as $col) {
+ $select->order('(' . $col . ' IS NOT NULL)');
+ $select->order($col);
+ }
+
+ return $select;
+ }
+
+ /**
+ * Provide access to our DB
+ *
+ * @return \Zend_Db_Adapter_Abstract
+ */
+ public function db()
+ {
+ return $this->db;
+ }
+
+ /**
+ * Whether our connection is PostgreSQL
+ *
+ * @return bool
+ */
+ public function isPgsql()
+ {
+ return $this->connection->getDbType() === 'pgsql';
+ }
+
+
+ /**
+ * This prepares the rollup query
+ *
+ * Inner query is wrapped in a subquery, summaries for all facts are
+ * fetched. Rollup considers all defined dimensions and expects them
+ * to exist as columns in the innerQuery
+ *
+ * @return \Zend_Db_Select
+ */
+ protected function prepareRollupQuery()
+ {
+ $alias = 'sub';
+
+ $dimensions = array_map(function ($val) {
+ return $this->db()->quoteIdentifier([$val]);
+ }, array_keys($this->listDimensions()));
+ $this->finalizeInnerQuery();
+ $columns = array();
+ foreach ($dimensions as $dimension) {
+ $columns[$dimension] = $alias . '.' . $dimension;
+ }
+
+ foreach ($this->listFacts() as $fact) {
+ $columns[$fact] = 'SUM(' . $fact . ')';
+ }
+
+ $select = $this->db()->select()->from(
+ array($alias => $this->innerQuery()->unwrap()),
+ $columns
+ );
+
+ if ($this->isPgsql()) {
+ $select->group('ROLLUP (' . implode(', ', $dimensions) . ')');
+ } else {
+ $select->group('(' . implode('), (', $dimensions) . ') WITH ROLLUP');
+ }
+
+ return $select;
+ }
+}
diff --git a/library/Cube/Ido/IdoCube.php b/library/Cube/Ido/IdoCube.php
new file mode 100644
index 0000000..ca76b21
--- /dev/null
+++ b/library/Cube/Ido/IdoCube.php
@@ -0,0 +1,198 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Application\Config;
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\QueryException;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Util\GlobFilter;
+
+/**
+ * IdoCube
+ *
+ * Base class for IDO-related cubes
+ *
+ * @package Icinga\Module\Cube\Ido
+ */
+abstract class IdoCube extends DbCube
+{
+ /** @var array */
+ protected $availableFacts = array();
+
+ /** @var string We ask for the IDO version for compatibility reasons */
+ protected $idoVersion;
+
+ /** @var MonitoringBackend */
+ protected $backend;
+
+ /**
+ * Cache for {@link filterProtectedCustomvars()}
+ *
+ * @var string|null
+ */
+ protected $protectedCustomvars;
+
+ /** @var GlobFilter The properties to hide from the user */
+ protected $blacklistedProperties;
+
+ /**
+ * We can steal the DB connection directly from a Monitoring backend
+ *
+ * @param MonitoringBackend $backend
+ * @return $this
+ */
+ public function setBackend(MonitoringBackend $backend)
+ {
+ $this->backend = $backend;
+
+ $resource = $backend->getResource();
+ $resource->getDbAdapter()
+ ->getConnection()
+ ->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_NATURAL);
+
+ $this->setConnection($resource);
+
+ return $this;
+ }
+
+ /**
+ * Provice access to our DB resource
+ *
+ * This lazy-loads the default monitoring backend in case no DB has been
+ * given
+ *
+ * @return \Zend_Db_Adapter_Abstract
+ */
+ public function db()
+ {
+ $this->requireBackend();
+ return parent::db();
+ }
+
+ /**
+ * Returns the Icinga IDO version
+ *
+ * @return string
+ */
+ protected function getIdoVersion()
+ {
+ if ($this->idoVersion === null) {
+ $db = $this->db();
+ $this->idoVersion = $db->fetchOne(
+ $db->select()->from('icinga_dbversion', 'version')
+ );
+ }
+
+ return $this->idoVersion;
+ }
+
+ /**
+ * Steal the default monitoring DB resource...
+ *
+ * ...in case none has been defined otherwise
+ *
+ * @return void
+ */
+ protected function requireBackend()
+ {
+ if ($this->db === null) {
+ $this->setBackend(MonitoringBackend::instance());
+ }
+ }
+
+ protected function getMonitoringRestriction()
+ {
+ $restriction = Filter::matchAny();
+ $restriction->setAllowedFilterColumns(array(
+ 'host_name',
+ 'hostgroup_name',
+ 'instance_name',
+ 'service_description',
+ 'servicegroup_name',
+ function ($c) {
+ return preg_match('/^_(?:host|service)_/i', $c);
+ }
+ ));
+
+ $filters = Auth::getInstance()->getUser()->getRestrictions('monitoring/filter/objects');
+
+ foreach ($filters as $filter) {
+ if ($filter === '*') {
+ return Filter::matchAny();
+ }
+ try {
+ $restriction->addFilter(Filter::fromQueryString($filter));
+ } catch (QueryException $e) {
+ throw new ConfigurationError(
+ 'Cannot apply restriction %s using the filter %s. You can only use the following columns: %s',
+ 'monitoring/filter/objects',
+ $filter,
+ implode(', ', array(
+ 'instance_name',
+ 'host_name',
+ 'hostgroup_name',
+ 'service_description',
+ 'servicegroup_name',
+ '_(host|service)_<customvar-name>'
+ )),
+ $e
+ );
+ }
+ }
+
+ return $restriction;
+ }
+
+ /**
+ * Return the given array without values matching the custom variables protected by the monitoring module
+ *
+ * @param string[] $customvars
+ *
+ * @return string[]
+ */
+ protected function filterProtectedCustomvars(array $customvars)
+ {
+ if ($this->blacklistedProperties === null) {
+ $this->blacklistedProperties = new GlobFilter(
+ Auth::getInstance()->getRestrictions('monitoring/blacklist/properties')
+ );
+ }
+
+ if ($this instanceof IdoServiceStatusCube) {
+ $type = 'service';
+ } else {
+ $type = 'host';
+ }
+
+ $customvars = $this->blacklistedProperties->removeMatching(
+ [$type => ['vars' => array_flip($customvars)]]
+ );
+
+ $customvars = isset($customvars[$type]['vars']) ? array_flip($customvars[$type]['vars']) : [];
+
+ if ($this->protectedCustomvars === null) {
+ $config = Config::module('monitoring')->get('security', 'protected_customvars');
+ $protectedCustomvars = array();
+
+ foreach (preg_split('~,~', $config, -1, PREG_SPLIT_NO_EMPTY) as $pattern) {
+ $regex = array();
+ foreach (explode('*', $pattern) as $literal) {
+ $regex[] = preg_quote($literal, '/');
+ }
+
+ $protectedCustomvars[] = implode('.*', $regex);
+ }
+
+ $this->protectedCustomvars = empty($protectedCustomvars)
+ ? '/^$/'
+ : '/^(?:' . implode('|', $protectedCustomvars) . ')$/';
+ }
+
+ return preg_grep($this->protectedCustomvars, $customvars, PREG_GREP_INVERT);
+ }
+}
diff --git a/library/Cube/Ido/IdoHostStatusCube.php b/library/Cube/Ido/IdoHostStatusCube.php
new file mode 100644
index 0000000..4881a9b
--- /dev/null
+++ b/library/Cube/Ido/IdoHostStatusCube.php
@@ -0,0 +1,112 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Module\Cube\CubeRenderer\HostStatusCubeRenderer;
+use Icinga\Module\Cube\Ido\DataView\Hoststatus;
+
+class IdoHostStatusCube extends IdoCube
+{
+ public function getRenderer()
+ {
+ return new HostStatusCubeRenderer($this);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getAvailableFactColumns()
+ {
+ return array(
+ 'hosts_cnt' => 'SUM(CASE WHEN hs.has_been_checked = 1 THEN 1 ELSE 0 END)',
+ 'hosts_down' => 'SUM(CASE WHEN hs.has_been_checked = 1 AND hs.current_state = 1'
+ . ' THEN 1 ELSE 0 END)',
+ 'hosts_unhandled_down' => 'SUM(CASE WHEN hs.has_been_checked = 1 AND hs.current_state = 1'
+ . ' AND hs.problem_has_been_acknowledged = 0 AND hs.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ 'hosts_unreachable' => 'SUM(CASE WHEN hs.current_state = 2 THEN 1 ELSE 0 END)',
+ 'hosts_unhandled_unreachable' => 'SUM(CASE WHEN hs.current_state = 2'
+ . ' AND hs.problem_has_been_acknowledged = 0 AND hs.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ );
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVarDimension($name, CustomVarDimension::TYPE_HOST);
+ }
+
+ /**
+ * Add a specific named dimension
+ *
+ * Right now this are just custom vars, we might support group memberships
+ * or other properties in future
+ *
+ * @param string $name
+ * @return $this
+ */
+ public function addDimensionByName($name)
+ {
+ if (count($this->filterProtectedCustomvars(array($name))) === 1) {
+ $this->addDimension($this->createDimension($name));
+ }
+
+ return $this;
+ }
+
+ /**
+ * This returns a list of all available Dimensions
+ *
+ * @return array
+ */
+ public function listAvailableDimensions()
+ {
+ $this->requireBackend();
+
+ $view = $this->backend->select()->from('hoststatus');
+
+ $view->applyFilter($this->getMonitoringRestriction());
+
+ $select = $view->getQuery()->clearOrder()->getSelectQuery();
+
+ $select
+ ->columns('cv.varname')
+ ->join(
+ ['cv' => $this->tableName('icinga_customvariablestatus')],
+ 'cv.object_id = ho.object_id',
+ []
+ )
+ ->group('cv.varname');
+
+ if (version_compare($this->getIdoVersion(), '1.12.0', '>=')) {
+ $select->where('cv.is_json = 0');
+ }
+
+ $select->order('cv.varname');
+
+ return $this->filterProtectedCustomvars($this->db()->fetchCol($select));
+ }
+
+ public function prepareInnerQuery()
+ {
+ $this->requireBackend();
+
+ $view = new Hoststatus($this->backend);
+
+ $view->getQuery()->requireColumn('host_state');
+
+ $view->applyFilter($this->getMonitoringRestriction());
+
+ $select = $view->getQuery()->clearOrder()->getSelectQuery();
+
+ return $select;
+ }
+}
diff --git a/library/Cube/Ido/IdoServiceStatusCube.php b/library/Cube/Ido/IdoServiceStatusCube.php
new file mode 100644
index 0000000..10a172a
--- /dev/null
+++ b/library/Cube/Ido/IdoServiceStatusCube.php
@@ -0,0 +1,113 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Module\Cube\CubeRenderer\ServiceStatusCubeRenderer;
+
+class IdoServiceStatusCube extends IdoCube
+{
+ public function getRenderer()
+ {
+ return new ServiceStatusCubeRenderer($this);
+ }
+
+ public function getAvailableFactColumns()
+ {
+ return [
+ 'services_cnt' => 'SUM(CASE WHEN ss.has_been_checked = 1 THEN 1 ELSE 0 END)',
+ 'services_critical' => 'SUM(CASE WHEN ss.has_been_checked = 1 AND ss.current_state = 2'
+ . ' THEN 1 ELSE 0 END)',
+ 'services_unhandled_critical' => 'SUM(CASE WHEN ss.has_been_checked = 1 AND ss.current_state = 2'
+ . ' AND ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ 'services_warning' => 'SUM(CASE WHEN ss.current_state = 1 THEN 1 ELSE 0 END)',
+ 'services_unhandled_warning' => 'SUM(CASE WHEN ss.current_state = 1'
+ . ' AND ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ 'services_unknown' => 'SUM(CASE WHEN ss.current_state = 3 THEN 1 ELSE 0 END)',
+ 'services_unhandled_unknown' => 'SUM(CASE WHEN ss.current_state = 3'
+ . ' AND ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ ];
+ }
+
+ /**
+ * This returns a list of all available Dimensions
+ *
+ * @return array
+ */
+ public function listAvailableDimensions()
+ {
+ $this->requireBackend();
+
+ $view = $this->backend->select()->from('servicestatus');
+
+ $view->applyFilter($this->getMonitoringRestriction());
+
+ $select = $view->getQuery()->clearOrder()->getSelectQuery();
+
+ $select
+ ->columns('cv.varname')
+ ->join(
+ ['cv' => $this->tableName('icinga_customvariablestatus')],
+ 'cv.object_id = so.object_id',
+ []
+ )
+ ->group('cv.varname');
+
+ if (version_compare($this->getIdoVersion(), '1.12.0', '>=')) {
+ $select->where('cv.is_json = 0');
+ }
+
+ $select->order('cv.varname');
+
+ return $this->filterProtectedCustomvars($this->db()->fetchCol($select));
+ }
+
+ public function prepareInnerQuery()
+ {
+ $this->requireBackend();
+
+ $view = $this->backend->select()->from('servicestatus');
+
+ $view->getQuery()->requireColumn('service_state');
+
+ $view->applyFilter($this->getMonitoringRestriction());
+
+ $select = $view->getQuery()->clearOrder()->getSelectQuery();
+
+ return $select;
+ }
+
+ /**
+ * Add a specific named dimension
+ *
+ * Right now this are just custom vars, we might support group memberships
+ * or other properties in future
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function addDimensionByName($name)
+ {
+ if (count($this->filterProtectedCustomvars([$name])) === 1) {
+ $this->addDimension($this->createDimension($name));
+ }
+
+ return $this;
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVarDimension($name, CustomVarDimension::TYPE_SERVICE);
+ }
+}
diff --git a/library/Cube/Ido/Query/HoststatusQuery.php b/library/Cube/Ido/Query/HoststatusQuery.php
new file mode 100644
index 0000000..6a9aa96
--- /dev/null
+++ b/library/Cube/Ido/Query/HoststatusQuery.php
@@ -0,0 +1,47 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2021 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido\Query;
+
+use Exception;
+use Icinga\Application\Version;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Exception\NotImplementedError;
+use Icinga\Module\Monitoring\Backend\Ido\Query\IdoQuery;
+
+class HoststatusQuery extends \Icinga\Module\Monitoring\Backend\Ido\Query\HoststatusQuery
+{
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup',
+ 'services' => 'servicestatus'
+ );
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'servicestatus') {
+ return ['s.host_object_id', 'ho.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+
+ protected function createSubQueryFilter(FilterExpression $filter, $queryName)
+ {
+ try {
+ return parent::createSubQueryFilter($filter, $queryName);
+ } catch (Exception $e) {
+ if (version_compare(Version::VERSION, '2.10.0', '>=')) {
+ throw $e;
+ }
+
+ if ($e->getMessage() === 'Undefined array key 0' && basename($e->getFile()) === 'IdoQuery.php') {
+ // Ensures compatibility with earlier Icinga Web 2 versions
+ throw new NotImplementedError('');
+ } else {
+ throw $e;
+ }
+ }
+ }
+}
diff --git a/library/Cube/Ido/ZfSelectWrapper.php b/library/Cube/Ido/ZfSelectWrapper.php
new file mode 100644
index 0000000..745d7a5
--- /dev/null
+++ b/library/Cube/Ido/ZfSelectWrapper.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Icinga\Module\Cube\Ido;
+
+/**
+ * Since version 1.1.0 we're using the monitoring module's queries as the cubes' base queries.
+ * Before, the host object table was available using the alias 'o'. Now it's 'ho'.
+ * Without this wrapper, the action link hook provided by the director would fail because it relies on the alias 'o'.
+ */
+class ZfSelectWrapper
+{
+ /** @var \Zend_Db_Select */
+ protected $select;
+
+ public function __construct(\Zend_Db_Select $select)
+ {
+ $this->select = $select;
+ }
+
+ /**
+ * Get the underlying Zend_Db_Select query
+ *
+ * @return \Zend_Db_Select
+ */
+ public function unwrap()
+ {
+ return $this->select;
+ }
+
+ /**
+ * {@see \Zend_Db_Select::reset()}
+ */
+ public function reset($part = null)
+ {
+ $this->select->reset($part);
+
+ return $this;
+ }
+
+ /**
+ * {@see \Zend_Db_Select::columns()}
+ */
+ public function columns($cols = '*', $correlationName = null)
+ {
+ if (is_array($cols)) {
+ foreach ($cols as $alias => &$col) {
+ if (substr($col, 0, 2) === 'o.') {
+ $col = 'ho.' . substr($col, 2);
+ }
+ }
+ }
+
+ return $this->select->columns($cols, $correlationName);
+ }
+
+ /**
+ * Proxy Zend_Db_Select method calls
+ *
+ * @param string $name The name of the method to call
+ * @param array $arguments Arguments for the method to call
+ *
+ * @return mixed
+ *
+ * @throws \BadMethodCallException If the called method does not exist
+ */
+ public function __call($name, array $arguments)
+ {
+ if (! method_exists($this->select, $name)) {
+ $class = get_class($this);
+ $message = "Call to undefined method $class::$name";
+
+ throw new \BadMethodCallException($message);
+ }
+
+ return call_user_func_array([$this->select, $name], $arguments);
+ }
+}
diff --git a/library/Cube/ProvidedHook/Cube/IcingaDbActions.php b/library/Cube/ProvidedHook/Cube/IcingaDbActions.php
new file mode 100644
index 0000000..c670ab4
--- /dev/null
+++ b/library/Cube/ProvidedHook/Cube/IcingaDbActions.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Icinga\Module\Cube\ProvidedHook\Cube;
+
+use Icinga\Module\Cube\Hook\IcingaDbActionsHook;
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use Icinga\Module\Cube\Icingadb\IcingadbServiceStatusCube;
+
+class IcingaDbActions extends IcingaDbActionsHook
+{
+ public function createActionLinks(IcingaDbCube $cube)
+ {
+ $type = 'host';
+ if ($cube instanceof IcingadbServiceStatusCube) {
+ $type = 'service';
+ }
+
+ $url = 'icingadb/' . $type . 's';
+
+ $paramsWithPrefix = [];
+ foreach ($cube->getSlices() as $dimension => $slice) {
+ $paramsWithPrefix[$type . '.vars.' . $dimension] = $slice;
+ }
+
+ if ($type === 'host') {
+ $this->addActionLink(
+ $this->makeUrl($url, $paramsWithPrefix),
+ t('Show hosts status'),
+ t('This shows all matching hosts and their current state in Icinga DB Web'),
+ 'server'
+ );
+ } else {
+ $this->addActionLink(
+ $this->makeUrl($url, $paramsWithPrefix),
+ t('Show services status'),
+ t('This shows all matching hosts and their current state in Icinga DB Web'),
+ 'cog'
+ );
+ }
+ }
+}
diff --git a/library/Cube/ProvidedHook/Cube/MonitoringActions.php b/library/Cube/ProvidedHook/Cube/MonitoringActions.php
new file mode 100644
index 0000000..ae65c67
--- /dev/null
+++ b/library/Cube/ProvidedHook/Cube/MonitoringActions.php
@@ -0,0 +1,53 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\ProvidedHook\Cube;
+
+use Icinga\Module\Cube\Hook\ActionsHook;
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Ido\IdoHostStatusCube;
+use Icinga\Module\Cube\Ido\IdoServiceStatusCube;
+use Icinga\Web\View;
+
+/**
+ * MonitoringActionLinks
+ *
+ * An action link hook implementation linking to matching hosts/services in the
+ * monitoring module
+ */
+class MonitoringActions extends ActionsHook
+{
+ public function prepareActionLinks(Cube $cube, View $view)
+ {
+ if ($cube instanceof IdoHostStatusCube) {
+ $vars = [];
+ foreach ($cube->getSlices() as $key => $val) {
+ $vars['_host_' . $key] = $val;
+ }
+
+ $url = 'monitoring/list/hosts';
+
+ $this->addActionLink(
+ $this->makeUrl($url, $vars),
+ $view->translate('Show hosts status'),
+ $view->translate('This shows all matching hosts and their current state in the monitoring module'),
+ 'host'
+ );
+ } elseif ($cube instanceof IdoServiceStatusCube) {
+ $vars = [];
+ foreach ($cube->getSlices() as $key => $val) {
+ $vars['_service_' . $key] = $val;
+ }
+
+ $url = 'monitoring/list/services';
+
+ $this->addActionLink(
+ $this->makeUrl($url, $vars),
+ $view->translate('Show services status'),
+ $view->translate('This shows all matching services and their current state in the monitoring module'),
+ 'host'
+ );
+ }
+ }
+}
diff --git a/library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php b/library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php
new file mode 100644
index 0000000..a32310a
--- /dev/null
+++ b/library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php
@@ -0,0 +1,11 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\ProvidedHook\Icingadb;
+
+use Icinga\Module\Icingadb\Hook\IcingadbSupportHook;
+
+class IcingadbSupport extends IcingadbSupportHook
+{
+}
diff --git a/library/Cube/Web/ActionLink.php b/library/Cube/Web/ActionLink.php
new file mode 100644
index 0000000..c9ad87b
--- /dev/null
+++ b/library/Cube/Web/ActionLink.php
@@ -0,0 +1,103 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Web;
+
+use Icinga\Web\Url;
+use Icinga\Web\View;
+
+/**
+ * ActionLink
+ *
+ * ActionLinksHook implementations return instances of this class
+ *
+ * @package Icinga\Module\Cube\Web
+ */
+class ActionLink
+{
+ /** @var Url */
+ protected $url;
+
+ /** @var string */
+ protected $title;
+
+ /** @var string */
+ protected $description;
+
+ /** @var string */
+ protected $icon;
+
+ /**
+ * ActionLink constructor.
+ * @param Url $url
+ * @param string $title
+ * @param string $description
+ * @param string $icon
+ */
+ public function __construct(Url $url, $title, $description, $icon)
+ {
+ $this->url = $url;
+ $this->title = $title;
+ $this->description = $description;
+ $this->icon = $icon;
+ }
+
+ /**
+ * @return Url
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * @return string
+ */
+ public function getIcon()
+ {
+ return $this->icon;
+ }
+
+ /**
+ * Render our icon
+ *
+ * @param View $view
+ * @return string
+ */
+ protected function renderIcon(View $view)
+ {
+ return $view->icon($this->getIcon());
+ }
+
+ /**
+ * @param View $view
+ * @return string
+ */
+ public function render(View $view)
+ {
+ return sprintf(
+ '<a href="%s">%s<span class="title">%s</span><p>%s</p></a>',
+ $this->getUrl(),
+ $this->renderIcon($view),
+ $view->escape($this->getTitle()),
+ $view->escape($this->getDescription())
+ );
+ }
+}
diff --git a/library/Cube/Web/ActionLinks.php b/library/Cube/Web/ActionLinks.php
new file mode 100644
index 0000000..0c6cba4
--- /dev/null
+++ b/library/Cube/Web/ActionLinks.php
@@ -0,0 +1,115 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Web;
+
+use Exception;
+use Icinga\Application\Hook;
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Hook\ActionsHook;
+use Icinga\Web\View;
+
+/**
+ * ActionLink
+ *
+ * ActionsHook implementations return instances of this class
+ *
+ * @package Icinga\Module\Cube\Web
+ */
+class ActionLinks
+{
+ /** @var ActionLink[] */
+ protected $links = array();
+
+ /**
+ * Get all links for all Hook implementations
+ *
+ * This is what the Cube calls when rendering details
+ *
+ * @param Cube $cube
+ * @param View $view
+ *
+ * @return string
+ */
+ public static function renderAll(Cube $cube, View $view)
+ {
+ $html = array();
+
+ /** @var ActionsHook $hook */
+ foreach (Hook::all('Cube/Actions') as $hook) {
+ try {
+ $hook->prepareActionLinks($cube, $view);
+ } catch (Exception $e) {
+ $html[] = static::renderErrorItem($e, $view);
+ }
+
+ foreach ($hook->getActionLinks()->getLinks() as $link) {
+ $html[] = '<li>' . $link->render($view) . '</li>';
+ }
+ }
+
+ if (empty($html)) {
+ $html[] = static::renderErrorItem(
+ $view->translate('No action links have been provided for this cube'),
+ $view
+ );
+ }
+
+ return implode("\n", $html) . "\n";
+ }
+
+ /**
+ * @param Exception|string $error
+ * @param View $view
+ * @return string
+ */
+ private static function renderErrorItem($error, View $view)
+ {
+ if ($error instanceof Exception) {
+ $error = $error->getMessage();
+ }
+ return '<li class="error">' . $view->escape($error) . '</li>';
+ }
+
+ /**
+ * Add an ActionLink to this set of actions
+ *
+ * @param ActionLink $link
+ * @return $this
+ */
+ public function add(ActionLink $link)
+ {
+ $this->links[] = $link;
+ return $this;
+ }
+
+ /**
+ * @return ActionLink[]
+ */
+ public function getLinks()
+ {
+ return $this->links;
+ }
+
+ /**
+ * @param View $view
+ *
+ * @return string
+ */
+ public function render(View $view)
+ {
+ $links = $this->getLinks();
+ if (empty($links)) {
+ return '';
+ }
+
+ $html = '<ul class="action-links">';
+ foreach ($links as $link) {
+ $html .= $link->render($view);
+ }
+ $html .= '</ul>';
+
+ return $html;
+ }
+}
diff --git a/library/Cube/Web/Controller.php b/library/Cube/Web/Controller.php
new file mode 100644
index 0000000..b7405f9
--- /dev/null
+++ b/library/Cube/Web/Controller.php
@@ -0,0 +1,115 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Web;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Cube\DimensionParams;
+use Icinga\Module\Cube\Forms\DimensionsForm;
+use Icinga\Module\Cube\Hook\IcingaDbActionsHook;
+use Icinga\Module\Cube\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Web\Controller as WebController;
+use Icinga\Web\View;
+
+abstract class Controller extends WebController
+{
+ /** @var View This helps IDEs to understand that this is not ZF view */
+ public $view;
+
+ /** @var \Icinga\Module\Cube\Cube */
+ protected $cube;
+
+ /**
+ * Return this controllers' cube
+ *
+ * @return \Icinga\Module\Cube\Cube
+ */
+ abstract protected function getCube();
+
+ protected function moduleInit()
+ {
+ $this->cube = $this->getCube();
+ }
+
+ public function detailsAction()
+ {
+ $this->getTabs()->add('details', [
+ 'label' => $this->translate('Cube details'),
+ 'url' => $this->getRequest()->getUrl()
+ ])->activate('details');
+
+ $this->cube->chooseFacts(array_keys($this->cube->getAvailableFactColumns()));
+ $vars = DimensionParams::fromString($this->params->shift('dimensions', ''))->getDimensions();
+ $wantNull = $this->params->shift('wantNull');
+
+ foreach ($vars as $var) {
+ $this->cube->addDimensionByName($var);
+ if ($wantNull) {
+ $this->cube->getDimension($var)->wantNull();
+ }
+ }
+
+ foreach (['renderLayout', 'showFullscreen', 'showCompact', 'view'] as $p) {
+ $this->params->shift($p);
+ }
+
+ foreach ($this->params->toArray() as $param) {
+ $this->cube->slice(rawurldecode($param[0]), rawurldecode($param[1]));
+ }
+
+ $this->view->title = $this->cube->getSlicesLabel();
+ if (Module::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend()) {
+ $this->view->links = IcingaDbActionsHook::renderAll($this->cube);
+ } else {
+ $this->view->links = ActionLinks::renderAll($this->cube, $this->view);
+ }
+
+ $this->render('cube-details', null, true);
+ }
+
+ protected function renderCube()
+ {
+ // Hint: order matters, we are shifting!
+ $showSettings = $this->params->shift('showSettings');
+
+ $this->cube->chooseFacts(array_keys($this->cube->getAvailableFactColumns()));
+ $vars = DimensionParams::fromString($this->params->shift('dimensions', ''))->getDimensions();
+ $wantNull = $this->params->shift('wantNull');
+
+ foreach ($vars as $var) {
+ $this->cube->addDimensionByName($var);
+ if ($wantNull) {
+ $this->cube->getDimension($var)->wantNull();
+ }
+ }
+
+ foreach (['renderLayout', 'showFullscreen', 'showCompact', 'view'] as $p) {
+ $this->params->shift($p);
+ }
+
+ foreach ($this->params->toArray() as $param) {
+ $this->cube->slice(rawurldecode($param[0]), rawurldecode($param[1]));
+ }
+
+ $this->view->title = sprintf(
+ $this->translate('Cube: %s'),
+ $this->cube->getPathLabel()
+ );
+
+ if (count($this->cube->listDimensions()) > 0) {
+ $this->view->cube = $this->cube;
+ } else {
+ $showSettings = true;
+ }
+
+ if ($showSettings) {
+ $this->view->form = (new DimensionsForm())->setCube($this->cube);
+ $this->view->form->handleRequest();
+ } else {
+ $this->setAutorefreshInterval(15);
+ }
+
+ $this->render('cube-index', null, true);
+ }
+}
diff --git a/library/Cube/Web/IdoController.php b/library/Cube/Web/IdoController.php
new file mode 100644
index 0000000..2a024ae
--- /dev/null
+++ b/library/Cube/Web/IdoController.php
@@ -0,0 +1,24 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Web;
+
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+
+abstract class IdoController extends Controller
+{
+ public function createTabs()
+ {
+ return $this->getTabs()
+ ->add('cube/hosts', [
+ 'label' => $this->translate('Hosts'),
+ 'url' => 'cube/hosts' . ($this->params->toString() === '' ? '' : '?' . $this->params->toString())
+ ])
+ ->add('cube/services', [
+ 'label' => $this->translate('Services'),
+ 'url' => 'cube/services' . ($this->params->toString() === '' ? '' : '?' . $this->params->toString())
+ ])
+ ->extend(new DashboardAction());
+ }
+}