diff --git a/README.md b/README.md index 45fce7a..0d19f46 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The Icinga Cube is a tiny but useful [Icinga Web](https://github.com/Icinga/icin module. It currently shows host and service statistics (total count, health) grouped by various custom variables in multiple dimensions. - + It will be your new best friend in case you are running a large environment and want to get a quick answers to questions like: @@ -31,31 +31,6 @@ want to get a quick answers to questions like: * Who is running the oldest ones? In production? * Which projects are still using physical servers in which environment? -For Businessmen - Drill and Slice ---------------------------------- - -Get answers to your questions. Quick and fully autonomous, using the cube -requires no technical skills. Choose amongst all available dimensions and rotate -the Cube to fit your needs. - - - -Want to drill down? Choose a slice and get your answers: - - - -All facts configured for systems monitored by [Icinga](https://www.icinga.com/) -can be used for your research. - -For Icinga Director users -------------------------- - -In case you are using the [Icinga Director](https://github.com/Icinga/icingaweb2-module-director), -in addition to the multi-selection/edit feature the cube provides a nice way to -modify multiple hosts at once. - - - Installation ------------ diff --git a/doc/01-About.md b/doc/01-About.md index bd9dd58..18697c0 100644 --- a/doc/01-About.md +++ b/doc/01-About.md @@ -4,7 +4,7 @@ The Icinga Cube is a tiny but useful [Icinga Web](https://github.com/Icinga/icin module. It currently shows host and service statistics (total count, health) grouped by various custom variables in multiple dimensions. - + It will be your new best friend in case you are running a large environment and want to get a quick answers to questions like: @@ -25,31 +25,6 @@ want to get a quick answers to questions like: * Who is running the oldest ones? In production? * Which projects are still using physical servers in which environment? -For Businessmen - Drill and Slice ---------------------------------- - -Get answers to your questions. Quick and fully autonomous, using the cube -requires no technical skills. Choose amongst all available dimensions and rotate -the Cube to fit your needs. - - - -Want to drill down? Choose a slice and get your answers: - - - -All facts configured for systems monitored by [Icinga](https://www.icinga.com/) -can be used for your research. - -For Icinga Director users -------------------------- - -In case you are using the [Icinga Director](https://github.com/Icinga/icingaweb2-module-director), -in addition to the multi-selection/edit feature the cube provides a nice way to -modify multiple hosts at once. - - - Installation ------------ diff --git a/doc/img/cube_action-links.png b/doc/img/cube_action-links.png deleted file mode 100644 index 2f3abe3..0000000 Binary files a/doc/img/cube_action-links.png and /dev/null differ diff --git a/doc/img/cube_director.png b/doc/img/cube_director.png deleted file mode 100644 index b081c1d..0000000 Binary files a/doc/img/cube_director.png and /dev/null differ diff --git a/doc/img/cube_move-up.png b/doc/img/cube_move-up.png deleted file mode 100644 index 1fb3c09..0000000 Binary files a/doc/img/cube_move-up.png and /dev/null differ diff --git a/doc/img/cube_overview.png b/doc/img/cube_overview.png new file mode 100644 index 0000000..265afbb Binary files /dev/null and b/doc/img/cube_overview.png differ diff --git a/doc/img/cube_simple.png b/doc/img/cube_simple.png deleted file mode 100644 index 91bb8f2..0000000 Binary files a/doc/img/cube_simple.png and /dev/null differ diff --git a/doc/img/cube_slice.png b/doc/img/cube_slice.png deleted file mode 100644 index 4880cc3..0000000 Binary files a/doc/img/cube_slice.png and /dev/null differ diff --git a/library/Cube/CubeRenderer.php b/library/Cube/CubeRenderer.php index 5e34ceb..10fa99c 100644 --- a/library/Cube/CubeRenderer.php +++ b/library/Cube/CubeRenderer.php @@ -5,12 +5,18 @@ namespace Icinga\Module\Cube; +use Generator; +use Icinga\Data\Tree\TreeNode; use Icinga\Module\Cube\IcingaDb\IcingaDbCube; +use Icinga\Module\Cube\Web\Widget\DimensionWidget; use Icinga\Web\View; +use ipl\Html\Attributes; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; +use ipl\Html\Text; use ipl\Stdlib\Filter; -use ipl\Web\Url; -use Generator; -use Icinga\Data\Tree\TreeNode; +use ipl\Stdlib\Filter\Rule; +use stdClass; /** * CubeRenderer base class @@ -24,41 +30,37 @@ */ abstract class CubeRenderer { - /** @var View */ - protected $view; + protected View $view; - /** @var Cube */ - protected $cube; + protected Cube $cube; /** @var array Our dimensions */ - protected $dimensions; + protected array $dimensions; /** @var array Our dimensions in regular order */ - protected $dimensionOrder; + protected array $dimensionOrder; /** @var array Our dimensions in reversed order as a quick lookup source */ - protected $reversedDimensions; + protected array $reversedDimensions; /** @var array Level (deepness) for each dimension (0, 1, 2...) */ - protected $dimensionLevels; + protected array $dimensionLevels; - protected $facts; + protected object|array $facts; /** @var object The row before the current one */ - protected $lastRow; + protected object $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 + * being its value * * @var object */ - protected $summaries; - - protected $started; + protected object $summaries; /** * CubeRenderer constructor. @@ -71,33 +73,46 @@ public function __construct(Cube $cube) } /** - * Render the given facts + * Render the given facts. * - * @param $facts - * @return string + * @param object $facts + * + * @return HtmlDocument */ - abstract public function renderFacts($facts); + abstract public function createFacts(object $facts): HtmlDocument; /** - * Returns the base url for the details action + * Get the severity sort columns. * - * @return string + * @return Generator */ - abstract protected function getDetailsBaseUrl(); + abstract protected function getSeveritySortColumns(): Generator; /** - * Get the severity sort columns + * Render the badges for the Icinga DB cube. * - * @return Generator + * @param object $parts An object of state class => count pairs + * @param object $facts The facts object containing information about the current cube + * + * @return HtmlDocument */ - abstract protected function getSeveritySortColumns(): Generator; + abstract protected function createIcingaDbCubeBadges(object $parts, object $facts): HtmlDocument; + + /** + * Create a dimension widget for the given dimension cache. + * + * @param array $dimensionCache + * @param View $view + * + * @return DimensionWidget + */ + abstract protected function createDimensionWidget(array $dimensionCache, View $view): DimensionWidget; /** - * Initialize all we need + * Initialize all. */ - protected function initialize() + protected function initialize(): void { - $this->started = false; $this->initializeDimensions() ->initializeFacts() ->initializeLastRow() @@ -105,9 +120,11 @@ protected function initialize() } /** + * Initialize the last row object. + * * @return $this */ - protected function initializeLastRow() + protected function initializeLastRow(): static { $object = (object) array(); foreach ($this->dimensions as $dimension) { @@ -120,9 +137,11 @@ protected function initializeLastRow() } /** + * Initialize the dimensions order and reversed order and the levels. + * * @return $this */ - protected function initializeDimensions() + protected function initializeDimensions(): static { $this->dimensions = $this->cube->listDimensions(); @@ -141,32 +160,43 @@ protected function initializeDimensions() $this->reversedDimensions = array_reverse($this->dimensionOrder); $this->dimensionLevels = array_flip($this->dimensionOrder); + return $this; } /** + * Initialize the cube facts. + * * @return $this */ - protected function initializeFacts() + protected function initializeFacts(): static { $this->facts = $this->cube->listFacts(); + return $this; } /** + * Initialize the summaries object. + * * @return $this */ - protected function initializeSummaries() + protected function initializeSummaries(): static { $this->summaries = (object) array(); + return $this; } /** + * Get whether the given row starts a new dimension. + * If so store the values as summary for the new dimension. + * * @param object $row + * * @return bool */ - protected function startsDimension($row) + protected function startsDimension(object $row): bool { foreach ($this->dimensionOrder as $name) { if ($row->$name === null) { @@ -179,10 +209,13 @@ protected function startsDimension($row) } /** - * @param $row + * Extract the facts from a row object. + * + * @param object $row + * * @return object */ - protected function extractFacts($row) + protected function extractFacts(object $row): object { $res = (object) array(); @@ -193,11 +226,37 @@ protected function extractFacts($row) return $res; } - public function render(View $view) + public function render(View $view): string { $this->view = $view; $this->initialize(); - $htm = $this->beginContainer(); + $lastRow = $this->lastRow; + $cubeContainer = new HtmlElement('div', new Attributes(['class' => 'cube'])); + + /** + * Cache dimension names, rows and summaries to add to the next lower dimension. + * + * - 'body' => The body of the dimension container as a HtmlElement + * - 'name' => The name of the dimension + * - 'row' => The row object for the dimension + * - 'summaries' => The summaries object for the dimension + */ + $dimensionCache = []; + + // Initialize the previous level to -1, so that the first dimension level is never smaller. + $lastLevel = -1; + + // Store the lowest dimension level to determine which dimension containers should + // be added directly to the cube container. + $lowestLevel = array_key_first($this->dimensionOrder); + + // If dimension 2 is the only dimension, we use a container for flexbox layout. + if ($lowestLevel === 2) { + $dimension2Container = new HtmlElement( + 'div', + new Attributes(['class' => 'dimension-2-container']) + ); + } $results = $this->cube->fetchAll(); @@ -209,18 +268,141 @@ public function render(View $view) } } + $isStartingRow = false; + foreach ($results as $row) { - $htm .= $this->renderRow($row); + // The first row of each dimension contains the dimension summaries, so we skip it. + if ($this->startsDimension($row)) { + $isStartingRow = true; + continue; + } + foreach ($this->dimensionOrder as $level => $dimensionName) { + if ( + (! $isStartingRow || $level === array_key_first($this->dimensionOrder)) + && $lastRow->$dimensionName === $row->$dimensionName + ) { + continue; + } + + // If the current dimension level is lower than the previous one, add the previous dimension level + // container to the current dimension level container or to the cube container if the current level + // is the lowest level. + if ($level < $lastLevel) { + if ($level === 0) { + // Add last dimension 1 container to dimension 0 container body. + $dimensionCache[0]['body']->addHtml( + $this->createDimensionWidget($dimensionCache[1], $view) + ); + + // Add dimension 0 container to cube + $cubeContainer->addHtml( + $this->createDimensionWidget($dimensionCache[0], $view) + ); + } elseif ($level === 1) { + if ($lowestLevel === 0) { + // Add dimension 1 container to dimension 0 container body. + $dimensionCache[0]['body']->addHtml( + $this->createDimensionWidget($dimensionCache[1], $view) + ); + } else { + // Add dimension 1 container directly to cube if dimension 1 is the lowest level. + $cubeContainer->addHtml( + $this->createDimensionWidget($dimensionCache[1], $view) + ); + } + } + } + + if ($level < 2) { + // Initialize the dimension body cache for the current level. + $dimensionCache[$level]['body'] = new HtmlDocument(); + + // Store the dimension name, row, and summaries for the current level. + $dimensionCache[$level]['name'] = $dimensionName; + $dimensionCache[$level]['row'] = clone $row; + $dimensionCache[$level]['summaries'] = clone $this->summaries; + } elseif ($level === 2) { + if ($lowestLevel < 2) { + // Add dimension 2 rect to dimension 1 container body. + $dimensionCache[1]['body']->addHtml( + $this->createDimensionWidget( + [ + 'body' => $this->createFacts($row), + 'name' => $dimensionName, + 'row' => $row, + 'summaries' => $this->summaries + ], + $view + ) + ); + } else { + // Initialize the dimension 2 container if it does not exist yet. + if (! isset($dimensionCache[2]['body'])) { + $dimensionCache[2]['body'] = new HtmlDocument(); + } + + // Add dimension 2 rect to dimension 2 container. + $dimension2Container->addHtml( + $this->createDimensionWidget( + [ + 'body' => $this->createFacts($row), + 'name' => $dimensionName, + 'row' => $row, + 'summaries' => $this->summaries + ], + $view + ) + ); + } + } + + // Store the current dimension level as last level to compare with the next row. + $lastLevel = $level; + } + + $isStartingRow = false; + + // Store the current row as the last row to compare with the next row. + $lastRow = $row; } - return $htm . $this->closeDimensions() . $this->endContainer(); + switch ($lowestLevel) { + case 0: + if (isset($dimensionCache[0]['body'])) { + // Add last dimension 1 container to dimension 0 container body. + $dimensionCache[0]['body']->addHtml( + $this->createDimensionWidget($dimensionCache[1], $view) + ); + // Add dimension 0 container to cube. + $cubeContainer->addHtml( + $this->createDimensionWidget($dimensionCache[0], $view) + ); + } + break; + case 1: + if (isset($dimensionCache[1]['body'])) { + // Add dimension 1 container directly to cube if no dimension 1 is the lowest level. + $cubeContainer->addHtml( + $this->createDimensionWidget($dimensionCache[1], $view) + ); + } + break; + case 2: + if (isset($dimensionCache[2]['body'])) { + // Add dimension 2 container directly to cube if dimension 2 is the lowest level. + $cubeContainer->addHtml($dimension2Container); + } + break; + } + + return $cubeContainer->render(); } /** - * Sort the results by severity + * Sort the results by severity. * - * @param $results array The fetched results + * @param $results array The fetched results * @param $isSortDirDesc bool Whether the sort direction is descending * * @return Generator @@ -296,215 +478,93 @@ private function sortBySeverity(array $results, bool $isSortDirDesc): Generator 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) + /** + * Get the level (deepness) of a given dimension name. + * + * This is used to determine the indentation level for the dimension + * container. + * + * @param string $name The name of the dimension + * + * @return int + */ + protected function getLevel(string $name): int { - $last = $this->lastRow; - foreach ($this->dimensionOrder as $name) { - if ($last->$name !== $row->$name) { - return $this->beginDimensionsUpFrom($name, $row); - } - } - - return ''; + return $this->dimensionLevels[$name]; } - protected function beginDimensionsUpFrom($dimension, $row) + /** + * Create the badges for the IDO cube. + * + * @param array $parts An array of state class => count pairs + * + * @return HtmlDocument + */ + protected function createIdoCubeBadges(array $parts): HtmlDocument { - $htm = ''; - $found = false; + $badges = new HtmlDocument(); + $others = new HtmlElement('span', new Attributes(['class' => 'others'])); + $mainDone = false; - foreach ($this->dimensionOrder as $name) { - if ($name === $dimension) { - $found = true; + foreach ($parts as $class => $count) { + if ($count === 0) { + continue; } - if ($found) { - $htm .= $this->beginDimension($name, $row); + if (! $mainDone) { + $badges->addHtml(new HtmlElement('span', new Attributes(['class' => $class]), new Text($count))); + $mainDone = true; + } else { + $others->addHtml(new HtmlElement('span', new Attributes(['class' => $class]), new Text($count))); } } - 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 . ' ' . "\n" . $indent . "\n"; - } - - protected function getIndent($name) - { - return str_repeat(' ', $this->getLevel($name)); - } + $badges->addHtml($others); - 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 . '