summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Web/Navigation/Renderer
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Web/Navigation/Renderer')
-rw-r--r--library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php139
-rw-r--r--library/Icinga/Web/Navigation/Renderer/HealthNavigationRenderer.php44
-rw-r--r--library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php235
-rw-r--r--library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php356
-rw-r--r--library/Icinga/Web/Navigation/Renderer/NavigationRendererInterface.php142
-rw-r--r--library/Icinga/Web/Navigation/Renderer/RecursiveNavigationRenderer.php186
-rw-r--r--library/Icinga/Web/Navigation/Renderer/SummaryNavigationItemRenderer.php72
7 files changed, 1174 insertions, 0 deletions
diff --git a/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php b/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php
new file mode 100644
index 0000000..8510f70
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php
@@ -0,0 +1,139 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Navigation\Renderer;
+
+use Icinga\Web\Navigation\NavigationItem;
+
+/**
+ * Abstract base class for a NavigationItem with a status badge
+ */
+abstract class BadgeNavigationItemRenderer extends NavigationItemRenderer
+{
+ const STATE_OK = 'ok';
+ const STATE_CRITICAL = 'critical';
+ const STATE_WARNING = 'warning';
+ const STATE_PENDING = 'pending';
+ const STATE_UNKNOWN = 'unknown';
+
+ /**
+ * The tooltip text for the badge
+ *
+ * @var string
+ */
+ protected $title;
+
+ /**
+ * The state identifier being used
+ *
+ * The state identifier defines the background color of the badge.
+ *
+ * @var string
+ */
+ protected $state;
+
+ /**
+ * Set the tooltip text for the badge
+ *
+ * @param string $title
+ *
+ * @return $this
+ */
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+ /**
+ * Return the tooltip text for the badge
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * Set the state identifier to use
+ *
+ * @param string $state
+ *
+ * @return $this
+ */
+ public function setState($state)
+ {
+ $this->state = $state;
+ return $this;
+ }
+
+ /**
+ * Return the state identifier to use
+ *
+ * @return string
+ */
+ public function getState()
+ {
+ return $this->state;
+ }
+
+ /**
+ * Return the amount of items represented by the badge
+ *
+ * @return int
+ */
+ abstract public function getCount();
+
+ /**
+ * Render the given navigation item as HTML anchor with a badge
+ *
+ * @param NavigationItem $item
+ *
+ * @return string
+ */
+ public function render(NavigationItem $item = null)
+ {
+ if ($item === null) {
+ $item = $this->getItem();
+ }
+
+ $cssClass = '';
+ if ($item->getCssClass() !== null) {
+ $cssClass = ' ' . $item->getCssClass();
+ }
+
+ $item->setCssClass('badge-nav-item' . $cssClass);
+ $this->setEscapeLabel(false);
+ $label = $this->view()->escape($item->getLabel());
+ $item->setLabel($this->renderBadge() . $label);
+ $html = parent::render($item);
+ return $html;
+ }
+
+ /**
+ * Render the badge
+ *
+ * @return string
+ */
+ protected function renderBadge()
+ {
+ if ($count = $this->getCount()) {
+ if ($count > 1000000) {
+ $count = round($count, -6) / 1000000 . 'M';
+ } elseif ($count > 1000) {
+ $count = round($count, -3) / 1000 . 'k';
+ }
+
+ $view = $this->view();
+ return sprintf(
+ '<span title="%s" class="badge state-%s">%s</span>',
+ $view->escape($this->getTitle()),
+ $view->escape($this->getState()),
+ $count
+ );
+ }
+
+ return '';
+ }
+}
diff --git a/library/Icinga/Web/Navigation/Renderer/HealthNavigationRenderer.php b/library/Icinga/Web/Navigation/Renderer/HealthNavigationRenderer.php
new file mode 100644
index 0000000..577895b
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Renderer/HealthNavigationRenderer.php
@@ -0,0 +1,44 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web\Navigation\Renderer;
+
+use Icinga\Application\Hook\HealthHook;
+
+class HealthNavigationRenderer extends BadgeNavigationItemRenderer
+{
+ public function getCount()
+ {
+ $count = 0;
+ $title = null;
+ $worstState = null;
+ foreach (HealthHook::collectHealthData()->select() as $result) {
+ if ($worstState === null || $result->state > $worstState) {
+ $worstState = $result->state;
+ $title = $result->message;
+ $count = 1;
+ } elseif ($worstState === $result->state) {
+ $count++;
+ }
+ }
+
+ switch ($worstState) {
+ case HealthHook::STATE_OK:
+ $count = 0;
+ break;
+ case HealthHook::STATE_WARNING:
+ $this->state = self::STATE_WARNING;
+ break;
+ case HealthHook::STATE_CRITICAL:
+ $this->state = self::STATE_CRITICAL;
+ break;
+ case HealthHook::STATE_UNKNOWN:
+ $this->state = self::STATE_UNKNOWN;
+ break;
+ }
+
+ $this->title = $title;
+
+ return $count;
+ }
+}
diff --git a/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php b/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php
new file mode 100644
index 0000000..51136ff
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php
@@ -0,0 +1,235 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Navigation\Renderer;
+
+use Icinga\Application\Icinga;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Util\StringHelper;
+use Icinga\Web\Navigation\NavigationItem;
+use Icinga\Web\Url;
+use Icinga\Web\View;
+
+/**
+ * NavigationItemRenderer
+ */
+class NavigationItemRenderer
+{
+ /**
+ * View
+ *
+ * @var View
+ */
+ protected $view;
+
+ /**
+ * The item being rendered
+ *
+ * @var NavigationItem
+ */
+ protected $item;
+
+ /**
+ * Internal link targets provided by Icinga Web 2
+ *
+ * @var array
+ */
+ protected $internalLinkTargets;
+
+ /**
+ * Whether to escape the label
+ *
+ * @var bool
+ */
+ protected $escapeLabel;
+
+ /**
+ * Create a new NavigationItemRenderer
+ *
+ * @param array $options
+ */
+ public function __construct(array $options = null)
+ {
+ if (! empty($options)) {
+ $this->setOptions($options);
+ }
+
+ $this->internalLinkTargets = array('_main', '_self', '_next');
+ $this->init();
+ }
+
+ /**
+ * Initialize this renderer
+ */
+ public function init()
+ {
+ }
+
+ /**
+ * Set the given options
+ *
+ * @param array $options
+ *
+ * @return $this
+ */
+ public function setOptions(array $options)
+ {
+ foreach ($options as $name => $value) {
+ $setter = 'set' . StringHelper::cname($name);
+ if (method_exists($this, $setter)) {
+ $this->$setter($value);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set the view
+ *
+ * @param View $view
+ *
+ * @return $this
+ */
+ public function setView(View $view)
+ {
+ $this->view = $view;
+ return $this;
+ }
+
+ /**
+ * Return the view
+ *
+ * @return View
+ */
+ public function view()
+ {
+ if ($this->view === null) {
+ $this->setView(Icinga::app()->getViewRenderer()->view);
+ }
+
+ return $this->view;
+ }
+
+ /**
+ * Set the navigation item to render
+ *
+ * @param NavigationItem $item
+ *
+ * @return $this
+ */
+ public function setItem(NavigationItem $item)
+ {
+ $this->item = $item;
+ return $this;
+ }
+
+ /**
+ * Return the navigation item being rendered
+ *
+ * @return NavigationItem
+ */
+ public function getItem()
+ {
+ return $this->item;
+ }
+
+ /**
+ * Set whether to escape the label
+ *
+ * @param bool $state
+ *
+ * @return $this
+ */
+ public function setEscapeLabel($state = true)
+ {
+ $this->escapeLabel = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return whether to escape the label
+ *
+ * @return bool
+ */
+ public function getEscapeLabel()
+ {
+ return $this->escapeLabel !== null ? $this->escapeLabel : true;
+ }
+
+ /**
+ * Render the given navigation item as HTML anchor
+ *
+ * @param NavigationItem $item
+ *
+ * @return string
+ */
+ public function render(NavigationItem $item = null)
+ {
+ if ($item !== null) {
+ $this->setItem($item);
+ } elseif (($item = $this->getItem()) === null) {
+ throw new ProgrammingError(
+ 'Cannot render nothing. Pass the item to render as part'
+ . ' of the call to render() or set it with setItem()'
+ );
+ }
+
+ $label = $this->getEscapeLabel()
+ ? $this->view()->escape($item->getLabel())
+ : $item->getLabel();
+ if (($icon = $item->getIcon()) !== null) {
+ $label = $this->view()->icon($icon) . $label;
+ } elseif ($item->getName()) {
+ $firstLetter = $item->getName()[0];
+ $label = $this->view()->icon('letter', null, ['data-letter' => strtolower($firstLetter)]) . $label;
+ }
+
+ if (($url = $item->getUrl()) !== null) {
+ $url->overwriteParams($item->getUrlParameters());
+
+ $target = $item->getTarget();
+ if ($url->isExternal() && (!$target || in_array($target, $this->internalLinkTargets, true))) {
+ $url = Url::fromPath('iframe', array('url' => $url));
+ }
+
+ $content = sprintf(
+ '<a%s href="%s"%s>%s</a>',
+ $this->view()->propertiesToString($item->getAttributes()),
+ $this->view()->escape($url->getAbsoluteUrl('&')),
+ $this->renderTargetAttribute(),
+ $label
+ );
+ } elseif ($label) {
+ $content = sprintf(
+ '<%1$s%2$s>%3$s</%1$s>',
+ $item::LINK_ALTERNATIVE,
+ $this->view()->propertiesToString($item->getAttributes()),
+ $label
+ );
+ } else {
+ $content = '';
+ }
+
+ return $content;
+ }
+
+ /**
+ * Render and return the attribute to provide a non-default target for the url
+ *
+ * @return string
+ */
+ protected function renderTargetAttribute()
+ {
+ $target = $this->getItem()->getTarget();
+ if ($target === null || $this->getItem()->getUrl()->getAbsoluteUrl() == '#') {
+ return '';
+ }
+
+ if (! in_array($target, $this->internalLinkTargets, true)) {
+ return ' target="' . $this->view()->escape($target) . '"';
+ }
+
+ return ' data-base-target="' . $target . '"';
+ }
+}
diff --git a/library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php b/library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php
new file mode 100644
index 0000000..00c0f9a
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php
@@ -0,0 +1,356 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Navigation\Renderer;
+
+use ArrayIterator;
+use Exception;
+use RecursiveIterator;
+use Icinga\Application\Icinga;
+use Icinga\Exception\IcingaException;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Navigation\NavigationItem;
+use Icinga\Web\View;
+
+/**
+ * Renderer for single level navigation
+ */
+class NavigationRenderer implements RecursiveIterator, NavigationRendererInterface
+{
+ /**
+ * The tag used for the outer element
+ *
+ * @var string
+ */
+ protected $elementTag;
+
+ /**
+ * The CSS class used for the outer element
+ *
+ * @var string
+ */
+ protected $cssClass;
+
+ /**
+ * The navigation's heading text
+ *
+ * @var string
+ */
+ protected $heading;
+
+ /**
+ * The content rendered so far
+ *
+ * @var array
+ */
+ protected $content;
+
+ /**
+ * Whether to skip rendering the outer element
+ *
+ * @var bool
+ */
+ protected $skipOuterElement;
+
+ /**
+ * The navigation's iterator
+ *
+ * @var ArrayIterator
+ */
+ protected $iterator;
+
+ /**
+ * The navigation
+ *
+ * @var Navigation
+ */
+ protected $navigation;
+
+ /**
+ * View
+ *
+ * @var View
+ */
+ protected $view;
+
+ /**
+ * Create a new NavigationRenderer
+ *
+ * @param Navigation $navigation
+ * @param bool $skipOuterElement
+ */
+ public function __construct(Navigation $navigation, $skipOuterElement = false)
+ {
+ $this->skipOuterElement = $skipOuterElement;
+ $this->iterator = $navigation->getIterator();
+ $this->navigation = $navigation;
+ $this->content = array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setElementTag($tag)
+ {
+ $this->elementTag = $tag;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getElementTag()
+ {
+ return $this->elementTag ?: static::OUTER_ELEMENT_TAG;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setCssClass($class)
+ {
+ $this->cssClass = $class;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCssClass()
+ {
+ return $this->cssClass;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setHeading($heading)
+ {
+ $this->heading = $heading;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getHeading()
+ {
+ return $this->heading;
+ }
+
+ /**
+ * Return the view
+ *
+ * @return View
+ */
+ public function view()
+ {
+ if ($this->view === null) {
+ $this->setView(Icinga::app()->getViewRenderer()->view);
+ }
+
+ return $this->view;
+ }
+
+ /**
+ * Set the view
+ *
+ * @param View $view
+ *
+ * @return $this
+ */
+ public function setView(View $view)
+ {
+ $this->view = $view;
+ return $this;
+ }
+
+ public function getChildren(): NavigationRenderer
+ {
+ return new static($this->current()->getChildren(), $this->skipOuterElement);
+ }
+
+ public function hasChildren(): bool
+ {
+ return $this->current()->hasChildren();
+ }
+
+ public function current(): NavigationItem
+ {
+ return $this->iterator->current();
+ }
+
+ public function key(): int
+ {
+ return $this->iterator->key();
+ }
+
+ public function next(): void
+ {
+ $this->iterator->next();
+ }
+
+ public function rewind(): void
+ {
+ $this->iterator->rewind();
+ if (! $this->skipOuterElement) {
+ $this->content[] = $this->beginMarkup();
+ }
+ }
+
+ public function valid(): bool
+ {
+ $valid = $this->iterator->valid();
+ if (! $this->skipOuterElement && !$valid) {
+ $this->content[] = $this->endMarkup();
+ }
+
+ return $valid;
+ }
+
+ /**
+ * Return the opening markup for the navigation
+ *
+ * @return string
+ */
+ public function beginMarkup()
+ {
+ $content = array();
+ $content[] = sprintf(
+ '<%s%s role="navigation">',
+ $this->getElementTag(),
+ $this->getCssClass() !== null ? ' class="' . $this->getCssClass() . '"' : ''
+ );
+ if (($heading = $this->getHeading()) !== null) {
+ $content[] = sprintf(
+ '<h%1$d id="navigation" class="sr-only" tabindex="-1">%2$s</h%1$d>',
+ static::HEADING_RANK,
+ $this->view()->escape($heading)
+ );
+ }
+ $content[] = $this->beginChildrenMarkup();
+ return join("\n", $content);
+ }
+
+ /**
+ * Return the closing markup for the navigation
+ *
+ * @return string
+ */
+ public function endMarkup()
+ {
+ $content = array();
+ $content[] = $this->endChildrenMarkup();
+ $content[] = '</' . $this->getElementTag() . '>';
+ return join("\n", $content);
+ }
+
+ /**
+ * Return the opening markup for multiple navigation items
+ *
+ * @param int $level
+ *
+ * @return string
+ */
+ public function beginChildrenMarkup($level = 1)
+ {
+ $cssClass = array(static::CSS_CLASS_NAV);
+ if ($this->navigation->getLayout() === Navigation::LAYOUT_TABS) {
+ $cssClass[] = static::CSS_CLASS_NAV_TABS;
+ } elseif ($this->navigation->getLayout() === Navigation::LAYOUT_DROPDOWN) {
+ $cssClass[] = static::CSS_CLASS_NAV_DROPDOWN;
+ }
+
+ $cssClass[] = 'nav-level-' . $level;
+
+ return '<ul class="' . join(' ', $cssClass) . '">';
+ }
+
+ /**
+ * Return the closing markup for multiple navigation items
+ *
+ * @return string
+ */
+ public function endChildrenMarkup()
+ {
+ return '</ul>';
+ }
+
+ /**
+ * Return the opening markup for the given navigation item
+ *
+ * @param NavigationItem $item
+ *
+ * @return string
+ */
+ public function beginItemMarkup(NavigationItem $item)
+ {
+ $cssClasses = array(static::CSS_CLASS_ITEM);
+
+ if ($item->hasChildren() && $item->getChildren()->getLayout() === Navigation::LAYOUT_DROPDOWN) {
+ $cssClasses[] = static::CSS_CLASS_DROPDOWN;
+ $item
+ ->setAttribute('class', static::CSS_CLASS_DROPDOWN_TOGGLE)
+ ->setIcon(static::DROPDOWN_TOGGLE_ICON)
+ ->setUrl('#');
+ }
+
+ if ($item->getActive()) {
+ $cssClasses[] = static::CSS_CLASS_ACTIVE;
+ }
+
+ if ($item->getSelected()) {
+ $cssClasses[] = static::CSS_CLASS_SELECTED;
+ }
+
+ if ($cssClass = $item->getCssClass()) {
+ $cssClasses[] = $cssClass;
+ }
+
+ $content = sprintf(
+ '<li class="%s">',
+ join(' ', $cssClasses)
+ );
+ return $content;
+ }
+
+ /**
+ * Return the closing markup for a navigation item
+ *
+ * @return string
+ */
+ public function endItemMarkup()
+ {
+ return '</li>';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render()
+ {
+ foreach ($this as $item) {
+ /** @var NavigationItem $item */
+ if ($item->shouldRender()) {
+ $content = $item->render();
+ $this->content[] = $this->beginItemMarkup($item);
+ $this->content[] = $content;
+ $this->content[] = $this->endItemMarkup();
+ }
+ }
+
+ return join("\n", $this->content);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ try {
+ return $this->render();
+ } catch (Exception $e) {
+ return IcingaException::describe($e);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Navigation/Renderer/NavigationRendererInterface.php b/library/Icinga/Web/Navigation/Renderer/NavigationRendererInterface.php
new file mode 100644
index 0000000..4495b73
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Renderer/NavigationRendererInterface.php
@@ -0,0 +1,142 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Navigation\Renderer;
+
+/**
+ * Interface for navigation renderers
+ */
+interface NavigationRendererInterface
+{
+ /**
+ * CSS class for items
+ *
+ * @var string
+ */
+ const CSS_CLASS_ITEM = 'nav-item';
+
+ /**
+ * CSS class for active items
+ *
+ * @var string
+ */
+ const CSS_CLASS_ACTIVE = 'active';
+
+ /**
+ * CSS class for selected items
+ *
+ * @var string
+ */
+ const CSS_CLASS_SELECTED = 'selected';
+
+ /**
+ * CSS class for dropdown items
+ *
+ * @var string
+ */
+ const CSS_CLASS_DROPDOWN = 'dropdown-nav-item';
+
+ /**
+ * CSS class for a dropdown item's trigger
+ *
+ * @var string
+ */
+ const CSS_CLASS_DROPDOWN_TOGGLE = 'dropdown-toggle';
+
+ /**
+ * CSS class for the ul element
+ *
+ * @var string
+ */
+ const CSS_CLASS_NAV = 'nav';
+
+ /**
+ * CSS class for the ul element with dropdown layout
+ *
+ * @var string
+ */
+ const CSS_CLASS_NAV_DROPDOWN = 'dropdown-nav';
+
+ /**
+ * CSS class for the ul element with tabs layout
+ *
+ * @var string
+ */
+ const CSS_CLASS_NAV_TABS = 'tab-nav';
+
+ /**
+ * Icon for a dropdown item's trigger
+ *
+ * @var string
+ */
+ const DROPDOWN_TOGGLE_ICON = 'menu';
+
+ /**
+ * Default tag for the outer element the navigation will be wrapped with
+ *
+ * @var string
+ */
+ const OUTER_ELEMENT_TAG = 'div';
+
+ /**
+ * The heading's rank
+ *
+ * @var int
+ */
+ const HEADING_RANK = 1;
+
+ /**
+ * Set the tag for the outer element the navigation is wrapped with
+ *
+ * @param string $tag
+ *
+ * @return $this
+ */
+ public function setElementTag($tag);
+
+ /**
+ * Return the tag for the outer element the navigation is wrapped with
+ *
+ * @return string
+ */
+ public function getElementTag();
+
+ /**
+ * Set the CSS class to use for the outer element
+ *
+ * @param string $class
+ *
+ * @return $this
+ */
+ public function setCssClass($class);
+
+ /**
+ * Get the CSS class used for the outer element
+ *
+ * @return string
+ */
+ public function getCssClass();
+
+ /**
+ * Set the navigation's heading text
+ *
+ * @param string $heading
+ *
+ * @return $this
+ */
+ public function setHeading($heading);
+
+ /**
+ * Return the navigation's heading text
+ *
+ * @return string
+ */
+ public function getHeading();
+
+ /**
+ * Return the navigation rendered to HTML
+ *
+ * @return string
+ */
+ public function render();
+}
diff --git a/library/Icinga/Web/Navigation/Renderer/RecursiveNavigationRenderer.php b/library/Icinga/Web/Navigation/Renderer/RecursiveNavigationRenderer.php
new file mode 100644
index 0000000..315c2aa
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Renderer/RecursiveNavigationRenderer.php
@@ -0,0 +1,186 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Navigation\Renderer;
+
+use Exception;
+use RecursiveIteratorIterator;
+use Icinga\Exception\IcingaException;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Navigation\NavigationItem;
+use Icinga\Web\Navigation\Renderer\NavigationItemRenderer;
+
+/**
+ * Renderer for multi level navigation
+ *
+ * @method NavigationRenderer getInnerIterator() {
+ * {@inheritdoc}
+ * }
+ */
+class RecursiveNavigationRenderer extends RecursiveIteratorIterator implements NavigationRendererInterface
+{
+ /**
+ * The content rendered so far
+ *
+ * @var array
+ */
+ protected $content;
+
+ /**
+ * Whether to use the standard item renderer
+ *
+ * @var bool
+ */
+ protected $useStandardRenderer;
+
+ /**
+ * Create a new RecursiveNavigationRenderer
+ *
+ * @param Navigation $navigation
+ */
+ public function __construct(Navigation $navigation)
+ {
+ $this->content = array();
+ parent::__construct(
+ new NavigationRenderer($navigation, true),
+ RecursiveIteratorIterator::SELF_FIRST
+ );
+ }
+
+ /**
+ * Set whether to use the standard navigation item renderer
+ *
+ * @param bool $state
+ *
+ * @return $this
+ */
+ public function setUseStandardItemRenderer($state = true)
+ {
+ $this->useStandardRenderer = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return whether to use the standard navigation item renderer
+ *
+ * @return bool
+ */
+ public function getUseStandardItemRenderer()
+ {
+ return $this->useStandardRenderer;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setElementTag($tag)
+ {
+ $this->getInnerIterator()->setElementTag($tag);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getElementTag()
+ {
+ return $this->getInnerIterator()->getElementTag();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setCssClass($class)
+ {
+ $this->getInnerIterator()->setCssClass($class);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCssClass()
+ {
+ return $this->getInnerIterator()->getCssClass();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setHeading($heading)
+ {
+ $this->getInnerIterator()->setHeading($heading);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getHeading()
+ {
+ return $this->getInnerIterator()->getHeading();
+ }
+
+ public function beginIteration(): void
+ {
+ $this->content[] = $this->getInnerIterator()->beginMarkup();
+ }
+
+ public function endIteration(): void
+ {
+ $this->content[] = $this->getInnerIterator()->endMarkup();
+ }
+
+ public function beginChildren(): void
+ {
+ $this->content[] = $this->getInnerIterator()->beginChildrenMarkup($this->getDepth() + 1);
+ }
+
+ public function endChildren(): void
+ {
+ $this->content[] = $this->getInnerIterator()->endChildrenMarkup();
+ $this->content[] = $this->getInnerIterator()->endItemMarkup();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render()
+ {
+ foreach ($this as $item) {
+ /** @var NavigationItem $item */
+ if ($item->shouldRender()) {
+ if ($this->getDepth() > 0) {
+ $item->setIcon(null);
+ }
+ if ($this->getUseStandardItemRenderer()) {
+ $renderer = new NavigationItemRenderer();
+ $content = $renderer->render($item);
+ } else {
+ $content = $item->render();
+ }
+ $this->content[] = $this->getInnerIterator()->beginItemMarkup($item);
+
+ $this->content[] = $content;
+
+ if (! $item->hasChildren()) {
+ $this->content[] = $this->getInnerIterator()->endItemMarkup();
+ }
+ }
+ }
+
+ return join("\n", $this->content);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ try {
+ return $this->render();
+ } catch (Exception $e) {
+ return IcingaException::describe($e);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Navigation/Renderer/SummaryNavigationItemRenderer.php b/library/Icinga/Web/Navigation/Renderer/SummaryNavigationItemRenderer.php
new file mode 100644
index 0000000..2916f4e
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Renderer/SummaryNavigationItemRenderer.php
@@ -0,0 +1,72 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Navigation\Renderer;
+
+/**
+ * Badge renderer summing up the worst state of its children
+ */
+class SummaryNavigationItemRenderer extends BadgeNavigationItemRenderer
+{
+ /**
+ * Cached count
+ *
+ * @var int
+ */
+ protected $count;
+
+ /**
+ * State to severity map
+ *
+ * @var array
+ */
+ protected static $stateSeverityMap = array(
+ self::STATE_OK => 0,
+ self::STATE_PENDING => 1,
+ self::STATE_UNKNOWN => 2,
+ self::STATE_WARNING => 3,
+ self::STATE_CRITICAL => 4,
+ );
+
+ /**
+ * Severity to state map
+ *
+ * @var array
+ */
+ protected static $severityStateMap = array(
+ self::STATE_OK,
+ self::STATE_PENDING,
+ self::STATE_UNKNOWN,
+ self::STATE_WARNING,
+ self::STATE_CRITICAL
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCount()
+ {
+ if ($this->count === null) {
+ $countMap = array_fill(0, 5, 0);
+ $maxSeverity = 0;
+ $titles = array();
+ foreach ($this->getItem()->getChildren() as $child) {
+ $renderer = $child->getRenderer();
+ if ($renderer instanceof BadgeNavigationItemRenderer) {
+ $count = $renderer->getCount();
+ if ($count) {
+ $severity = static::$stateSeverityMap[$renderer->getState()];
+ $countMap[$severity] += $count;
+ $titles[] = $renderer->getTitle();
+ $maxSeverity = max($maxSeverity, $severity);
+ }
+ }
+ }
+ $this->count = $countMap[$maxSeverity];
+ $this->state = static::$severityStateMap[$maxSeverity];
+ $this->title = implode('. ', $titles);
+ }
+
+ return $this->count;
+ }
+}