diff --git a/application/layouts/scripts/parts/menu.phtml b/application/layouts/scripts/parts/menu.phtml
new file mode 100644
index 000000000..7fdd67c23
--- /dev/null
+++ b/application/layouts/scripts/parts/menu.phtml
@@ -0,0 +1,38 @@
+= $this->sub ? '
' : '
diff --git a/application/layouts/scripts/parts/navigation.phtml b/application/layouts/scripts/parts/navigation.phtml
index 025587b2c..f64fb92ed 100755
--- a/application/layouts/scripts/parts/navigation.phtml
+++ b/application/layouts/scripts/parts/navigation.phtml
@@ -1,51 +1,17 @@
getRequest()->getRequestUri();
-$currentKey = isset($this->navkey) ? $this->navkey : $url;
+$url = Icinga\Web\Url::fromRequest()->getRelativeUrl();
+$menu = Icinga\Web\Menu::fromConfig();
-$item = $this->navigation->keys("menu");
-?>
-auth()->isAuthenticated()): ?>
-
-
-
- auth()->isAuthenticated()):
+ echo $this->partial(
+ 'parts/menu.phtml',
+ array(
+ 'items' => $menu->getChildren(),
+ 'sub' => false,
+ 'url' => $url
+ )
+ );
+endif;
- $item = $this->navigation->menu->$itemName;
- $active = false;
- $url = "";
- $iconClass = '';
-
- if (is_string($item)) {
- $active = !$activeSet && $this->baseUrl($item) == $currentKey;
- $url = $this->baseUrl($item);
- } else {
- $url = $this->baseUrl(isset($item->route) ? $item->route : "");
- $itemName = isset($item->title) ? $item->title : $itemName;
- $active = !$activeSet && (isset($item->key) ? $item->key : $url) === $currentKey;
- if (isset($item->iconClass)) {
- $iconClass = $item->iconClass;
- }
- }
- $activeSet = $activeSet || $active;
- ?>
- - ">
-
-
-
-
- = $itemName ?>
-
-
-
-
- endif ?>
+?>
\ No newline at end of file
diff --git a/config/menu.ini b/config/menu.ini
index ffb8fffba..73f3ebe40 100755
--- a/config/menu.ini
+++ b/config/menu.ini
@@ -1,9 +1,17 @@
-[menu]
-Dashboard.title = "Dashboard"
-Dashboard.route = "/dashboard/index"
-Dashboard.iconClass = "icinga-icon-dashboard-petrol"
+[Home]
+title = "Home"
+url = "/index/welcome"
+icon = "/img/icons/down_petrol.png"
+priority = 1
-Configuration.title = "Configuration"
-Configuration.route = "/config/index"
-Configuration.iconClass = "icinga-icon-configuration-petrol"
+[Configuration]
+title = "Configuration"
+url = "/config/index"
+icon_class = "icinga-icon-configuration-petrol"
+priority = 2
+[Dashboard]
+title = "Dashboard"
+url = "/dashboard/index"
+icon = "/img/icons/dashboard_petrol.png"
+priority = 3
diff --git a/config/modules/monitoring/menu.ini b/config/modules/monitoring/menu.ini
index e54c3c819..0ffe74dc6 100755
--- a/config/modules/monitoring/menu.ini
+++ b/config/modules/monitoring/menu.ini
@@ -1,49 +1,43 @@
-[menu]
-;Remove component as of #4583 since it's not working
-;Issues.title = "Issues" ; Extended version
-;Issues.route = "/monitoring/list/services?problems=1&sort=severity" ; Explicit route
-;Issues.key = "issues" ; When this key is set in the controller, the item is active
+[Hosts]
+title = "Hosts"
+url = "/monitoring/list/hosts"
+icon_class = "icinga-icon-host-petrol"
-;Remove component as of #4583 since it's not working
-;Changes.title = "Recent Changes"
-;Changes.route = "/monitoring/list/services?sort=service_last_state_change"
-;_1 = 1 ;Spacer after this section
+[Services]
+title = "Services"
+url = "/monitoring/list/services"
+icon_class = "icinga-icon-service-petrol"
-Hosts.title = "Hosts"
-Hosts.route = "/monitoring/list/hosts"
-Hosts.iconClass = "icinga-icon-host-petrol"
+[Downtimes]
+title = "Downtimes"
+url = "/monitoring/list/downtimes"
+icon_class = "icinga-icon-down-petrol"
-Services.title = "Services"
-Services.route = "/monitoring/list/services"
-Services.iconClass = "icinga-icon-service-petrol"
+[Notifications]
+title = "Notifications"
+url = "/monitoring/list/notifications"
+icon_class = "icinga-icon-notification-petrol"
-Downtimes.title = "Downtimes"
-Downtimes.route = "/monitoring/list/downtimes"
-Downtimes.iconClass = "icinga-icon-down-petrol"
+[Comments]
+title = "Comments"
+url = "/monitoring/list/comments"
+icon_class = "icinga-icon-comment-petrol"
-Notifications.title = "Notifications"
-Notifications.route = "/monitoring/list/notifications"
-Notifications.iconClass = "icinga-icon-notification-petrol"
+[Servicegroups]
+title = "Servicegroups"
+url = "/monitoring/list/servicegroups"
+icon_class = "icinga-icon-servicegroup-petrol"
-Comments.title = "Comments"
-Comments.route = "/monitoring/list/comments"
-Comments.iconClass = "icinga-icon-comment-petrol"
+[Hostgroups]
+title = "Hostgroups"
+url = "/monitoring/list/hostgroups"
+icon_class = "icinga-icon-hostgroup-petrol"
-;Contacts = "/monitoring/list/contacts"
+[History]
+title = "History"
+url = "/monitoring/list/eventhistory"
+icon_class = "icinga-icon-history-petrol"
-;Contact Groups = "/monitoring/list/contactgroups"
-
-Servicegroups.title = "Servicegroups"
-Servicegroups.route = "/monitoring/list/servicegroups"
-Servicegroups.iconClass = "icinga-icon-servicegroup-petrol"
-
-Hostgroups.title = "Hostgroups"
-Hostgroups.route = "/monitoring/list/hostgroups"
-Hostgroups.iconClass = "icinga-icon-hostgroup-petrol"
-
-History.title = "History"
-History.route = "/monitoring/list/eventhistory"
-History.iconClass = "icinga-icon-history-petrol"
-
-Performance.title = "Performance"
-Performance.route ="/monitoring/process/performance"
+[Performance]
+title = "Performance"
+url = "/monitoring/process/performance"
diff --git a/config/modules/monitoring/menu.ini.in b/config/modules/monitoring/menu.ini.in
new file mode 100644
index 000000000..ef874f6a7
--- /dev/null
+++ b/config/modules/monitoring/menu.ini.in
@@ -0,0 +1,69 @@
+[menu]
+;Remove component as of #4583 since it's not working
+;Issues.title = "Issues" ; Extended version
+;Issues.route = "/monitoring/list/services?problems=1&sort=severity" ; Explicit route
+;Issues.key = "issues" ; When this key is set in the controller, the item is active
+
+;Remove component as of #4583 since it's not working
+;Changes.title = "Recent Changes"
+;Changes.route = "/monitoring/list/services?sort=service_last_state_change"
+;_1 = 1 ;Spacer after this section
+
+Hosts.title = "Hosts"
+Hosts.route = "/monitoring/list/hosts"
+Hosts.iconClass = "icinga-icon-host-petrol"
+
+Services.title = "Services"
+Services.route = "/monitoring/list/services"
+Services.iconClass = "icinga-icon-service-petrol"
+
+Downtimes.title = "Downtimes"
+Downtimes.route = "/monitoring/list/downtimes"
+Downtimes.iconClass = "icinga-icon-down-petrol"
+
+Notifications.title = "Notifications"
+Notifications.route = "/monitoring/list/notifications"
+Notifications.iconClass = "icinga-icon-notification-petrol"
+
+Comments.title = "Comments"
+Comments.route = "/monitoring/list/comments"
+Comments.iconClass = "icinga-icon-comment-petrol"
+
+;Contacts = "/monitoring/list/contacts"
+
+;Contact Groups = "/monitoring/list/contactgroups"
+
+Servicegroups.title = "Servicegroups"
+Servicegroups.route = "/monitoring/list/servicegroups"
+Servicegroups.iconClass = "icinga-icon-servicegroup-petrol"
+
+Hostgroups.title = "Hostgroups"
+Hostgroups.route = "/monitoring/list/hostgroups"
+Hostgroups.iconClass = "icinga-icon-hostgroup-petrol"
+
+History.title = "History"
+History.route = "/monitoring/list/eventhistory"
+History.iconClass = "icinga-icon-history-petrol"
+
+Performance.title = "Performance"
+Performance.route ="/monitoring/process/performance"
+
+
+[Hosts]
+; New section
+; Title property unset means title is "Hosts"
+url="#"
+iconClass="icon-hosts"
+priority=1
+
+[Hosts.Problems]
+; New section beneath section hosts
+title="Problem Hosts"
+url="/monitoring/list/hosts?problem=1"
+priority=2
+
+[Hosts.A Link]
+title="Wiki"
+url="https://wiki.somewhere.com"
+priority=1
+icon="https://wiki.somewhere.com/icon.png"
\ No newline at end of file
diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php
new file mode 100644
index 000000000..31f5846dc
--- /dev/null
+++ b/library/Icinga/Web/Menu.php
@@ -0,0 +1,82 @@
+
+ * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
+ * @author Icinga Development Team
+ *
+ */
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Web;
+
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Web\Menu\Item as MenuItem;
+
+class Menu extends MenuItem
+{
+ /**
+ * Create menu from the application's menu config file plus the config files from all enabled modules
+ *
+ * @return Menu
+ */
+ public static function fromConfig() {
+ $menu = new static('menu');
+ $manager = Icinga::app()->getModuleManager();
+ $menuConfigs = array(Config::app('menu'));
+ foreach ($manager->listEnabledModules() as $moduleName) {
+ $menuConfigs[] = Config::module($moduleName, 'menu');
+ }
+ return $menu->loadMenuItems($menu->flattenConfigs($menuConfigs));
+ }
+
+ /**
+ * Flatten configs into a key-value array
+ */
+ public function flattenConfigs(array $configs)
+ {
+ $flattened = array();
+ foreach ($configs as $menuConfig) {
+ foreach ($menuConfig as $section => $itemConfig) {
+ while (array_key_exists($section, $flattened)) {
+ $section .= '_dup';
+ }
+ $flattened[$section] = $itemConfig;
+ }
+ }
+ ksort($flattened);
+ return $flattened;
+ }
+
+ /**
+ * Load menu items from a key-value array
+ */
+ public function loadMenuItems(array $flattened)
+ {
+ foreach ($flattened as $id => $itemConfig) {
+ $this->addChild($id, $itemConfig);
+ }
+ return $this;
+ }
+
+}
diff --git a/library/Icinga/Web/Menu/Item.php b/library/Icinga/Web/Menu/Item.php
new file mode 100644
index 000000000..c233faac1
--- /dev/null
+++ b/library/Icinga/Web/Menu/Item.php
@@ -0,0 +1,317 @@
+id = $id;
+ if ($config !== null) {
+ $this->setConfig($config);
+ }
+ }
+
+ /**
+ * Setter for id
+ *
+ * @param string $id
+ *
+ * @return self
+ */
+ public function setId($id)
+ {
+ $this->id = $id;
+ return $this;
+ }
+
+ /**
+ * Getter for id
+ *
+ * @return string
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * Setter for title
+ *
+ * @param string $title
+ *
+ * @return self
+ */
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+
+ /**
+ * Getter for title
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title ? $this->title : $this->id;
+ }
+
+ /**
+ * Setter for priority
+ *
+ * @param int $priority
+ *
+ * @return self
+ */
+ public function setPriority($priority)
+ {
+ $this->priority = (int) $priority;
+ return $this;
+ }
+
+ /**
+ * Getter for priority
+ *
+ * @return int
+ */
+ public function getPriority()
+ {
+ return $this->priority;
+ }
+
+ /**
+ * Setter for URL
+ *
+ * @param string $url
+ *
+ * @return self
+ */
+ public function setUrl($url)
+ {
+ $this->url = $url;
+ return $this;
+ }
+
+ /**
+ * Getter for URL
+ *
+ * @return string
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * Setter for icon path
+ *
+ * @param string $path
+ *
+ * @return self
+ */
+ public function setIcon($path)
+ {
+ $this->icon = $path;
+ return $this;
+ }
+
+ /**
+ * Getter for icon path
+ *
+ * @return string
+ */
+ public function getIcon()
+ {
+ return $this->icon;
+ }
+
+ /**
+ * Setter for icon class
+ *
+ * @param string $iconClass
+ *
+ * @return self
+ */
+ public function setIconClass($iconClass)
+ {
+ $this->iconClass = $iconClass;
+ return $this;
+ }
+
+ /**
+ * Getter for icon class
+ *
+ * @return string
+ */
+ public function getIconClass()
+ {
+ return $this->iconClass;
+ }
+
+ /**
+ * Set the configuration for the item
+ *
+ * @param object $config
+ */
+ public function setConfig($config)
+ {
+ foreach ($config as $key => $value) {
+ $method = 'set' . implode('', array_map('ucfirst', explode('_', strtolower($key))));
+ if (method_exists($this, $method)) {
+ $this->{$method}($value);
+ }
+ }
+ }
+
+ /**
+ * Add a child to MenuItem
+ *
+ * @param string $id
+ * @param object $itemConfig
+ *
+ * @return self
+ */
+ public function addChild($id, $itemConfig = null)
+ {
+ if (false === ($pos = strpos($id, '.'))) {
+ // Root item
+ $menuItem = new self($id, $itemConfig);
+ $this->children[$id] = $menuItem;
+ } else {
+ // Submenu item
+ list($parentId, $id) = explode('.', $id, 2);
+ $parent = $this->getChild($parentId);
+ if ($parent !== null) {
+ $menuItem = $parent->addChild($id, $itemConfig);
+ } else {
+ // Parent does not exist, auto-generate a knot to inform the user
+ $autoId = 'Auto-generated knot because submenu items refer to a non-existent parent menu item';
+ if (($auto = $this->getChild($autoId)) === null) {
+ $auto = $this->addChild($autoId);
+ }
+ $menuItem = $auto->addChild($id, $itemConfig);
+ }
+ }
+ return $menuItem;
+ }
+
+ /**
+ * Check whether the item has children
+ *
+ * @return bool
+ */
+ public function hasChildren()
+ {
+ return !empty($this->children);
+ }
+
+ /**
+ * Get children sorted
+ *
+ * @return array
+ * @see cmpChildren()
+ */
+ public function getChildren()
+ {
+ usort($this->children, array($this, 'cmpChildren'));
+ return $this->children;
+ }
+
+ /**
+ * Get child by its id
+ *
+ * @param string $id
+ * @param mixed $default
+ *
+ * @return self|$default
+ */
+ public function getChild($id, $default = null)
+ {
+ if (array_key_exists($id, $this->children)) {
+ return $this->children[$id];
+ }
+ return $default;
+ }
+
+
+ /**
+ * Compare children based on priority and title
+ *
+ * @param MenuItem $a
+ * @param MenuItem $b
+ *
+ * @return int
+ */
+ protected function cmpChildren($a, $b)
+ {
+ if ($a->priority === $b->priority) {
+ return ($a->getTitle() > $b->getTitle()) ? 1 : -1;
+ }
+ return ($a->priority > $b->priority) ? 1 : -1;
+ }
+}
diff --git a/public/css/icinga/navigation.less b/public/css/icinga/navigation.less
index b6b2f4ce7..f165492ce 100644
--- a/public/css/icinga/navigation.less
+++ b/public/css/icinga/navigation.less
@@ -76,6 +76,20 @@
.nav-stacked > li:first-child {
}
+.submenu {
+ background-image: none;
+ padding-left: 15px;
+}
+
+.submenu > a {
+ color: red;
+}
+
+.active > a{
+ font-weight: bold;
+ text-decoration: underline;
+}
+
.icinga-subnavigation > li {
padding-top: 8px;
padding-bottom: 8px;
@@ -185,3 +199,8 @@ ul.icinga-subnavigation {
padding-left: 15px;
margin-top: 12px;
}
+
+.nav > .subclass {
+ text-decoration: underline;
+ font-size: 16px;
+}