diff options
Diffstat (limited to 'library/Cube/CubeRenderer.php')
-rw-r--r-- | library/Cube/CubeRenderer.php | 512 |
1 files changed, 512 insertions, 0 deletions
diff --git a/library/Cube/CubeRenderer.php b/library/Cube/CubeRenderer.php new file mode 100644 index 0000000..e88a938 --- /dev/null +++ b/library/Cube/CubeRenderer.php @@ -0,0 +1,512 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube; + +use Icinga\Module\Cube\IcingaDb\IcingaDbCube; +use Icinga\Web\View; +use ipl\Stdlib\Filter; +use ipl\Web\Url; +use Generator; +use Icinga\Data\Tree\TreeNode; + +/** + * 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(); + + /** + * Get the severity sort columns + * + * @return Generator + */ + abstract protected function getSeveritySortColumns(): Generator; + + /** + * 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(); + + $results = $this->cube->fetchAll(); + + if (! empty($results) && $this->cube::isUsingIcingaDb()) { + $sortBy = $this->cube->getSortBy(); + if ($sortBy && $sortBy[0] === $this->cube::DIMENSION_SEVERITY_SORT_PARAM) { + $isSortDirDesc = isset($sortBy[1]) && $sortBy[1] !== 'asc'; + $results = $this->sortBySeverity($results, $isSortDirDesc); + } + } + + foreach ($results as $row) { + $htm .= $this->renderRow($row); + } + + return $htm . $this->closeDimensions() . $this->endContainer(); + } + + + /** + * Sort the results by severity + * + * @param $results array The fetched results + * @param $isSortDirDesc bool Whether the sort direction is descending + * + * @return Generator + */ + private function sortBySeverity(array $results, bool $isSortDirDesc): Generator + { + $perspective = end($this->dimensionOrder); + $resultsCount = count($results); + $tree = [new TreeNode()]; + + $prepareHeaders = function (array $tree, object $row): TreeNode { + $node = (new TreeNode()) + ->setValue($row); + $parent = end($tree); + $parent->appendChild($node); + + return $node; + }; + + $i = 0; + do { + $row = $results[$i]; + while ($row->$perspective === null) { + $tree[] = $prepareHeaders($tree, $row); + + if (! isset($results[++$i])) { + break; + } + + $row = $results[$i]; + } + + for (; $i < $resultsCount; $i++) { + $row = $results[$i]; + + $anyNull = false; + foreach ($this->dimensionOrder as $dimension) { + if ($row->$dimension === null) { + $anyNull = true; + array_pop($tree); + } + } + + if ($anyNull) { + break; + } + + $prepareHeaders($tree, $row); + } + } while ($i < $resultsCount); + + $nodes = function (TreeNode $node) use (&$nodes, $isSortDirDesc): Generator { + yield $node->getValue(); + $children = $node->getChildren(); + + uasort($children, function (TreeNode $a, TreeNode $b) use ($isSortDirDesc): int { + foreach ($this->getSeveritySortColumns() as $column) { + $comparison = $a->getValue()->$column <=> $b->getValue()->$column; + if ($comparison !== 0) { + return $comparison * ($isSortDirDesc ? -1 : 1); + } + } + + // $a and $b are equal in terms of $priorities. + return 0; + }); + + foreach ($children as $node) { + yield from $nodes($node); + } + }; + + return $nodes($tree[1]); + } + + 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) + { + $url = Url::fromPath($this->getDetailsBaseUrl()); + + if ($this->cube instanceof IcingaDbCube && $this->cube->hasBaseFilter()) { + /** @var Filter\Rule $baseFilter */ + $baseFilter = $this->cube->getBaseFilter(); + $url->setFilter($baseFilter); + } + + $urlParams = $url->getParams(); + + $dimensions = array_merge(array_keys($this->cube->listDimensions()), $this->cube->listSlices()); + $urlParams->add('dimensions', DimensionParams::update($dimensions)->getParams()); + + foreach ($this->cube->listDimensionsUpTo($name) as $dimensionName) { + $urlParams->add($this->cube::SLICE_PREFIX . $dimensionName, $row->$dimensionName); + } + + foreach ($this->cube->getSlices() as $key => $val) { + $urlParams->add($this->cube::SLICE_PREFIX . $key, $val); + } + + return $url; + } + + protected function getSliceUrl($name, $row) + { + return $this->view->url() + ->setParam($this->cube::SLICE_PREFIX . $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); + } +} |