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. -![Cube - Overview](doc/img/cube_simple.png) +![Cube - Overview](doc/img/cube_overview.png) 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. - -![Cube - Configure Dimensions](doc/img/cube_move-up.png) - -Want to drill down? Choose a slice and get your answers: - -![Cube - Configure Dimensions](doc/img/cube_slice.png) - -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. - -![Cube - Director multi-edit](doc/img/cube_director.png) - 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. -![Cube - Overview](img/cube_simple.png) +![Cube - Overview](img/cube_overview.png) 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. - -![Cube - Configure Dimensions](img/cube_move-up.png) - -Want to drill down? Choose a slice and get your answers: - -![Cube - Configure Dimensions](img/cube_slice.png) - -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. - -![Cube - Director multi-edit](img/cube_director.png) - 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 . '
' . "\n" - . $indent . '
' - . $this->renderDimensionLabel($name, $row) - . '
' . "\n" - . $indent . '
' . "\n"; + return $badges; } /** - * Render the label for a given dimension name + * Get the filter for the badges. * - * To have some context available, also + * @param object $facts The facts object containing information about the current cube * - * @param $name - * @param $row - * @return string + * @return Rule */ - protected function renderDimensionLabel($name, $row) + protected function getBadgeFilter(object $facts): Rule { - $caption = $row->$name; - if (empty($caption)) { - $caption = '_'; - } - - return $this->view->escape($caption); - } - - protected function getDetailsUrl($name, $row) - { - $url = Url::fromPath($this->getDetailsBaseUrl()); + $filter = Filter::all(); if ($this->cube instanceof IcingaDbCube && $this->cube->hasBaseFilter()) { - /** @var Filter\Rule $baseFilter */ - $baseFilter = $this->cube->getBaseFilter(); - $url->setFilter($baseFilter); + $filter->add($this->cube->getBaseFilter()); } - $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->listDimensions() as $dimensionName => $_) { + $filter->add(Filter::equal($dimensionName, $facts->$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 $filter; } /** - * @return string + * Get the main badge and remove it from the parts. + * + * @param stdClass $parts An object of state class => count pairs + * + * @return stdClass The main badge as an object with a single property */ - protected function beginContainer() + protected function getMainBadge(stdClass $parts): stdClass { - return '
' . "\n"; - } + $mainKey = array_key_first((array) $parts); + $mainBadge = new stdClass(); + $mainBadge->$mainKey = $parts->$mainKey; + $parts->$mainKey = null; - /** - * @return string - */ - protected function endContainer() - { - return '
' . "\n"; + return $mainBadge; } /** - * Well... just to be on the safe side + * Well... just to be on the safe side. */ public function __destruct() { diff --git a/library/Cube/CubeRenderer/HostStatusCubeRenderer.php b/library/Cube/CubeRenderer/HostStatusCubeRenderer.php index ad05457..137bef7 100644 --- a/library/Cube/CubeRenderer/HostStatusCubeRenderer.php +++ b/library/Cube/CubeRenderer/HostStatusCubeRenderer.php @@ -7,65 +7,41 @@ use Generator; use Icinga\Module\Cube\CubeRenderer; +use Icinga\Module\Cube\Web\Widget\HostDimensionWidget; +use Icinga\Module\Icingadb\Widget\HostStateBadges; +use Icinga\Web\View; +use ipl\Html\Attributes; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; +use stdClass; class HostStatusCubeRenderer extends CubeRenderer { - protected function renderDimensionLabel($name, $row) + public function createFacts(object $facts): HtmlDocument { - $htm = parent::renderDimensionLabel($name, $row); - - if (($next = $this->cube->getDimensionAfter($name)) && isset($this->summaries->{$next->getName()})) { - $htm .= ' (' . $this->summaries->{$next->getName()}->hosts_cnt . ')'; - } - - return $htm; - } - - protected function getDimensionClasses($name, $row) - { - $classes = parent::getDimensionClasses($name, $row); - $sums = $row; - - $next = $this->cube->getDimensionAfter($name); - if ($next && isset($this->summaries->{$next->getName()})) { - $sums = $this->summaries->{$next->getName()}; - } - - $severityClass = []; - if ($sums->hosts_unhandled_down > 0) { - $severityClass[] = 'critical'; - } elseif (isset($sums->hosts_unhandled_unreachable) && $sums->hosts_unhandled_unreachable > 0) { - $severityClass[] = 'unreachable'; - } - - if (empty($severityClass)) { - if ($sums->hosts_down > 0) { - $severityClass = ['critical', 'handled']; - } elseif (isset($sums->hosts_unreachable) && $sums->hosts_unreachable > 0) { - $severityClass = ['unreachable', 'handled']; - } else { - $severityClass[] = 'ok'; - } - } - - return array_merge($classes, $severityClass); - } - - public function renderFacts($facts) - { - $indent = str_repeat(' ', 3); - $parts = array(); + $parts = []; + $partsObj = new stdClass(); if ($facts->hosts_unhandled_down > 0) { $parts['critical'] = $facts->hosts_unhandled_down; + $partsObj->hosts_down_unhandled = $facts->hosts_unhandled_down; } if (isset($facts->hosts_unhandled_unreachable) && $facts->hosts_unhandled_unreachable > 0) { $parts['unreachable'] = $facts->hosts_unhandled_unreachable; + $partsObj->hosts_unreachable_unhandled = $facts->hosts_unhandled_unreachable; + } + + if (isset($facts->hosts_pending) && $facts->hosts_pending > 0) { + $parts['pending'] = $facts->hosts_pending; + $partsObj->hosts_pending = $facts->hosts_pending; } if ($facts->hosts_down > 0 && $facts->hosts_down > $facts->hosts_unhandled_down) { - $parts['critical handled'] = $facts->hosts_down - $facts->hosts_unhandled_down; + $downHandled = $facts->hosts_down - $facts->hosts_unhandled_down; + + $parts['critical handled'] = $downHandled; + $partsObj->hosts_down_handled = $downHandled; } if ( @@ -74,64 +50,81 @@ public function renderFacts($facts) && $facts->hosts_unreachable > $facts->hosts_unhandled_unreachable ) { - $parts['unreachable handled'] = $facts->hosts_unreachable - $facts->hosts_unhandled_unreachable; + $unreachableHandled = $facts->hosts_unreachable - $facts->hosts_unhandled_unreachable; + + $parts['unreachable handled'] = $unreachableHandled; + $partsObj->hosts_unreachable_handled = $unreachableHandled; } if ( $facts->hosts_cnt > $facts->hosts_down && (! isset($facts->hosts_unreachable) || $facts->hosts_cnt > $facts->hosts_unreachable) + && (! isset($facts->hosts_pending) || $facts->hosts_cnt > $facts->hosts_pending) ) { $ok = $facts->hosts_cnt - $facts->hosts_down; if (isset($facts->hosts_unreachable)) { $ok -= $facts->hosts_unreachable; } - $parts['ok'] = $ok; - } - - $main = ''; - $sub = ''; - foreach ($parts as $class => $count) { - if ($count === 0) { - continue; + if (isset($facts->hosts_pending)) { + $ok -= $facts->hosts_pending; } - if ($main === '') { - $main = $this->makeBadgeHtml($class, $count); - } else { - $sub .= $this->makeBadgeHtml($class, $count); - } + $parts['ok'] = $ok; + $partsObj->hosts_up = $ok; } - if ($sub !== '') { - $sub = $indent - . '' - . "\n " - . $sub - . $indent - . "\n"; + + if ($this->cube::isUsingIcingaDb()) { + return $this->createIcingaDbCubeBadges($partsObj, $facts); } - return $main . $sub; + return $this->createIdoCubeBadges($parts); } - protected function makeBadgeHtml($class, $count) + protected function getSeveritySortColumns(): Generator { - $indent = str_repeat(' ', 3); - return sprintf( - '%s%s', - $indent, - $class, - $count - ) . "\n"; + yield from ['hosts_unhandled_down', 'hosts_down']; } - protected function getDetailsBaseUrl() + protected function createIcingaDbCubeBadges(object $parts, object $facts): HtmlDocument { - return 'cube/hosts/details'; + $filter = $this->getBadgeFilter($facts); + $mainBadge = $this->getMainBadge($parts); + + $partsBottom = new stdClass(); + $bottomKeys = [ + 'hosts_unreachable_unhandled', + 'hosts_unreachable_handled', + 'hosts_pending' + ]; + + foreach ($bottomKeys as $key) { + if (property_exists($parts, $key)) { + $partsBottom->$key = $parts->$key; + unset($parts->$key); + } + } + + $main = (new HostStateBadges($mainBadge)) + ->setBaseFilter($filter) + ->addAttributes(new Attributes(['data-base-target' => '_next'])); + + $others = new HtmlElement( + 'span', + new Attributes(['class' => 'others']), + (new HostStateBadges($parts)) + ->setBaseFilter($filter) + ->addAttributes(new Attributes(['data-base-target' => '_next'])), + (new HostStateBadges($partsBottom)) + ->setBaseFilter($filter) + ->addAttributes(new Attributes(['data-base-target' => '_next'])) + ); + + return (new HtmlDocument())->addHtml($main, $others); } - protected function getSeveritySortColumns(): Generator + protected function createDimensionWidget(array $dimensionCache, View $view): HostDimensionWidget { - yield from ['hosts_unhandled_down', 'hosts_down']; + return new HostDimensionWidget($dimensionCache, $this->cube, $view, $this->getLevel($dimensionCache['name'])); } } diff --git a/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php index 86b35dc..250c9ff 100644 --- a/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php +++ b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php @@ -7,144 +7,141 @@ use Generator; use Icinga\Module\Cube\CubeRenderer; +use Icinga\Module\Cube\Web\Widget\ServiceDimensionWidget; +use Icinga\Module\Icingadb\Widget\ServiceStateBadges; +use Icinga\Web\View; +use ipl\Html\Attributes; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; +use stdClass; class ServiceStatusCubeRenderer extends CubeRenderer { - public function renderFacts($facts) + public function createFacts(object $facts): HtmlDocument { - $indent = str_repeat(' ', 3); $parts = []; + $partsObj = new stdClass(); if ($facts->services_unhandled_critical > 0) { $parts['critical'] = $facts->services_unhandled_critical; + $partsObj->services_critical_unhandled = $facts->services_unhandled_critical; } if ($facts->services_unhandled_unknown > 0) { $parts['unknown'] = $facts->services_unhandled_unknown; + $partsObj->services_unknown_unhandled = $facts->services_unhandled_unknown; } if ($facts->services_unhandled_warning > 0) { $parts['warning'] = $facts->services_unhandled_warning; + $partsObj->services_warning_unhandled = $facts->services_unhandled_warning; + } + + if (isset($facts->services_pending) && $facts->services_pending > 0) { + $partsObj->services_pending = $facts->services_pending; } if ($facts->services_critical > 0 && $facts->services_critical > $facts->services_unhandled_critical) { - $parts['critical handled'] = $facts->services_critical - $facts->services_unhandled_critical; + $criticalHandled = $facts->services_critical - $facts->services_unhandled_critical; + + $parts['critical handled'] = $criticalHandled; + $partsObj->services_critical_handled = $criticalHandled; } if ($facts->services_unknown > 0 && $facts->services_unknown > $facts->services_unhandled_unknown) { - $parts['unknown handled'] = $facts->services_unknown - $facts->services_unhandled_unknown; + $unknownHandled = $facts->services_unknown - $facts->services_unhandled_unknown; + + $parts['unknown handled'] = $unknownHandled; + $partsObj->services_unknown_handled = $unknownHandled; } if ($facts->services_warning > 0 && $facts->services_warning > $facts->services_unhandled_warning) { - $parts['warning handled'] = $facts->services_warning - $facts->services_unhandled_warning; + $warningHandled = $facts->services_warning - $facts->services_unhandled_warning; + + $parts['warning handled'] = $warningHandled; + $partsObj->services_warning_handled = $warningHandled; } if ( - $facts->services_cnt > $facts->services_critical && $facts->services_cnt > $facts->services_warning + $facts->services_cnt > $facts->services_critical + && $facts->services_cnt > $facts->services_warning && $facts->services_cnt > $facts->services_unknown + && (! isset($facts->services_pending) || $facts->services_cnt > $facts->services_pending) ) { - $parts['ok'] = $facts->services_cnt - $facts->services_critical - $facts->services_warning - + $ok = $facts->services_cnt - $facts->services_critical - $facts->services_warning - $facts->services_unknown; - } - $main = ''; - $sub = ''; - foreach ($parts as $class => $count) { - if ($count === 0) { - continue; + if (isset($facts->services_pending)) { + $ok -= $facts->services_pending; } - if ($main === '') { - $main = $this->makeBadgeHtml($class, $count); - } else { - $sub .= $this->makeBadgeHtml($class, $count); - } + $parts['ok'] = $ok; + $partsObj->services_ok = $ok; } - if ($sub !== '') { - $sub = $indent - . '' - . "\n " - . $sub - . $indent - . "\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 .= ' (' . $this->summaries->{$next->getName()}->services_cnt . ')'; + if ($this->cube::isUsingIcingaDb()) { + return $this->createIcingaDbCubeBadges($partsObj, $facts); } - return $htm; + return $this->createIdoCubeBadges($parts); } - protected function getDimensionClasses($name, $row) + protected function getSeveritySortColumns(): Generator { - $classes = parent::getDimensionClasses($name, $row); - $sums = $row; - - $next = $this->cube->getDimensionAfter($name); - if ($next && isset($this->summaries->{$next->getName()})) { - $sums = $this->summaries->{$next->getName()}; - } - - if ($sums->services_unhandled_critical > 0) { - $severityClass[] = 'critical'; - } elseif ($sums->services_unhandled_unknown > 0) { - $severityClass[] = 'unknown'; - } elseif ($sums->services_unhandled_warning > 0) { - $severityClass[] = 'warning'; + $columns = ['critical', 'unknown', 'warning']; + foreach ($columns as $column) { + yield "services_unhandled_$column"; } - if (empty($severityClass)) { - if ($sums->services_critical > 0) { - $severityClass = ['critical', 'handled']; - } elseif ($sums->services_unknown > 0) { - $severityClass = ['unknown', 'handled']; - } elseif ($sums->services_warning > 0) { - $severityClass = ['warning', 'handled']; - } else { - $severityClass[] = 'ok'; - } + foreach ($columns as $column) { + yield "services_$column"; } - - return array_merge($classes, $severityClass); } - protected function makeBadgeHtml($class, $count) + protected function createIcingaDbCubeBadges(object $parts, object $facts): HtmlDocument { - $indent = str_repeat(' ', 3); - - return sprintf( - '%s%s', - $indent, - $class, - $count - ) . "\n"; - } + $filter = $this->getBadgeFilter($facts); + $mainBadge = $this->getMainBadge($parts); + + $partsBottom = new stdClass(); + $bottomKeys = [ + 'services_unknown_unhandled', + 'services_unknown_handled', + 'services_pending' + ]; + + foreach ($bottomKeys as $key) { + if (property_exists($parts, $key)) { + $partsBottom->$key = $parts->$key; + unset($parts->$key); + } + } - protected function getDetailsBaseUrl() - { - return 'cube/services/details'; + $main = (new ServiceStateBadges($mainBadge)) + ->setBaseFilter($filter) + ->addAttributes(new Attributes(['data-base-target' => '_next'])); + + $others = new HtmlElement( + 'span', + new Attributes(['class' => 'others']), + (new ServiceStateBadges($parts)) + ->setBaseFilter($filter) + ->addAttributes(new Attributes(['data-base-target' => '_next'])), + (new ServiceStateBadges($partsBottom)) + ->setBaseFilter($filter) + ->addAttributes(new Attributes(['data-base-target' => '_next'])) + ); + + return (new HtmlDocument())->addHtml($main, $others); } - protected function getSeveritySortColumns(): Generator + protected function createDimensionWidget(array $dimensionCache, View $view): ServiceDimensionWidget { - $columns = ['critical', 'unknown', 'warning']; - foreach ($columns as $column) { - yield "services_unhandled_$column"; - } - - foreach ($columns as $column) { - yield "services_$column"; - } + return new ServiceDimensionWidget( + $dimensionCache, + $this->cube, + $view, + $this->getLevel($dimensionCache['name']) + ); } } diff --git a/library/Cube/IcingaDb/IcingaDbHostStatusCube.php b/library/Cube/IcingaDb/IcingaDbHostStatusCube.php index 73ab8aa..19784b5 100644 --- a/library/Cube/IcingaDb/IcingaDbHostStatusCube.php +++ b/library/Cube/IcingaDb/IcingaDbHostStatusCube.php @@ -9,7 +9,6 @@ use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\HoststateSummary; use ipl\Stdlib\Filter; -use ipl\Stdlib\Str; class IcingaDbHostStatusCube extends IcingaDbCube { @@ -24,6 +23,7 @@ public function getAvailableFactColumns() 'hosts_cnt' => 'hosts_total', 'hosts_down' => 'hosts_down_handled + f.hosts_down_unhandled', 'hosts_unhandled_down' => 'hosts_down_unhandled', + 'hosts_pending' => 'hosts_pending', ]; } diff --git a/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php b/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php index 6539c0e..d98a570 100644 --- a/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php +++ b/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php @@ -9,7 +9,6 @@ use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\ServicestateSummary; use ipl\Stdlib\Filter; -use ipl\Stdlib\Str; class IcingaDbServiceStatusCube extends IcingaDbCube { @@ -39,6 +38,7 @@ public function getAvailableFactColumns() 'services_unhandled_warning' => 'services_warning_unhandled', 'services_unknown' => 'services_unknown_handled + f.services_unknown_unhandled', 'services_unhandled_unknown' => 'services_unknown_unhandled', + 'services_pending' => 'services_pending' ]; } diff --git a/library/Cube/Web/Widget/DimensionWidget.php b/library/Cube/Web/Widget/DimensionWidget.php new file mode 100644 index 0000000..ad5c48e --- /dev/null +++ b/library/Cube/Web/Widget/DimensionWidget.php @@ -0,0 +1,200 @@ + +// SPDX-License-Identifier: GPL-3.0-or-later + +namespace Icinga\Module\Cube\Web\Widget; + +use Icinga\Exception\ProgrammingError; +use Icinga\Module\Cube\Cube; +use Icinga\Module\Cube\Dimension; +use Icinga\Module\Cube\DimensionParams; +use Icinga\Module\Cube\IcingaDb\IcingaDbCube; +use Icinga\Web\Url as IcingaUrl; +use Icinga\Web\View; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\I18n\Translation; +use ipl\Stdlib\Filter\Rule; +use ipl\Web\Url; + +abstract class DimensionWidget extends BaseHtmlElement +{ + use Translation; + + protected $tag = 'div'; + + public function __construct( + protected array $dimension, + protected Cube $cube, + protected View $view, + protected int $level + ) { + $this->addAttributes(new Attributes(['class' => $this->getDimensionClassString()])); + } + + /** + * Get the base url for the details action. + * + * @return string + */ + abstract protected function getDetailsBaseUrl(): string; + + /** + * Get the state and handled classes for a given dimension name and row. + * + * This is used to create class attributes for the dimension container. + * + * @return array + */ + abstract protected function getDimensionClasses(): array; + + /** + * Get the sum for a given dimension. + * + * This is used to display the sum of the next dimension in the label. + * + * @param Dimension $next The next dimension + * + * @return int + */ + abstract protected function getDimensionSum(Dimension $next): int; + + public function assemble(): void + { + $this->addHtml( + new HtmlElement( + 'div', + new Attributes(['class' => 'header']), + new HtmlElement( + 'a', + new Attributes([ + 'href' => $this->getDetailsUrl(), + 'title' => sprintf( + $this->translate('Show details for %s: %s'), + $this->cube->getDimension($this->dimension['name'])->getLabel(), + $this->dimension['row']->{$this->dimension['name']} + ), + 'data-base-target' => '_next' + ]), + $this->getDimensionLabel() + ), + new HtmlElement('a', new Attributes([ + 'class' => 'icon-filter', + 'href' => $this->getSliceUrl(), + 'title' => $this->translate('Slice this cube') + ])) + ) + ); + + $this->addHtml(new HtmlElement('div', new Attributes(['class' => 'body']), $this->dimension['body'])); + } + + /** + * Get the URL for the details action. + * + * This is used to create a link to the details action of the cube. + * + * @return Url + * @throws ProgrammingError + */ + protected function getDetailsUrl(): Url + { + $prefix = ''; + $url = Url::fromPath($this->getDetailsBaseUrl()); + + if ($this->cube instanceof IcingaDbCube && $this->cube->hasBaseFilter()) { + /** @var Rule $baseFilter */ + $baseFilter = $this->cube->getBaseFilter(); + $url->setFilter($baseFilter); + } + + $urlParams = $url->getParams(); + + if (! $this->cube::isUsingIcingaDb()) { + $dimensions = array_merge(array_keys($this->cube->listDimensions()), $this->cube->listSlices()); + $urlParams->add('dimensions', DimensionParams::update($dimensions)->getParams()); + $prefix = $this->cube::SLICE_PREFIX; + } + + foreach ($this->cube->listDimensionsUpTo($this->dimension['name']) as $dimensionName) { + $urlParams->add($prefix . $dimensionName, $this->dimension['row']->$dimensionName); + } + + foreach ($this->cube->getSlices() as $key => $val) { + $urlParams->add($prefix . $key, $val); + } + + return $url; + } + + /** + * Get the URL for a slice. + * + * This is used to create a link to slice the cube by a given dimension. + * + * @return Url + */ + protected function getSliceUrl(): IcingaUrl + { + return $this->view->url() + ->setParam( + $this->cube::SLICE_PREFIX . $this->dimension['name'], + $this->dimension['row']->{$this->dimension['name']} + ); + } + + /** + * Get the class string for a given dimension name and row. + * + * This is used to create the class attribute for the dimension container. + * + * @return string + */ + protected function getDimensionClassString(): string + { + return implode(' ', $this->getDimensionClasses()); + } + + /** + * Get the base class for the dimension that indicates the dimension level. + * + * @return string + */ + protected function getDimensionBaseClass(): string + { + return 'cube-dimension' . $this->level; + } + + /** + * Render the label for a given dimension name. + * + * @return HtmlDocument + */ + protected function getDimensionLabel(): HtmlDocument + { + $label = new HtmlDocument(); + $label->addHtml( + new Text($this->dimension['row']->{$this->dimension['name']} ?: '_') + ); + + // If there is a next dimension and it has a summary, append the sum to the label + if ( + ($next = $this->cube->getDimensionAfter($this->dimension['name'])) + && isset($this->dimension['summaries']->{$next->getName()}) + ) { + $label->addHtml( + new HtmlElement( + 'span', + new Attributes(['class' => 'sum']), + new Text(' (' . $this->getDimensionSum($next) . ')') + ) + ); + } + + return $label; + } +} diff --git a/library/Cube/Web/Widget/HostDimensionWidget.php b/library/Cube/Web/Widget/HostDimensionWidget.php new file mode 100644 index 0000000..013bee1 --- /dev/null +++ b/library/Cube/Web/Widget/HostDimensionWidget.php @@ -0,0 +1,64 @@ + +// SPDX-License-Identifier: GPL-3.0-or-later + +namespace Icinga\Module\Cube\Web\Widget; + +use Icinga\Module\Cube\Dimension; + +class HostDimensionWidget extends DimensionWidget +{ + /** + * If the cube is using Icinga DB, the URL leads to the Icinga DB host list. + * If not, the URL leads to the details action of the IdoHostsController. + * + * @return string + */ + protected function getDetailsBaseUrl(): string + { + if ($this->cube::isUsingIcingaDb()) { + return 'icingadb/hosts'; + } + + return 'cube/hosts/details'; + } + + /** + * @inheritdoc + */ + protected function getDimensionClasses(): array + { + $classes = [$this->getDimensionBaseClass()]; + $sums = $this->dimension['row']; + + $next = $this->cube->getDimensionAfter($this->dimension['name']); + if ($next && isset($this->dimension['summaries']->{$next->getName()})) { + $sums = $this->dimension['summaries']->{$next->getName()}; + } + + if ($sums->hosts_unhandled_down > 0) { + $severityClass = ['critical']; + } elseif (isset($sums->hosts_unhandled_unreachable) && $sums->hosts_unhandled_unreachable > 0) { + $severityClass = ['unreachable']; + } elseif (isset($sums->hosts_pending) && $sums->hosts_pending > 0) { + $severityClass = ['pending']; + } elseif ($sums->hosts_down > 0) { + $severityClass = ['critical', 'handled']; + } elseif (isset($sums->hosts_unreachable) && $sums->hosts_unreachable > 0) { + $severityClass = ['unreachable', 'handled']; + } else { + $severityClass = ['ok']; + } + + return array_merge($classes, $severityClass); + } + + /** + * @inheritdoc + */ + protected function getDimensionSum(Dimension $next): int + { + return $this->dimension['summaries']->{$next->getName()}->hosts_cnt; + } +} diff --git a/library/Cube/Web/Widget/ServiceDimensionWidget.php b/library/Cube/Web/Widget/ServiceDimensionWidget.php new file mode 100644 index 0000000..6779137 --- /dev/null +++ b/library/Cube/Web/Widget/ServiceDimensionWidget.php @@ -0,0 +1,68 @@ + +// SPDX-License-Identifier: GPL-3.0-or-later + +namespace Icinga\Module\Cube\Web\Widget; + +use Icinga\Module\Cube\Dimension; + +class ServiceDimensionWidget extends DimensionWidget +{ + /** + * If the cube is using Icinga DB, the URL leads to the Icinga DB service list. + * If not, the URL leads to the details action of the IdoServicesController. + * + * @return string + */ + protected function getDetailsBaseUrl(): string + { + if ($this->cube::isUsingIcingaDb()) { + return 'icingadb/services'; + } + + return 'cube/services/details'; + } + + /** + * @inheritdoc + */ + protected function getDimensionClasses(): array + { + $classes = [$this->getDimensionBaseClass()]; + $sums = $this->dimension['row']; + + $next = $this->cube->getDimensionAfter($this->dimension['name']); + if ($next && isset($this->dimension['summaries']->{$next->getName()})) { + $sums = $this->dimension['summaries']->{$next->getName()}; + } + + if ($sums->services_unhandled_critical > 0) { + $severityClass = ['critical']; + } elseif ($sums->services_unhandled_unknown > 0) { + $severityClass = ['unknown']; + } elseif ($sums->services_unhandled_warning > 0) { + $severityClass = ['warning']; + } elseif (isset($sums->services_pending) && $sums->services_pending > 0) { + $severityClass = ['pending']; + } elseif ($sums->services_critical > 0) { + $severityClass = ['critical', 'handled']; + } elseif ($sums->services_unknown > 0) { + $severityClass = ['unknown', 'handled']; + } elseif ($sums->services_warning > 0) { + $severityClass = ['warning', 'handled']; + } else { + $severityClass = ['ok']; + } + + return array_merge($classes, $severityClass); + } + + /** + * @inheritdoc + */ + protected function getDimensionSum(Dimension $next): int + { + return $this->dimension['summaries']->{$next->getName()}->services_cnt; + } +} diff --git a/public/css/module.less b/public/css/module.less index 150c83a..5a32c63 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -10,48 +10,162 @@ } div.cube { - div.cube-dimension0 > .header { - font-size: 1.2em; - font-weight: bold; - border-bottom: 1px solid @text-color; - } + div.cube-dimension0 { + > .header { + font-size: 1.2em; + font-weight: bold; + border-bottom: 1px solid @text-color; + } - div.cube-dimension1 { - clear: both; - border-left: 0.5em solid transparent; + &.critical { + > .header { + color: @color-critical; + border-color: @color-critical; + } + + &.handled > .header { + color: @color-critical-handled; + border-color: @color-critical-handled; + } + } + + &.warning { + > .header { + color: @color-warning; + border-color: @color-warning; + } + + &.handled > .header { + color: @color-warning-handled; + border-color: @color-warning-handled; + } + } + + &.unknown { + > .header { + color: @color-unknown; + border-color: @color-unknown; + } + + &.handled > .header { + color: @color-unknown-handled; + border-color: @color-unknown-handled; + } + } + + &.unreachable { + > .header { + color: @color-unreachable; + border-color: @color-unreachable; + } + + &.handled > .header { + color: @color-unreachable-handled; + border-color: @color-unreachable-handled; + } + } + + &.pending { + > .header { + color: @color-pending; + border-color: @color-pending; + } + } + + &.ok { + > .header { + color: @color-ok; + border-color: @color-ok; + } + } } div.cube-dimension1 > .header, div.cube-dimension2 { - display: inline-block; width: 10em; - height: 10em; - margin: 0; padding: 0.2em; - margin-top: 0.3em; + margin: 0.3em 0 0; overflow: hidden; } - div.cube-dimension1 > .header { + div.cube-dimension1 { display: flex; - float: left; - font-weight: bold; - text-align: center; - padding-left: 0; + border-left: 0.5em solid transparent; + + &:not(:last-child) { + padding-bottom: .2em; + } + + > .header { + display: flex; + flex: 0 0 10em; + font-weight: bold; + text-align: center; + padding-left: 0; + + > a:first-child { + flex: 1; + max-width: 8em; + word-wrap: break-word; + } + } - > a:first-child { - flex: 1; - max-width: 8em; - word-wrap: break-word; + > .body { + display: flex; + column-gap: .3em; + flex-wrap: wrap; + } + + &.critical { + border-left-color: @color-critical; + + &.handled { + border-left-color: @color-critical-handled; + } + } + + &.warning { + border-left-color: @color-warning; + + &.handled { + border-left-color: @color-warning-handled; + } + } + + &.unknown { + border-left-color: @color-unknown; + + &.handled { + border-left-color: @color-unknown-handled; + } + } + + &.unreachable { + border-left-color: @color-unreachable; + + &.handled { + border-left-color: @color-unreachable-handled; + } + } + + &.pending { + border-left-color: @color-pending; + } + + &.ok { + border-left-color: @color-ok; } } div.cube-dimension2 { + background: @color-ok; + height: 10em; + > .header { display: flex; text-align: center; color: @text-color-on-icinga-blue; - margin-bottom: 1em; + margin-bottom: 0.5em; > a:first-child { flex: 1; @@ -62,22 +176,46 @@ div.cube { } } - background: @color-ok; + &.critical, &.warning, &.unknown, &.unreachable { + span.sum { + font-size: 1em; + } + } &.critical { background: @color-critical; + + &.handled { + background: @color-critical-handled; + } } &.warning { background: @color-warning; + + &.handled { + background: @color-warning-handled; + } } &.unknown { background: @color-unknown; + + &.handled { + background: @color-unknown-handled; + } } &.unreachable { background: @color-unreachable; + + &.handled { + background: @color-unreachable-handled; + } + } + + &.pending { + background: @color-pending; } &.ok { @@ -89,134 +227,96 @@ div.cube { } } - span.critical, span.warning, span.unknown, span.unreachable, span.ok { - display: inline; - border-radius: 0.1em; - padding: 0.05em 0.2em; - color: @text-color-on-icinga-blue; - font-size: 3em; - } - - span.ok { - background: @color-ok; - } - - .others { + div.dimension-2-container { display: flex; - justify-content: center; - span { - margin-left: 0.2em; - margin-top: 0.8em; - font-size: 1em; - } - } - - span.critical { - background: @color-critical; - } - - span.critical.handled { - background: @color-critical-handled; + column-gap: .3em; + flex-wrap: wrap; } - div.cube-dimension0.critical > .header { - color: @color-critical; - border-color: @color-critical; - } + span { + &.critical, &.warning, &.unknown, &.unreachable, &.pending, &.ok { + display: inline; + border-radius: 0.1em; + padding: 0.05em 0.2em; + color: @text-color-on-icinga-blue; + font-size: 3em; + } - div.cube-dimension1.critical { - border-left-color: @color-critical; + &.critical { + background: @color-critical; - &.handled { - border-left-color: @color-critical-handled; + &.handled { + background: @color-critical-handled; + } } - } - div.cube-dimension2.critical { - span.sum { - font-size: 1em; - } - } + &.warning { + background: @color-warning; - span.warning { - background: @color-warning; - } + &.handled { + background: @color-warning-handled; + } + } - span.warning.handled { - background: @color-warning-handled; - } + &.unknown { + background: @color-unknown; - div.cube-dimension0.warning > .header { - color: @color-warning; - border-color: @color-warning; - } + &.handled { + background: @color-unknown-handled; + } + } - div.cube-dimension1.warning { - border-left-color: @color-warning; + &.unreachable { + background: @color-unreachable; - &.handled { - border-left-color: @color-warning-handled; + &.handled { + background: @color-unreachable-handled; + } } - } - div.cube-dimension2.warning { - span.sum { - font-size: 1em; + &.pending { + background: @color-pending; } - } - span.unknown{ - background: @color-unknown; + &.ok { + background: @color-ok; + } } - span.unknown.handled { - background: @color-unknown-handled; - } + .state-badge { + font-size: 1.6em; - div.cube-dimension0.unknown > .header { - color: @color-unknown; - border-color: @color-unknown; - } + &.handled { + &.state-down { + background-color: @color-down-handled; + } - div.cube-dimension1.unknown { - border-left-color: @color-unknown; + &.state-critical { + background-color: @color-critical-handled; + } - &.handled { - border-left-color: @color-unknown-handled; - } - } + &.state-warning { + background-color: @color-warning-handled; + } - div.cube-dimension2.unknown { - span.sum { - font-size: 1em; + &.state-unknown { + background-color: @color-unknown-handled; + } } } -} - -span.unreachable{ - background: @color-unreachable; -} - -span.unreachable.handled { - background: @color-unreachable-handled; -} - -div.cube-dimension0.unreachable > .header { - color: @color-unreachable; - border-color: @color-unreachable; -} -div.cube-dimension1.unreachable { - border-left-color: @color-unreachable; - - &.handled { - border-left-color: @color-unreachable-handled; - } -} + .others { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; -div.cube-dimension2.unreachable { - span.sum { - font-size: 1em; + span { + font-size: 0.9em; + &:not(.state-badge) { + margin-left: 0.2em; + } + } } } @@ -328,6 +428,7 @@ ul.action-links { font-size: 3em; display: inline-block; } + &:hover { background: @tr-hover-color; text-decoration: none; diff --git a/public/css/state-badges.less b/public/css/state-badges.less new file mode 100644 index 0000000..a143471 --- /dev/null +++ b/public/css/state-badges.less @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2026 Icinga GmbH +// SPDX-License-Identifier: GPL-3.0-or-later + +.state-badges { + padding: 0; + margin: 0.1em 0; + min-height: 2em; + + ul { + padding: 0; + } + + li { + display: inline-block; + } + + li > ul > li:first-child:not(:last-child) .state-badge { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + } + + li > ul > li:last-child:not(:first-child) .state-badge { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + } + + > li:not(:last-child) { + margin-right: .25em; + } + + li > ul > li + li { + margin-left: 1px; + } +} diff --git a/run.php b/run.php index 82b7c18..3b72043 100644 --- a/run.php +++ b/run.php @@ -10,6 +10,8 @@ $this->provideHook('icingadb/icingadbSupport'); +$this->provideCssFile('state-badges.less'); + if (! Cube::isUsingIcingaDb()) { $this->addRoute('cube/hosts', new Zend_Controller_Router_Route_Static( 'cube/hosts',