Merge branch 'master' into bugfix/commands-6593

This commit is contained in:
Eric Lippmann 2014-09-15 09:27:56 +02:00
commit 8bf66425d1
33 changed files with 758 additions and 520 deletions

View File

@ -26,12 +26,12 @@
%define revision 1
%define configdir %{_sysconfdir}/icingaweb
%define sharedir %{_datadir}/icingaweb
%define prefixdir %{_datadir}/icingaweb
%define logdir %{sharedir}/log
%define configdir %{_sysconfdir}/%{name}
%define sharedir %{_datadir}/%{name}
%define prefixdir %{_datadir}/%{name}
%define usermodparam -a -G
%define logdir %{_localstatedir}/log/icingaweb
%define logdir %{_localstatedir}/log/%{name}
%define docdir %{sharedir}/log
%if "%{_vendor}" == "suse"
%define phpname php5
@ -172,25 +172,26 @@ install -D -m0644 packages/rpm/etc/httpd/conf.d/icingaweb.conf %{buildroot}/%{ap
# install public, library, modules
%{__mkdir} -p %{buildroot}/%{sharedir}
%{__mkdir} -p %{buildroot}/%{logdir}
%{__mkdir} -p %{buildroot}/%{_sysconfdir}/icingaweb
%{__mkdir} -p %{buildroot}/%{docdir}
%{__mkdir} -p %{buildroot}/%{_sysconfdir}/%{name}
%{__mkdir} -p %{buildroot}/%{_sysconfdir}/dashboard
%{__mkdir} -p %{buildroot}/%{_sysconfdir}/icingaweb/modules
%{__mkdir} -p %{buildroot}/%{_sysconfdir}/icingaweb/modules/monitoring
%{__mkdir} -p %{buildroot}/%{_sysconfdir}/icingaweb/enabledModules
%{__mkdir} -p %{buildroot}/%{_sysconfdir}/%{name}/modules
%{__mkdir} -p %{buildroot}/%{_sysconfdir}/%{name}/modules/monitoring
%{__mkdir} -p %{buildroot}/%{_sysconfdir}/%{name}/enabledModules
%{__cp} -r application library modules public %{buildroot}/%{sharedir}/
%{__cp} -r application doc library modules public %{buildroot}/%{sharedir}/
## config
# authentication is db only
install -D -m0644 packages/rpm/etc/icingaweb/authentication.ini %{buildroot}/%{_sysconfdir}/icingaweb/authentication.ini
install -D -m0644 packages/rpm/etc/%{name}/authentication.ini %{buildroot}/%{_sysconfdir}/%{name}/authentication.ini
# custom resource paths
install -D -m0644 packages/rpm/etc/icingaweb/resources.ini %{buildroot}/%{_sysconfdir}/icingaweb/resources.ini
install -D -m0644 packages/rpm/etc/%{name}/resources.ini %{buildroot}/%{_sysconfdir}/%{name}/resources.ini
# monitoring module (icinga2)
install -D -m0644 packages/rpm/etc/icingaweb/modules/monitoring/backends.ini %{buildroot}/%{_sysconfdir}/icingaweb/modules/monitoring/backends.ini
install -D -m0644 packages/rpm/etc/icingaweb/modules/monitoring/instances.ini %{buildroot}/%{_sysconfdir}/icingaweb/modules/monitoring/instances.ini
install -D -m0644 packages/rpm/etc/%{name}/modules/monitoring/backends.ini %{buildroot}/%{_sysconfdir}/%{name}/modules/monitoring/backends.ini
install -D -m0644 packages/rpm/etc/%{name}/modules/monitoring/instances.ini %{buildroot}/%{_sysconfdir}/%{name}/modules/monitoring/instances.ini
# enable the monitoring module by default
ln -s %{sharedir}/modules/monitoring %{buildroot}/%{_sysconfdir}/icingaweb/enabledModules/monitoring
ln -s %{sharedir}/modules/monitoring %{buildroot}/%{_sysconfdir}/%{name}/enabledModules/monitoring
## config
# install icingacli
@ -228,6 +229,8 @@ fi
%config(noreplace) %attr(-,%{apacheuser},%{apachegroup}) %{configdir}
# logs
%attr(2775,%{apacheuser},%{apachegroup}) %dir %{logdir}
# shipped docs
%attr(755,%{apacheuser},%{apachegroup}) %{sharedir}/doc
%files -n php-Icinga
%attr(755,%{apacheuser},%{apachegroup}) %{sharedir}/application

View File

@ -174,18 +174,6 @@ class Hook
}
}
/**
* Register your hook
*
* Alias for Hook::registerClass()
*
* @see Hook::registerClass()
*/
public static function register($name, $key, $class)
{
self::registerClass($name, $key, $class);
}
/**
* Register a class
*
@ -194,45 +182,12 @@ class Hook
* @param string $class Your class name, must inherit one of the
* classes in the Icinga/Web/Hook folder
*/
public static function registerClass($name, $key, $class)
public static function register($name, $key, $class)
{
if (!class_exists($class)) {
throw new ProgrammingError(
'"%s" is not an existing class',
$class
);
}
if (!isset(self::$hooks[$name])) {
self::$hooks[$name] = array();
}
self::$hooks[$name][$key] = $class;
}
/**
* Register an object
*
* @param string $name One of the predefined hook names
* @param string $key The identifier of a specific subtype
* @param object $object The instantiated hook to register
*
* @throws ProgrammingError
*/
public static function registerObject($name, $key, $object)
{
if (!is_object($object)) {
throw new ProgrammingError(
'"%s" is not an instantiated class',
$object
);
}
if (!isset(self::$instances[$name])) {
self::$instances[$name] = array();
}
self::$instances[$name][$key] = $object;
self::registerClass($name, $key, get_class($object));
}
}

View File

@ -18,10 +18,15 @@ class JavaScript
'js/icinga/ui.js',
'js/icinga/timer.js',
'js/icinga/loader.js',
'js/icinga/eventlistener.js',
'js/icinga/events.js',
'js/icinga/history.js',
'js/icinga/module.js',
'js/icinga/timezone.js',
'js/icinga/behavior/tooltip.js',
'js/icinga/behavior/sparkline.js',
'js/icinga/behavior/tristate.js',
'js/icinga/behavior/navigation.js'
);
protected static $vendorFiles = array(

View File

@ -4,7 +4,9 @@
namespace Icinga\Web;
use Exception;
use RecursiveIteratorIterator;
use Icinga\Logger\Logger;
/**
* A renderer to draw a menu with its sub-menus using an unordered html list
@ -106,7 +108,11 @@ class MenuRenderer extends RecursiveIteratorIterator
public function renderChild(Menu $child)
{
if ($child->getRenderer() !== null && $this->useCustomRenderer) {
try {
return $child->getRenderer()->render($child);
} catch (Exception $e) {
Logger::error('Could not invoke custom renderer. Exception: '. $e->getMessage());
}
}
return sprintf(
'<a href="%s">%s%s</a>',

View File

@ -17,7 +17,7 @@ class Response extends Zend_Controller_Response_Http
$url->getParams()->setSeparator('&');
if (Icinga::app()->getFrontController()->getRequest()->isXmlHttpRequest()) {
$this->setHeader('X-Icinga-Redirect', rawurlencode($url));
$this->setHeader('X-Icinga-Redirect', rawurlencode($url->getAbsoluteUrl()));
} else {
$this->setRedirect($url->getAbsoluteUrl());
}

View File

@ -90,8 +90,7 @@ $this->addHelperFunction('propertiesToString', function ($properties) use ($view
return ' ' . implode(' ', $attributes);
});
$this->addHelperFunction('attributeToString', function ($key, $value)
{
$this->addHelperFunction('attributeToString', function ($key, $value) use ($view) {
// TODO: Doublecheck this!
if (! preg_match('~^[a-zA-Z0-9-]+$~', $key)) {
throw new ProgrammingError(
@ -103,7 +102,7 @@ $this->addHelperFunction('attributeToString', function ($key, $value)
return sprintf(
'%s="%s"',
$key,
$value
$view->escape($value)
);
});

View File

@ -15,11 +15,15 @@ use DateInterval;
*/
class HistoryColorGrid extends AbstractWidget {
const ORIENTATION_VERTICAL = 'vertical';
const CAL_GROW_INTO_PAST = 'past';
const CAL_GROW_INTO_PRESENT = 'present';
const ORIENTATION_VERTICAL = 'vertical';
const ORIENTATION_HORIZONTAL = 'horizontal';
public $weekFlow = self::CAL_GROW_INTO_PAST;
public $orientation = self::ORIENTATION_VERTICAL;
public $weekStartMonday = true;
private $maxValue = 1;
@ -158,7 +162,7 @@ class HistoryColorGrid extends AbstractWidget {
$html = '<table class="historycolorgrid">';
$html .= '<tr>';
for ($i = 0; $i < 7; $i++) {
$html .= '<th>' . $this->weekdayName($i) . "</th>";
$html .= '<th>' . $this->weekdayName($this->weekStartMonday ? $i + 1 : $i) . "</th>";
}
$html .= '</tr>';
$old = -1;
@ -192,7 +196,9 @@ class HistoryColorGrid extends AbstractWidget {
*/
private function renderWeekdayHorizontal($weekday, &$weeks)
{
$html = '<tr><td class="weekday">' . $this->weekdayName($weekday) . '</td>';
$html = '<tr><td class="weekday">'
. $this->weekdayName($this->weekStartMonday ? $weekday + 1 : $weekday)
. '</td>';
foreach ($weeks as $week) {
if (array_key_exists($weekday, $week)) {
$html .= '<td>' . $this->renderDay($week[$weekday]) . '</td>';
@ -219,6 +225,10 @@ class HistoryColorGrid extends AbstractWidget {
$month = intval(date('n', $start));
$day = intval(date('j', $start));
$weekday = intval(date('w', $start));
if ($this->weekStartMonday) {
// 0 => monday, 6 => sunday
$weekday = $weekday === 0 ? 6 : $weekday - 1;
}
$date = $this->toDateStr($day, $month, $year);
$weeks[0][$weekday] = $date;
@ -229,9 +239,12 @@ class HistoryColorGrid extends AbstractWidget {
if ($weekday > 6) {
$weekday = 0;
$weeks[] = array();
$week++;
// PRESENT => The last day of week determines the month
if ($this->weekFlow === self::CAL_GROW_INTO_PRESENT) {
$months[$week] = $month;
}
$week++;
}
if ($day > cal_days_in_month(CAL_GREGORIAN, $month, $year)) {
$month++;
if ($month > 12) {
@ -240,10 +253,22 @@ class HistoryColorGrid extends AbstractWidget {
}
$day = 1;
}
if ($weekday === 0) {
// PAST => The first day of each week determines the month
if ($this->weekFlow === self::CAL_GROW_INTO_PAST) {
$months[$week] = $month;
}
}
$date = $this->toDateStr($day, $month, $year);
$weeks[$week][$weekday] = $date;
};
$months[$week] = $month;
if ($this->weekFlow == self::CAL_GROW_INTO_PAST) {
return array(
'weeks' => array_reverse($weeks),
'months' => array_reverse($months)
);
}
return array(
'weeks' => $weeks,
'months' => $months

View File

@ -6,7 +6,7 @@ $currentUrl = Url::fromRequest()->without('limit')->getRelativeUrl();
?>
<h3 class="tinystatesummary" <?= $this->compact ? ' data-base-target="col1"' : '' ?>>
<?= $this->qlink(sprintf($this->translate('%s service configured:'), $this->stats->services_total), $selfUrl) ?>
<?= $this->qlink(sprintf($this->translate('%s configured services:'), $this->stats->services_total), $selfUrl) ?>
<?php if ($this->stats->services_ok > 0): ?>
<span class="state ok<?= $currentUrl === $selfUrl->with('service_state', 0)->getRelativeUrl() ? ' active' : '' ?>"><?= $this->qlink(
$this->stats->services_ok,

View File

@ -38,7 +38,10 @@ class StatehistoryQuery extends IdoQuery
{
if ($col === 'UNIX_TIMESTAMP(sh.state_time)') {
return 'sh.state_time ' . $sign . ' ' . $this->timestampForSql($this->valueToTimestamp($expression));
} elseif ($col === $this->columnMap['statehistory']['type']) {
} elseif ($col === $this->columnMap['statehistory']['type']
&& is_array($expression) === false
&& array_key_exists($expression, $this->types) === true
) {
return 'sh.state_type ' . $sign . ' ' . $this->types[$expression];
} else {
return parent::whereToSql($col, $sign, $expression);
@ -58,4 +61,3 @@ class StatehistoryQuery extends IdoQuery
$this->joinedVirtualTables = array('statehistory' => true);
}
}

View File

@ -0,0 +1,165 @@
# Introduction
Icinga Web 2 provides localization out of the box - for the core application and the modules, that means
that you can with a lightness use existent localizations, update or even create you own localizations.
The chapters [Translation for Developers](Translation for Developers),
[Translation for Translators](Translation for Translators) and [Testing Translations](Testing Translations) will
introduce and explain you, how to take part on localizing Icinga Web 2 for different languages and how to use the
`translation module` to make your life much easier.
# Translation for Developers
To make use of the built-in translations in your applications code or views, you should use the method
`$this->translate('String to be translated')`, let's have a look at an example:
```php
<?php
class ExampleController extends Controller
{
public function indexAction()
{
$this->view->title = $this->translate('Hello World');
}
}
```
So if there a translation available for the `Hello World` string you will get an translated output, depends on the
language which is setted in your configuration as the default language, if it is `de_DE` the output would be
`Hallo Welt`.
The same works also for views:
```
<h1><?= $this->title ?></h1>
<p>
<?= $this->translate('Hello World') ?>
<?= $this->translate('String to be translated') ?>
</p>
```
If you need to provide placeholders in your messages, you should wrap the `$this->translate()` with `sprintf()` for e.g.
sprintf($this->translate('Hello User: (%s)'), $user->getName())
# Translation for Translators
Icinga Web 2 internally uses the UNIX standard gettext tool to perform internationalization, this means translation
files in the .po file format are supplied for text strings used in the code.
There are a lot of tools and techniques to work with .po localization files, you can choose what ever you prefer. We
won't let you alone on your first steps and therefore we'll introduce you a nice tool, called Poedit.
### Poedit
First of all, you have to download and install Poedit from http://poedit.net, when you are done, you have to do some
configuration under the Preferences.
#### Configuration
__Personalize__: Please provide your Name and E-Mail under Identity.
![Personalize](/img/translation/doc/poedit_001.png)
__Editor__: Under the Behavior the Automatically compile .mo files on save, should be disabled.
![Editor](/img/translation/doc/poedit_002.png)
__Translations Memory__: Under the Database please add your languages, for which are you writing translations.
![Translations Memory](/img/translation/doc/poedit_003.png)
When you are done, just save your new settings.
#### Editing .po files
To work with Icinga Web 2 .po files, you can open for e.g. the german icinga.po file which is located under
`application/locale/de_DE/LC_MESSAGES/icinga.po`, as shown below, you will get then a full list of all available
translation strings for the core application. Each module names it's translation files `%module_name%.po`. For a
module called __yourmodule__ the .po translation file will be named `yourmodule.po`.
![Full list of strings](/img/translation/doc/poedit_004.png)
Now you can make changes and when there is no translation available, Poedit would mark it with a blue color, as shown
below.
![Untranslated strings](/img/translation/doc/poedit_005.png)
And when you want to test your changes, please read more about under the chapter
[Testing Translations](Testing Translations).
# Testing Translations
If you want to try out your translation changes in Icinga Web 2, you can make use of the the CLI translations commands.
** NOTE: Please make sure that the gettext package is installed **
To get an easier development with translations, you can activate the `translation module` which provides CLI commands,
after that you would be able to refresh and compile your .po files.
** NOTE: the ll_CC stands for ll=language and CC=country code for e.g de_DE, fr_FR, ru_RU, it_IT etc. **
## Application
To refresh the __icinga.po__:
icingacli translation refresh icinga ll_CC
And to compile it:
icingacli translation compile icinga ll_CC
** NOTE: After a compile you need to restart the web server to get new translations available in your application. **
## Modules
Let's assume, we want to provide german translations for our just new created module __yourmodule__.
If we haven't yet any translations strings in our .po file or even the .po file, we can use the CLI command, to do the
job for us:
icingacli translation refresh module development ll_CC
This will go through all .php and .phtml files inside the module and a look after `$this->translate()` if there is
something to translate - if there is something and is not available in the __yourmodule.po__ it will updates this file
for us with new
strings.
Now you can open the __yourmodule.po__ and you will see something similar:
# Icinga Web 2 - Head for multiple monitoring backends.
# Copyright (C) 2014 Icinga Development Team
# This file is distributed under the same license as Development Module.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: Development Module (0.0.1)\n"
"Report-Msgid-Bugs-To: dev@icinga.org\n"
"POT-Creation-Date: 2014-09-09 10:12+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: ll_CC\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: /modules/yourmodule/configuration.php:6
msgid "yourmodule"
msgstr ""
Great, now you can adjust the file and provide the german `msgstr` for `yourmodule`.
#: /modules/yourmodule/configuration.php:6
msgid "Dummy"
msgstr "Attrappe"
The last step is to compile the __yourmodule.po__ to the __yourmodule.mo__:
icingacli translation compile module development ll_CC
At this moment, everywhere in the module where the `Dummy` should be translated, it would returns the translated
string `Attrappe`.

View File

@ -294,11 +294,12 @@ class GettextTranslationHelper
$headerInfo['translator_name'] = $translatorInfo[1];
$headerInfo['translator_mail'] = $translatorInfo[2];
}
$languageInfo = array();
if (preg_match('@Language-Team: (.+) <(.+)>@', $content, $languageInfo)) {
$headerInfo['language_team_name'] = $languageInfo[1];
$headerInfo['language_team_url'] = $languageInfo[2];
$languageTeamInfo = array();
if (preg_match('@Language-Team: (.+) <(.+)>@', $content, $languageTeamInfo)) {
$headerInfo['language_team_name'] = $languageTeamInfo[1];
$headerInfo['language_team_url'] = $languageTeamInfo[2];
}
$languageInfo = array();
if (preg_match('@Language: ([a-z]{2}_[A-Z]{2})@', $content, $languageInfo)) {
$headerInfo['language'] = $languageInfo[1];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -42,8 +42,8 @@ Decide whether to use MySQL or PostgreSQL.
FLUSH PRIVILEGES;
quit
mysql -u root -p icingaweb < /usr/share/doc/icingaweb2-*/schema/accounts.mysql.sql
mysql -u root -p icingaweb < /usr/share/doc/icingaweb2-*/schema/preferences.mysql.sql
mysql -u root -p icingaweb < /usr/share/doc/icingaweb2*/schema/accounts.mysql.sql
mysql -u root -p icingaweb < /usr/share/doc/icingaweb2*/schema/preferences.mysql.sql
### PostgreSQL
@ -62,8 +62,8 @@ in `/var/lib/pgsql/data/pg_hba.conf` and restart the PostgreSQL server.
Now install the `icingaweb` schema
bash$ psql -U icingaweb -a -f /usr/share/doc/icingaweb2-*/schema/accounts.pgsql.sql
bash$ psql -U icingaweb -a -f /usr/share/doc/icingaweb2-*/schema/preferences.pgsql.sql
bash$ psql -U icingaweb -a -f /usr/share/doc/icingaweb2*/schema/accounts.pgsql.sql
bash$ psql -U icingaweb -a -f /usr/share/doc/icingaweb2*/schema/preferences.pgsql.sql
## Configuration
@ -74,16 +74,16 @@ The monitoring module is enabled by default.
### Backend configuration
`/etc/icingaweb/resources.ini` contains the database backend information.
`/etc/icingaweb2/resources.ini` contains the database backend information.
By default the Icinga 2 DB IDO is used by the monitoring module in
`/etc/icingaweb/modules/monitoring/backends.ini`
`/etc/icingaweb2/modules/monitoring/backends.ini`
The external command pipe is required for sending commands
and configured for Icinga 2 in
`/etc/icingaweb/modules/monitoring/instances.ini`
`/etc/icingaweb2/modules/monitoring/instances.ini`
### Authentication configuration
The `/etc/icingaweb/authentication.ini` file uses the internal database as
The `/etc/icingaweb2/authentication.ini` file uses the internal database as
default. This requires the database being installed properly before
allowing users to login via web console.

View File

@ -1,6 +1,6 @@
Alias /icingaweb "/usr/share/icingaweb/public"
Alias /icingaweb "/usr/share/icingaweb2/public"
<Directory "/usr/share/icingaweb/public">
<Directory "/usr/share/icingaweb2/public">
Options SymLinksIfOwnerMatch
AllowOverride None
@ -17,7 +17,7 @@ Alias /icingaweb "/usr/share/icingaweb/public"
Allow from all
</IfModule>
SetEnv ICINGAWEB_CONFIGDIR /etc/icingaweb
SetEnv ICINGAWEB_CONFIGDIR /etc/icingaweb2
EnableSendfile Off

View File

@ -22,7 +22,7 @@ socket = /var/run/icinga2/cmd/livestatus
[logfile]
type = file
filename = "/var/log/icingaweb/icingaweb.log"
filename = "/var/log/icingaweb2/icingaweb2.log"
fields = "/^(?<datetime>[0-9]{4}(-[0-9]{2}){2}T[0-9]{2}(:[0-9]{2}){2}(\\+[0-9]{2}:[0-9]{2})?) - (?<loglevel>[A-Za-z]+) - (?<message>.*)$/"
; format: PCRE
;

View File

@ -2,5 +2,5 @@
<?php
use Icinga\Application\Cli;
require_once '/usr/share/icingaweb/library/Icinga/Application/Cli.php';
require_once '/usr/share/icingaweb2/library/Icinga/Application/Cli.php';
Cli::start()->dispatch();

View File

@ -60,6 +60,11 @@
*/
this.utils = null;
/**
* Additional site behavior
*/
this.behaviors = {};
/**
* Loaded modules
*/
@ -90,6 +95,10 @@
this.loader = new Icinga.Loader(this);
this.events = new Icinga.Events(this);
this.history = new Icinga.History(this);
var self = this;
$.each(Icinga.Behaviors, function(name, Behavior) {
self.behaviors[name.toLowerCase()] = new Behavior(self);
});
this.timezone.initialize();
this.timer.initialize();
@ -97,6 +106,7 @@
this.history.initialize();
this.ui.initialize();
this.loader.initialize();
this.logger.info('Icinga is ready, running on jQuery ', $().jquery);
this.initialized = true;
},

View File

@ -0,0 +1,175 @@
// {{{ICINGA_LICENSE_HEADER}}}
// {{{ICINGA_LICENSE_HEADER}}}
(function(Icinga, $) {
"use strict";
var activeMenuId;
Icinga.Behaviors = Icinga.Behaviors || {};
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('mouseenter', 'li.dropdown', this.dropdownHover, this);
this.on('mouseleave', 'li.dropdown', this.dropdownLeave, this);
this.on('mouseenter', '#menu > ul > li', this.menuTitleHovered, this);
this.on('mouseleave', '#sidebar', this.leaveSidebar, this);
this.on('rendered', this.onRendered);
};
Navigation.prototype = new Icinga.EventListener();
Navigation.prototype.onRendered = function(evt) {
// get original source element of the rendered-event
var el = evt.target;
// restore old menu state
if (activeMenuId) {
$('[role="navigation"] li.active', el).removeClass('active');
var $selectedMenu = $('#' + activeMenuId).addClass('active');
var $outerMenu = $selectedMenu.parent().closest('li');
if ($outerMenu.size()) {
$outerMenu.addClass('active');
}
} else {
// store menu state
var $menus = $('[role="navigation"] li.active', el);
if ($menus.size()) {
activeMenuId = $menus[0].id;
}
}
};
Navigation.prototype.linkClicked = function(event) {
var $a = $(this);
var href = $a.attr('href');
var $li;
var icinga = event.data.self.icinga;
if (href.match(/#/)) {
// ...it may be a menu section without a dedicated link.
// Switch the active menu item:
$li = $a.closest('li');
$('#menu .active').removeClass('active');
$li.addClass('active');
activeMenuId = $($li).attr('id');
if ($li.hasClass('hover')) {
$li.removeClass('hover');
}
if (href === '#') {
// Allow to access dropdown menu by keyboard
if ($a.hasClass('dropdown-toggle')) {
$a.closest('li').toggleClass('hover');
}
return;
}
} else {
activeMenuId = $(event.target).closest('li').attr('id');
}
// update target url of the menu container to the clicked link
var $menu = $('#menu');
var menuDataUrl = icinga.utils.parseUrl($menu.data('icinga-url'));
menuDataUrl = icinga.utils.addUrlParams(menuDataUrl.path, { url: href });
$menu.data('icinga-url', menuDataUrl);
};
/**
* Change the active menu element
*
* @param $el {jQuery} A selector pointing to the active element
*/
Navigation.prototype.setActive = function($el) {
$el.closest('li').addClass('active');
$el.parents('li').addClass('active');
activeMenuId = $el.closest('li').attr('id');
};
Navigation.prototype.resetActive = function() {
$('#menu .active').removeClass('active');
activeMenuId = null;
};
Navigation.prototype.menuTitleHovered = function(event) {
var $li = $(this),
delay = 800,
self = event.data.self;
if ($li.hasClass('active')) {
$li.siblings().removeClass('hover');
return;
}
if ($li.children('ul').children('li').length === 0) {
return;
}
if ($('#menu').scrollTop() > 0) {
return;
}
if ($('#layout').hasClass('hoveredmenu')) {
delay = 0;
}
setTimeout(function () {
if (! $li.is('li:hover')) {
return;
}
if ($li.hasClass('active')) {
return;
}
$li.siblings().each(function () {
var $sibling = $(this);
if ($sibling.is('li:hover')) {
return;
}
if ($sibling.hasClass('hover')) {
$sibling.removeClass('hover');
}
});
self.hoverElement($li);
}, delay);
};
Navigation.prototype.leaveSidebar = function (event) {
var $sidebar = $(this),
$li = $sidebar.find('li.hover'),
self = event.data.self;
if (! $li.length) {
$('#layout').removeClass('hoveredmenu');
return;
}
setTimeout(function () {
if ($li.is('li:hover') || $sidebar.is('sidebar:hover') ) {
return;
}
$li.removeClass('hover');
$('#layout').removeClass('hoveredmenu');
}, 500);
};
Navigation.prototype.hoverElement = function ($li) {
$('#layout').addClass('hoveredmenu');
$li.addClass('hover');
};
Navigation.prototype.dropdownHover = function () {
$(this).addClass('hover');
};
Navigation.prototype.dropdownLeave = function (event) {
var $li = $(this),
self = event.data.self;
setTimeout(function () {
// TODO: make this behave well together with keyboard navigation
if (! $li.is('li:hover') /*&& ! $li.find('a:focus')*/) {
$li.removeClass('hover');
}
}, 300);
};
Icinga.Behaviors.Navigation = Navigation;
}) (Icinga, jQuery);

View File

@ -0,0 +1,54 @@
// {{{ICINGA_LICENSE_HEADER}}}
// {{{ICINGA_LICENSE_HEADER}}}
(function(Icinga, $) {
"use strict";
Icinga.Behaviors = Icinga.Behaviors || {};
var Sparkline = function (icinga) {
Icinga.EventListener.call(this, icinga);
this.on('rendered', this.onRendered, this);
};
Sparkline.prototype = new Icinga.EventListener();
Sparkline.prototype.onRendered = function(evt) {
var el = evt.target;
$('span.sparkline', el).each(function(i, element) {
// read custom options
var $spark = $(element);
var labels = $spark.attr('labels').split('|');
var formatted = $spark.attr('formatted').split('|');
var tooltipChartTitle = $spark.attr('sparkTooltipChartTitle') || '';
var format = $spark.attr('tooltipformat');
var hideEmpty = $spark.attr('hideEmptyLabel') === 'true';
$spark.sparkline(
'html',
{
enableTagOptions: true,
tooltipFormatter: function (sparkline, options, fields) {
var out = format;
if (hideEmpty && fields.offset === 3) {
return '';
}
var replace = {
title: tooltipChartTitle,
label: labels[fields.offset] ? labels[fields.offset] : fields.offset,
formatted: formatted[fields.offset] ? formatted[fields.offset] : '',
value: fields.value,
percent: Math.round(fields.percent * 100) / 100
};
$.each(replace, function(key, value) {
out = out.replace('{{' + key + '}}', value);
});
return out;
}
});
});
};
Icinga.Behaviors.Sparkline = Sparkline;
}) (Icinga, jQuery);

View File

@ -0,0 +1,63 @@
// {{{ICINGA_LICENSE_HEADER}}}
// {{{ICINGA_LICENSE_HEADER}}}
(function(Icinga, $) {
"use strict";
Icinga.Behaviors = Icinga.Behaviors || {};
var Tooltip = function (icinga) {
Icinga.EventListener.call(this, icinga);
this.mouseX = 0;
this.mouseY = 0;
this.on('mousemove', this.onMousemove, this);
this.on('rendered', this.onRendered, this);
};
Tooltip.prototype = new Icinga.EventListener();
Tooltip.prototype.onMousemove = function(event) {
event.data.self.mouseX = event.pageX;
event.data.self.mouseY = event.pageY;
};
Tooltip.prototype.onRendered = function(evt) {
var self = evt.data.self, icinga = evt.data.icinga, el = evt.target;
$('[title]', el).each(function () {
var $el = $(this);
$el.attr('title', $el.data('title-rich') || $el.attr('title'));
});
$('svg rect.chart-data[title]', el).tipsy({ gravity: 'se', html: true });
$('.historycolorgrid a[title]', el).tipsy({ gravity: 's', offset: 2 });
$('img.icon[title]', el).tipsy({ gravity: $.fn.tipsy.autoNS, offset: 2 });
$('[title]', el).tipsy({ gravity: $.fn.tipsy.autoNS, delayIn: 500 });
// migrate or remove all orphaned tooltips
$('.tipsy').each(function () {
var arrow = $('.tipsy-arrow', this)[0];
if (!icinga.utils.elementsOverlap(arrow, $('#main')[0])) {
$(this).remove();
return;
}
if (!icinga.utils.elementsOverlap(arrow, el)) {
return;
}
var title = $(this).find('.tipsy-inner').html();
var atMouse = document.elementFromPoint(self.mouseX, self.mouseY);
var nearestTip = $(atMouse).closest('[original-title="' + title + '"]')[0];
if (nearestTip) {
var tipsy = $.data(nearestTip, 'tipsy');
tipsy.$tip = $(this);
$.data(this, 'tipsy-pointee', nearestTip);
} else {
// doesn't match delete
$(this).remove();
}
});
};
// Export
Icinga.Behaviors.Tooltip = Tooltip;
}) (Icinga, jQuery);

View File

@ -0,0 +1,51 @@
// {{{ICINGA_LICENSE_HEADER}}}
// {{{ICINGA_LICENSE_HEADER}}}
(function(Icinga, $) {
"use strict";
Icinga.Behaviors = Icinga.Behaviors || {};
var Tristate = function (icinga) {
Icinga.EventListener.call(this, icinga);
this.on('click', 'div.tristate .tristate-dummy', this.clickTriState, this);
};
Tristate.prototype = new Icinga.EventListener();
Tristate.prototype.clickTriState = function (event) {
var self = event.data.self;
var $tristate = $(this);
var triState = parseInt($tristate.data('icinga-tristate'), 10);
// load current values
var old = $tristate.data('icinga-old').toString();
var value = $tristate.parent().find('input:radio:checked').first().prop('checked', false).val();
// calculate the new value
if (triState) {
// 1 => 0
// 0 => unchanged
// unchanged => 1
value = value === '1' ? '0' : (value === '0' ? 'unchanged' : '1');
} else {
// 1 => 0
// 0 => 1
value = value === '1' ? '0' : '1';
}
// update form value
$tristate.parent().find('input:radio[value="' + value + '"]').prop('checked', true);
// update dummy
if (value !== old) {
$tristate.parent().find('b.tristate-changed').css('visibility', 'visible');
} else {
$tristate.parent().find('b.tristate-changed').css('visibility', 'hidden');
}
self.icinga.ui.setTriState(value.toString(), $tristate);
};
Icinga.Behaviors.Tristate = Tristate;
}) (Icinga, jQuery);

View File

@ -0,0 +1,74 @@
// {{{ICINGA_LICENSE_HEADER}}}
// {{{ICINGA_LICENSE_HEADER}}}
/**
* EventListener contains event handlers and can bind / and unbind them from
* event emitting objects
*/
(function(Icinga, $) {
"use strict";
var EventListener = function (icinga) {
this.icinga = icinga;
this.handlers = [];
};
/**
* Add an handler to this EventLister
*
* @param evt {String} The name of the triggering event
* @param cond {String} The filter condition
* @param fn {Function} The event handler to execute
* @param scope {Object} The optional 'this' of the called function
*/
EventListener.prototype.on = function(evt, cond, fn, scope) {
if (typeof cond === 'function') {
scope = fn;
fn = cond;
cond = 'body';
}
this.icinga.logger.debug('on: ' + evt + '(' + cond + ')');
this.handlers.push({ evt: evt, cond: cond, fn: fn, scope: scope });
};
/**
* Bind all listeners to the given event emitter
*
* All event handlers will be executed when the associated event is
* triggered on the given Emitter.
*
* @param emitter {String} An event emitter that supports the function
* 'on' to register listeners
*/
EventListener.prototype.bind = function (emitter) {
var self = this;
$.each(this.handlers, function(i, handler) {
self.icinga.logger.debug('bind: ' + handler.evt + '(' + handler.cond + ')');
emitter.on(
handler.evt, handler.cond,
{
self: handler.scope || emitter,
icinga: self.icinga
}, handler.fn
);
});
};
/**
* Unbind all listeners from the given event emitter
*
* @param emitter {String} An event emitter that supports the function
* 'off' to un-register listeners.
*/
EventListener.prototype.unbind = function (emitter) {
var self = this;
$.each(this.handlers, function(i, handler) {
self.icinga.logger.debug('unbind: ' + handler.evt + '(' + handler.cond + ')');
emitter.off(handler.evt, handler.cond, handler.fn);
});
};
Icinga.EventListener = EventListener;
}) (Icinga, jQuery);

View File

@ -10,10 +10,6 @@
'use strict';
var activeMenuId;
var mouseX, mouseY;
Icinga.Events = function (icinga) {
this.icinga = icinga;
@ -33,14 +29,17 @@
*/
initialize: function () {
this.applyGlobalDefaults();
this.applyHandlers($('#layout'));
this.icinga.ui.prepareContainers();
$('#layout').trigger('rendered');
//$('.container').trigger('rendered');
$('.container').each(function(idx, el) {
icinga.ui.initializeControls($(el));
});
},
// TODO: What's this?
applyHandlers: function (el) {
var icinga = this.icinga;
applyHandlers: function (evt) {
var el = $(evt.target), self = evt.data.self;
var icinga = self.icinga;
$('.dashboard > div', el).each(function(idx, el) {
var url = $(el).data('icingaUrl');
@ -78,93 +77,10 @@
$('input.autofocus', el).focus();
// replace all sparklines
$('span.sparkline', el).each(function(i, element) {
// read custom options
var $spark = $(element);
var labels = $spark.attr('labels').split('|');
var formatted = $spark.attr('formatted').split('|');
var tooltipChartTitle = $spark.attr('sparkTooltipChartTitle') || '';
var format = $spark.attr('tooltipformat');
var hideEmpty = $spark.attr('hideEmptyLabel') === 'true';
$spark.sparkline(
'html',
{
enableTagOptions: true,
tooltipFormatter: function (sparkline, options, fields) {
var out = format;
if (hideEmpty && fields.offset === 3) {
return '';
}
var replace = {
title: tooltipChartTitle,
label: labels[fields.offset] ? labels[fields.offset] : fields.offset,
formatted: formatted[fields.offset] ? formatted[fields.offset] : '',
value: fields.value,
percent: Math.round(fields.percent * 100) / 100
};
$.each(replace, function(key, value) {
out = out.replace('{{' + key + '}}', value);
});
return out;
}
});
});
var searchField = $('#menu input.search', el);
// Remember initial search field value if any
if (searchField.length && searchField.val().length) {
this.searchValue = searchField.val();
}
$('[title]').each(function () {
var $el = $(this);
$el.attr('title', $el.data('title-rich') || $el.attr('title'));
});
$('svg rect.chart-data[title]', el).tipsy({ gravity: 'se', html: true });
$('.historycolorgrid a[title]', el).tipsy({ gravity: 's', offset: 2 });
$('img.icon[title]', el).tipsy({ gravity: $.fn.tipsy.autoNS, offset: 2 });
$('[title]', el).tipsy({ gravity: $.fn.tipsy.autoNS, delayIn: 500 });
// migrate or remove all orphaned tooltips
$('.tipsy').each(function () {
var arrow = $('.tipsy-arrow', this)[0];
if (!icinga.utils.elementsOverlap(arrow, $('#main')[0])) {
$(this).remove();
return;
}
if (!icinga.utils.elementsOverlap(arrow, el)) {
return;
}
var title = $(this).find('.tipsy-inner').html();
var atMouse = document.elementFromPoint(mouseX, mouseY);
var nearestTip = $(atMouse)
.closest('[original-title="' + title + '"]')[0];
if (nearestTip) {
var tipsy = $.data(nearestTip, 'tipsy');
tipsy.$tip = $(this);
$.data(this, 'tipsy-pointee', nearestTip);
} else {
// doesn't match delete
$(this).remove();
}
});
// restore menu state
if (activeMenuId) {
$('li.active', el).removeClass('active');
var $selectedMenu = $('#' + activeMenuId, el);
var $outerMenu = $selectedMenu.parent().closest('li');
if ($outerMenu.size()) {
$selectedMenu = $outerMenu;
}
$selectedMenu.addClass('active');
} else {
// store menu state
var $menus = $('[role="navigation"] li.active', el);
if ($menus.size()) {
activeMenuId = $menus[0].id;
}
self.searchValue = searchField.val();
}
},
@ -172,6 +88,13 @@
* Global default event handlers
*/
applyGlobalDefaults: function () {
$.each(self.icinga.behaviors, function (name, behavior) {
behavior.bind($(document));
});
// Apply element-specific behavior whenever the layout is rendered
$(document).on('rendered', { self: this }, this.applyHandlers);
// We catch resize events
$(window).on('resize', { self: this.icinga.ui }, this.icinga.ui.onWindowResize);
@ -203,103 +126,12 @@
$(document).on('keyup', '#menu input.search', {self: this}, this.autoSubmitSearch);
$(document).on('mouseenter', '.historycolorgrid td', this.historycolorgridHover);
$(document).on('mouseleave', '.historycolorgrid td', this.historycolorgidUnhover);
$(document).on('mouseenter', 'li.dropdown', this.dropdownHover);
$(document).on('mouseleave', 'li.dropdown', {self: this}, this.dropdownLeave);
$(document).on('mouseenter', '#menu > ul > li', { self: this }, this.menuTitleHovered);
$(document).on('mouseleave', '#sidebar', { self: this }, this.leaveSidebar);
$(document).on('click', '.tree .handle', { self: this }, this.treeNodeToggle);
// Toggle all triStateButtons
$(document).on('click', 'div.tristate .tristate-dummy', { self: this }, this.clickTriState);
// TBD: a global autocompletion handler
// $(document).on('keyup', 'form.auto input', this.formChangeDelayed);
// $(document).on('change', 'form.auto input', this.formChanged);
// $(document).on('change', 'form.auto select', this.submitForm);
$(document).on('mousemove', function (event) {
mouseX = event.pageX;
mouseY = event.pageY;
});
},
menuTitleHovered: function (event) {
var $li = $(this),
delay = 800,
self = event.data.self;
if ($li.hasClass('active')) {
$li.siblings().removeClass('hover');
return;
}
if ($li.children('ul').children('li').length === 0) {
return;
}
if ($('#menu').scrollTop() > 0) {
return;
}
if ($('#layout').hasClass('hoveredmenu')) {
delay = 0;
}
setTimeout(function () {
if (! $li.is('li:hover')) {
return;
}
if ($li.hasClass('active')) {
return;
}
$li.siblings().each(function () {
var $sibling = $(this);
if ($sibling.is('li:hover')) {
return;
}
if ($sibling.hasClass('hover')) {
$sibling.removeClass('hover');
}
});
$('#layout').addClass('hoveredmenu');
$li.addClass('hover');
}, delay);
},
leaveSidebar: function (event) {
var $sidebar = $(this),
$li = $sidebar.find('li.hover'),
self = event.data.self;
if (! $li.length) {
$('#layout').removeClass('hoveredmenu');
return;
}
setTimeout(function () {
if ($li.is('li:hover') || $sidebar.is('sidebar:hover') ) {
return;
}
$li.removeClass('hover');
$('#layout').removeClass('hoveredmenu');
}, 500);
},
dropdownHover: function () {
$(this).addClass('hover');
},
dropdownLeave: function (event) {
var $li = $(this),
self = event.data.self;
setTimeout(function () {
// TODO: make this behave well together with keyboard navigation
if (! $li.is('li:hover') /*&& ! $li.find('a:focus')*/) {
$li.removeClass('hover');
}
}, 300);
},
treeNodeToggle: function () {
@ -313,7 +145,7 @@
},
onLoad: function (event) {
$('.container').trigger('rendered');
//$('.container').trigger('rendered');
},
onUnload: function (event) {
@ -330,14 +162,6 @@
icinga.ui.fixControls();
},
historycolorgridHover: function () {
$(this).addClass('hover');
},
historycolorgidUnhover: function() {
$(this).removeClass('hover');
},
autoSubmitSearch: function(event) {
var self = event.data.self;
if ($('#menu input.search').val() === self.searchValue) {
@ -351,39 +175,6 @@
return event.data.self.submitForm(event, true);
},
clickTriState: function (event) {
var self = event.data.self;
var $tristate = $(this);
var triState = parseInt($tristate.data('icinga-tristate'), 10);
// load current values
var old = $tristate.data('icinga-old').toString();
var value = $tristate.parent().find('input:radio:checked').first().prop('checked', false).val();
// calculate the new value
if (triState) {
// 1 => 0
// 0 => unchanged
// unchanged => 1
value = value === '1' ? '0' : (value === '0' ? 'unchanged' : '1');
} else {
// 1 => 0
// 0 => 1
value = value === '1' ? '0' : '1';
}
// update form value
$tristate.parent().find('input:radio[value="' + value + '"]').prop('checked', true);
// update dummy
if (value !== old) {
$tristate.parent().find('b.tristate-changed').css('visibility', 'visible');
} else {
$tristate.parent().find('b.tristate-changed').css('visibility', 'hidden');
}
self.icinga.ui.setTriState(value.toString(), $tristate);
},
/**
*
*/
@ -527,9 +318,7 @@
var $a = $(this);
var href = $a.attr('href');
var linkTarget = $a.attr('target');
var $li;
var $target;
var isMenuLink = $a.closest('#menu').length > 0;
var formerUrl;
var remote = /^(?:[a-z]+:)\/\//;
if (href.match(/^(mailto|javascript):/)) {
@ -580,26 +369,9 @@
// If link has hash tag...
if (href.match(/#/)) {
// ...it may be a menu section without a dedicated link.
// Switch the active menu item:
if (isMenuLink) {
$li = $a.closest('li');
$('#menu .active').removeClass('active');
$li.addClass('active');
activeMenuId = $($li).attr('id');
if ($li.hasClass('hover')) {
$li.removeClass('hover');
}
}
if (href === '#') {
// Allow to access dropdown menu by keyboard
if ($a.hasClass('dropdown-toggle')) {
$a.closest('li').toggleClass('hover');
}
// Ignore link, no action
return false;
}
$target = self.getLinkTargetFor($a);
formerUrl = $target.data('icingaUrl');
@ -612,21 +384,13 @@
return false;
}
} else {
if (isMenuLink) {
activeMenuId = $(event.target).closest('li').attr('id');
}
$target = self.getLinkTargetFor($a);
}
// Load link URL
icinga.loader.loadUrl(href, $target);
if (isMenuLink) {
// update target url of the menu container to the clicked link
var menuDataUrl = icinga.utils.parseUrl($('#menu').data('icinga-url'));
menuDataUrl = icinga.utils.addUrlParams(menuDataUrl.path, { url: href });
$('#menu').data('icinga-url', menuDataUrl);
if ($a.closest('#menu').length > 0) {
// Menu links should remove all but the first layout column
icinga.ui.layout1col();
}
@ -697,6 +461,9 @@
*/
unbindGlobalHandlers: function () {
$.each(self.icinga.behaviors, function (name, behavior) {
behavior.unbind($(document));
});
$(window).off('resize', this.onWindowResize);
$(window).off('load', this.onLoad);
$(window).off('unload', this.onUnload);
@ -708,12 +475,6 @@
$(document).off('submit', 'form', this.submitForm);
$(document).off('click', 'button', this.submitForm);
$(document).off('change', 'form select.autosubmit', this.submitForm);
$(document).off('mouseenter', '.historycolorgrid td', this.historycolorgridHover);
$(document).off('mouseleave', '.historycolorgrid td', this.historycolorgidUnhover);
$(document).off('mouseenter', 'li.dropdown', this.dropdownHover);
$(document).off('mouseleave', 'li.dropdown', this.dropdownLeave);
$(document).off('click', 'div.tristate .tristate-dummy', this.clickTriState);
$(document).off('mousemove');
},
destroy: function() {

View File

@ -345,7 +345,7 @@
$matches.each(function (idx, el) {
if ($(el).closest('#menu').length) {
$('#menu .active').removeClass('active');
self.icinga.behaviors.navigation.resetActive();
} else if ($(el).closest('table.action').length) {
$(el).closest('table.action').find('.active').removeClass('active');
}
@ -357,8 +357,7 @@
if ($el.is('form')) {
$('input', $el).addClass('active');
} else {
$el.closest('li').addClass('active');
$el.parents('li').addClass('active');
self.icinga.behaviors.navigation.setActive($el);
}
// Interrupt .each, only on menu item shall be active
return false;
@ -540,7 +539,6 @@
}).addClass('active');
}
}
req.$target.trigger('rendered');
},
/**
@ -726,8 +724,10 @@
}
// TODO: this.icinga.events.refreshContainer(container);
$container.trigger('rendered');
var icinga = this.icinga;
icinga.events.applyHandlers($container);
//icinga.events.applyHandlers($container);
icinga.ui.initializeControls($container);
icinga.ui.fixControls();

View File

@ -282,20 +282,6 @@
return $('#main > .container').length;
},
prepareContainers: function () {
var icinga = this.icinga;
$('.container').each(function(idx, el) {
icinga.events.applyHandlers($(el));
icinga.ui.initializeControls($(el));
});
/*
$('#icinga-main').attr(
'icingaurl',
window.location.pathname + window.location.search
);
*/
},
/**
* Add the given table-row to the selection of the closest
* table and deselect all other rows of the closest table.

View File

@ -2,23 +2,38 @@
// {{{ICINGA_LICENSE_HEADER}}}
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Web\Hook;
use Icinga\Web\Hook;
class TestHook extends Hook {}
namespace Tests\Icinga\Web;
use Mockery;
use Exception;
use Icinga\Web\Hook;
use Icinga\Test\BaseTestCase;
use Icinga\Web\Hook;
use Icinga\Web\Hook\TestHook;
use Exception;
class ErrorProneHookImplementation
class NoHook {}
class MyHook extends TestHook {}
class AnotherHook extends TestHook {}
class FailingHook extends TestHook
{
public function __construct()
{
throw new Exception();
throw new Exception("I'm failing");
}
}
class HookTest extends BaseTestCase
{
protected $invalidHook = '\\Tests\\Icinga\\Web\\NoHook';
protected $validHook = '\\Tests\\Icinga\\Web\\MyHook';
protected $anotherHook = '\\Tests\\Icinga\\Web\\AnotherHook';
protected $failingHook = '\\Tests\\Icinga\\Web\\FailingHook';
protected $testBaseClass = '\\Icinga\\Web\\Hook\\TestHook';
public function setUp()
{
parent::setUp();
@ -31,83 +46,28 @@ class HookTest extends BaseTestCase
Hook::clean();
}
public function testWhetherHasReturnsTrueIfGivenAKnownHook()
public function testKnowsWhichHooksAreRegistered()
{
Hook::registerClass('TestHook', __FUNCTION__, get_class(Mockery::mock(Hook::$BASE_NS . 'TestHook')));
$this->assertTrue(Hook::has('TestHook'), 'Hook::has does not return true if given a known hook');
Hook::register('test', __FUNCTION__, $this->validHook);
$this->assertTrue(Hook::has('test'));
$this->assertFalse(Hook::has('no_such_hook'));
}
public function testWhetherHasReturnsFalseIfGivenAnUnknownHook()
public function testCorrectlyHandlesMultipleInstances()
{
$this->assertFalse(Hook::has('not_existing'), 'Hook::has does not return false if given an unknown hook');
}
public function testWhetherHooksCanBeRegisteredWithRegisterClass()
{
Hook::registerClass('TestHook', __FUNCTION__, get_class(Mockery::mock(Hook::$BASE_NS . 'TestHook')));
$this->assertTrue(Hook::has('TestHook'), 'Hook::registerClass does not properly register a given hook');
}
/**
* @depends testWhetherHooksCanBeRegisteredWithRegisterClass
*/
public function testWhetherMultipleHooksOfTheSameTypeCanBeRegisteredWithRegisterClass()
{
$firstHook = Mockery::mock(Hook::$BASE_NS . 'TestHook');
$secondHook = Mockery::mock('overload:' . get_class($firstHook));
Hook::registerClass('TestHook', 'one', get_class($firstHook));
Hook::registerClass('TestHook', 'two', get_class($secondHook));
Hook::register('test', 'one', $this->validHook);
Hook::register('test', 'two', $this->anotherHook);
$this->assertInstanceOf(
get_class($secondHook),
Hook::createInstance('TestHook', 'two'),
'Hook::registerClass is not able to register different hooks of the same type'
$this->anotherHook,
Hook::createInstance('test', 'two')
);
$this->assertInstanceOf(
$this->validHook,
Hook::createInstance('test', 'one')
);
}
/**
* @expectedException Icinga\Exception\ProgrammingError
*/
public function testWhetherOnlyClassesCanBeRegisteredAsHookWithRegisterClass()
{
Hook::registerClass('TestHook', __FUNCTION__, 'nope');
}
public function testWhetherHooksCanBeRegisteredWithRegisterObject()
{
Hook::registerObject('TestHook', __FUNCTION__, Mockery::mock(Hook::$BASE_NS . 'TestHook'));
$this->assertTrue(Hook::has('TestHook'), 'Hook::registerObject does not properly register a given hook');
}
/**
* @depends testWhetherHooksCanBeRegisteredWithRegisterObject
*/
public function testWhetherMultipleHooksOfTheSameTypeCanBeRegisteredWithRegisterObject()
{
$firstHook = Mockery::mock(Hook::$BASE_NS . 'TestHook');
$secondHook = Mockery::mock('overload:' . get_class($firstHook));
Hook::registerObject('TestHook', 'one', $firstHook);
Hook::registerObject('TestHook', 'two', $secondHook);
$this->assertInstanceOf(
get_class($secondHook),
Hook::createInstance('TestHook', 'two'),
'Hook::registerObject is not able to register different hooks of the same type'
);
}
/**
* @expectedException Icinga\Exception\ProgrammingError
*/
public function testWhetherOnlyObjectsCanBeRegisteredAsHookWithRegisterObject()
{
Hook::registerObject('TestHook', __FUNCTION__, 'nope');
}
public function testWhetherCreateInstanceReturnsNullIfGivenAnUnknownHookName()
public function testReturnsNullForInvalidHooks()
{
$this->assertNull(
Hook::createInstance('not_existing', __FUNCTION__),
@ -115,98 +75,41 @@ class HookTest extends BaseTestCase
);
}
/**
* @depends testWhetherHooksCanBeRegisteredWithRegisterClass
*/
public function testWhetherCreateInstanceInitializesHooksInheritingFromAPredefinedAbstractHook()
public function testReturnsNullForFailingHook()
{
$baseHook = Mockery::mock(Hook::$BASE_NS . 'TestHook');
Hook::registerClass(
'TestHook',
__FUNCTION__,
get_class(Mockery::mock('overload:' . get_class($baseHook)))
);
Hook::register('test', __FUNCTION__, $this->failingHook);
$this->assertNull(Hook::createInstance('test', __FUNCTION__));
}
public function testChecksWhetherCreatedInstancesInheritBaseClasses()
{
Hook::register('test', __FUNCTION__, $this->validHook);
$this->assertInstanceOf(
get_class($baseHook),
Hook::createInstance('TestHook', __FUNCTION__),
'Hook::createInstance does not initialize hooks inheriting from a predefined abstract hook'
$this->testBaseClass,
Hook::createInstance('test', __FUNCTION__)
);
}
/**
* @depends testWhetherHooksCanBeRegisteredWithRegisterClass
*/
public function testWhetherCreateInstanceDoesNotInitializeMultipleHooksForASpecificIdentifier()
{
Hook::registerClass('TestHook', __FUNCTION__, get_class(Mockery::mock(Hook::$BASE_NS . 'TestHook')));
$secondHook = Hook::createInstance('TestHook', __FUNCTION__);
$thirdHook = Hook::createInstance('TestHook', __FUNCTION__);
$this->assertSame(
$secondHook,
$thirdHook,
'Hook::createInstance initializes multiple hooks for a specific identifier'
);
}
/**
* @depends testWhetherHooksCanBeRegisteredWithRegisterClass
*/
public function testWhetherCreateInstanceReturnsNullIfHookCannotBeInitialized()
{
Hook::registerClass('TestHook', __FUNCTION__, 'Tests\Icinga\Web\ErrorProneHookImplementation');
$this->assertNull(Hook::createInstance('TestHook', __FUNCTION__));
}
/**
* @expectedException Icinga\Exception\ProgrammingError
* @depends testWhetherHooksCanBeRegisteredWithRegisterClass
*/
public function testWhetherCreateInstanceThrowsAnErrorIfGivenAHookNotInheritingFromAPredefinedAbstractHook()
public function testThrowsErrorsForInstancesNotInheritingBaseClasses()
{
Mockery::mock(Hook::$BASE_NS . 'TestHook');
Hook::registerClass('TestHook', __FUNCTION__, get_class(Mockery::mock('TestHook')));
Hook::createInstance('TestHook', __FUNCTION__);
Hook::register('test', __FUNCTION__, $this->invalidHook);
Hook::createInstance('test', __FUNCTION__);
}
/**
* @depends testWhetherHooksCanBeRegisteredWithRegisterObject
*/
public function testWhetherAllReturnsAllRegisteredHooks()
public function testCreatesIdenticalInstancesOnlyOnce()
{
$hook = Mockery::mock(Hook::$BASE_NS . 'TestHook');
Hook::registerObject('TestHook', 'one', $hook);
Hook::registerObject('TestHook', 'two', $hook);
Hook::registerObject('TestHook', 'three', $hook);
Hook::register('test', __FUNCTION__, $this->validHook);
$first = Hook::createInstance('test', __FUNCTION__);
$second = Hook::createInstance('test', __FUNCTION__);
$this->assertCount(3, Hook::all('TestHook'), 'Hook::all does not return all registered hooks');
$this->assertSame($first, $second);
}
public function testWhetherAllReturnsNothingIfGivenAnUnknownHookName()
public function testReturnsAnEmptyArrayWithNoRegisteredHook()
{
$this->assertEmpty(
Hook::all('not_existing'),
'Hook::all does not return an empty array if given an unknown hook'
);
}
/**
* @depends testWhetherHooksCanBeRegisteredWithRegisterObject
*/
public function testWhetherFirstReturnsTheFirstRegisteredHook()
{
$firstHook = Mockery::mock(Hook::$BASE_NS . 'TestHook');
$secondHook = Mockery::mock(Hook::$BASE_NS . 'TestHook');
Hook::registerObject('TestHook', 'first', $firstHook);
Hook::registerObject('TestHook', 'second', $secondHook);
$this->assertSame(
$firstHook,
Hook::first('TestHook'),
'Hook::first does not return the first registered hook'
);
$this->assertEquals(array(), Hook::all('not_existing'));
}
}