diff --git a/icingaweb2.spec b/icingaweb2.spec
index 5d5b9ec9c..db0929f7b 100644
--- a/icingaweb2.spec
+++ b/icingaweb2.spec
@@ -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
diff --git a/library/Icinga/Web/Hook.php b/library/Icinga/Web/Hook.php
index 2e85f570e..36974fb5b 100644
--- a/library/Icinga/Web/Hook.php
+++ b/library/Icinga/Web/Hook.php
@@ -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));
- }
}
diff --git a/library/Icinga/Web/JavaScript.php b/library/Icinga/Web/JavaScript.php
index cacadb600..6aa36d608 100644
--- a/library/Icinga/Web/JavaScript.php
+++ b/library/Icinga/Web/JavaScript.php
@@ -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(
diff --git a/library/Icinga/Web/MenuRenderer.php b/library/Icinga/Web/MenuRenderer.php
index c026e5553..2fbcedc01 100644
--- a/library/Icinga/Web/MenuRenderer.php
+++ b/library/Icinga/Web/MenuRenderer.php
@@ -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) {
- return $child->getRenderer()->render($child);
+ try {
+ return $child->getRenderer()->render($child);
+ } catch (Exception $e) {
+ Logger::error('Could not invoke custom renderer. Exception: '. $e->getMessage());
+ }
}
return sprintf(
'%s%s',
diff --git a/library/Icinga/Web/Response.php b/library/Icinga/Web/Response.php
index 33e0ea2a3..6ad4f6f87 100644
--- a/library/Icinga/Web/Response.php
+++ b/library/Icinga/Web/Response.php
@@ -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());
}
diff --git a/library/Icinga/Web/View/helpers/url.php b/library/Icinga/Web/View/helpers/url.php
index 3056d3644..935a55a97 100644
--- a/library/Icinga/Web/View/helpers/url.php
+++ b/library/Icinga/Web/View/helpers/url.php
@@ -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)
);
});
diff --git a/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php b/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
index bb402009b..974e02060 100644
--- a/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+++ b/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
@@ -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 = '
';
$html .= '';
for ($i = 0; $i < 7; $i++) {
- $html .= '' . $this->weekdayName($i) . " | ";
+ $html .= '' . $this->weekdayName($this->weekStartMonday ? $i + 1 : $i) . " | ";
}
$html .= '
';
$old = -1;
@@ -192,7 +196,9 @@ class HistoryColorGrid extends AbstractWidget {
*/
private function renderWeekdayHorizontal($weekday, &$weeks)
{
- $html = '' . $this->weekdayName($weekday) . ' | ';
+ $html = '
'
+ . $this->weekdayName($this->weekStartMonday ? $weekday + 1 : $weekday)
+ . ' | ';
foreach ($weeks as $week) {
if (array_key_exists($weekday, $week)) {
$html .= '' . $this->renderDay($week[$weekday]) . ' | ';
@@ -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,8 +239,11 @@ class HistoryColorGrid extends AbstractWidget {
if ($weekday > 6) {
$weekday = 0;
$weeks[] = array();
+ // PRESENT => The last day of week determines the month
+ if ($this->weekFlow === self::CAL_GROW_INTO_PRESENT) {
+ $months[$week] = $month;
+ }
$week++;
- $months[$week] = $month;
}
if ($day > cal_days_in_month(CAL_GREGORIAN, $month, $year)) {
$month++;
@@ -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
diff --git a/modules/monitoring/application/views/scripts/show/components/hostservicesummary.phtml b/modules/monitoring/application/views/scripts/show/components/hostservicesummary.phtml
index 0b4b9c9fe..ffa3c806e 100644
--- a/modules/monitoring/application/views/scripts/show/components/hostservicesummary.phtml
+++ b/modules/monitoring/application/views/scripts/show/components/hostservicesummary.phtml
@@ -6,7 +6,7 @@ $currentUrl = Url::fromRequest()->without('limit')->getRelativeUrl();
?>
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) ?>
stats->services_ok > 0): ?>
= $this->qlink(
$this->stats->services_ok,
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php
index 75b5ab284..e979cdc08 100644
--- a/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php
@@ -38,8 +38,11 @@ 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']) {
- return 'sh.state_type ' . $sign . ' ' . $this->types[$expression];
+ } 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);
}
}
-
diff --git a/modules/translation/doc/translation.md b/modules/translation/doc/translation.md
new file mode 100644
index 000000000..74cd54004
--- /dev/null
+++ b/modules/translation/doc/translation.md
@@ -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
+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:
+
+```
+= $this->title ?>
+
+ = $this->translate('Hello World') ?>
+ = $this->translate('String to be translated') ?>
+
+```
+
+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.
+
+
+
+__Editor__: Under the Behavior the Automatically compile .mo files on save, should be disabled.
+
+
+
+__Translations Memory__: Under the Database please add your languages, for which are you writing translations.
+
+
+
+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`.
+
+
+
+
+Now you can make changes and when there is no translation available, Poedit would mark it with a blue color, as shown
+below.
+
+
+
+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 , 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 \n"
+ "Language: ll_CC\n"
+ "Language-Team: LANGUAGE \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`.
\ No newline at end of file
diff --git a/modules/translation/library/Translation/Util/GettextTranslationHelper.php b/modules/translation/library/Translation/Util/GettextTranslationHelper.php
index be0497a31..9d72b9c77 100644
--- a/modules/translation/library/Translation/Util/GettextTranslationHelper.php
+++ b/modules/translation/library/Translation/Util/GettextTranslationHelper.php
@@ -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];
}
diff --git a/modules/translation/public/img/doc/poedit_001.png b/modules/translation/public/img/doc/poedit_001.png
new file mode 100644
index 000000000..2d07b8e1b
Binary files /dev/null and b/modules/translation/public/img/doc/poedit_001.png differ
diff --git a/modules/translation/public/img/doc/poedit_002.png b/modules/translation/public/img/doc/poedit_002.png
new file mode 100644
index 000000000..d31e5baf6
Binary files /dev/null and b/modules/translation/public/img/doc/poedit_002.png differ
diff --git a/modules/translation/public/img/doc/poedit_003.png b/modules/translation/public/img/doc/poedit_003.png
new file mode 100644
index 000000000..5f285f9fa
Binary files /dev/null and b/modules/translation/public/img/doc/poedit_003.png differ
diff --git a/modules/translation/public/img/doc/poedit_004.png b/modules/translation/public/img/doc/poedit_004.png
new file mode 100644
index 000000000..2c85dd904
Binary files /dev/null and b/modules/translation/public/img/doc/poedit_004.png differ
diff --git a/modules/translation/public/img/doc/poedit_005.png b/modules/translation/public/img/doc/poedit_005.png
new file mode 100644
index 000000000..3ae59bafa
Binary files /dev/null and b/modules/translation/public/img/doc/poedit_005.png differ
diff --git a/packages/rpm/README.md b/packages/rpm/README.md
index d218a51eb..5dd1006c5 100644
--- a/packages/rpm/README.md
+++ b/packages/rpm/README.md
@@ -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.
diff --git a/packages/rpm/etc/httpd/conf.d/icingaweb.conf b/packages/rpm/etc/httpd/conf.d/icingaweb.conf
index cf2fd82a4..1db9f20f2 100644
--- a/packages/rpm/etc/httpd/conf.d/icingaweb.conf
+++ b/packages/rpm/etc/httpd/conf.d/icingaweb.conf
@@ -1,6 +1,6 @@
-Alias /icingaweb "/usr/share/icingaweb/public"
+Alias /icingaweb "/usr/share/icingaweb2/public"
-
+
Options SymLinksIfOwnerMatch
AllowOverride None
@@ -17,7 +17,7 @@ Alias /icingaweb "/usr/share/icingaweb/public"
Allow from all
- SetEnv ICINGAWEB_CONFIGDIR /etc/icingaweb
+ SetEnv ICINGAWEB_CONFIGDIR /etc/icingaweb2
EnableSendfile Off
diff --git a/packages/rpm/etc/icingaweb/authentication.ini b/packages/rpm/etc/icingaweb2/authentication.ini
similarity index 100%
rename from packages/rpm/etc/icingaweb/authentication.ini
rename to packages/rpm/etc/icingaweb2/authentication.ini
diff --git a/packages/rpm/etc/icingaweb/modules/monitoring/backends.ini b/packages/rpm/etc/icingaweb2/modules/monitoring/backends.ini
similarity index 100%
rename from packages/rpm/etc/icingaweb/modules/monitoring/backends.ini
rename to packages/rpm/etc/icingaweb2/modules/monitoring/backends.ini
diff --git a/packages/rpm/etc/icingaweb/modules/monitoring/instances.ini b/packages/rpm/etc/icingaweb2/modules/monitoring/instances.ini
similarity index 100%
rename from packages/rpm/etc/icingaweb/modules/monitoring/instances.ini
rename to packages/rpm/etc/icingaweb2/modules/monitoring/instances.ini
diff --git a/packages/rpm/etc/icingaweb/resources.ini b/packages/rpm/etc/icingaweb2/resources.ini
similarity index 92%
rename from packages/rpm/etc/icingaweb/resources.ini
rename to packages/rpm/etc/icingaweb2/resources.ini
index 1c1e62eb1..833f84ba8 100644
--- a/packages/rpm/etc/icingaweb/resources.ini
+++ b/packages/rpm/etc/icingaweb2/resources.ini
@@ -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 = "/^(?[0-9]{4}(-[0-9]{2}){2}T[0-9]{2}(:[0-9]{2}){2}(\\+[0-9]{2}:[0-9]{2})?) - (?[A-Za-z]+) - (?.*)$/"
; format: PCRE
;
diff --git a/packages/rpm/usr/bin/icingacli b/packages/rpm/usr/bin/icingacli
index 03ed43116..d6c4010b6 100755
--- a/packages/rpm/usr/bin/icingacli
+++ b/packages/rpm/usr/bin/icingacli
@@ -2,5 +2,5 @@
dispatch();
diff --git a/public/js/icinga.js b/public/js/icinga.js
index 04c7cdcba..c883a6bba 100644
--- a/public/js/icinga.js
+++ b/public/js/icinga.js
@@ -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;
},
diff --git a/public/js/icinga/behavior/navigation.js b/public/js/icinga/behavior/navigation.js
new file mode 100644
index 000000000..90cda9713
--- /dev/null
+++ b/public/js/icinga/behavior/navigation.js
@@ -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);
diff --git a/public/js/icinga/behavior/sparkline.js b/public/js/icinga/behavior/sparkline.js
new file mode 100644
index 000000000..33cc0d34b
--- /dev/null
+++ b/public/js/icinga/behavior/sparkline.js
@@ -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);
diff --git a/public/js/icinga/behavior/tooltip.js b/public/js/icinga/behavior/tooltip.js
new file mode 100644
index 000000000..de5b66d05
--- /dev/null
+++ b/public/js/icinga/behavior/tooltip.js
@@ -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);
diff --git a/public/js/icinga/behavior/tristate.js b/public/js/icinga/behavior/tristate.js
new file mode 100644
index 000000000..036c25cea
--- /dev/null
+++ b/public/js/icinga/behavior/tristate.js
@@ -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);
diff --git a/public/js/icinga/eventlistener.js b/public/js/icinga/eventlistener.js
new file mode 100644
index 000000000..e1946c6ae
--- /dev/null
+++ b/public/js/icinga/eventlistener.js
@@ -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);
diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js
index a48336147..f226836cc 100644
--- a/public/js/icinga/events.js
+++ b/public/js/icinga/events.js
@@ -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() {
diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js
index 9d14ebb7f..9b6c2bd00 100644
--- a/public/js/icinga/loader.js
+++ b/public/js/icinga/loader.js
@@ -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();
diff --git a/public/js/icinga/ui.js b/public/js/icinga/ui.js
index f6ae29b35..ef5e43f70 100644
--- a/public/js/icinga/ui.js
+++ b/public/js/icinga/ui.js
@@ -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.
diff --git a/test/php/library/Icinga/Web/HookTest.php b/test/php/library/Icinga/Web/HookTest.php
index 9e82ad90d..16300f5ca 100644
--- a/test/php/library/Icinga/Web/HookTest.php
+++ b/test/php/library/Icinga/Web/HookTest.php
@@ -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'));
}
}