From 1578baebe5d1ef8561af32424c5d97e847357348 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Tue, 12 Apr 2022 15:58:57 +0200 Subject: [PATCH 01/42] Introduce new widget `MobileNavigation` --- .../Navigation/Mobile/MobileNavigation.php | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 library/Icinga/Web/Navigation/Mobile/MobileNavigation.php diff --git a/library/Icinga/Web/Navigation/Mobile/MobileNavigation.php b/library/Icinga/Web/Navigation/Mobile/MobileNavigation.php new file mode 100644 index 0000000000..39de10b22e --- /dev/null +++ b/library/Icinga/Web/Navigation/Mobile/MobileNavigation.php @@ -0,0 +1,238 @@ +items = $menu; + + $this->items->order(); + } + + protected function createMoreItem() + { + $moreMenu = new HtmlElement('li'); + + $this->assembleMoreItem($moreMenu); + + return $moreMenu; + } + + 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 createHealthBadge() + { + $stateBadge = null; + if ($this->getHealthCount() > 0) { + $stateBadge = new StateBadge($this->getHealthCount(), $this->state); + $stateBadge->addAttributes(['class' => 'disabled']); + } + + return $stateBadge; + } + + protected function assembleMoreItem(BaseHtmlElement $moreMenu) + { + $moreMenu->add( + HtmlElement::create( + 'button', + Attributes::create(['id' => 'toggle-more']), + [ + new Icon('ellipsis-v'), + Text::create('More') + ] + ) + ); + + $moreMenu->add($this->createMoreFlyout()); + } + + protected function createNavItemIcon($item) + { + if ($item->getUrl() !== null && substr($item->getUrl()->getPath(), 0, 9) === 'icingadb/') { + $icon = new Icon($item->getIcon(), [ 'aria-hidden' => 1]); + return $icon; + } + + return new HtmlString(Icinga::app()->getViewRenderer()->view->icon($item->getIcon())); + } + + protected function createMoreFlyout() + { + $moreFlyout = new HtmlElement('div', Attributes::create(['class' => 'flyout'])); + + $this->assembleMoreFlyout($moreFlyout); + + return $moreFlyout; + } + + protected function assembleMoreFlyout($moreFlyout) + { + $flyoutContent = HtmlElement::create('div', ['class' => 'flyout-content']); + $ul = HtmlElement::create('ul', ['class' => 'nav nav-level-2']); + + $startIndex = 0; + foreach ($this->items as $key => $item) { + if ($this->isValidItem($key, $item)) { + if (++$startIndex > self::MAX_NUM_OF_ITEMS - 1) { + $li = $this->createMenuItem($item, $key); + $ul->add($li); + } + } + } + + $flyoutContent->add($ul); + $moreFlyout->add($flyoutContent); + } + + protected function createMenuItem($item, $key) + { + if (in_array($key, self::EXCLUDED_ITEMS)) { + return null; + } + + if (isset($item->permission) && ! Auth::getInstance()->hasPermission($item->permission)) { + return null; + } + + $class = $item->getCssClass() . + ' '. + ($item->getActive() ? ' active' : '') . + ' ' . + ($item->getSelected() ? ' selected' : ''); + + $menuItem = HtmlElement::create( + 'li', + ['class' => $class], + [ + HtmlElement::create('a', [ + 'href' => $item->getUrl(), + ], [ + $this->createNavItemIcon($item), + Text::create($item->getLabel()) + ]) + ] + ); + + return $menuItem; + } + + protected function getValidItemCount() + { + $count = 0; + foreach ($this->items as $key => $item) { + if ($this->isValidItem($key, $item)) { + $count++; + } + } + + return $count; + } + + protected function isValidItem($key, $item) + { + if (in_array($key, self::EXCLUDED_ITEMS) || !($item->hasChildren())) { + if ($key === 'dashboard') { + return true; + } + return false; + } + return true; + } + + protected function assemble() + { + $ul = new HtmlElement('ul', Attributes::create(['class' => 'nav nav-level-1'])); + $validItemCount = $this->getValidItemCount(); + + $count = 0; + foreach ($this->items as $key => $item) { + if ($this->isValidItem($key, $item)) { + $count++; + + if ($validItemCount == self::MAX_NUM_OF_ITEMS && $count == self::MAX_NUM_OF_ITEMS) { + $ul->add($this->createMenuItem($item, $key)); + break; + } + + if ($count > self::MAX_NUM_OF_ITEMS - 1) { + $ul->add($this->createMoreItem()); + break; + } + + $ul->add($this->createMenuItem($item, $key)); + } + } + + $this->add($ul); + } +} From 4a36de160f1e7767f2df5634b82b76675d59e757 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Tue, 12 Apr 2022 14:48:52 +0200 Subject: [PATCH 02/42] Add mobilemenu.less --- public/css/icinga/mobilemenu.less | 101 ++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 public/css/icinga/mobilemenu.less diff --git a/public/css/icinga/mobilemenu.less b/public/css/icinga/mobilemenu.less new file mode 100644 index 0000000000..e52a3f96f6 --- /dev/null +++ b/public/css/icinga/mobilemenu.less @@ -0,0 +1,101 @@ +#mobile-menu { + border-top: 1px solid @gray-lighter; + background: @body-bg-color; + + .nav-level-1 { + display: flex; + align-items: stretch; + + > li { + flex: 1 1 auto; + width: 0; + text-align: center; + + &.active > a, + &.active > button { + background: @menu-highlight-color; + color: @text-color-inverted; + } + + &:not(.active) a:hover { + background: @menu-hover-bg-color; + } + + > a, + > button { + font-size: .5em; + height: 100%; + + // Reset font-size for child elements + > * { + font-size: 1/.5em; + } + + > i { + display: block; + font-size: 2.5em; + opacity: .8; + line-height: 1.25; + + &:before { + margin-right: 0; + } + } + } + } + } + + .nav-level-2 { + >li { + &.selected { + background: @menu-2ndlvl-active-bg-color; + color: @menu-2ndlvl-active-color; + } + } + } + + .active, .selected { + background: @menu-active-bg-color; + color: @menu-active-color; + } + + .flyout { + position: fixed; + bottom: 3.5em; + left: 1em; + right: 1em; + font-size: 12em/16; + + &:before { + right: 10%; + margin-right: -1.25em; + } + } + + button { + .appearance(none); + background: none; + border: none; + width: 100%; + } +} + +#layout.more-menu-open { + #mobile-menu .flyout { + display: block; + } + + #toggle-more { + background: @menu-hover-bg-color; + color: @menu-active-color; + } +} + +// Show #mobile-menu only on mobile +#layout:not(.minimal-layout) #mobile-menu { + display: none; +} + +.nav a:hover { + text-decoration: none; +} From d9eda70ea79372c8e6d3c323fceb359d06e67101 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Tue, 12 Apr 2022 14:49:53 +0200 Subject: [PATCH 03/42] Load mobilemenu.less --- library/Icinga/Web/StyleSheet.php | 1 + 1 file changed, 1 insertion(+) diff --git a/library/Icinga/Web/StyleSheet.php b/library/Icinga/Web/StyleSheet.php index 9ca6d9af75..6cc91727f6 100644 --- a/library/Icinga/Web/StyleSheet.php +++ b/library/Icinga/Web/StyleSheet.php @@ -65,6 +65,7 @@ class StyleSheet 'css/icinga/layout.less', 'css/icinga/layout-structure.less', 'css/icinga/menu.less', + 'css/icinga/mobilemenu.less', 'css/icinga/tabs.less', 'css/icinga/forms.less', 'css/icinga/setup.less', From 81b4163947dff7860e8aa27958410e0b0da7704d Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Tue, 12 Apr 2022 15:59:22 +0200 Subject: [PATCH 04/42] body.html: Use MobileNavigation --- application/layouts/scripts/body.phtml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/application/layouts/scripts/body.phtml b/application/layouts/scripts/body.phtml index 87b570bfe5..bd5562f67f 100644 --- a/application/layouts/scripts/body.phtml +++ b/application/layouts/scripts/body.phtml @@ -1,5 +1,6 @@ layout()->inlineLayout) { 'id' => 'header-logo' ) ); ?> -
- -
+ + +
+ i
render('parts/navigation.phtml'); ?> @@ -68,7 +72,6 @@ if ($this->layout()->inlineLayout) { + From 5183428dddf194bae83f35a804cf471b38107107 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Thu, 19 May 2022 18:34:34 +0200 Subject: [PATCH 05/42] navigation.js: Handle toggle-more click --- public/js/icinga/behavior/navigation.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/js/icinga/behavior/navigation.js b/public/js/icinga/behavior/navigation.js index 3befe4bd34..41bf389081 100644 --- a/public/js/icinga/behavior/navigation.js +++ b/public/js/icinga/behavior/navigation.js @@ -21,6 +21,8 @@ this.on('keydown', '#menu .config-menu .config-nav-item', this.onKeyDown, this); + this.on('click', '#toggle-more', this.toggleMoreFlyout, this); + /** * The DOM-Path of the active item * @@ -422,6 +424,10 @@ } } + Navigation.prototype.toggleMoreFlyout = function(e) { + $('#layout').toggleClass('more-button-open'); + } + /** * Called when the history changes * From b8a34414f5872fe85c8c656d9af617286c6f8256 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Thu, 19 May 2022 18:55:53 +0200 Subject: [PATCH 06/42] body.phtml: Add search bar and user-menu placeholders --- application/layouts/scripts/body.phtml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/layouts/scripts/body.phtml b/application/layouts/scripts/body.phtml index bd5562f67f..54a434e099 100644 --- a/application/layouts/scripts/body.phtml +++ b/application/layouts/scripts/body.phtml @@ -46,9 +46,9 @@ if ($this->layout()->inlineLayout) { ) ); ?> - +
i
From 9dc71cc7c829d6f51c6c1a5a6b74aacaf173bf44 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Thu, 19 May 2022 18:56:18 +0200 Subject: [PATCH 07/42] layout-structure.less: Keep default flex-direction in `.minimal-layout` --- public/css/icinga/layout-structure.less | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/css/icinga/layout-structure.less b/public/css/icinga/layout-structure.less index b1ca8e65a5..f9ff26e102 100644 --- a/public/css/icinga/layout-structure.less +++ b/public/css/icinga/layout-structure.less @@ -26,11 +26,14 @@ body { #sidebar { width: 16em; display: flex; - flex-direction: column; position: relative; z-index: 2; } +#layout:not(.minimal-layout) #sidebar { + flex-direction: column; +} + #layout:not(.minimal-layout) #sidebar:after { content: ""; display: block; From 8af044df020fd4914dea0f2ff01a10eed69ebe2a Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Wed, 1 Jun 2022 17:10:48 +0200 Subject: [PATCH 08/42] Introduce new Widget `MobileConfigMenu` --- .../Navigation/Mobile/MobileConfigMenu.php | 279 ++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 library/Icinga/Web/Navigation/Mobile/MobileConfigMenu.php diff --git a/library/Icinga/Web/Navigation/Mobile/MobileConfigMenu.php b/library/Icinga/Web/Navigation/Mobile/MobileConfigMenu.php new file mode 100644 index 0000000000..dba8f8dc7e --- /dev/null +++ b/library/Icinga/Web/Navigation/Mobile/MobileConfigMenu.php @@ -0,0 +1,279 @@ + 'nav user-nav-item', + 'id' => 'mobile-config-menu' + ]; + + protected $children; + + protected $healthBadge; + + 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'), + 'icon' => 'power-off', + 'atts' => [ + 'target' => '_self', + 'class' => 'nav-item-logout' + ], + 'url' => 'authentication/logout' + ] + ] + ] + ]; + + $this->healthBadge = $this->createHealthBadge(); + } + + protected function createHealthBadge() + { + $stateBadge = null; + if ($this->getHealthCount() > 0) { + $stateBadge = new StateBadge($this->getHealthCount(), $this->state); + $stateBadge->addAttributes(['class' => 'disabled']); + } + + return $stateBadge; + } + + 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 createLevel2MenuItem($item, $key) + { + if (isset($item['permission']) && ! Auth::getInstance()->hasPermission($item['permission'])) { + return null; + } + + $class = null; + if ($key === 'health') { + $class = 'badge-nav-item'; + } + + $icon = null; + $iconClass = null; + if (isset($item['icon'])) { + $icon = new Icon($item['icon']); + $iconClass = 'has-icon'; + } + + $li = HtmlElement::create( + 'li', + isset($item['atts']) ? $item['atts'] : null, + [ + HtmlElement::create( + 'a', + Attributes::create(['href' => Url::fromPath($item['url'])]), + [ + $icon, + $item['label'], + $key === 'health' ? $this->healthBadge : null + ] + ), + ] + ); + + $li->addAttributes(['class' => $class]); + $li->addAttributes(['class' => $iconClass]); + + if ($this->isSelectedItem($item)) { + $li->addAttributes(['class' => 'selected']); + $this->cogItemActive = true; + } + + return $li; + } + + protected function isSelectedItem($item) + { + if ($item !== null && Icinga::app()->getRequest()->getUrl()->matches($item['url'])) { + $this->selected = $item; + return true; + } + + return false; + } + + protected function createLevel2Menu() + { + $level2Nav = HtmlElement::create( + 'div', + Attributes::create(['class' => 'nav-level-1 flyout']) + ); + + $this->assembleLevel2Nav($level2Nav); + + return $level2Nav; + } + + protected function assembleLevel2Nav(BaseHtmlElement $level2Nav) + { + $username = Auth::getInstance()->getUser()->getUsername(); + $navContent = HtmlElement::create('div', ['class' => 'flyout-content']); + $navContent->add(HtmlElement::create('ul', ['class' => 'nav'], [ + HtmlElement::create('li', ['class' => 'has-icon'], [ + HtmlElement::create( + 'a', + Attributes::create(['href' => Url::fromPath('account')]), + [ + HtmlElement::create('i', ['class' => 'user-ball'], Text::create($username[0])), + Text::create($username) + ] + ) + ]) + ])); + 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 assemble() + { + $username = Auth::getInstance()->getUser()->getUsername(); + + $button = HtmlElement::create( + 'button', + ['id' => 'toggle-mobile-config-flyout'], + [ + new HtmlElement( + 'i', + Attributes::create(['class' => 'user-ball']), + Text::create($username[0]) + ) + ] + ); + + $this->add(HtmlElement::create('li', null, [ + $button, + $this->createLevel2Menu(), + $this->healthBadge + ])); + } +} From 997cc8da3083cbf62223f0b56b444d945f34a86b Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Wed, 1 Jun 2022 17:12:09 +0200 Subject: [PATCH 09/42] Add `mobilconfigmenu` Stylesheet --- public/css/icinga/mobileconfigmenu.less | 155 ++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 public/css/icinga/mobileconfigmenu.less diff --git a/public/css/icinga/mobileconfigmenu.less b/public/css/icinga/mobileconfigmenu.less new file mode 100644 index 0000000000..6be716f398 --- /dev/null +++ b/public/css/icinga/mobileconfigmenu.less @@ -0,0 +1,155 @@ +#mobile-searchbar { + input.search { + @height: 2.5em; + + width: 100%; + background: @low-sat-blue-dark url(../img/icons/search.png) no-repeat .5em center; + padding-left: 2em; + background-size: 1em; + border: none; + height: @height; + border-radius: @height/2; + + &:focus { + outline: 3px solid rgba(0, 195, 237, 0.5); + + &::placeholder { + color: @gray; + } + } + } + + .search-reset { + right: .5em; + } + + .search-control { + width: 100%; + } +} + +#mobile-config-menu { + position: relative; + + button { + .appearance(none); + background: none; + border: none; + .rounded-corners(); + padding: .25em; + margin-right: -.25em; + + > .user-ball { + .ball-size-l(); + } + } + + > li { + margin-right: .75em; + } + + .state-badge { + font-size: .857em; + line-height: 1.5; + } + + button ~ .state-badge { + position: absolute; + top: .25em; + left: -.5em; + } + + .user-ball { + .ball(); + .ball-solid(@icinga-blue); + + // icingadb-web/public/css/common.less: .user-ball + font-weight: bold; + text-transform: uppercase; + + // compensate border + margin: -1px; + border: 1px solid @text-color-inverted; + font-style: normal; + line-height: 1.2; + + font-size: 1.75em; + } + + .nav-level-1 { + position: fixed; + right: 1em; + left: 1em; + top: 4em; + + li { + &.badge-nav-item > a { + display: flex; + align-items: baseline; + width: 100%; + + .state-badge { + margin-left: auto; + } + } + } + + li:not(.has-icon) a { + padding-left: 2em; + } + + li .icon { + width: 1em; + margin-left: .3em; + margin-right: .2em; + } + + li .user-ball { + .ball-size-m(); + line-height: .25; + margin-right: .2em; + } + + &:before { + transform: rotate(225deg); + height: 1.1em; + width: 1.1em; + top: -.6em; + right: .6em; + } + + .nav-item-logout { + color: @color-critical; + border-top: 1px solid @gray-lighter; + } + } + + > li.active button { + background: @icinga-blue + } + + .nav-level-1 li.active.selected { + background: @menu-2ndlvl-active-bg-color + } +} + +#layout:not(.minimal-layout) { + #mobile-config-menu, + #mobile-searchbar { + display: none; + } +} + +#layout.mobile-config-flyout-open #mobile-config-menu { + button ~ .state-badge { + display: none; + } + + .nav-level-1 { + display: block; + } + + button { + background: rgba(0, 0, 0, 0.25); + } +} From db8ddd56370846470a9752ef69c7a2dbc49192a1 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Wed, 1 Jun 2022 17:12:29 +0200 Subject: [PATCH 10/42] Load `mobilemenuconfig` Stylesheet --- library/Icinga/Web/StyleSheet.php | 1 + 1 file changed, 1 insertion(+) diff --git a/library/Icinga/Web/StyleSheet.php b/library/Icinga/Web/StyleSheet.php index 6cc91727f6..e169106b24 100644 --- a/library/Icinga/Web/StyleSheet.php +++ b/library/Icinga/Web/StyleSheet.php @@ -78,6 +78,7 @@ class StyleSheet 'css/icinga/compat.less', 'css/icinga/print.less', 'css/icinga/responsive.less', + 'css/icinga/mobileconfigmenu.less', 'css/icinga/modal.less', 'css/icinga/audit.less', 'css/icinga/health.less', From d4fdb8d491b99d3394ed11325cfc460e9ab8b18f Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Wed, 1 Jun 2022 17:12:53 +0200 Subject: [PATCH 11/42] body.phtml: Use `MobileConfigMenu` --- application/layouts/scripts/body.phtml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/application/layouts/scripts/body.phtml b/application/layouts/scripts/body.phtml index 54a434e099..a710e12927 100644 --- a/application/layouts/scripts/body.phtml +++ b/application/layouts/scripts/body.phtml @@ -1,5 +1,6 @@ layout()->inlineLayout) { ) ); ?> - -
- i -
+ render('parts/navigation.phtml'); ?> From 2128fd33900db889a781023fcf1431089aca0fb0 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Wed, 1 Jun 2022 17:13:24 +0200 Subject: [PATCH 12/42] layout.less: Adjust `#sidebar` styles for mobile --- public/css/icinga/layout.less | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/css/icinga/layout.less b/public/css/icinga/layout.less index c37da7920e..5eef758751 100644 --- a/public/css/icinga/layout.less +++ b/public/css/icinga/layout.less @@ -3,11 +3,14 @@ #footer { bottom: 0; right: 0; - left: 12em; - position: fixed; z-index: 999; } +.layout:not(.minimal-layout) #footer { + position: fixed; + left: 12em; +} + #layout.minimal-layout #footer { left: 0; } @@ -150,6 +153,8 @@ // Mobile menu #layout.minimal-layout #sidebar { background-color: @menu-bg-color; + justify-content: space-between; + align-items: center; } #mobile-menu-toggle { From 3838d7602453e7481489e905a47a44dd7576f191 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Wed, 1 Jun 2022 17:13:51 +0200 Subject: [PATCH 13/42] responsive.less: Adjust styles for `#sidebar` --- public/css/icinga/responsive.less | 1 - 1 file changed, 1 deletion(-) diff --git a/public/css/icinga/responsive.less b/public/css/icinga/responsive.less index 6d1cae856f..bf6908802b 100644 --- a/public/css/icinga/responsive.less +++ b/public/css/icinga/responsive.less @@ -70,7 +70,6 @@ #layout.minimal-layout { #sidebar { width: 100%; - overflow: auto; } #header-logo-container { From 1372aba62ceebe602727627a7f77075c376a3956 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Wed, 1 Jun 2022 17:14:29 +0200 Subject: [PATCH 14/42] navigation.js: Add event handlers for `#mobile-menu` --- public/js/icinga/behavior/navigation.js | 37 +++++++++++++++++++------ 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/public/js/icinga/behavior/navigation.js b/public/js/icinga/behavior/navigation.js index 41bf389081..38d51a1208 100644 --- a/public/js/icinga/behavior/navigation.js +++ b/public/js/icinga/behavior/navigation.js @@ -8,11 +8,14 @@ var Navigation = function (icinga) { Icinga.EventListener.call(this, icinga); - 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 .primary-nav .nav-level-1 > .nav-item', this.showFlyoutMenu, this); - this.on('mouseleave', '#menu .primary-nav', this.hideFlyoutMenu, this); + this.on('click', '#menu a, #mobile-menu a, #mobile-config-menu a', this.linkClicked, this); + this.on('click', '#menu tr[href], #mobile-menu tr[href]', this.linkClicked, this); + + this.on('rendered', '#menu, #mobile-menu', this.onRendered, this); + + this.on('mouseenter', '#menu .primary-nav .nav-level-1 > .nav-item, #mobile-menu .primary-nav .nav-level-1 > .nav-item', this.showFlyoutMenu, this); + this.on('mouseleave', '#menu .primary-nav, #mobile-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); @@ -22,6 +25,10 @@ this.on('keydown', '#menu .config-menu .config-nav-item', this.onKeyDown, this); this.on('click', '#toggle-more', this.toggleMoreFlyout, this); + this.on('click', '#toggle-more + .flyout a', this.hideMoreFlyout, this); + + this.on('click', '#toggle-mobile-config-flyout', this.toggleMobileConfigMenu, this); + this.on('click', '#toggle-mobile-config-flyout ~ .nav-level-1 a', this.hideMobileConfigMenu, this); /** * The DOM-Path of the active item @@ -64,7 +71,7 @@ Navigation.prototype.onRendered = function(e) { var _this = e.data.self; - _this.$menu = $(e.target); + _this.$menu = $('#menu, #mobile-menu, #mobile-config-menu'); if (! _this.active) { // There is no stored menu item, therefore it is assumed that this is the first rendering @@ -131,7 +138,7 @@ } // update target url of the menu container to the clicked link - var $menu = $('#menu'); + var $menu = $('#menu, #mobile-menu, #mobile-config-menu'); var menuDataUrl = icinga.utils.parseUrl($menu.data('icinga-url')); menuDataUrl = icinga.utils.addUrlParams(menuDataUrl.path, { url: href }); $menu.data('icinga-url', menuDataUrl); @@ -145,7 +152,7 @@ * @param url {String} The url to match */ Navigation.prototype.setActiveAndSelectedByUrl = function(url) { - var $menu = $('#menu'); + var $menu = $('#menu, #mobile-menu, #mobile-config-menu'); if (! $menu.length) { return; @@ -425,7 +432,19 @@ } Navigation.prototype.toggleMoreFlyout = function(e) { - $('#layout').toggleClass('more-button-open'); + $('#layout').toggleClass('more-menu-open'); + } + + Navigation.prototype.hideMoreFlyout = function(e) { + $('#layout').removeClass('more-menu-open'); + } + + Navigation.prototype.toggleMobileConfigMenu = function(e) { + $('#layout').toggleClass('mobile-config-flyout-open'); + } + + Navigation.prototype.hideMobileConfigMenu = function(e) { + $('#layout').removeClass('mobile-config-flyout-open'); } /** From a3e6df7b5323c32efe0661ef342cfd3a221fb48c Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Tue, 7 Jun 2022 15:10:07 +0200 Subject: [PATCH 15/42] flyout.less: Add separate file for flyout --- public/css/icinga/configmenu.less | 56 +------------ public/css/icinga/flyout.less | 131 ++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 54 deletions(-) create mode 100644 public/css/icinga/flyout.less diff --git a/public/css/icinga/configmenu.less b/public/css/icinga/configmenu.less index 05e50e8647..5a7b0654c5 100644 --- a/public/css/icinga/configmenu.less +++ b/public/css/icinga/configmenu.less @@ -163,7 +163,7 @@ } } - .flyout { + .nav-level-1 { bottom: 100%; right: -2em; width: 15em; @@ -210,7 +210,7 @@ } } - .flyout { + .nav-level-1 { bottom: 0; left: 100%; width: 14em; @@ -223,58 +223,6 @@ } } -.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 { diff --git a/public/css/icinga/flyout.less b/public/css/icinga/flyout.less new file mode 100644 index 0000000000..998b927343 --- /dev/null +++ b/public/css/icinga/flyout.less @@ -0,0 +1,131 @@ +#layout:not(.minimal-layout) .flyout:not(.mobile-only), +#layout.minimal-layout .flyout { + display: none; + 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(); + + &.flyout-open { + display: block; + } + + // 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; + } + + // Flyout Menu + .flyout-menu { + display: block; + margin: 0; + + form { + width: 100% + } + + a, + button { + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + display: block; + width: 100%; + + &:hover { + text-decoration: none; + background: @menu-2ndlvl-highlight-bg-color; + } + } + + > li { + margin: 0; + + > a, + button { + padding: 0.364em 0.545em; + } + + &.badge-nav-item > a { + display: flex; + align-items: baseline; + width: 100%; + + .state-badge { + margin-left: auto; + } + } + + .icon, + i[class^="icon-"]{ + width: 1em; + margin-left: .3em; + margin-right: .2em; + opacity: .8; + } + + .user-ball { + height: 0.75em; + width: 0.75em; + padding: 0; + line-height: 1.25; + + margin-right: .2em; + } + + &:not(.has-icon) > a { + padding-left: 2em; + } + + &.nav-item-logout { + color: @color-critical; + border-top: 1px solid @gray-lighter; + } + } + } + + 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; + } +} + +.flyout:not(.mobile-only) { + position: absolute; +} + +#layout:not(.minimal-layout) .flyout.mobile-only { + + button { + display: none; + } +} + +#layout.minimal-layout .flyout-content { + a, + h3, + button { + line-height: 2.75 + } +} From 1a9eda4e1ce9e6cab16c52ffd2df9edd80d0c027 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Tue, 7 Jun 2022 15:10:24 +0200 Subject: [PATCH 16/42] Stylesheet.php: Load flyout.less --- library/Icinga/Web/StyleSheet.php | 1 + 1 file changed, 1 insertion(+) diff --git a/library/Icinga/Web/StyleSheet.php b/library/Icinga/Web/StyleSheet.php index e169106b24..754a80b29e 100644 --- a/library/Icinga/Web/StyleSheet.php +++ b/library/Icinga/Web/StyleSheet.php @@ -58,6 +58,7 @@ class StyleSheet 'css/icinga/badges.less', 'css/icinga/configmenu.less', 'css/icinga/mixins.less', + 'css/icinga/flyout.less', 'css/icinga/grid.less', 'css/icinga/nav.less', 'css/icinga/main.less', From 0d0ba52072e621fda07070bcad9402ec49074188 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Wed, 8 Jun 2022 15:42:07 +0200 Subject: [PATCH 17/42] =?UTF-8?q?configmenu.less:=20Don=E2=80=99t=20use=20?= =?UTF-8?q?level-2=20only=20for=20`.config-menu`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/css/icinga/configmenu.less | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/css/icinga/configmenu.less b/public/css/icinga/configmenu.less index 5a7b0654c5..c1c9d50426 100644 --- a/public/css/icinga/configmenu.less +++ b/public/css/icinga/configmenu.less @@ -97,6 +97,12 @@ } } + .nav-level-1 li a { + // Match effective padding left of level-2 flyouts. (1.5 + .5 em) + padding-left: 2em; + font-size: 11/12em; + } + .state-badge { line-height: 1.2; padding: .25em; From d6ce32b847002da113a56bee466f2dced3495327 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Tue, 14 Jun 2022 12:25:07 +0200 Subject: [PATCH 18/42] LayoutController: Add actions for mobile menus --- application/controllers/LayoutController.php | 22 ++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/application/controllers/LayoutController.php b/application/controllers/LayoutController.php index 237681cfa5..dffdc343a3 100644 --- a/application/controllers/LayoutController.php +++ b/application/controllers/LayoutController.php @@ -3,13 +3,15 @@ namespace Icinga\Controllers; -use Icinga\Web\Controller\ActionController; use Icinga\Web\Menu; +use Icinga\Web\Navigation\Mobile\MobileConfigMenu; +use Icinga\Web\Navigation\Mobile\MobileMenu; +use ipl\Web\Compat\CompatController; /** * Create complex layout parts */ -class LayoutController extends ActionController +class LayoutController extends CompatController { /** * Render the menu @@ -21,6 +23,22 @@ public function menuAction() $this->view->menuRenderer = (new Menu())->getRenderer(); } + public function mobileConfigMenuAction() + { + $this->setAutorefreshInterval(15); + $this->_helper->layout()->disableLayout(); + $this->view->compact = true; + $this->getDocument()->addHtml(new MobileConfigMenu()); + } + + public function mobileMenuAction() + { + $this->setAutorefreshInterval(15); + $this->_helper->layout()->disableLayout(); + $this->view->compact = true; + $this->getDocument()->addHtml(new MobileMenu()); + } + public function announcementsAction() { $this->_helper->layout()->disableLayout(); From 29c18750812a5c7f31a58221f6f72cba2ad96812 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Tue, 14 Jun 2022 12:25:41 +0200 Subject: [PATCH 19/42] body.phtml: Enable autorefresh for mobile menus --- application/layouts/scripts/body.phtml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/application/layouts/scripts/body.phtml b/application/layouts/scripts/body.phtml index a710e12927..c398e115ca 100644 --- a/application/layouts/scripts/body.phtml +++ b/application/layouts/scripts/body.phtml @@ -1,10 +1,11 @@ layout()->inlineLayout) { $inlineLayoutScript = 'inline.phtml'; } +$searchDashboard = new SearchDashboard(); +$searchDashboard->setUser($this->Auth()->getUser()); + ?> @@ -100,5 +109,8 @@ if ($this->layout()->inlineLayout) { } ?>
+
+ +
- From 6d81ee521d00592a5756fa3e265901a442e2d486 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Tue, 14 Jun 2022 12:26:06 +0200 Subject: [PATCH 20/42] Create new Trait `HealthBadgeTrait` --- library/Icinga/Common/HealthBadgeTrait.php | 78 ++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 library/Icinga/Common/HealthBadgeTrait.php diff --git a/library/Icinga/Common/HealthBadgeTrait.php b/library/Icinga/Common/HealthBadgeTrait.php new file mode 100644 index 0000000000..298d58900d --- /dev/null +++ b/library/Icinga/Common/HealthBadgeTrait.php @@ -0,0 +1,78 @@ +getHealthCount() > 0) { + $stateBadge = new StateBadge($this->getHealthCount(), $this->state); + $stateBadge->addAttributes(['class' => 'disabled', 'title' => $this->title]); + } + + return $stateBadge; + } + + /** + * Get the number of health problems + * + * @return int $count + */ + 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 = $this->STATE_WARNING; + break; + case HealthHook::STATE_CRITICAL: + $this->state = $this->STATE_CRITICAL; + break; + case HealthHook::STATE_UNKNOWN: + $this->state = $this->STATE_UNKNOWN; + break; + } + + $this->title = $title; + + return $count; + } +} From 6988871344cf9f9ca0fbcfb4dcc100542a0318c1 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Tue, 14 Jun 2022 12:39:19 +0200 Subject: [PATCH 21/42] Use `HealthBadgeTrait` --- library/Icinga/Web/Navigation/ConfigMenu.php | 55 +----------------- .../Navigation/Mobile/MobileConfigMenu.php | 55 +----------------- .../Navigation/Mobile/MobileNavigation.php | 57 +------------------ 3 files changed, 5 insertions(+), 162 deletions(-) diff --git a/library/Icinga/Web/Navigation/ConfigMenu.php b/library/Icinga/Web/Navigation/ConfigMenu.php index 4d5caee4ed..9a5ae332cc 100644 --- a/library/Icinga/Web/Navigation/ConfigMenu.php +++ b/library/Icinga/Web/Navigation/ConfigMenu.php @@ -3,25 +3,20 @@ namespace Icinga\Web\Navigation; -use Icinga\Application\Hook\HealthHook; use Icinga\Application\Icinga; use Icinga\Application\Logger; use Icinga\Authentication\Auth; +use Icinga\Common\HealthBadgeTrait; use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\Web\Url; use ipl\Web\Widget\Icon; -use ipl\Web\Widget\StateBadge; class ConfigMenu extends BaseHtmlElement { - const STATE_OK = 'ok'; - const STATE_CRITICAL = 'critical'; - const STATE_WARNING = 'warning'; - const STATE_PENDING = 'pending'; - const STATE_UNKNOWN = 'unknown'; + use HealthBadgeTrait; protected $tag = 'ul'; @@ -176,41 +171,6 @@ protected function assembleLevel2Nav(BaseHtmlElement $level2Nav) $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'])) { @@ -221,17 +181,6 @@ protected function isSelectedItem($item) 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( diff --git a/library/Icinga/Web/Navigation/Mobile/MobileConfigMenu.php b/library/Icinga/Web/Navigation/Mobile/MobileConfigMenu.php index dba8f8dc7e..ff305c3d18 100644 --- a/library/Icinga/Web/Navigation/Mobile/MobileConfigMenu.php +++ b/library/Icinga/Web/Navigation/Mobile/MobileConfigMenu.php @@ -2,7 +2,6 @@ namespace Icinga\Web\Navigation\Mobile; -use Icinga\Application\Hook\HealthHook; use Icinga\Application\Icinga; use Icinga\Authentication\Auth; use Icinga\Common\HealthBadgeTrait; @@ -12,16 +11,10 @@ use ipl\Html\Text; use ipl\Web\Url; use ipl\Web\Widget\Icon; -use ipl\Web\Widget\StateBadge; class MobileConfigMenu extends BaseHtmlElement { - // Maybe states as a trait?, see also in ConfigMenu.php - const STATE_OK = 'ok'; - const STATE_CRITICAL = 'critical'; - const STATE_WARNING = 'warning'; - const STATE_PENDING = 'pending'; - const STATE_UNKNOWN = 'unknown'; + use HealthBadgeTrait; protected $tag = 'ul'; @@ -102,52 +95,6 @@ public function __construct() $this->healthBadge = $this->createHealthBadge(); } - protected function createHealthBadge() - { - $stateBadge = null; - if ($this->getHealthCount() > 0) { - $stateBadge = new StateBadge($this->getHealthCount(), $this->state); - $stateBadge->addAttributes(['class' => 'disabled']); - } - - return $stateBadge; - } - - 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 createLevel2MenuItem($item, $key) { if (isset($item['permission']) && ! Auth::getInstance()->hasPermission($item['permission'])) { diff --git a/library/Icinga/Web/Navigation/Mobile/MobileNavigation.php b/library/Icinga/Web/Navigation/Mobile/MobileNavigation.php index 39de10b22e..1ccad1b0c4 100644 --- a/library/Icinga/Web/Navigation/Mobile/MobileNavigation.php +++ b/library/Icinga/Web/Navigation/Mobile/MobileNavigation.php @@ -3,9 +3,9 @@ namespace Icinga\Web\Navigation\Mobile; -use Icinga\Application\Hook\HealthHook; use Icinga\Application\Icinga; use Icinga\Authentication\Auth; +use Icinga\Common\HealthBadgeTrait; use Icinga\Web\Menu; use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; @@ -13,17 +13,10 @@ use ipl\Html\HtmlString; use ipl\Html\Text; use ipl\Web\Widget\Icon; -use ipl\Web\Widget\StateBadge; class MobileNavigation extends BaseHtmlElement { - - // Maybe states as a trait?, see also in ConfigMenu.php - const STATE_OK = 'ok'; - const STATE_CRITICAL = 'critical'; - const STATE_WARNING = 'warning'; - const STATE_PENDING = 'pending'; - const STATE_UNKNOWN = 'unknown'; + use HealthBadgeTrait; const MAX_NUM_OF_ITEMS = 5; @@ -54,52 +47,6 @@ protected function createMoreItem() return $moreMenu; } - 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 createHealthBadge() - { - $stateBadge = null; - if ($this->getHealthCount() > 0) { - $stateBadge = new StateBadge($this->getHealthCount(), $this->state); - $stateBadge->addAttributes(['class' => 'disabled']); - } - - return $stateBadge; - } - protected function assembleMoreItem(BaseHtmlElement $moreMenu) { $moreMenu->add( From 71dabf0d475e706f8765213c460fa6c46da8b638 Mon Sep 17 00:00:00 2001 From: Florian Strohmaier Date: Tue, 14 Jun 2022 12:28:53 +0200 Subject: [PATCH 22/42] navigation.phtml --- application/layouts/scripts/parts/navigation.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/layouts/scripts/parts/navigation.phtml b/application/layouts/scripts/parts/navigation.phtml index dd973f5c5b..48c1f4fad5 100644 --- a/application/layouts/scripts/parts/navigation.phtml +++ b/application/layouts/scripts/parts/navigation.phtml @@ -20,7 +20,7 @@ if (! $this->auth()->isAuthenticated()) {