From 4a95ba34688b993538d30e08ac604da559a76be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannis=20Mo=C3=9Fhammer?= Date: Thu, 17 Oct 2013 19:48:46 +0200 Subject: [PATCH] Change url handling to detail on hashtag, add service filter The url is now http://%mainUrl%#%anchor%!detail=%detailUrl% which allows us to better support IE and prevents the detail url from appearing on the server side. refs #4868 --- application/controllers/FilterController.php | 33 +- application/views/helpers/MainDetail.php | 2 + .../scripts/config/module/overview.phtml | 4 +- library/Icinga/Data/DataArray/Datasource.php | 8 +- library/Icinga/Filter/Filterable.php | 30 ++ library/Icinga/Filter/Registry.php | 43 +++ .../Web/Controller/ActionController.php | 42 +-- library/Icinga/Web/Request.php | 2 + .../Icinga/Web/Widget/FilterBadgeRenderer.php | 2 +- library/Icinga/Web/Widget/FilterBox.php | 5 +- library/Icinga/Web/Widget/SortBox.php | 4 +- library/Icinga/Web/Widget/Tab.php | 4 +- .../controllers/ListController.php | 12 +- .../controllers/ShowController.php | 11 +- .../views/scripts/list/services.phtml | 15 +- .../library/Monitoring/Controller.php | 1 + .../library/Monitoring/DataView/DataView.php | 7 +- .../library/Monitoring/Filter/Registry.php | 128 ++++++- .../Monitoring/Filter/Type/StatusFilter.php | 1 - .../Monitoring/Filter/UrlViewFilter.php | 10 + .../Monitoring/Object/AbstractObject.php | 3 +- .../icinga/components/ajaxPostSubmitForm.js | 2 +- public/js/icinga/components/container.js | 319 +++++++----------- public/js/icinga/components/mainDetailGrid.js | 31 +- public/js/icinga/components/semanticsearch.js | 22 +- public/js/icinga/icinga.js | 116 ++++--- public/js/icinga/util/url.js | 194 +++++++++++ public/js/main.js | 15 +- public/js/vendor/history.js | 2 +- public/js/vendor/jquery.scrollto.js | 8 + .../test/icinga/components/containerTest.js | 41 +-- .../php/library/Icinga/Web/Widget/TabTest.php | 2 +- 32 files changed, 722 insertions(+), 397 deletions(-) create mode 100644 library/Icinga/Filter/Registry.php create mode 100644 public/js/icinga/util/url.js create mode 100644 public/js/vendor/jquery.scrollto.js diff --git a/application/controllers/FilterController.php b/application/controllers/FilterController.php index 213e8eea2..00e598ea9 100644 --- a/application/controllers/FilterController.php +++ b/application/controllers/FilterController.php @@ -33,9 +33,6 @@ use Icinga\Filter\Filter; use Icinga\Filter\FilterAttribute; use Icinga\Filter\Type\TextFilter; use Icinga\Application\Logger; -use Icinga\Module\Monitoring\Filter\Type\StatusFilter; -use Icinga\Module\Monitoring\Filter\UrlViewFilter; -use Icinga\Module\Monitoring\DataView\HostStatus; use Icinga\Web\Url; /** @@ -50,6 +47,8 @@ class FilterController extends ActionController */ private $registry; + private $moduleRegistry; + /** * Entry point for filtering, uses the filter_domain and filter_module request parameter * to determine which filter registry should be used @@ -57,19 +56,27 @@ class FilterController extends ActionController public function indexAction() { $this->registry = new Filter(); + $query = $this->getRequest()->getParam('query', ''); + $target = $this->getRequest()->getParam('filter_domain', ''); if ($this->getRequest()->getHeader('accept') == 'application/json') { $this->getResponse()->setHeader('Content-Type', 'application/json'); - $this->setupQueries( - $this->getParam('filter_domain', ''), + $target, $this->getParam('filter_module', '') ); - - $this->_helper->json($this->parse($this->getRequest()->getParam('query', ''))); + $this->_helper->json($this->parse($query, $target)); } else { - $this->redirect('index/welcome'); + $this->setupQueries( + $target, + $this->getParam('filter_module') + ); + $urlTarget = $this->parse($query, $target); + die(print_r($urlTarget,true)); + $this->redirect($urlTarget['urlParam']); } + + } /** @@ -82,6 +89,7 @@ class FilterController extends ActionController { $class = '\\Icinga\\Module\\' . ucfirst($module) . '\\Filter\\Registry'; $factory = strtolower($domain) . 'Filter'; + $this->moduleRegistry = $class; $this->registry->addDomain($class::$factory()); } @@ -91,17 +99,16 @@ class FilterController extends ActionController * @param String $text The query to parse * @return array The result structure to be returned in json format */ - private function parse($text) + private function parse($text, $target) { try { - $view = HostStatus::fromRequest($this->getRequest()); - $urlParser = new UrlViewFilter($view); - $queryTree = $this->registry->createQueryTreeForFilter($text); + $queryTree = $this->registry->createQueryTreeForFilter($text); + $registry = $this->moduleRegistry; return array( 'state' => 'success', 'proposals' => $this->registry->getProposalsForQuery($text), - 'urlParam' => $urlParser->fromTree($queryTree) + 'urlParam' => $registry::getUrlForTarget($target, $queryTree) ); } catch (\Exception $exc) { Logger::error($exc); diff --git a/application/views/helpers/MainDetail.php b/application/views/helpers/MainDetail.php index bba30ad7a..e229c41a4 100644 --- a/application/views/helpers/MainDetail.php +++ b/application/views/helpers/MainDetail.php @@ -31,6 +31,8 @@ /** * Helper to render main and detail contents into a container */ +use Icinga\Application\Icinga; + class Zend_View_Helper_MainDetail extends Zend_View_Helper_Abstract { /** diff --git a/application/views/scripts/config/module/overview.phtml b/application/views/scripts/config/module/overview.phtml index 652967b8d..9eaa5357a 100644 --- a/application/views/scripts/config/module/overview.phtml +++ b/application/views/scripts/config/module/overview.phtml @@ -31,10 +31,10 @@ $modules = $this->modules->paginate(); enabled): ?> {{OK_ICON}} - escape($module->name); ?> + escape($module->name); ?> {{REMOVE_ICON}} - escape($module->name); ?> + escape($module->name); ?> (enabled ? ($module->loaded ? 'enabled' : 'failed') : 'disabled' ?>) diff --git a/library/Icinga/Data/DataArray/Datasource.php b/library/Icinga/Data/DataArray/Datasource.php index 2f59cdff6..e511c623b 100644 --- a/library/Icinga/Data/DataArray/Datasource.php +++ b/library/Icinga/Data/DataArray/Datasource.php @@ -72,16 +72,10 @@ class Datasource implements DatasourceInterface return $this; } $result = array(); - $filters = $query->listFilters(); + $columns = $query->getColumns(); foreach ($this->data as & $row) { - // Skip rows that do not match applied filters - foreach ($filters as $f) { - if ($row->{$f[0]} !== $f[1]) { - continue 2; - } - } // Get only desired columns if asked so if (empty($columns)) { diff --git a/library/Icinga/Filter/Filterable.php b/library/Icinga/Filter/Filterable.php index d95155af1..d54c77cef 100644 --- a/library/Icinga/Filter/Filterable.php +++ b/library/Icinga/Filter/Filterable.php @@ -29,11 +29,41 @@ namespace Icinga\Filter; +/** + * Interface for filterable data sources + */ interface Filterable { + /** + * Return true when this field is filterable, otherwise false + * + * @param string $field The field to test for being filterable + * @return boolean True when this field is filterable, otherwise false + */ public function isValidFilterTarget($field); + + /** + * Return the internal, resolved name of the given field + * + * @param string $field The field to resolve + * @return string The resolved name or null if the field is not resolvable + */ public function getMappedField($field); + + /** + * Apply all filters of this filterable on the datasource + */ public function applyFilter(); + + /** + * Remove all filters from this datasource + */ public function clearFilter(); + + /** + * Add a filter to this datasource + * + * @param mixed $filter The filter to use + */ public function addFilter($filter); } diff --git a/library/Icinga/Filter/Registry.php b/library/Icinga/Filter/Registry.php new file mode 100644 index 000000000..83b8a726c --- /dev/null +++ b/library/Icinga/Filter/Registry.php @@ -0,0 +1,43 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Filter; + + +use Icinga\Filter\Query\Tree; + +/** + * Interface for filter registries + * Class Registry + * @package Icinga\Filter + */ +interface Registry +{ + public static function getUrlForTarget($domain, Tree $filter); +} \ No newline at end of file diff --git a/library/Icinga/Web/Controller/ActionController.php b/library/Icinga/Web/Controller/ActionController.php index 5974dd958..e5ee5a5b3 100755 --- a/library/Icinga/Web/Controller/ActionController.php +++ b/library/Icinga/Web/Controller/ActionController.php @@ -106,23 +106,6 @@ class ActionController extends Zend_Controller_Action } } - private function dispatchDetailView($url) - { - // strip the base URL from the detail $url - $url = substr($url, strlen($this->getRequest()->getBaseUrl())); - // the host is mandatory, but ignored in Zend - $req = new Request('http://ignoredhost/' . $url); - $req->setUser($this->getRequest()->getUser()); - $req->setBaseUrl($this->getRequest()->getBaseUrl()); - $router = Zend_Controller_Front::getInstance()->getRouter(); - $router->route($req); - Zend_Controller_Front::getInstance()->setRequest($req); - $detailHtml = $this->view->action($req->getActionName(), $req->getControllerName(), $req->getModuleName()); - Zend_Controller_Front::getInstance()->setRequest($this->getRequest()); - $this->_helper->layout->assign('detailContent', $detailHtml); - $this->_helper->layout->assign('detailClass', 'col-sm-12 col-xs-12 col-md-12 col-lg-6'); - $this->_helper->layout->assign('mainClass', 'col-sm-12 col-xs-12 col-md-12 col-lg-6'); - } /** * Check whether the controller requires a login. That is when the controller requires authentication and the @@ -223,30 +206,9 @@ class ActionController extends Zend_Controller_Action Benchmark::measure('Action::postDispatch()'); if ($this->_request->isXmlHttpRequest()) { - $this->_helper->layout()->setLayout('body'); + $target = ($this->getParam('render') === 'detail') ? 'inline' : 'body'; + $this->_helper->layout()->setLayout($target); } - - if ($this->getParam('detail', false)) { - $detail = $this->getParam('detail'); - - // Zend uses the GET variables when calling getParam, therefore we have to persist the params, - // clear the $_GET array, call the detail view with the url set in $detail and afterwards recreate - // the $_GET array. If this is not done the following issues occur: - // - // - A stackoverflow issue due to infinite nested calls of buildDetailView (as the detailview has the same - // postDispatch method) when 'detail' is not set to null - // - // - Params (like filters in the URL) from the detail view would be applied on all links of the master view - // as those would be in the $_GET array after building the detail view. E.g. if you have a grid in the - // master and a detail view filtering showing one host in detail, the pagination links of the grid would - // contain the host filter of the detail view - // - $params = $_GET; - $_GET['detail'] = null; - $this->dispatchDetailView($detail); - $_GET = $params; - } - } /** diff --git a/library/Icinga/Web/Request.php b/library/Icinga/Web/Request.php index ca620916d..1ee620514 100644 --- a/library/Icinga/Web/Request.php +++ b/library/Icinga/Web/Request.php @@ -62,4 +62,6 @@ class Request extends Zend_Controller_Request_Http { return $this->user; } + + } diff --git a/library/Icinga/Web/Widget/FilterBadgeRenderer.php b/library/Icinga/Web/Widget/FilterBadgeRenderer.php index d5bde0006..aa274f89f 100644 --- a/library/Icinga/Web/Widget/FilterBadgeRenderer.php +++ b/library/Icinga/Web/Widget/FilterBadgeRenderer.php @@ -75,7 +75,7 @@ class FilterBadgeRenderer implements Widget $url = $this->urlFilter->fromTree($newTree); $url = $basePath . (empty($allParams) ? '?' : '&') . $url; - return ' ' + return ' ' . $this->conjunctionCellar . ' ' . ucfirst($node->left) . ' ' . $node->operator . ' ' diff --git a/library/Icinga/Web/Widget/FilterBox.php b/library/Icinga/Web/Widget/FilterBox.php index 784b987f0..1d5b503c0 100644 --- a/library/Icinga/Web/Widget/FilterBox.php +++ b/library/Icinga/Web/Widget/FilterBox.php @@ -99,10 +99,10 @@ EOT; $form->setTokenDisabled(); $form->addElement( 'text', - 'filter', + 'query', array( 'label' => 'Filter Results', - 'name' => 'filter', + 'name' => 'query', 'data-icinga-component' => 'app/semanticsearch', 'data-icinga-filter-domain' => $this->domain, 'data-icinga-filter-module' => $this->module @@ -111,7 +111,6 @@ EOT; $form->removeAttrib('data-icinga-component'); $form->setIgnoreChangeDiscarding(true); - $badges = new FilterBadgeRenderer($this->initialFilter); $html = str_replace('{{FORM}}', $form->render($view), self::$TPL); return str_replace('{{BADGES}}', $badges->render($view), $html); diff --git a/library/Icinga/Web/Widget/SortBox.php b/library/Icinga/Web/Widget/SortBox.php index 2b2c9051a..63fd1244b 100644 --- a/library/Icinga/Web/Widget/SortBox.php +++ b/library/Icinga/Web/Widget/SortBox.php @@ -34,6 +34,7 @@ use Zend_View_Abstract; use Icinga\Web\Form\Decorator\ConditionalHidden; use Zend_Form_Element_Submit; + /** * Sortbox widget * @@ -77,7 +78,7 @@ class SortBox implements Widget /** * A request object used for initial form population * - * @var Icinga\Web\Request + * @var \Icinga\Web\Request */ private $request; @@ -166,6 +167,7 @@ class SortBox implements Widget $form->addElement($this->createFallbackSubmitButton()); if ($this->request) { + $form->setAction($this->request->getRequestUri()); $form->populate($this->request->getParams()); } return $form->render($view); diff --git a/library/Icinga/Web/Widget/Tab.php b/library/Icinga/Web/Widget/Tab.php index ae8b13355..424076824 100644 --- a/library/Icinga/Web/Widget/Tab.php +++ b/library/Icinga/Web/Widget/Tab.php @@ -221,7 +221,7 @@ class Tab implements Widget */ public function render(Zend_View_Abstract $view) { - $class = $this->active ? ' class="active"' : ''; + $class = $this->active ? ' class="active" ' : ''; $caption = $this->title; if ($this->icon !== null) { @@ -238,7 +238,7 @@ class Tab implements Widget $tagParams .= ' ' . $key . '="' . $value . '"'; } } - $tab = '' . $caption . ''; + $tab = '' . $caption . ''; } else { $tab = $caption; } diff --git a/modules/monitoring/application/controllers/ListController.php b/modules/monitoring/application/controllers/ListController.php index e8c6d54fc..497296f54 100644 --- a/modules/monitoring/application/controllers/ListController.php +++ b/modules/monitoring/application/controllers/ListController.php @@ -52,6 +52,7 @@ use Icinga\Module\Monitoring\DataView\Comment as CommentView; use Icinga\Module\Monitoring\DataView\Groupsummary as GroupsummaryView; use Icinga\Module\Monitoring\DataView\EventHistory as EventHistoryView; use Icinga\Module\Monitoring\Filter\UrlViewFilter; +use Icinga\Module\Monitoring\DataView\ServiceStatus; use Icinga\Filter\Filterable; class Monitoring_ListController extends MonitoringController @@ -131,7 +132,8 @@ class Monitoring_ListController extends MonitoringController ) ); $query = $dataview->getQuery(); - $this->setupFilterControl($dataview); + $this->setupFilterControl($dataview, 'host'); + $this->setupSortControl(array( 'host_last_check' => 'Last Host Check', 'host_severity' => 'Host Severity', @@ -152,6 +154,8 @@ class Monitoring_ListController extends MonitoringController { $this->compactView = 'services-compact'; $this->view->services = $this->fetchServices(); + + $this->setupFilterControl(ServiceStatus::fromRequest($this->getRequest()), 'service'); $this->setupSortControl(array( 'service_last_check' => 'Last Service Check', 'service_severity' => 'Severity', @@ -437,12 +441,12 @@ class Monitoring_ListController extends MonitoringController $this->view->sortControl->applyRequest($this->getRequest()); } - private function setupFilterControl(Filterable $dataview) + private function setupFilterControl(Filterable $dataview, $domain) { $parser = new UrlViewFilter($dataview); $this->view->filterBox = new FilterBox( - $parser->parseUrl(), - 'host', + $parser->fromRequest($this->getRequest()), + $domain, 'monitoring' ); diff --git a/modules/monitoring/application/controllers/ShowController.php b/modules/monitoring/application/controllers/ShowController.php index 1742b5648..977aaa1b2 100644 --- a/modules/monitoring/application/controllers/ShowController.php +++ b/modules/monitoring/application/controllers/ShowController.php @@ -55,7 +55,16 @@ class Monitoring_ShowController extends MonitoringController */ public function init() { - $this->view->object = AbstractObject::fromRequest($this->getRequest()); + + if ($this->getRequest()->getActionName() === 'host') { + $this->view->object = new Host($this->getRequest()); + } elseif ($this->getRequest()->getActionName() === 'service') { + $this->view->object = new Service($this->getRequest()); + + } else { + $this->view->object = AbstractObject::fromRequest($this->getRequest()); + } + $this->createTabs(); } diff --git a/modules/monitoring/application/views/scripts/list/services.phtml b/modules/monitoring/application/views/scripts/list/services.phtml index 7c7e828aa..18b171658 100644 --- a/modules/monitoring/application/views/scripts/list/services.phtml +++ b/modules/monitoring/application/views/scripts/list/services.phtml @@ -5,8 +5,19 @@ $viewHelper = $this->getHelper('MonitoringState'); tabs->render($this); ?>

Services Status

- sortControl->render($this); ?> - paginationControl($services, null, null, array('preserve' => $this->preserve)); ?> +
+
+
+ filterBox->render($this); ?> +
+
+ sortControl->render($this); ?> +
+
+
+ paginationControl($services, null, null, array('preserve' => $this->preserve)); ?> +
+
diff --git a/modules/monitoring/library/Monitoring/Controller.php b/modules/monitoring/library/Monitoring/Controller.php index 79ef9fac2..c4d6d152c 100644 --- a/modules/monitoring/library/Monitoring/Controller.php +++ b/modules/monitoring/library/Monitoring/Controller.php @@ -13,6 +13,7 @@ use Icinga\Web\Controller\ActionController; */ class Controller extends ActionController { + /** * Retrieve services from either given parameters or request * diff --git a/modules/monitoring/library/Monitoring/DataView/DataView.php b/modules/monitoring/library/Monitoring/DataView/DataView.php index 1a5d805c6..aad28053b 100644 --- a/modules/monitoring/library/Monitoring/DataView/DataView.php +++ b/modules/monitoring/library/Monitoring/DataView/DataView.php @@ -74,7 +74,7 @@ abstract class DataView implements Filterable $view = new static(Backend::createBackend($request->getParam('backend')), $columns); $parser = new UrlViewFilter($view); - $view->getQuery()->setFilter($parser->parseUrl()); + $view->getQuery()->setFilter($parser->fromRequest($request)); $order = $request->getParam('dir'); if ($order !== null) { @@ -102,8 +102,11 @@ abstract class DataView implements Filterable public static function fromParams(array $params, array $columns = null) { $view = new static(Backend::createBackend($params['backend']), $columns); + foreach ($params as $key => $value) { - $view->getQuery()->where($key, $value); + if ($view->isValidFilterTarget($key)) { + $view->getQuery()->where($key, $value); + } } $order = isset($params['order']) ? $params['order'] : null; if ($order !== null) { diff --git a/modules/monitoring/library/Monitoring/Filter/Registry.php b/modules/monitoring/library/Monitoring/Filter/Registry.php index 08f54b783..3f8f7a232 100644 --- a/modules/monitoring/library/Monitoring/Filter/Registry.php +++ b/modules/monitoring/library/Monitoring/Filter/Registry.php @@ -28,20 +28,35 @@ namespace Icinga\Module\Monitoring\Filter; +use Icinga\Application\Icinga; +use Icinga\Application\Logger; use Icinga\Filter\Domain; use Icinga\Filter\FilterAttribute; use Icinga\Filter\Query\Node; +use Icinga\Filter\Query\Tree; use Icinga\Filter\Type\BooleanFilter; use Icinga\Filter\Type\TextFilter; use Icinga\Filter\Type\TimeRangeSpecifier; +use Icinga\Module\Monitoring\DataView\HostStatus; +use Icinga\Module\Monitoring\DataView\ServiceStatus; use Icinga\Module\Monitoring\Filter\Type\StatusFilter; +use Icinga\Filter\Registry as FilterRegistry; +use Icinga\Module\Monitoring\Object\Host; +use Icinga\Web\Request; +use Zend_Controller_Request_Exception; +use Icinga\Web\Url; /** * Factory class to create filter for different monitoring objects * */ -class Registry +class Registry implements FilterRegistry { + /** + * Return a TimeRangeSpecifier for the 'Next Check' query + * + * @return TimeRangeSpecifier + */ public static function getNextCheckFilterType() { $type = new TimeRangeSpecifier(); @@ -54,6 +69,11 @@ class Registry return $type; } + /** + * Return a TimeRangeSpecifier for the 'Last Check' query + * + * @return TimeRangeSpecifier + */ public static function getLastCheckFilterType() { $type = new TimeRangeSpecifier(); @@ -68,6 +88,11 @@ class Registry return $type; } + /** + * Registry function for the host domain + * + * @return Domain the domain to use in the filter registry + */ public static function hostFilter() { $domain = new Domain('Host'); @@ -78,7 +103,6 @@ class Registry ->setField('host_name') )->registerAttribute( FilterAttribute::create(StatusFilter::createForHost()) - ->setHandledAttributes('State', 'Status', 'Current Status') ->setField('host_state') )->registerAttribute( FilterAttribute::create(new BooleanFilter( @@ -103,4 +127,104 @@ class Registry ); return $domain; } + + /** + * Registry function for the service domain + * + * @return Domain the domain to use in the filter registry + */ + public static function serviceFilter() + { + $domain = new Domain('Service'); + + $domain->registerAttribute( + FilterAttribute::create(new TextFilter()) + ->setHandledAttributes('Name', 'Servicename') + ->setField('service_name') + )->registerAttribute( + FilterAttribute::create(StatusFilter::createForService()) + ->setField('service_state') + )->registerAttribute( + FilterAttribute::create(StatusFilter::createForHost()) + ->setHandledAttributes('Host') + ->setField('host_state') + )->registerAttribute( + FilterAttribute::create(new BooleanFilter( + array( + 'service_is_flapping' => 'Flapping', + 'service_problem' => 'In Problem State', + 'service_notifications_enabled' => 'Sending Notifications', + 'service_active_checks_enabled' => 'Active', + 'service_passive_checks_enabled' => 'Accepting Passive Checks', + 'service_handled' => 'Handled', + 'service_in_downtime' => 'In Downtime', + 'host_in_downtime' => 'In Host Downtime' + ) + )) + )->registerAttribute( + FilterAttribute::create(self::getLastCheckFilterType()) + ->setHandledAttributes('Last Check', 'Check') + ->setField('service_last_check') + )->registerAttribute( + FilterAttribute::create(self::getNextCheckFilterType()) + ->setHandledAttributes('Next Check') + ->setField('service_next_check') + ); + return $domain; + } + + /** + * Resolve the given filter to an url, using the referer as the base url and base filter + * + * @param $domain The domain to filter for + * @param Tree $filter The tree representing the fiter + * + * @return string An url + * @throws Zend_Controller_Request_Exception Called if no referer is available + */ + public static function getUrlForTarget($domain, Tree $filter) + { + if (!isset($_SERVER['HTTP_REFERER'])) { + throw new Zend_Controller_Request_Exception('You can\'t use this method without an referer'); + } + $request = Icinga::app()->getFrontController()->getRequest(); + switch ($domain) { + case 'host': + $view = HostStatus::fromRequest($request); + break; + case 'service': + $view = ServiceStatus::fromRequest($request); + break; + default: + Logger::error('Invalid filter domain requested : %s', $domain); + throw new Exception('Unknown Domain ' . $domain); + } + $urlParser = new UrlViewFilter($view); + $lastQuery = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY); + $lastPath = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_PATH); + $lastFilter = $urlParser->parseUrl($lastQuery); + $lastParameters = array(); + + parse_str($lastQuery, $lastParameters); + if ($lastFilter->root) { + $filter->insert($lastFilter->root); + } + $params = array(); + foreach ($lastParameters as $key => $param) { + if (!$filter->hasNodeWithAttribute($key)) { + $params[$key] = $param; + } + } + + $baseUrl = Url::fromPath($lastPath, $params); + $urlString = $baseUrl->getRelativeUrl(); + if (stripos($urlString, '?') === false) { + $urlString .= '?'; + } else { + $urlString .= '&'; + } + $urlString .= $urlParser->fromTree($filter); + return '/' . $urlString; + } + } diff --git a/modules/monitoring/library/Monitoring/Filter/Type/StatusFilter.php b/modules/monitoring/library/Monitoring/Filter/Type/StatusFilter.php index fb93a2bea..fd23004f5 100644 --- a/modules/monitoring/library/Monitoring/Filter/Type/StatusFilter.php +++ b/modules/monitoring/library/Monitoring/Filter/Type/StatusFilter.php @@ -127,7 +127,6 @@ class StatusFilter extends FilterType public static function createForService() { $status = new StatusFilter(); - $status->setType(self::TYPE_SERVICE); $status->setBaseStates( array( 'Ok' => 0, diff --git a/modules/monitoring/library/Monitoring/Filter/UrlViewFilter.php b/modules/monitoring/library/Monitoring/Filter/UrlViewFilter.php index 5434218d1..cc5900d5b 100644 --- a/modules/monitoring/library/Monitoring/Filter/UrlViewFilter.php +++ b/modules/monitoring/library/Monitoring/Filter/UrlViewFilter.php @@ -33,6 +33,7 @@ namespace Icinga\Module\Monitoring\Filter; use Icinga\Filter\Filterable; use Icinga\Filter\Query\Tree; use Icinga\Filter\Query\Node; +use Icinga\Web\Request; use Icinga\Web\Url; use Icinga\Application\Logger; @@ -114,6 +115,15 @@ class UrlViewFilter return $tree->getCopyForFilterable($this->target); } + public function fromRequest($request) + { + if($request->getParam('query')) { + return $this->parseUrl(urldecode($request->getParam('query'))); + } else { + return $this->parseUrl(parse_url($request->getBaseUrl(), PHP_URL_QUERY)); + } + } + /** * Convert a tree node and it's subnodes to a request string * diff --git a/modules/monitoring/library/Monitoring/Object/AbstractObject.php b/modules/monitoring/library/Monitoring/Object/AbstractObject.php index e2ea7259f..d66d12f4f 100644 --- a/modules/monitoring/library/Monitoring/Object/AbstractObject.php +++ b/modules/monitoring/library/Monitoring/Object/AbstractObject.php @@ -52,6 +52,7 @@ abstract class AbstractObject $this->comments = Comment::fromRequest( $this->request, array( + 'comment_internal_id', 'comment_timestamp', 'comment_author', 'comment_data', @@ -59,6 +60,7 @@ abstract class AbstractObject ) )->getQuery() ->where('comment_objecttype_id', 1) + ->fetchAll(); return $this; @@ -175,7 +177,6 @@ abstract class AbstractObject abstract public function populate(); - public static function fromRequest(Request $request) { if ($request->has('service') && $request->has('host')) { diff --git a/public/js/icinga/components/ajaxPostSubmitForm.js b/public/js/icinga/components/ajaxPostSubmitForm.js index a6ece8fe8..f5beb30bf 100644 --- a/public/js/icinga/components/ajaxPostSubmitForm.js +++ b/public/js/icinga/components/ajaxPostSubmitForm.js @@ -74,7 +74,7 @@ define(['components/app/container', 'jquery'], function(Container, $) { } }).done(function() { var container = getOwnerContainer(form); - container.replaceDomFromUrl(container.getContainerHref()); + container.setUrl(container.getUrl()); }).error(function() { submit.removeAttr('disabled'); }); diff --git a/public/js/icinga/components/container.js b/public/js/icinga/components/container.js index b568abb33..758079d04 100644 --- a/public/js/icinga/components/container.js +++ b/public/js/icinga/components/container.js @@ -26,11 +26,11 @@ */ // {{{ICINGA_LICENSE_HEADER}}} -define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITemplate'], - function($, logger, componentLoader, URI) { +define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITemplate', 'icinga/util/url'], + function($, logger, componentLoader, URI, Tpl, urlMgr) { "use strict"; - var Icinga; + var Icinga; /** * Enumeration of possible container types @@ -57,6 +57,7 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe */ var detailContainer = null; + var pendingDetailRequest = null; /** * A handler for accessing icinga containers, i.e. the #icingamain, #icingadetail containers and specific 'app/container' * components. @@ -74,12 +75,6 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe */ var Container = function(target) { - /** - * Set to true when no history changes should be made - * - * @type {boolean} true to disable History.js calls, false to reenable them - */ - this.freezeHistory = false; /** * Return the container that is at the nearest location to this element, or the element itself if it is a container @@ -116,7 +111,6 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe } else { this.containerType = CONTAINER_TYPES.GENERIC; } - this.containerDom.attr('data-icinga-href', this.getContainerHref()); if (this.containerDom.data('loadIndicator') !== true) { this.installDefaultLoadIndicator(); @@ -124,6 +118,7 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe } }; + /** * Returns the window without the hostname * @@ -133,99 +128,6 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe return window.location.pathname + window.location.search + window.location.hash; }; - /** - * Extract and return the main container's location from the current Url - * - * This takes the window's Url and removes the detail part - * - * @returns {string} The Url of the main container - */ - var getMainContainerHrefFromUrl = function(baseUrl) { - // main has the url without the icingadetail part - var href = URI(getWindowLocationWithoutHost(baseUrl)); - href.removeQuery('detail'); - return href.href(); - }; - - /** - * Return the detail container's location from the current Url - * - * This takes the detail parameter of the url and returns it or - * undefined if no location is given - * - * @returns {string|undefined} The Url of the detail container or undefined if no detail container is active - */ - var getDetailContainerHrefFromUrl = function(baseUrl) { - var location = new URI(baseUrl); - var href = URI.parseQuery(location.query()).detail; - if (!href) { - return; - } - // detail is a query param, so it is possible that (due to a bug or whatever) multiple - // detail fields are declared and returned as arrays - if (typeof href !== 'string') { - href = href[0]; - } - // transform the detail parmameter to an Url - return URI(href).href(); - }; - - /** - * Return the Url of this container - * - * This is mostly determined by the Url of the window, but for generic containers we have to rely on the - * "data-icinga-href" attribute of the container (which is also available for main and detail, but less - * reliable during history changes) - * - * @returns {String|undefined} The Url of the container or undefined if the container has no Url set - */ - this.getContainerHref = function(baseUrl) { - baseUrl = baseUrl || getWindowLocationWithoutHost(); - switch (this.containerType) { - case CONTAINER_TYPES.MAIN: - return getMainContainerHrefFromUrl(baseUrl); - case CONTAINER_TYPES.DETAIL: - return getDetailContainerHrefFromUrl(baseUrl); - case CONTAINER_TYPES.GENERIC: - if (this.containerDom.attr('data-icinga-href')) { - return URI(this.containerDom.attr('data-icinga-href')); - } else { - return URI(baseUrl).href(); - } - } - }; - - /** - * Return a href with representing the current view, but url as the main container - * - * @param {URI} url The main Url to use as an URI.js object - * - * @returns {URI} The modified URI.js containing the new main and the current detail link - */ - var setMainContainerHref = function(url, baseUrl) { - var detail = getDetailContainerHrefFromUrl(baseUrl); - if (detail) { - url.addQuery('detail', detail); - } - return url; - }; - - /** - * Return a complete Href string representing the current detail href and the provided main Url - * - * @param {URI} url The detail Url to use as an URI.js object - * - * @returns {URI} The modified URI.js containing the new detail and the current main link - */ - var setDetailContainerHref = function(url, baseUrl) { - var location = new URI(baseUrl); - location.removeQuery('detail'); - if (typeof url !== 'undefined') { // no detail Url given - location.addQuery('detail', url); - } - return location; - }; - /** * Create default load mask * @@ -234,7 +136,6 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe var createDefaultLoadIndicator = function() { this.showDetail(); - if (this.containerDom.find('div.load-indicator').length === 0) { var content = '
' + '
' + @@ -253,46 +154,6 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe this.containerDom.find('div.load-indicator').remove(); }; - /** - * Update the Url of this container and let the Url reflect the new changes, if required - * - * This updates the window Url and the data-icinga-href attribute of the container. The latter one is required - * to see which url is the last one the container displayed (e.g. after History changes, the url has changed - * but the containers data-icinga-href still points to the containers element). - * - * @param {String|URI} url An Url string or a URI.js object representing the new Url for this container - * - * @return {String} url The new Url of the application (main and detail) - */ - this.updateContainerHref = function(url, baseUrl) { - baseUrl = baseUrl || getWindowLocationWithoutHost(); - if (typeof url === "string") { - url = URI(url); - } - var containerUrl, windowUrl; - switch (this.containerType) { - case CONTAINER_TYPES.MAIN: - windowUrl = setMainContainerHref(url, baseUrl); - containerUrl = windowUrl.clone().removeQuery('detail'); - break; - case CONTAINER_TYPES.DETAIL: - windowUrl = setDetailContainerHref(url, baseUrl); - containerUrl = url; - break; - case CONTAINER_TYPES.GENERIC: - containerUrl = url; - windowUrl = baseUrl; - break; - } - - if (containerUrl) { - this.containerDom.attr('data-icinga-href', containerUrl); - } else { - this.containerDom.removeAttr('data-icinga-href'); - } - - return windowUrl.href(); - }; /** * Load the provided url, stop all pending requests for this container and call replaceDom for the returned html @@ -301,9 +162,59 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe * * @param {String, URI} url The Url to load or and URI.js object encapsulating it */ - this.replaceDomFromUrl = function(url) { + this.updateFromUrl = function(url) { + + if (this.containerType === CONTAINER_TYPES.DETAIL) { + urlMgr.setDetailUrl(url); + } else { + urlMgr.setMainUrl(url); + + } + }; + + this.replaceDomAsync = function(url) { + if (url === '') { + this.containerDom.empty(); + this.hideDetail(); + return; + } + if (pendingDetailRequest) { + pendingDetailRequest.abort(); + } this.containerDom.trigger('showLoadIndicator'); - Icinga.replaceBodyFromUrl(this.updateContainerHref(url)); + pendingDetailRequest = $.ajax({ + 'url' : url, + 'data' : { + 'render' : 'detail' + } + }).done( + (function(response) { + this.replaceDom($(response)); + }).bind(this) + ).fail( + (function(response, reason) { + var errorReason; + if (response.statusCode.toString()[0] === '4') { + errorReason = 'The Requested View Couldn\'t Be Found
'; + } else { + errorReason = 'An Internal Error Occured'; + } + this.replaceDom( + $('
').text(errorReason) + ); + }).bind(this) + ).always((function() { + this.containerDom.trigger('hideLoadIndicator'); + }).bind(this)); + }; + + this.getUrl = function() { + if (this.containerType === CONTAINER_TYPES.DETAIL) { + return urlMgr.detailUrl; + } else { + return urlMgr.mainUrl; + } + }; /** @@ -377,6 +288,51 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe this.containerDom.off('hideLoadIndicator'); }; + this.onLinkClick = function(ev, target) { + if ($.trim($(target).attr('href')) === '#') { + return true; + } + var url = URI($(target).attr('href')); + var explicitTarget = $(target).attr('data-icinga-target'); + + var isHash = ('#' + url.fragment() === url.href()); + if (isHash) { + + explicitTarget = this.containerType === CONTAINER_TYPES.MAIN ? 'main' : 'detail'; + } + if (explicitTarget) { + + urlMgr[{ + 'main' : 'setMainUrl', + 'detail' : 'setDetailUrl', + 'self' : 'setUrl' + }[explicitTarget]](url.href()); + + } else if (this.containerType === CONTAINER_TYPES.MAIN) { + urlMgr.setDetailUrl(url.href()); + } else { + urlMgr.setMainUrl(url.href()); + } + + + ev.preventDefault(); + ev.stopPropagation(); + return false; + + }; + + this.setUrl = function(url) { + if (typeof url === 'string') { + url = URI(url); + } + console.log(url); + if (this.containerType === CONTAINER_TYPES.MAIN) { + urlMgr.setMainUrl(url.href()); + } else { + urlMgr.setDetailUrl(url.href()); + } + } + this.construct(target); }; @@ -389,12 +345,11 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe * when the link should be catched and processed internally */ Container.isExternalLink = function(link) { - if (link[0] === '#') { - return true; - } return (/^\/\//).test(URI(link).relativeTo(window.location.href).href()); }; + + /** * Return the page's detail container (which is always there) * @@ -402,7 +357,7 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe */ Container.getDetailContainer = function() { detailContainer = detailContainer || new Container('#icingadetail'); - if(!jQuery.contains(document.body, detailContainer)) { + if(!jQuery.contains(document.body, mainContainer)) { detailContainer = new Container('#icingadetail'); } return detailContainer; @@ -450,6 +405,7 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe * Available as a static method on the Container object or as an instance method */ Container.prototype.hideDetail = Container.hideDetail = function() { + urlMgr.setDetailUrl(''); var mainDom = Container.getMainContainer().containerDom, detailDom = Container.getDetailContainer().containerDom; @@ -464,47 +420,7 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe mainDom.addClass('col-sm-12'); detailDom.addClass('hidden-sm'); detailDom.removeAttr('data-icinga-href'); - if (typeof this.freezeHistory === 'undefined' || !this.freezeHistory) { - History.replaceState( - {}, - document.title, - URI(window.location.href).removeQuery('detail').href() - ); - } }; - if (Modernizr.history) { - /** - * Register the click behaviour of the main container, which means that every link, if not catched in a - * more specific handler, causes an update of the main container if it's not external or a browser behaviour link - * (those starting with '#'). - */ - $('body').on('click', '#icingamain, #icingadetail', function(ev) { - - var targetEl = ev.target || ev.toElement || ev.relatedTarget; - if (targetEl.tagName.toLowerCase() !== 'a') { - return true; - } - - if (Container.isExternalLink($(targetEl).attr('href'))) { - return true; - } else { - if ($(targetEl).attr('data-icinga-target') === 'detail') { - Icinga.replaceBodyFromUrl( - detailContainer.updateContainerHref(URI($(targetEl).attr('href')).href()) - ); - } else { - Icinga.replaceBodyFromUrl( - mainContainer.updateContainerHref(URI($(targetEl).attr('href')).href()) - ); - } - - ev.preventDefault(); - ev.stopPropagation(); - return false; - } - }); - } - /** * Injects the icinga object into the Container class * @@ -516,5 +432,28 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe Icinga = icingaObj; }; + $('body').on('click', '*[data-icinga-component="app/container"], #icingamain, #icingadetail', function(ev) { + var targetEl = ev.target || ev.toElement || ev.relatedTarget; + + if (targetEl.tagName.toLowerCase() !== 'a') { + targetEl = $(targetEl).parents('a')[0]; + if (!targetEl) { + return true; + } + } + return (new Container(targetEl)).onLinkClick(ev, targetEl); + + }); + + $(window).on('hashchange', (function() { + urlMgr.syncWithUrl(); + Container.getDetailContainer().replaceDomAsync(urlMgr.detailUrl); + })); + + + if (urlMgr.detailUrl) { + Container.getDetailContainer().replaceDomAsync(urlMgr.detailUrl); + } + return Container; }); diff --git a/public/js/icinga/components/mainDetailGrid.js b/public/js/icinga/components/mainDetailGrid.js index da8232413..5b817a088 100644 --- a/public/js/icinga/components/mainDetailGrid.js +++ b/public/js/icinga/components/mainDetailGrid.js @@ -25,8 +25,8 @@ * @author Icinga Development Team */ // {{{ICINGA_LICENSE_HEADER}}} -define(['components/app/container', 'jquery', 'logging', 'URIjs/URI', 'URIjs/URITemplate'], -function(Container, $, logger, URI) { +define(['components/app/container', 'jquery', 'logging', 'URIjs/URI', 'URIjs/URITemplate', 'icinga/util/url'], +function(Container, $, logger, URI, tpl, urlMgr) { "use strict"; /** @@ -141,7 +141,7 @@ function(Container, $, logger, URI) { } } - Container.getDetailContainer().replaceDomFromUrl($('a', this).attr('href')); + urlMgr.setDetailUrl($('a', this).attr('href')); if (!ev.ctrlKey && !ev.metaKey) { $('tr', $(this).parent()).removeClass('active'); } @@ -160,24 +160,31 @@ function(Container, $, logger, URI) { controlForms.on('submit', function(evt) { var container = (new Container(this)); var form = $(this); - var url = URI(container.getContainerHref()); - url.search(URI.parseQuery(form.serialize())); - container.replaceDomFromUrl(url.href()); + var url = container.getUrl(); + + if (url.indexOf('?') >= 0) { + url += '&'; + } else { + url += '?'; + } + url += form.serialize(); + container.setUrl(url); evt.preventDefault(); evt.stopPropagation(); return false; }); - $('.pagination li a', contentNode.parent()).on('click', function(ev) { + $('.pagination li a, a.filter-badge', contentNode.parent()).on('click', function(ev) { var container = (new Container(this)); - logger.debug("Pagination clicked in " + container.containerType); + // Detail will be removed when main pagination changes if (container.containerType === 'icingamain') { - Icinga.replaceBodyFromUrl(URI($(this).attr('href')).removeQuery('detail')); + urlMgr.setMainUrl(URI($(this).attr('href'))); + urlMgr.setDetailUrl(''); } else { - container.replaceDomFromUrl($(this).attr('href')); + urlMgr.setDetailUrl(URI($(this).attr('href'))); } ev.preventDefault(); @@ -187,7 +194,7 @@ function(Container, $, logger, URI) { }; var getSelectedRows = function() { - return $('a[href="' + Container.getDetailContainer().getContainerHref() + '"]', determineContentTable()). + return $('a[href="' + urlMgr.getDetailUrl() + '"]', determineContentTable()). parentsUntil('table', 'tr'); }; @@ -214,9 +221,11 @@ function(Container, $, logger, URI) { this.container.removeDefaultLoadIndicator(); controlForms = determineControlForms(); contentNode = determineContentTable(); + this.syncSelectionWithDetail(); this.registerControls(); this.registerTableLinks(); this.registerHistoryChanges(); + }; this.construct(gridDomNode); diff --git a/public/js/icinga/components/semanticsearch.js b/public/js/icinga/components/semanticsearch.js index 648bb80de..1ba0dc449 100644 --- a/public/js/icinga/components/semanticsearch.js +++ b/public/js/icinga/components/semanticsearch.js @@ -53,8 +53,7 @@ define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function( * Request new proposals for the given input box */ this.getProposal = function() { - var text = this.inputDom.val().trim(); - + var text = $.trim(this.inputDom.val()); if (this.pendingRequest) { this.pendingRequest.abort(); @@ -114,9 +113,9 @@ define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function( return { data: { 'cache' : (new Date()).getTime(), - 'query' : query, - 'filter_domain' : this.domain, - 'filter_module' : this.module + 'query' : query, + 'filter_domain' : this.domain, + 'filter_module' : this.module }, headers: { 'Accept': 'application/json' @@ -131,7 +130,8 @@ define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function( * @param {Object} response The jquery response object inheritn XHttpResponse Attributes */ this.showProposals = function(response) { - if (response.proposals.length === 0) { + + if (!response || !response.proposals || response.proposals.length === 0) { this.inputDom.popover('destroy'); return; } @@ -167,10 +167,12 @@ define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function( var query = $.trim(this.inputDom.val()); this.pendingRequest = $.ajax(this.getRequestParams(query)) .done((function(response) { - var container = new Container($(this.inputDom)); - var url = container.getContainerHref(); - url += ( url.indexOf('?') === -1 ? '?' : '&' ) + response.urlParam; - container.replaceDomFromUrl(url); + var domContainer = new Container(this.inputDom); + var url = response.urlParam; + + if (url) { + domContainer.setUrl(url); + } }).bind(this)); }; diff --git a/public/js/icinga/icinga.js b/public/js/icinga/icinga.js index ec3b6a474..48840f4e3 100755 --- a/public/js/icinga/icinga.js +++ b/public/js/icinga/icinga.js @@ -31,90 +31,94 @@ define([ 'logging', 'icinga/componentLoader', 'components/app/container', - 'URIjs/URI' -], function ($, log, components, Container, URI) { + 'URIjs/URI', + 'icinga/util/url' +], function ($, log, components, Container, URI, urlMgr) { 'use strict'; /** * Icinga prototype */ var Icinga = function() { + var pendingRequest = null; - var ignoreHistoryChanges = false; - + /** + * Initia + */ var initialize = function () { components.load(); - ignoreHistoryChanges = true; - registerGenericHistoryHandler(); - ignoreHistoryChanges = false; log.debug("Initialization finished"); - }; /** - * Register handler for handling the history state generically + * Globally open the given url and reload the main/detail box to represent it * + * @param url The url to load */ - var registerGenericHistoryHandler = function() { - var lastUrl = URI(window.location.href); - History.Adapter.bind(window, 'popstate', function() { - if (ignoreHistoryChanges) { + this.openUrl = function(url) { + if (pendingRequest) { + pendingRequest.abort(); + } + pendingRequest = $.ajax({ + "url": url + }).done(function(response) { + var dom = $(response); + var detailDom = null; + if (urlMgr.detailUrl) { + detailDom = $('#icingadetail'); + } + $(document.body).empty().append(dom); + if (detailDom && detailDom.length) { + $('#icingadetail').replaceWith(detailDom); + Container.showDetail(); + } + components.load(); + Container.getMainContainer(); + }).fail(function(response, reason) { + if (reason === 'abort') { return; } - - gotoUrl(History.getState().url); - lastUrl = URI(window.location.href); + log.error("Request failed: ", response.message); }); }; - var gotoUrl = function(href) { - if (typeof document.body.pending !== 'undefined') { - document.body.pending.abort(); - } - if (typeof href === 'string') { - href = URI(href); - } - document.body.pending = $.ajax({ - url: href.href() - }).done(function(domNodes) { - $('body').empty().append(jQuery.parseHTML(domNodes)); - ignoreHistoryChanges = true; - History.pushState({}, document.title, href.href()); - ignoreHistoryChanges = false; - components.load(); - }).error(function(xhr, textStatus, errorThrown) { - if (xhr.responseText) { - $('body').empty().append(jQuery.parseHTML(xhr.responseText)); - } else if (textStatus !== 'abort') { - logging.emergency('Could not load URL', xhr.href, textStatus, errorThrown); - } - }); - - return false; - }; - if (Modernizr.history) { - $(document.body).on('click', '#icinganavigation', function(ev) { - var targetEl = ev.target || ev.toElement || ev.relatedTarget; - if (targetEl.tagName.toLowerCase() !== 'a') { - return true; + /** + * Event handler that will be called when the url change + */ + urlMgr.syncWithUrl(); + var lastMain = urlMgr.mainUrl; + $(window).on('pushstate', (function() { + urlMgr.syncWithUrl(); + if (urlMgr.mainUrl !== lastMain) { + this.openUrl(urlMgr.getUrl()); + lastMain = urlMgr.mainUrl; + } + // If an anchor is set, scroll to it's position + if ($('#' + urlMgr.anchor).length) { + $(document.body).scrollTo($('#' + urlMgr.anchor)); + } + }).bind(this)); + + /** + * Event handler for browser back/forward events + */ + $(window).on('popstate', (function() { + var lastMain = urlMgr.mainUrl; + urlMgr.syncWithUrl(); + if (urlMgr.mainUrl !== lastMain) { + this.openUrl(urlMgr.getUrl()); } - var href = $(targetEl).attr('href'); - if (Container.isExternalLink(href)) { - return true; - } - ev.preventDefault(); - ev.stopPropagation(); - gotoUrl(href); - return false; - }); + }).bind(this)); } + $(document).ready(initialize.bind(this)); Container.setIcinga(this); + this.components = components; - this.replaceBodyFromUrl = gotoUrl; + }; diff --git a/public/js/icinga/util/url.js b/public/js/icinga/util/url.js new file mode 100644 index 000000000..7fb337a33 --- /dev/null +++ b/public/js/icinga/util/url.js @@ -0,0 +1,194 @@ +// {{{ICINGA_LICENSE_HEADER}}} +/** + * This file is part of Icinga 2 Web. + * + * Icinga 2 Web - Head for multiple monitoring backends. + * Copyright (C) 2013 Icinga Development Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * @copyright 2013 Icinga Development Team + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +define(['jquery', 'URIjs/URI'], function($, URI, Container) { + "use strict"; + + var currentUrl = URI(window.location.href); + + /** + * Utility class for Url handling + * + */ + var URLMgr = function() { + /** + * The current url of the main part + * @type {string} + */ + this.mainUrl = ''; + + /** + * The current main anchor + * @type {string} + */ + this.anchor = ''; + + /** + * The current detail url + * + * @type {string} + */ + this.detailUrl = ''; + + /** + * The current anchor of the detail url + * + * @type {string} + */ + this.detailAnchor = ''; + + /** + * Extract the anchor of the main url part from the given url + * + * @param {String|URI} url An URL object to extract the information from + * @returns {*} + */ + this.getMainAnchor = function(url) { + url = url || URI(window.location.href); + if (typeof url === 'string') { + url = URI(url); + } + var fragment = url.fragment(); + if (fragment.length === 0) { + return ''; + } + var parts = fragment.split('!'); + if (parts.length > 0) { + return parts[0]; + } else { + return ''; + } + }; + + /** + * Extract the detail url a the given url. Returns a [URL, ANCHOR] Tupel + * + * @param String url An optional url to parse (otherwise window.location.href is used) + * @returns {Array} A [{String} Url, {String} anchor] tupel + */ + this.getDetailUrl = function(url) { + url = url || URI(window.location.href); + if (typeof url === 'string') { + url = URI(url); + } + + var fragment = url.fragment(); + if (fragment.length === 0) { + return ''; + } + var parts = fragment.split('!', 2); + + if (parts.length === 2) { + var result = /detail=(.*)$/.exec(parts[1]); + if (!result || result.length < 2) { + return ''; + } + return result[1].replace('%23', '#').split('#'); + } else { + return ''; + } + }; + + /** + * Overwrite the detail Url and update the hash + * + * @param String url The url to use for the detail part + */ + this.setDetailUrl = function(url) { + if (typeof url === 'string') { + url = URI(url); + } + if( !url.fragment() || url.href() !== '#' + url.fragment()) { + this.detailUrl = url.clone().fragment('').href(); + } + this.detailAnchor = this.getMainAnchor(url); + + window.location.hash = this.getUrlHash(); + }; + + /** + * Get the hash of the current detail url and anchor i + * + * @returns {string} + */ + this.getUrlHash = function() { + var anchor = '#' + this.anchor + + '!' + ($.trim(this.detailUrl) ? 'detail=' : '') + this.detailUrl + + (this.detailAnchor ? '%23' : '') + this.detailAnchor; + anchor = $.trim(anchor); + if (anchor === '#!' || anchor === '#') { + anchor = ''; + } + return anchor; + }; + + /** + * Set the main url to be used + * + * This triggers the pushstate event or causes a page reload if the history api is + * not available + * + * @param url + */ + this.setMainUrl = function(url) { + this.anchor = this.getMainAnchor(url); + this.mainUrl = URI(url).clone().fragment('').href(); + if (!Modernizr.history) { + window.location.href = this.mainUrl + this.getUrlHash(); + } else { + window.history.pushState({}, document.title, this.mainUrl + this.getUrlHash()); + $(window).trigger('pushstate'); + } + }; + + /** + * Return the href (main path + hash) + * + * @returns {string} + */ + this.getUrl = function() { + return this.mainUrl + this.getUrlHash(); + }; + + /** + * Take the current url and sync the internal state of this url manager with it + */ + this.syncWithUrl = function() { + this.mainUrl = URI(window.location.href).clone().fragment('').href(); + this.anchor = this.getMainAnchor(); + var urlAnchorTupel = this.getDetailUrl(); + this.detailUrl = urlAnchorTupel[0] || ''; + this.detailAnchor = urlAnchorTupel[1] || ''; + }; + + + this.syncWithUrl(); + }; + var urlMgr = new URLMgr(); + + return urlMgr; +}); diff --git a/public/js/main.js b/public/js/main.js index 6bdf90485..79cf52d29 100755 --- a/public/js/main.js +++ b/public/js/main.js @@ -3,9 +3,8 @@ requirejs.config({ 'urlArgs': "bust=" + (new Date()).getTime(), 'paths': { 'jquery': 'vendor/jquery-1.8.3', - 'jqueryPlugins': 'vendor/jqueryPlugins/', + 'jquery_scrollto': 'vendor/jquery.scrollto', 'bootstrap': 'vendor/bootstrap/bootstrap.min', - 'history': 'vendor/history', 'logging': 'icinga/util/logging', 'URIjs': 'vendor/uri', 'datetimepicker': 'vendor/bootstrap/datetimepicker.min' @@ -14,21 +13,23 @@ requirejs.config({ 'datetimepicker': { 'exports': 'datetimepicker' }, + 'jquery_scrollto': { + exports: 'jquery_scrollto' + }, 'jquery' : { exports: 'jquery' } } }); -define(['jquery', 'history'], function ($) { +define(['jquery'], function ($, history) { + window.$ = $; + window.jQuery = $; - requirejs(['bootstrap'], function() { + requirejs(['bootstrap','jquery_scrollto'], function() { requirejs(['datetimepicker']); }); - requirejs(['icinga/icinga'], function (Icinga) { - window.$ = $; - window.jQuery = $; window.Icinga = Icinga; }); diff --git a/public/js/vendor/history.js b/public/js/vendor/history.js index fa8bdd192..df9a84f94 100644 --- a/public/js/vendor/history.js +++ b/public/js/vendor/history.js @@ -1 +1 @@ -(function(e,t){"use strict";var n=e.History=e.History||{};if(typeof n.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");n.Adapter={handlers:{},_uid:1,uid:function(e){return e._uid||(e._uid=n.Adapter._uid++)},bind:function(e,t,r){var i=n.Adapter.uid(e);n.Adapter.handlers[i]=n.Adapter.handlers[i]||{},n.Adapter.handlers[i][t]=n.Adapter.handlers[i][t]||[],n.Adapter.handlers[i][t].push(r),e["on"+t]=function(e,t){return function(r){n.Adapter.trigger(e,t,r)}}(e,t)},trigger:function(e,t,r){r=r||{};var i=n.Adapter.uid(e),s,o;n.Adapter.handlers[i]=n.Adapter.handlers[i]||{},n.Adapter.handlers[i][t]=n.Adapter.handlers[i][t]||[];for(s=0,o=n.Adapter.handlers[i][t].length;s")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) +typeof JSON!="object"&&(JSON={}),function(){"use strict";function f(e){return e<10?"0"+e:e}function quote(e){return escapable.lastIndex=0,escapable.test(e)?'"'+e.replace(escapable,function(e){var t=meta[e];return typeof t=="string"?t:"\\u"+("0000"+e.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+e+'"'}function str(e,t){var n,r,i,s,o=gap,u,a=t[e];a&&typeof a=="object"&&typeof a.toJSON=="function"&&(a=a.toJSON(e)),typeof rep=="function"&&(a=rep.call(t,e,a));switch(typeof a){case"string":return quote(a);case"number":return isFinite(a)?String(a):"null";case"boolean":case"null":return String(a);case"object":if(!a)return"null";gap+=indent,u=[];if(Object.prototype.toString.apply(a)==="[object Array]"){s=a.length;for(n=0;n")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) diff --git a/public/js/vendor/jquery.scrollto.js b/public/js/vendor/jquery.scrollto.js new file mode 100644 index 000000000..3d1f1f2bc --- /dev/null +++ b/public/js/vendor/jquery.scrollto.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) 2007-2013 Ariel Flesler - afleslergmailcom | http://flesler.blogspot.com + * Dual licensed under MIT and GPL. + * @author Ariel Flesler + * @version 1.4.6 + */ +;(function($){var h=$.scrollTo=function(a,b,c){$(window).scrollTo(a,b,c)};h.defaults={axis:'xy',duration:parseFloat($.fn.jquery)>=1.3?0:1,limit:true};h.window=function(a){return $(window)._scrollable()};$.fn._scrollable=function(){return this.map(function(){var a=this,isWin=!a.nodeName||$.inArray(a.nodeName.toLowerCase(),['iframe','#document','html','body'])!=-1;if(!isWin)return a;var b=(a.contentWindow||a).document||a.ownerDocument||a;return/webkit/i.test(navigator.userAgent)||b.compatMode=='BackCompat'?b.body:b.documentElement})};$.fn.scrollTo=function(e,f,g){if(typeof f=='object'){g=f;f=0}if(typeof g=='function')g={onAfter:g};if(e=='max')e=9e9;g=$.extend({},h.defaults,g);f=f||g.duration;g.queue=g.queue&&g.axis.length>1;if(g.queue)f/=2;g.offset=both(g.offset);g.over=both(g.over);return this._scrollable().each(function(){if(e==null)return;var d=this,$elem=$(d),targ=e,toff,attr={},win=$elem.is('html,body');switch(typeof targ){case'number':case'string':if(/^([+-]=?)?\d+(\.\d+)?(px|%)?$/.test(targ)){targ=both(targ);break}targ=$(targ,this);if(!targ.length)return;case'object':if(targ.is||targ.style)toff=(targ=$(targ)).offset()}$.each(g.axis.split(''),function(i,a){var b=a=='x'?'Left':'Top',pos=b.toLowerCase(),key='scroll'+b,old=d[key],max=h.max(d,a);if(toff){attr[key]=toff[pos]+(win?0:old-$elem.offset()[pos]);if(g.margin){attr[key]-=parseInt(targ.css('margin'+b))||0;attr[key]-=parseInt(targ.css('border'+b+'Width'))||0}attr[key]+=g.offset[pos]||0;if(g.over[pos])attr[key]+=targ[a=='x'?'width':'height']()*g.over[pos]}else{var c=targ[pos];attr[key]=c.slice&&c.slice(-1)=='%'?parseFloat(c)/100*max:c}if(g.limit&&/^\d+$/.test(attr[key]))attr[key]=attr[key]<=0?0:Math.min(attr[key],max);if(!i&&g.queue){if(old!=attr[key])animate(g.onAfterFirst);delete attr[key]}});animate(g.onAfter);function animate(a){$elem.animate(attr,f,g.easing,a&&function(){a.call(this,targ,g)})}}).end()};h.max=function(a,b){var c=b=='x'?'Width':'Height',scroll='scroll'+c;if(!$(a).is('html,body'))return a[scroll]-$(a)[c.toLowerCase()]();var d='client'+c,html=a.ownerDocument.documentElement,body=a.ownerDocument.body;return Math.max(html[scroll],body[scroll])-Math.min(html[d],body[d])};function both(a){return typeof a=='object'?a:{top:a,left:a}}})(jQuery); +;(function($){var h=location.href.replace(/#.*/,'');var i=$.localScroll=function(a){$('body').localScroll(a)};i.defaults={duration:1000,axis:'y',event:'click',stop:true,target:window};i.hash=function(a){if(location.hash){a=$.extend({},i.defaults,a);a.hash=false;if(a.reset){var d=a.duration;delete a.duration;$(a.target).scrollTo(0,a);a.duration=d}scroll(0,location,a)}};$.fn.localScroll=function(b){b=$.extend({},i.defaults,b);return b.lazy?this.bind(b.event,function(e){var a=$([e.target,e.target.parentNode]).filter(filter)[0];if(a)scroll(e,a,b)}):this.find('a,area').filter(filter).bind(b.event,function(e){scroll(e,this,b)}).end().end();function filter(){return!!this.href&&!!this.hash&&this.href.replace(this.hash,'')==h&&(!b.filter||$(this).is(b.filter))}};function scroll(e,a,b){var c=a.hash.slice(1),elem=document.getElementById(c)||document.getElementsByName(c)[0];if(!elem)return;if(e)e.preventDefault();var d=$(b.target);if(b.lock&&d.is(':animated')||b.onBefore&&b.onBefore(e,elem,d)===false)return;if(b.stop)d._scrollable().stop(true);if(b.hash){var f=b.offset;f=f&&f.top||f||0;var g=elem.id==c?'id':'name',$a=$(' ').attr(g,c).css({position:'absolute',top:$(window).scrollTop()+f,left:$(window).scrollLeft()});elem[g]='';$('body').prepend($a);location=a.hash;$a.remove();elem[g]=c}d.scrollTo(elem,b).trigger('notify.serialScroll',[elem])}})(jQuery); \ No newline at end of file diff --git a/test/js/test/icinga/components/containerTest.js b/test/js/test/icinga/components/containerTest.js index 04bb0fad3..2bc65d5e0 100644 --- a/test/js/test/icinga/components/containerTest.js +++ b/test/js/test/icinga/components/containerTest.js @@ -96,8 +96,10 @@ describe('The container component', function() { */ it('should provide access to the main and detail component', function() { createDOM(); + rjsmock.registerDependencies({ - 'URIjs/URI' : URI + 'URIjs/URI' : URI, + 'icinga/util/url' : 'icinga/util/url.js' }); requireNew('icinga/components/container.js'); var Container = rjsmock.getDefine(); @@ -109,41 +111,4 @@ describe('The container component', function() { $('#icingadetail')[0], 'Assert the DOM of the detail container being #icingadetail'); }); - /** - * Test dynamic Url update - */ - it('should automatically update its part of the URL if assigning a new URL', function() { - rjsmock.registerDependencies({ - 'URIjs/URI' : URI - }); - requireNew('icinga/components/container.js'); - createDOM(); - var Container = rjsmock.getDefine(); - var url = Container.getMainContainer().updateContainerHref('/some/other/url?test'); - window.setWindowUrl(url); - Container.getMainContainer().containerDom.attr('data-icinga-href').should.equal('/some/other/url?test'); - - url.should.equal( - '/some/other/url?test', - 'Assert the main container updating the url correctly'); - - url = Container.getDetailContainer().updateContainerHref('/some/detail/url?test'); - window.setWindowUrl(url); - - Container.getDetailContainer().containerDom.attr('data-icinga-href').should.equal('/some/detail/url?test'); - url.should.equal( - '/some/other/url?test&detail=' + encodeURIComponent('/some/detail/url?test'), - 'Assert the detail container only updating the "detail" portion of the URL' - ); - - url = Container.getMainContainer().updateContainerHref('/some/other2/url?test=test'); - - window.setWindowUrl(Container.getMainContainer().getContainerHref(window.location.href)); - Container.getMainContainer().containerDom.attr('data-icinga-href').should.equal('/some/other2/url?test=test'); - url.should.equal( - '/some/other2/url?test=test&detail=' + encodeURIComponent('/some/detail/url?test'), - 'Assert the main container keeping the detail portion untouched if being assigned a new URL' - ); - }); - }); diff --git a/test/php/library/Icinga/Web/Widget/TabTest.php b/test/php/library/Icinga/Web/Widget/TabTest.php index c9bf03180..02b7dd9cb 100644 --- a/test/php/library/Icinga/Web/Widget/TabTest.php +++ b/test/php/library/Icinga/Web/Widget/TabTest.php @@ -107,7 +107,7 @@ class TabTest extends PHPUnit_Framework_TestCase $this->assertEquals( 1, preg_match( - '/
  • Title text<\/a><\/li>/i', + '/
  • Title text<\/a><\/li>/i', $html ), 'Asserting an url being rendered inside an HTML anchor. got ' . $html