diff --git a/application/views/scripts/layout/menu.phtml b/application/views/scripts/layout/menu.phtml
index c82d5cc84..dfb544d63 100644
--- a/application/views/scripts/layout/menu.phtml
+++ b/application/views/scripts/layout/menu.phtml
@@ -1,5 +1,6 @@
search('dummy')->getPane('search')->hasDashlets()): ?>
= $menuRenderer->setCssClass('primary-nav')->setElementTag('nav')->setHeading(t('Navigation')); ?>
+
diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php
index 3289b56d9..dc1cdc82a 100644
--- a/library/Icinga/Web/Menu.php
+++ b/library/Icinga/Web/Menu.php
@@ -33,6 +33,7 @@ class Menu extends Navigation
'priority' => 10
]);
$this->addItem('system', [
+ 'cssClass' => 'system-nav-item',
'label' => t('System'),
'icon' => 'services',
'priority' => 700,
@@ -74,6 +75,7 @@ class Menu extends Navigation
]
]);
$this->addItem('configuration', [
+ 'cssClass' => 'configuration-nav-item',
'label' => t('Configuration'),
'icon' => 'wrench',
'permission' => 'config/*',
diff --git a/library/Icinga/Web/Navigation/ConfigMenu.php b/library/Icinga/Web/Navigation/ConfigMenu.php
new file mode 100644
index 000000000..ca4540bcf
--- /dev/null
+++ b/library/Icinga/Web/Navigation/ConfigMenu.php
@@ -0,0 +1,300 @@
+ 'nav'];
+
+ protected $children;
+
+ protected $selected;
+
+ protected $cogItemActive = false;
+
+ protected $state;
+
+ public function __construct()
+ {
+ $this->children = [
+ 'system' => [
+ 'title' => t('System'),
+ 'items' => [
+ 'about' => [
+ 'label' => t('About'),
+ 'url' => 'about'
+ ],
+ 'health' => [
+ 'label' => t('Health'),
+ 'url' => 'health',
+ ],
+ 'announcements' => [
+ 'label' => t('Announcements'),
+ 'url' => 'announcements'
+ ],
+ 'sessions' => [
+ 'label' => t('User Sessions'),
+ 'permission' => 'application/sessions',
+ 'url' => 'manage-user-devices'
+ ]
+ ]
+ ],
+ 'configuration' => [
+ 'title' => t('Configuration'),
+ 'permission' => 'config/*',
+ 'items' => [
+ 'application' => [
+ 'label' => t('Application'),
+ 'url' => 'config/general'
+ ],
+ 'authentication' => [
+ 'label' => t('Access Control'),
+ 'permission' => 'config/access-control/*',
+ 'url' => 'role/list'
+ ],
+ 'navigation' => [
+ 'label' => t('Shared Navigation'),
+ 'permission' => 'config/navigation',
+ 'url' => 'navigation'
+ ],
+ 'modules' => [
+ 'label' => t('Modules'),
+ 'permission' => 'config/modules',
+ 'url' => 'config/modules'
+ ]
+ ]
+ ],
+ 'logout' => [
+ 'items' => [
+ 'logout' => [
+ 'label' => t('Logout'),
+ 'atts' => [
+ 'target' => '_self',
+ 'class' => 'nav-item-logout'
+ ],
+ 'url' => 'authentication/logout'
+ ]
+ ]
+ ]
+ ];
+ }
+
+ protected function assembleUserMenuItem(BaseHtmlElement $userMenuItem)
+ {
+ $username = Auth::getInstance()->getUser()->getUsername();
+
+ $userMenuItem->add(
+ new HtmlElement(
+ 'a',
+ Attributes::create(['href' => Url::fromPath('account')]),
+ new HtmlElement(
+ 'i',
+ Attributes::create(['class' => 'user-ball']),
+ Text::create($username[0])
+ ),
+ Text::create($username)
+ )
+ );
+
+ if (Icinga::app()->getRequest()->getUrl()->matches('account')) {
+ $userMenuItem->addAttributes(['class' => 'selected active']);
+ }
+ }
+
+ protected function assembleCogMenuItem($cogMenuItem)
+ {
+ $cogMenuItem->add([
+ HtmlElement::create(
+ 'button',
+ null,
+ [
+ new Icon('cog'),
+ $this->createHealthBadge(),
+ ]
+ ),
+ $this->createLevel2Menu()
+ ]);
+
+ if ($this->cogItemActive) {
+ $cogMenuItem->addAttributes([ 'class' => 'active' ]);
+ }
+ }
+
+ protected function assembleLevel2Nav(BaseHtmlElement $level2Nav)
+ {
+ $navContent = HtmlElement::create('div', ['class' => 'flyout-content']);
+ foreach ($this->children as $c) {
+ if (isset($c['permission']) && ! Auth::getInstance()->hasPermission($c['permission'])) {
+ continue;
+ }
+
+ if (isset($c['title'])) {
+ $navContent->add(HtmlElement::create(
+ 'h3',
+ null,
+ $c['title']
+ ));
+ }
+
+ $ul = HtmlElement::create('ul', ['class' => 'nav']);
+ foreach ($c['items'] as $key => $item) {
+ $ul->add($this->createLevel2MenuItem($item, $key));
+ }
+
+ $navContent->add($ul);
+ }
+
+ $level2Nav->add($navContent);
+ }
+
+ protected function getHealthCount()
+ {
+ $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;
+ }
+
+ protected function isSelectedItem($item)
+ {
+ if ($item !== null && Icinga::app()->getRequest()->getUrl()->matches($item['url'])) {
+ $this->selected = $item;
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function createHealthBadge()
+ {
+ $stateBadge = null;
+ if ($this->getHealthCount() > 0) {
+ $stateBadge = new StateBadge($this->getHealthCount(), $this->state);
+ $stateBadge->addAttributes(['class' => 'disabled']);
+ }
+
+ return $stateBadge;
+ }
+
+ protected function createLevel2Menu()
+ {
+ $level2Nav = HtmlElement::create(
+ 'div',
+ Attributes::create(['class' => 'nav-level-1 flyout'])
+ );
+
+ $this->assembleLevel2Nav($level2Nav);
+
+ return $level2Nav;
+ }
+
+ protected function createLevel2MenuItem($item, $key)
+ {
+ if (isset($item['permission']) && ! Auth::getInstance()->hasPermission($item['permission'])) {
+ return null;
+ }
+
+ $healthBadge = null;
+ $class = null;
+ if ($key === 'health') {
+ $class = 'badge-nav-item';
+ $healthBadge = $this->createHealthBadge();
+ }
+
+ $li = HtmlElement::create(
+ 'li',
+ isset($item['atts']) ? $item['atts'] : [],
+ [
+ HtmlElement::create(
+ 'a',
+ Attributes::create(['href' => $item['url']]),
+ [
+ $item['label'],
+ isset($healthBadge) ? $healthBadge : ''
+ ]
+ ),
+ ]
+ );
+ $li->addAttributes(['class' => $class]);
+
+ if ($this->isSelectedItem($item)) {
+ $li->addAttributes(['class' => 'selected']);
+ $this->cogItemActive = true;
+ }
+
+ return $li;
+ }
+
+ protected function createUserMenuItem()
+ {
+ $userMenuItem = HtmlElement::create('li', ['class' => 'user-nav-item']);
+
+ $this->assembleUserMenuItem($userMenuItem);
+
+ return $userMenuItem;
+ }
+
+ protected function createCogMenuItem()
+ {
+ $cogMenuItem = HtmlElement::create('li', ['class' => 'config-nav-item']);
+
+ $this->assembleCogMenuItem($cogMenuItem);
+
+ return $cogMenuItem;
+ }
+
+ protected function assemble()
+ {
+ $this->add([
+ $this->createUserMenuItem(),
+ $this->createCogMenuItem()
+ ]);
+ }
+}
diff --git a/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php b/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php
index cabac9ae9..8510f7070 100644
--- a/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php
+++ b/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php
@@ -97,7 +97,13 @@ abstract class BadgeNavigationItemRenderer extends NavigationItemRenderer
if ($item === null) {
$item = $this->getItem();
}
- $item->setCssClass('badge-nav-item');
+
+ $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);
diff --git a/library/Icinga/Web/StyleSheet.php b/library/Icinga/Web/StyleSheet.php
index 6da3fc9b0..02087302d 100644
--- a/library/Icinga/Web/StyleSheet.php
+++ b/library/Icinga/Web/StyleSheet.php
@@ -56,6 +56,7 @@ class StyleSheet
'css/vendor/normalize.css',
'css/icinga/base.less',
'css/icinga/badges.less',
+ 'css/icinga/configmenu.less',
'css/icinga/mixins.less',
'css/icinga/grid.less',
'css/icinga/nav.less',
diff --git a/modules/monitoring/configuration.php b/modules/monitoring/configuration.php
index 8415f889c..663db93f5 100644
--- a/modules/monitoring/configuration.php
+++ b/modules/monitoring/configuration.php
@@ -289,18 +289,6 @@ $section = $this->menuSection(N_('Reporting'), array(
'priority' => 100
));
-/*
- * System Section
- */
-$section = $this->menuSection(N_('System'));
-$section->add(N_('Monitoring Health'), array(
- 'icon' => 'check',
- 'description' => $this->translate('Open monitoring health'),
- 'url' => 'monitoring/health/info',
- 'priority' => 720,
- 'renderer' => 'BackendAvailabilityNavigationItemRenderer'
-));
-
/*
* Current Incidents
*/
diff --git a/public/css/icinga/configmenu.less b/public/css/icinga/configmenu.less
new file mode 100644
index 000000000..05e50e864
--- /dev/null
+++ b/public/css/icinga/configmenu.less
@@ -0,0 +1,303 @@
+#menu {
+ margin-bottom: 3em;
+}
+
+.sidebar-collapsed #menu {
+ margin-bottom: 8em;
+}
+
+#menu .config-menu {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background-color: @menu-bg-color;
+ margin-top: auto;
+
+ > ul {
+ display: flex;
+ flex-wrap: nowrap;
+ padding: 0;
+
+ > li {
+ > a {
+ padding: 0.5em 0.5em 0.5em 0.75em;
+ line-height: 2.167em;
+ white-space: nowrap;
+ text-decoration: none;
+
+ }
+
+ &:hover .nav-level-1 {
+ display: block;
+ }
+ }
+
+ li.active a:after {
+ display: none;
+ }
+
+ .user-nav-item {
+ width: 100%;
+ overflow: hidden; // necessary for .text-ellipsis of
+
+ > a {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &:not(.active):hover a,
+ &:not(.active) a:focus {
+ background: @menu-hover-bg-color;
+ }
+ }
+
+ .config-nav-item {
+ line-height: 2;
+ display: flex;
+ align-items: center;
+ position: relative;
+
+ > button {
+ background: none;
+ border: none;
+ display: block;
+ .rounded-corners();
+
+ > .state-badge {
+ position: absolute;
+ pointer-events: none;
+ }
+
+ .icon {
+ opacity: .8;
+ font-size: 1.25em;
+
+ &:before {
+ margin-right: 0;
+ }
+ }
+ }
+
+ &:hover > button {
+ background: fade(@menu-hover-bg-color, 25);
+
+ > .state-badge {
+ display: none;
+ }
+ }
+
+ button:focus {
+ background: fade(@menu-hover-bg-color, 25);
+ }
+
+ &.active > button {
+ color: @text-color-inverted;
+ background: @icinga-blue;
+ }
+ }
+
+ .state-badge {
+ line-height: 1.2;
+ padding: .25em;
+ font-family: @font-family-wide;
+ }
+ }
+
+ .nav-level-1 li {
+ &.badge-nav-item > a {
+ display: flex;
+ align-items: baseline;
+ width: 100%;
+
+ .state-badge {
+ margin-left: auto;
+ }
+ }
+ }
+
+ .nav-item-logout {
+ color: @color-critical;
+ border-top: 1px solid @gray-lighter;
+ }
+
+ .user-ball {
+ .ball();
+ .ball-size-l();
+ .ball-solid(@icinga-blue);
+
+ // icingadb-web/public/css/common.less: .user-ball
+ font-weight: bold;
+ text-transform: uppercase;
+
+ // compensate border vertically and add space to the right;
+ margin: -1px .2em -2px 0;
+ border: 1px solid @text-color-inverted;
+ font-style: normal;
+ line-height: 1.2;
+ }
+}
+
+#layout:not(.sidebar-collapsed) #menu .config-menu {
+ .user-nav-item {
+ > a {
+ padding-right: 4.75em;
+ }
+
+ &.active.selected + .config-nav-item {
+ > button {
+ color: @text-color-inverted;
+ }
+ }
+ }
+
+ .config-nav-item {
+ position: absolute;
+ right: 2.5em;
+ bottom: 0;
+ top: 0;
+
+ .state-badge {
+ left: -1em;
+ top: 0;
+ }
+ }
+
+ .flyout {
+ bottom: 100%;
+ right: -2em;
+ width: 15em;
+ }
+}
+
+.sidebar-collapsed #menu .config-menu {
+ ul {
+ flex-direction: column;
+
+ .user-ball {
+ margin-left: .25em * 1.5/2;
+ margin-right: .5em + .25em * 1.5/2;
+ width: 2em * 1.5/2 ;
+ height: 2em * 1.5/2;
+ font-size: 2/1.5em;
+ line-height: 1;
+ }
+
+ .config-nav-item {
+ padding-right: 0;
+ margin-bottom: 3em;
+
+ .icon {
+ font-size: 1.5em;
+ }
+
+ button {
+ position: relative;
+ width: 3em;
+ margin: .125em .5em;
+ padding: .5em .75em;
+
+ .state-badge {
+ right: -.25em;
+ bottom: -.25em;
+ font-size: .75em;
+
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 4em;
+ }
+ }
+ }
+ }
+
+ .flyout {
+ bottom: 0;
+ left: 100%;
+ width: 14em;
+
+ &:before {
+ left: -.6em;
+ bottom: 1em;
+ transform: rotate(135deg);
+ }
+ }
+}
+
+.flyout {
+ display: none;
+ position: absolute;
+ border: 1px solid @gray-lighter;
+ background: @body-bg-color;
+ box-shadow: 0 0 1em 0 rgba(0,0,0,.25);
+ z-index: 1;
+ .rounded-corners();
+
+ a {
+ font-size: 11/12em;
+ padding: 0.364em 0.545em 0.364em 2em;
+ line-height: 2;
+
+ &:hover {
+ text-decoration: none;
+ background: @menu-2ndlvl-highlight-bg-color;
+ }
+ }
+
+ h3 {
+ font-size: 10/12em;
+ color: @text-color-light;
+ letter-spacing: .1px;
+ padding: 0.364em 0.545em 0.364em 0.545em;
+ margin: 0;
+ }
+
+ .flyout-content {
+ overflow: auto;
+ // Partially escape to have ems calculated
+ max-height: calc(~"100vh - " 50/12em);
+ padding: .5em 0;
+ position: relative;
+ }
+
+ // Caret
+ &:before {
+ content: "";
+ display: block;
+ position: absolute;
+ transform: rotate(45deg);
+ background: @body-bg-color;
+ border-bottom: 1px solid @gray-lighter;
+ border-right: 1px solid @gray-lighter;
+ height: 1.1em;
+ width: 1.1em;
+ bottom: -.6em;
+ right: 2.5em;
+ }
+}
+
+// Prevent flyout to vanish on autorefresh
+#layout.config-flyout-open .config-nav-item {
+ .flyout {
+ display: block;
+ }
+
+ > button > .state-badge {
+ display: none;
+ }
+}
+
+#layout.minimal-layout .config-menu {
+ display: none;
+}
+
+#layout.minimal-layout #menu {
+ margin-bottom: 0;
+}
+
+#layout:not(.minimal-layout) #menu .primary-nav {
+ .user-nav-item,
+ .configuration-nav-item,
+ .system-nav-item {
+ display: none;
+ }
+}
diff --git a/public/css/icinga/layout.less b/public/css/icinga/layout.less
index f2a97e100..c37da7920 100644
--- a/public/css/icinga/layout.less
+++ b/public/css/icinga/layout.less
@@ -2,12 +2,20 @@
#footer {
bottom: 0;
- left: 0;
right: 0;
+ left: 12em;
position: fixed;
z-index: 999;
}
+#layout.minimal-layout #footer {
+ left: 0;
+}
+
+.sidebar-collapsed #footer {
+ left: 3em;
+}
+
#guest-error {
background-color: @icinga-blue;
height: 100%;
@@ -208,10 +216,6 @@
padding: 0;
}
-#layout:not(.minimal-layout) #notifications {
- padding-left: 12em;
-}
-
#notifications > li {
color: @text-color;
display: block;
diff --git a/public/css/icinga/menu.less b/public/css/icinga/menu.less
index 6101c3806..7933b6358 100644
--- a/public/css/icinga/menu.less
+++ b/public/css/icinga/menu.less
@@ -17,15 +17,6 @@
overflow-x: hidden;
}
-#layout:not(.minimal-layout) #menu {
- // Space for the #toggle-sidebar button
- &:after {
- content: "";
- display: block;
- padding-bottom: 2.25em;
- }
-}
-
#menu .nav-item {
vertical-align: middle;
@@ -480,9 +471,10 @@ input[type=text].search-input {
padding: 0;
color: @text-color-light;
position: absolute;
- bottom: 0.2em;
+ bottom: 0;
right: 0;
z-index: 3;
+ line-height: 2;
i {
background-color: @body-bg-color;
diff --git a/public/css/icinga/widgets.less b/public/css/icinga/widgets.less
index 8015537eb..949a6bc03 100644
--- a/public/css/icinga/widgets.less
+++ b/public/css/icinga/widgets.less
@@ -68,7 +68,6 @@
color: @text-color-on-icinga-blue;
line-height: 1.5em;
padding: 0.5em 1em 0.5em 3em;
- margin-left: 12em;
position: relative;
diff --git a/public/js/icinga/behavior/navigation.js b/public/js/icinga/behavior/navigation.js
index 7186d9040..a8c507b33 100644
--- a/public/js/icinga/behavior/navigation.js
+++ b/public/js/icinga/behavior/navigation.js
@@ -11,10 +11,16 @@
this.on('click', '#menu a', this.linkClicked, this);
this.on('click', '#menu tr[href]', this.linkClicked, this);
this.on('rendered', '#menu', this.onRendered, this);
- this.on('mouseenter', '#menu .nav-level-1 > .nav-item', this.showFlyoutMenu, this);
- this.on('mouseleave', '#menu', this.hideFlyoutMenu, this);
+ this.on('mouseenter', '#menu .primary-nav .nav-level-1 > .nav-item', this.showFlyoutMenu, this);
+ this.on('mouseleave', '#menu .primary-nav', this.hideFlyoutMenu, this);
this.on('click', '#toggle-sidebar', this.toggleSidebar, this);
+ this.on('click', '#menu .config-nav-item button', this.toggleConfigFlyout, this);
+ this.on('mouseenter', '#menu .config-menu .config-nav-item', this.showConfigFlyout, this);
+ this.on('mouseleave', '#menu .config-menu .config-nav-item', this.hideConfigFlyout, this);
+
+ this.on('keydown', '#menu .config-menu .config-nav-item', this.onKeyDown, this);
+
/**
* The DOM-Path of the active item
*
@@ -121,6 +127,7 @@
} else {
_this.setActiveAndSelected($(event.target));
}
+
// update target url of the menu container to the clicked link
var $menu = $('#menu');
var menuDataUrl = icinga.utils.parseUrl($menu.data('icinga-url'));
@@ -322,7 +329,8 @@
*/
Navigation.prototype.hideFlyoutMenu = function(e) {
var $layout = $('#layout');
- var $hovered = $('#menu').find('.nav-level-1 > .nav-item.hover');
+ var $nav = $(e.currentTarget);
+ var $hovered = $nav.find('.nav-level-1 > .nav-item.hover');
if (! $hovered.length) {
$layout.removeClass('menu-hovered');
@@ -332,7 +340,7 @@
setTimeout(function() {
try {
- if ($hovered.is(':hover') || $('#menu').is(':hover')) {
+ if ($hovered.is(':hover') || $nav.is(':hover')) {
return;
}
} catch(e) { /* Bypass because if IE8 */ };
@@ -354,6 +362,55 @@
$(window).trigger('resize');
};
+ /**
+ * Toggle config flyout visibility
+ *
+ * @param {Object} e Event
+ */
+ Navigation.prototype.toggleConfigFlyout = function(e) {
+ var _this = e.data.self;
+ if ($('#layout').is('.config-flyout-open')) {
+ _this.hideConfigFlyout(e);
+ } else {
+ _this.showConfigFlyout(e);
+ }
+ }
+
+ /**
+ * Hide config flyout
+ *
+ * @param {Object} e Event
+ */
+ Navigation.prototype.hideConfigFlyout = function(e) {
+ $('#layout').removeClass('config-flyout-open');
+ if (e.target) {
+ delete $(e.target).closest('.container')[0].dataset.suspendAutorefresh;
+ }
+ }
+
+ /**
+ * Show config flyout
+ *
+ * @param {Object} e Event
+ */
+ Navigation.prototype.showConfigFlyout = function(e) {
+ $('#layout').addClass('config-flyout-open');
+ $(e.target).closest('.container')[0].dataset.suspendAutorefresh = '';
+ }
+
+ /**
+ * Hide, config flyout when "Enter" key is pressed to follow `.flyout` nav item link
+ *
+ * @param {Object} e Event
+ */
+ Navigation.prototype.onKeyDown = function(e) {
+ var _this = e.data.self;
+
+ if (e.key == 'Enter' && $(document.activeElement).is('.flyout a')) {
+ _this.hideConfigFlyout(e);
+ }
+ }
+
/**
* Called when the history changes
*