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 
This commit is contained in:
Jannis Moßhammer 2013-10-17 19:48:46 +02:00
parent 3df8cacea8
commit 4a95ba3468
32 changed files with 722 additions and 397 deletions

@ -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);

@ -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
{
/**

@ -31,10 +31,10 @@ $modules = $this->modules->paginate();
<td>
<? if ($module->enabled): ?>
<i>{{OK_ICON}}</i>
<a href="<?= $disableUrl ?>"><?= $this->escape($module->name); ?></a>
<a href="<?= $disableUrl ?>" data-icinga-target="main"><?= $this->escape($module->name); ?></a>
<? else: ?>
<i>{{REMOVE_ICON}}</i>
<a href="<?= $enableUrl ?>"><?= $this->escape($module->name); ?></a>
<a href="<?= $enableUrl ?>" data-icinga-target="main"><?= $this->escape($module->name); ?></a>
<? endif ?>
(<?=
$module->enabled ? ($module->loaded ? 'enabled' : 'failed') : 'disabled' ?>)

@ -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)) {

@ -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);
}

@ -0,0 +1,43 @@
<?php
// {{{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 <info@icinga.org>
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
* @author Icinga Development Team <info@icinga.org>
*/
// {{{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);
}

@ -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;
}
}
/**

@ -62,4 +62,6 @@ class Request extends Zend_Controller_Request_Http
{
return $this->user;
}
}

@ -75,7 +75,7 @@ class FilterBadgeRenderer implements Widget
$url = $this->urlFilter->fromTree($newTree);
$url = $basePath . (empty($allParams) ? '?' : '&') . $url;
return ' <a class="btn btn-default btn-xs" href="' . $url . '">'
return ' <a class="filter-badge btn btn-default btn-xs" href="' . $url . '">'
. $this->conjunctionCellar . ' '
. ucfirst($node->left) . ' '
. $node->operator . ' '

@ -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);

@ -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);

@ -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 = '<a' . $tagParams .' href="' . $this->url->getAbsoluteUrl() . '">' . $caption . '</a>';
$tab = '<a' . $tagParams .' href="' . $this->url->getAbsoluteUrl() . '" data-icinga-target="self">' . $caption . '</a>';
} else {
$tab = $caption;
}

@ -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'
);

@ -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();
}

@ -5,8 +5,19 @@ $viewHelper = $this->getHelper('MonitoringState');
<?= $this->tabs->render($this); ?>
<h1>Services Status</h1>
<div data-icinga-component="app/mainDetailGrid">
<?= $this->sortControl->render($this); ?>
<?= $this->paginationControl($services, null, null, array('preserve' => $this->preserve)); ?>
<div class="container">
<div class="row">
<div class="col-md-5">
<?= $this->filterBox->render($this); ?>
</div>
<div class="col-md-7">
<?= $this->sortControl->render($this); ?>
</div>
</div>
<div class="row">
<?= $this->paginationControl($services, null, null, array('preserve' => $this->preserve)); ?>
</div>
</div>
<table class="table table-condensed">
<tbody>
<?php foreach ($services as $service): ?>

@ -13,6 +13,7 @@ use Icinga\Web\Controller\ActionController;
*/
class Controller extends ActionController
{
/**
* Retrieve services from either given parameters or request
*

@ -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) {

@ -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;
}
}

@ -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,

@ -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
*

@ -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')) {

@ -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');
});

@ -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 = '<div class="load-indicator">' +
'<div class="mask"></div>' +
@ -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<br/>';
} else {
errorReason = 'An Internal Error Occured';
}
this.replaceDom(
$('<div class="alert alert-danger">').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;
});

@ -25,8 +25,8 @@
* @author Icinga Development Team <info@icinga.org>
*/
// {{{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);

@ -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));
};

@ -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;
};

@ -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 <info@icinga.org>
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
* @author Icinga Development Team <info@icinga.org>
*/
// {{{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;
});

@ -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;
});

File diff suppressed because one or more lines are too long

8
public/js/vendor/jquery.scrollto.js vendored Normal file

@ -0,0 +1,8 @@
/**
* Copyright (c) 2007-2013 Ariel Flesler - aflesler<a>gmail<d>com | 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=$('<a> </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);

@ -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'
);
});
});

@ -107,7 +107,7 @@ class TabTest extends PHPUnit_Framework_TestCase
$this->assertEquals(
1,
preg_match(
'/<li *><a href="\/my\/url">Title text<\/a><\/li>/i',
'/<li *><a href="\/my\/url".*>Title text<\/a><\/li>/i',
$html
),
'Asserting an url being rendered inside an HTML anchor. got ' . $html