Merge pull request #4697 from Icinga/feature/redesigned-user-menu-new

Feature/redesigned user menu
This commit is contained in:
Johannes Meyer 2022-05-16 09:04:26 +02:00 committed by GitHub
commit 74022ae4e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 689 additions and 33 deletions

View File

@ -1,5 +1,6 @@
<?php
use Icinga\Web\Navigation\ConfigMenu;
use Icinga\Web\Widget\SearchDashboard;
$searchDashboard = new SearchDashboard();
@ -14,3 +15,6 @@ if ($searchDashboard->search('dummy')->getPane('search')->hasDashlets()): ?>
</form>
<?php endif; ?>
<?= $menuRenderer->setCssClass('primary-nav')->setElementTag('nav')->setHeading(t('Navigation')); ?>
<nav class="config-menu">
<?= new ConfigMenu() ?>
</nav>

View File

@ -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/*',

View File

@ -0,0 +1,300 @@
<?php
/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
namespace Icinga\Web\Navigation;
use Icinga\Application\Hook\HealthHook;
use Icinga\Application\Icinga;
use Icinga\Authentication\Auth;
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';
protected $tag = 'ul';
protected $defaultAttributes = ['class' => '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()
]);
}
}

View File

@ -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);

View File

@ -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',

View File

@ -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
*/

View File

@ -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>
> 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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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
*