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 .= '"; + $html .= '"; } $html .= ''; $old = -1; @@ -192,7 +196,9 @@ class HistoryColorGrid extends AbstractWidget { */ private function renderWeekdayHorizontal($weekday, &$weeks) { - $html = ''; + $html = ''; foreach ($weeks as $week) { if (array_key_exists($weekday, $week)) { $html .= ''; @@ -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"' : '' ?>> -qlink(sprintf($this->translate('%s service configured:'), $this->stats->services_total), $selfUrl) ?> +qlink(sprintf($this->translate('%s configured services:'), $this->stats->services_total), $selfUrl) ?> stats->services_ok > 0): ?> 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: + +``` +

title ?>

+

+ translate('Hello World') ?> + 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. + +![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 , 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')); } }

' . $this->weekdayName($i) . "' . $this->weekdayName($this->weekStartMonday ? $i + 1 : $i) . "
' . $this->weekdayName($weekday) . '
' + . $this->weekdayName($this->weekStartMonday ? $weekday + 1 : $weekday) + . '' . $this->renderDay($week[$weekday]) . '