Allow declarative definitions of badge renderers and improve interface

Allow the data backend, columns and generated tooltips to be defined in the configuration instead of providing subclasses for every new configuration. Provide an abstract BadgeMenuItemRenderer that allows creating Badges with less boilerplate.

fixes #9694
This commit is contained in:
Matthias Jentsch 2015-08-20 17:38:55 +02:00
parent 41d68f6a74
commit 44271471e3
13 changed files with 461 additions and 326 deletions

View File

@ -117,9 +117,19 @@ class Menu implements RecursiveIterator
foreach ($props as $key => $value) {
$method = 'set' . implode('', array_map('ucfirst', explode('_', strtolower($key))));
if ($key === 'renderer') {
// nested configuration is used to pass multiple arguments to the item renderer
if ($value instanceof ConfigObject) {
$args = $value;
$value = $value->get('0');
}
$value = '\\' . ltrim($value, '\\');
if (class_exists($value)) {
$value = new $value;
if (isset($args)) {
$value = new $value($args);
} else {
$value = new $value;
}
} else {
$class = '\Icinga\Web\Menu' . $value;
if (!class_exists($class)) {
@ -127,7 +137,11 @@ class Menu implements RecursiveIterator
sprintf('ItemRenderer with class "%s" does not exist', $class)
);
}
$value = new $class;
if (isset($args)) {
$value = new $class($args);
} else {
$value = new $class;
}
}
}
if (method_exists($this, $method)) {
@ -226,7 +240,6 @@ class Menu implements RecursiveIterator
$auth = Auth::getInstance();
if ($auth->isAuthenticated()) {
$this->add(t('Dashboard'), array(
'url' => 'dashboard',
'icon' => 'dashboard',
@ -236,7 +249,10 @@ class Menu implements RecursiveIterator
$section = $this->add(t('System'), array(
'icon' => 'services',
'priority' => 700,
'renderer' => 'ProblemMenuItemRenderer'
'renderer' => array(
'SummaryMenuItemRenderer',
'state' => 'critical'
)
));
$section->add(t('About'), array(
'url' => 'about',
@ -297,7 +313,10 @@ class Menu implements RecursiveIterator
$section->add(t('Logout'), array(
'url' => 'authentication/logout',
'priority' => 990,
'renderer' => 'ForeignMenuItemRenderer'
'renderer' => array(
'MenuItemRenderer',
'target' => '_self'
)
));
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace Icinga\Web\Menu;
use Icinga\Web\Menu;
abstract class BadgeMenuItemRenderer extends MenuItemRenderer
{
const STATE_OK = 'ok';
const STATE_CRITICAL = 'critical';
const STATE_WARNING = 'warning';
const STATE_PENDING = 'pending';
const STATE_UNKNOWN = 'unknown';
/**
* Defines the color of the badge
*
* @return string
*/
abstract public function getState();
/**
* The amount of items to display in the badge
*
* @return int
*/
abstract public function getCount();
/**
* The tooltip title
*
* @return string
*/
abstract public function getTitle();
/**
* Renders the html content of a single menu item
*
* @param Menu $menu
*
* @return string
*/
public function render(Menu $menu)
{
return $this->renderBadge() . $this->createLink($menu);
}
/**
* Render the badge
*
* @return string
*/
protected function renderBadge()
{
if ($count = $this->getCount()) {
return sprintf(
'<div title="%s" class="badge-container"><span class="badge badge-%s">%s</span></div>',
$this->getView()->escape($this->getTitle()),
$this->getView()->escape($this->getState()),
$count
);
}
return '';
}
}

View File

@ -1,17 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Menu;
use Icinga\Web\Menu;
use Icinga\Web\Url;
/**
* A menu item with a link that surpasses the regular navigation link behavior
*/
class ForeignMenuItemRenderer extends MenuItemRenderer
{
protected $attributes = array(
'target' => '_self'
);
}

View File

@ -7,6 +7,7 @@ use Icinga\Application\Icinga;
use Icinga\Web\Menu;
use Icinga\Web\Url;
use Icinga\Web\View;
use Icinga\Data\ConfigObject;
/**
* Default MenuItemRenderer class
@ -14,38 +15,39 @@ use Icinga\Web\View;
class MenuItemRenderer
{
/**
* Contains <a> element specific attributes
*
* @var array
*/
protected $attributes = array();
/**
* View
* The view this menu item is being rendered to
*
* @var View|null
*/
protected $view;
protected $view = null;
/**
* Set the view
* The link target
*
* @param View $view
*
* @return $this
* @var string
*/
public function setView(View $view)
protected $target = null;
/**
* Create a new instance of MenuItemRenderer
*
* Is is possible to configure the link target using the option 'target'
*
* @param ConfigObject|null $configuration
*/
public function __construct(ConfigObject $configuration = null)
{
$this->view = $view;
return $this;
if ($configuration !== null) {
$this->target = $configuration->get('target', null);
}
}
/**
* Get the view
* Get the view this menu item is being rendered to
*
* @return View
*/
public function getView()
protected function getView()
{
if ($this->view === null) {
$this->view = Icinga::app()->getViewRenderer()->view;
@ -53,6 +55,36 @@ class MenuItemRenderer
return $this->view;
}
/**
* Creates a menu item link element
*
* @param Menu $menu
*
* @return string
*/
public function createLink(Menu $menu)
{
$attributes = isset($this->target) ? sprintf(' target="%s"', $this->getView()->escape($this->target)) : '';
if ($menu->getIcon() && strpos($menu->getIcon(), '.') === false) {
return sprintf(
'<a href="%s"%s><i aria-hidden="true" class="icon-%s"></i>%s</a>',
$menu->getUrl() ? : '#',
$attributes,
$menu->getIcon(),
$this->getView()->escape($menu->getTitle())
);
}
return sprintf(
'<a href="%s"%s>%s%s<span></span></a>',
$menu->getUrl() ? : '#',
$attributes,
$menu->getIcon() ? '<img aria-hidden="true" src="' . Url::fromPath($menu->getIcon()) . '" class="icon" /> ' : '',
$this->getView()->escape($menu->getTitle())
);
}
/**
* Renders the html content of a single menu item
*
@ -64,47 +96,4 @@ class MenuItemRenderer
{
return $this->createLink($menu);
}
/**
* Creates a menu item link element
*
* @param Menu $menu
*
* @return string
*/
public function createLink(Menu $menu)
{
if ($menu->getIcon() && strpos($menu->getIcon(), '.') === false) {
return sprintf(
'<a href="%s"%s><i aria-hidden="true" class="icon-%s"></i>%s</a>',
$menu->getUrl() ? : '#',
$this->getAttributes(),
$menu->getIcon(),
$this->getView()->escape($menu->getTitle())
);
}
return sprintf(
'<a href="%s"%s>%s%s<span></span></a>',
$menu->getUrl() ? : '#',
$this->getAttributes(),
$menu->getIcon() ? '<img aria-hidden="true" src="' . Url::fromPath($menu->getIcon()) . '" class="icon" /> ' : '',
$this->getView()->escape($menu->getTitle())
);
}
/**
* Returns <a> element specific attributes if present
*
* @return string
*/
protected function getAttributes()
{
$attributes = '';
$view = $this->getView();
foreach ($this->attributes as $attribute => $value) {
$attributes .= ' ' . $view->escape($attribute) . '="' . $view->escape($value) . '"';
}
return $attributes;
}
}

View File

@ -1,64 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Menu;
use Icinga\Web\Menu;
class ProblemMenuItemRenderer extends MenuItemRenderer
{
/**
* Set of summarized problems from submenus
*
* @var array
*/
protected $summary = array();
/**
* Renders the html content of a single menu item and summarizes submenu problems
*
* @param Menu $menu
*
* @return string
*/
public function render(Menu $menu)
{
if ($menu->getParent() !== null && $menu->hasSubMenus()) {
/** @var $submenu Menu */
foreach ($menu->getSubMenus() as $submenu) {
$renderer = $submenu->getRenderer();
if (method_exists($renderer, 'getSummary')) {
if ($renderer->getSummary() !== null) {
$this->summary[] = $renderer->getSummary();
}
}
}
}
return $this->getBadge() . $this->createLink($menu);
}
/**
* Get the problem badge
*
* @return string
*/
protected function getBadge()
{
if (count($this->summary) > 0) {
$problems = 0;
$titles = array();
foreach ($this->summary as $summary) {
$problems += $summary['problems'];
$titles[] = $summary['title'];
}
return sprintf(
'<div title="%s" class="badge-container"><span class="badge badge-critical">%s</span></div>',
implode(', ', $titles),
$problems
);
}
return '';
}
}

View File

@ -0,0 +1,94 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Menu;
use Icinga\Web\Menu;
use Icinga\Data\ConfigObject;
/**
* Summary badge adding up all badges in the sub-menus that have the same state
*/
class SummaryMenuItemRenderer extends BadgeMenuItemRenderer
{
/**
* Set of summarized problems from submenus
*
* @var array
*/
protected $titles = array();
/**
* The amount of problems
*
* @var int
*/
protected $count = 0;
/**
* The state that should be summarized
*
* @var string
*/
protected $state;
/**
* The amount of problems
*/
public function __construct(ConfigObject $configuration)
{
$this->state = $configuration->get('state', self::STATE_CRITICAL);
}
/**
* Renders the html content of a single menu item and summarized sub-menus
*
* @param Menu $menu
*
* @return string
*/
public function render(Menu $menu)
{
/** @var $submenu Menu */
foreach ($menu->getSubMenus() as $submenu) {
$renderer = $submenu->getRenderer();
if ($renderer instanceof BadgeMenuItemRenderer) {
if ($renderer->getState() === $this->state) {
$this->titles[] = $renderer->getTitle();
$this->count += $renderer->getCount();
}
}
}
return $this->renderBadge() . $this->createLink($menu);
}
/**
* The amount of items to display in the badge
*
* @return int
*/
public function getCount()
{
return $this->count;
}
/**
* Defines the color of the badge
*
* @return string
*/
public function getState()
{
return $this->state;
}
/**
* The tooltip title
*
* @return string
*/
public function getTitle()
{
return implode(', ', $this->titles);
}
}

View File

@ -89,17 +89,34 @@ $this->provideSearchUrl($this->translate('Servicegroups'), 'monitoring/list/serv
* Problems Section
*/
$section = $this->menuSection($this->translate('Problems'), array(
'renderer' => 'Icinga\Module\Monitoring\Web\Menu\ProblemMenuItemRenderer',
'renderer' => array(
'SummaryMenuItemRenderer',
'state' => 'critical'
),
'icon' => 'block',
'priority' => 20
));
$section->add($this->translate('Unhandled Hosts'), array(
'renderer' => 'Icinga\Module\Monitoring\Web\Menu\UnhandledHostMenuItemRenderer',
'renderer' => array(
'Icinga\Module\Monitoring\Web\Menu\MonitoringBadgeMenuItemRenderer',
'columns' => array(
'hosts_down_unhandled' => $this->translate('%d unhandled hosts down')
),
'state' => 'critical',
'dataView' => 'statussummary'
),
'url' => 'monitoring/list/hosts?host_problem=1&host_handled=0',
'priority' => 30
));
$section->add($this->translate('Unhandled Services'), array(
'renderer' => 'Icinga\Module\Monitoring\Web\Menu\UnhandledServiceMenuItemRenderer',
'renderer' => array(
'Icinga\Module\Monitoring\Web\Menu\MonitoringBadgeMenuItemRenderer',
'columns' => array(
'services_critical_unhandled' => $this->translate('%d unhandled services critical')
),
'state' => 'critical',
'dataView' => 'statussummary'
),
'url' => 'monitoring/list/services?service_problem=1&service_handled=0&sort=service_severity',
'priority' => 40
));
@ -204,7 +221,7 @@ $section = $this->menuSection($this->translate('System'));
$section->add($this->translate('Monitoring Health'), array(
'url' => 'monitoring/process/info',
'priority' => 720,
'renderer' => 'Icinga\Module\Monitoring\Web\Menu\BackendAvailabilityMenuItemRenderer'
'renderer' => 'Icinga\Module\Monitoring\Web\Menu\BackendAvailabilityMenuItemRenderer'
));
/*

View File

@ -4,10 +4,10 @@
namespace Icinga\Module\Monitoring\Web\Menu;
use Icinga\Web\Menu;
use Icinga\Web\Menu\MenuItemRenderer;
use Icinga\Web\Menu\BadgeMenuItemRenderer;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
class BackendAvailabilityMenuItemRenderer extends MenuItemRenderer
class BackendAvailabilityMenuItemRenderer extends BadgeMenuItemRenderer
{
/**
* Get whether or not the monitoring backend is currently running
@ -27,47 +27,39 @@ class BackendAvailabilityMenuItemRenderer extends MenuItemRenderer
}
/**
* {@inheritdoc}
*/
public function render(Menu $menu)
{
return $this->getBadge() . $this->createLink($menu);
}
/**
* Get the problem badge HTML
* The css class of the badge
*
* @return string
*/
protected function getBadge()
public function getState()
{
if (! $this->isCurrentlyRunning()) {
return sprintf(
'<div title="%s" class="badge-container"><span class="badge badge-critical">%d</span></div>',
sprintf(
mt('monitoring', 'Monitoring backend %s is not running'), MonitoringBackend::instance()->getName()
),
1
);
}
return '';
return self::STATE_CRITICAL;
}
/**
* Get the problem data for the summary
* The amount of items to display in the badge
*
* @return array|null
* @return int
*/
public function getSummary()
public function getCount()
{
if (! $this->isCurrentlyRunning()) {
return array(
'problems' => 1,
'title' => sprintf(
mt('monitoring', 'Monitoring backend %s is not running'), MonitoringBackend::instance()->getName()
)
);
return 1;
}
return null;
return 0;
}
/**
* The tooltip title
*
* @return string
* @throws \Icinga\Exception\ConfigurationError
*/
public function getTitle()
{
return sprintf(
mt('monitoring', 'Monitoring backend %s is not running'),
MonitoringBackend::instance()->getName()
);
}
}

View File

@ -0,0 +1,183 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Monitoring\Web\Menu;
use Icinga\Authentication\Auth;
use Icinga\Data\ConfigObject;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Filterable;
use Icinga\Web\Menu;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Web\Menu\BadgeMenuItemRenderer;
/**
* Render generic dataView columns as badges in MenuItems
*
* Renders numeric data view column values into menu item badges, fully configurable
* and with a caching mechanism to prevent needless requests to the same data view
*/
class MonitoringBadgeMenuItemRenderer extends BadgeMenuItemRenderer
{
/**
* Caches the responses for all executed summaries
*
* @var array
*/
protected static $summaries = array();
/**
* Accumulates all needed columns for a view to allow fetching the needed columns in
* one single query
*
* @var array
*/
protected static $dataViews = array();
/**
* The data view displayed by this menu item
*
* @var string
*/
protected $dataView;
/**
* The columns and titles displayed in the badge
*
* @var array
*/
protected $columns;
/**
* The titles that will be used to render this menu item tooltip
*
* @var String[]
*/
protected $titles;
/**
* The class of the badge element
*
* @var string
*/
protected $state;
/**
* Create a new instance of ColumnMenuItemRenderer
*
* It is possible to configure the class of the rendered badge as option 'class', the column
* to fetch using the option 'column' and the dataView from which the columns will be
* fetched using the option 'dataView'.
*
* @param $configuration ConfigObject The configuration to use
*/
public function __construct(ConfigObject $configuration)
{
parent::__construct($configuration);
$this->columns = $configuration->get('columns');
$this->state = $configuration->get('state');
$this->dataView = $configuration->get('dataView');
// clear the outdated summary cache, since new columns are being added. Optimally all menu item are constructed
// before any rendering is going on to avoid trashing too man old requests
if (isset(self::$summaries[$this->dataView])) {
unset(self::$summaries[$this->dataView]);
}
// add the new columns to this view
if (! isset(self::$dataViews[$this->dataView])) {
self::$dataViews[$this->dataView] = array();
}
foreach ($this->columns as $column => $title) {
if (! array_search($column, self::$dataViews[$this->dataView])) {
self::$dataViews[$this->dataView][] = $column;
}
$this->titles[$column] = $title;
}
}
/**
* Apply a restriction on the given data view
*
* @param string $restriction The name of restriction
* @param Filterable $filterable The filterable to restrict
*
* @return Filterable The filterable
*/
protected static function applyRestriction($restriction, Filterable $filterable)
{
$restrictions = Filter::matchAny();
foreach (Auth::getInstance()->getRestrictions($restriction) as $filter) {
$restrictions->addFilter(Filter::fromQueryString($filter));
}
$filterable->applyFilter($restrictions);
return $filterable;
}
/**
* Fetch the response from the database or access cache
*
* @param $view
*
* @return null
* @throws \Icinga\Exception\ConfigurationError
*/
protected static function summary($view)
{
if (! isset(self::$summaries[$view])) {
$summary = MonitoringBackend::instance()->select()->from(
$view,
self::$dataViews[$view]
);
static::applyRestriction('monitoring/filter/objects', $summary);
self::$summaries[$view] = $summary->fetchRow();
}
return isset(self::$summaries[$view]) ? self::$summaries[$view] : null;
}
/**
* Defines the color of the badge
*
* @return string
*/
public function getState()
{
return $this->state;
}
/**
* The amount of items to display in the badge
*
* @return int
*/
public function getCount()
{
$sum = self::summary($this->dataView);
$count = 0;
foreach ($this->columns as $col => $title) {
if (isset($sum->$col)) {
$count += $sum->$col;
}
}
return $count;
}
/**
* The tooltip title
*
* @return string
*/
public function getTitle()
{
$titles = array();
$sum = $this->summary($this->dataView);
foreach ($this->columns as $column => $value) {
if (isset($sum->$column) && $sum->$column > 0) {
$titles[] = sprintf($this->titles[$column], $sum->$column);
}
}
return implode(', ', $titles);
}
}

View File

@ -1,109 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Monitoring\Web\Menu;
use Icinga\Authentication\Auth;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Filterable;
use Icinga\Web\Menu;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Web\Menu\MenuItemRenderer;
class MonitoringMenuItemRenderer extends MenuItemRenderer
{
protected static $summary;
protected $columns = array();
/**
* Apply a restriction on the given data view
*
* @param string $restriction The name of restriction
* @param Filterable $filterable The filterable to restrict
*
* @return Filterable The filterable
*/
protected static function applyRestriction($restriction, Filterable $filterable)
{
$restrictions = Filter::matchAny();
foreach (Auth::getInstance()->getRestrictions($restriction) as $filter) {
$restrictions->addFilter(Filter::fromQueryString($filter));
}
$filterable->applyFilter($restrictions);
return $filterable;
}
protected static function summary($column = null)
{
if (self::$summary === null) {
$summary = MonitoringBackend::instance()->select()->from(
'statussummary',
array(
'hosts_down_unhandled',
'services_critical_unhandled'
)
);
static::applyRestriction('monitoring/filter/objects', $summary);
self::$summary = $summary->fetchRow();
}
if ($column === null) {
return self::$summary;
} elseif (isset(self::$summary->$column)) {
return self::$summary->$column;
} else {
return null;
}
}
protected function getBadgeTitle()
{
$translations = array(
'hosts_down_unhandled' => mt('monitoring', '%d unhandled hosts down'),
'services_critical_unhandled' => mt('monitoring', '%d unhandled services critical')
);
$titles = array();
$sum = $this->summary();
foreach ($this->columns as $col) {
if (isset($sum->$col) && $sum->$col > 0) {
$titles[] = sprintf($translations[$col], $sum->$col);
}
}
return implode(', ', $titles);
}
protected function countItems()
{
$sum = self::summary();
$count = 0;
foreach ($this->columns as $col) {
if (isset($sum->$col)) {
$count += $sum->$col;
}
}
return $count;
}
public function render(Menu $menu)
{
return $this->getBadge() . $this->createLink($menu);
}
protected function getBadge()
{
if ($count = $this->countItems()) {
return sprintf(
'<div title="%s" class="badge-container"><span class="badge badge-critical">%s</span></div>',
$this->getBadgeTitle(),
$count
);
}
return '';
}
}

View File

@ -1,12 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Monitoring\Web\Menu;
class ProblemMenuItemRenderer extends MonitoringMenuItemRenderer
{
protected $columns = array(
'hosts_down_unhandled',
'services_critical_unhandled'
);
}

View File

@ -1,11 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Monitoring\Web\Menu;
class UnhandledHostMenuItemRenderer extends MonitoringMenuItemRenderer
{
protected $columns = array(
'hosts_down_unhandled',
);
}

View File

@ -1,11 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Monitoring\Web\Menu;
class UnhandledServiceMenuItemRenderer extends MonitoringMenuItemRenderer
{
protected $columns = array(
'services_critical_unhandled'
);
}