Document form and container behaviour and simplify
Now the whole page gets refreshed on container changes, if we encounter issues with that we can improve it afterwards or roll back some cahnges already made in previous commits refs #4611
This commit is contained in:
parent
b4000d34fb
commit
376dc8cd0f
|
@ -0,0 +1,142 @@
|
|||
# The Container Component (app/container)
|
||||
|
||||
The container component is the most basic building block for icingaweb. Even when displaying an empty controller,
|
||||
you always have at least two containers in your viewport which are implicitly created: The main and the detail container.
|
||||
|
||||
Container handle the following tasks:
|
||||
|
||||
* Updating the url part responsible for the container
|
||||
* Handling Url changes like they occur when the browser history is used by synchronizing their content with the
|
||||
associated Url part
|
||||
* Informing subcomponents about changes in the container
|
||||
|
||||
|
||||
## The Container Api
|
||||
|
||||
You can find the sourcecode for containers along with jsdoc comments at *./public/js/icinga/components/container.js*.
|
||||
Here we will discuss the most important calls and their synopsis:
|
||||
|
||||
### Accessing Containers:
|
||||
|
||||
The container component returns a 'Container' object which allows you to access responsible containers for dom nodes via
|
||||
the following methods:
|
||||
|
||||
* using `new Container($myDomNodes)` which returns a stateless container object wrapping the container responsible for
|
||||
the first node in $myDomNodes
|
||||
* using `Container.getMainContainer()` or `Container.getDetailContainer()` which remove the main or detail container
|
||||
(this one is stateful with a few notes, read on)
|
||||
|
||||
**Note:** `new Container($('#icingamain')) != Container.getMainContainer()`, but
|
||||
`(new Container($('#icingamain'))).containerDom == Container.getMainContainer().containerDom`
|
||||
|
||||
** Example #1 getting the container responsible for a dom node **
|
||||
|
||||
**HTML**
|
||||
|
||||
<div id="icingamain">
|
||||
<div class="myNode">
|
||||
Some kind of node
|
||||
</div>
|
||||
<div id="somecontainer" data-icinga-component="app/container">
|
||||
<div class="mySecondNode">
|
||||
Some other kind of node
|
||||
<p>
|
||||
Insert your lorem ipsum here
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
**JS**:
|
||||
|
||||
require(['jquery', 'app/container'], function($, Container) {
|
||||
var firstContainer = new Container($('.myNode')); // firstContainer wraps '#icingamain'
|
||||
var mainContainer = Container.getMainContainer(); // also wraps '#icingamain'
|
||||
var secondContainer = new Container($('.myNode p')); // #somecontainer is wrapped by secondContainer
|
||||
|
||||
firstContainer.someProperty = 'What a nice property!';
|
||||
mainContainer.someState = 'I have some state';
|
||||
console.log(firstContainer.someProperty); // return 'What a nice property'
|
||||
console.log(main.someProperty); // return 'undefined'
|
||||
console.log(Container.getMainContainer().someState) // return 'I have some state' when page hasn't been refreshed
|
||||
});
|
||||
|
||||
## Containers And The Browser Url
|
||||
|
||||
As noted before (and indicated by the `getMainContainer()` and `getDetailContainer()` function), the main and detail
|
||||
container have a special role. Considering the following Url:
|
||||
|
||||
http://my.monitoringhost.org/icingaweb/monitoring/list/host?page=4&detail=%2Fmonitoring%2Fshow%2Fhost%3Fhost%3Dlocalhost
|
||||
|
||||
This URL displays the 4th page of your host list in the main container (monitoring/list/host?page=4) and the host information
|
||||
for localhost in the detail container (monitoring/show/host?host=localhost). When you split this Url up in logical pieces
|
||||
it looks like this:
|
||||
|
||||
http://my.monitoringhost.org/icingaweb/monitoring/list/host?page=4&detail=%2Fmonitoring%2Fshow%2Fhost%3Fhost%3Dlocalhost
|
||||
\___________ _______________________/\_________ ______________/ \_ ____/\________________ _______________________/
|
||||
\/ \/ \/ \/
|
||||
1. Base URL 2.Main container URL and Query 3.Detail param 4. Encoded detail URL and params
|
||||
|
||||
1. **Base URL** : I don't think this needs much explanation.
|
||||
2. **Main container URL and query** : This is the *normal* part of your Url and denotes the controller route that is
|
||||
being displayed in your main container
|
||||
3. **Detail parameter**: This parameter will be ignored by the main container and used for rendering the detail container,
|
||||
if omitted there's simple no detail view to be displayed
|
||||
4 **Encoded detail URL**: The value of the "detail" parameter is the Url (without the base Url) that returns the content
|
||||
of the detail area
|
||||
|
||||
|
||||
### Updating A Container's Url
|
||||
|
||||
If you want your container to display content from a different Url, you can use the *replaceDomFromUrl()* on your
|
||||
Container object:
|
||||
|
||||
**Example #2 Updating A Containers URL**
|
||||
|
||||
**HTML:**
|
||||
|
||||
<div id="icingamain">
|
||||
<div id"mainSub"></div>
|
||||
</div>
|
||||
<div id="icingadetail">
|
||||
<div id"detailSub"></div>
|
||||
</div>
|
||||
|
||||
**JS:**
|
||||
|
||||
// this loads the page with the new main container
|
||||
require(['jquery', 'app/container'], function($, Container) {
|
||||
new Container('#mainSub').replaceDomFormUrl('/another/url');
|
||||
}
|
||||
|
||||
// this loads the page with the new detail container
|
||||
require(['jquery', 'app/container'], function($, Container) {
|
||||
new Container('#detailSub').replaceDomFormUrl('/another/url');
|
||||
}
|
||||
|
||||
// this does NOT work:
|
||||
require(['jquery', 'app/container'], function($, Container) {
|
||||
Container.getMainContainer().replaceDomFormUrl('/another/url');
|
||||
// will never be reached due to a reload
|
||||
Container.getMainContainer().replaceDomFormUrl('/another/url2');
|
||||
}
|
||||
|
||||
// this loads the page with both main and detail changed (this is rarely needed and should be avoided)
|
||||
require(['icinga', 'jquery', 'app/container'], function('Icinga', $, Container) {
|
||||
// it's better to use this:
|
||||
var mainContainer = Container.getMainContainer();
|
||||
var detailContainer = Container.getDetailContainer();
|
||||
|
||||
mainContainer.updateContainerHref('/another/url'); // first update the main container href
|
||||
detailContainer.updateContainerHref('/another/url2'); // update the detail href
|
||||
|
||||
var url = mainContainer.getContainerHref(detailContainer.getContainerHref()); // fetch the new url
|
||||
Icinga.replaceBodyFromUrl(url); // and update manual
|
||||
}
|
||||
|
||||
This results in the URL changing to './another/url?detail=%2Fanother%2Fdetail%2Furl.
|
||||
The advantage of using a Container instance with the subelements (i.e. '\#mainSub') over calling getMain/DetailContainer
|
||||
directly is that you don't need to know in what container your view is displayed - when you move 'mainSub' into the
|
||||
detail container, the detail container would be updated afterwards.
|
||||
|
||||
**NOTE**: You should read the '...' section in order to understand why you shouldn't do it like in this example
|
19
doc/form.md
19
doc/form.md
|
@ -11,10 +11,6 @@ Forms are basically Zend_Form classes with Zend_Form_Element items as controls.
|
|||
To ensure common functionallity and control dependent fields Icinga 2 Web
|
||||
provides sub classes to build forms on that.
|
||||
|
||||
![Basic form design][form1]
|
||||
|
||||
*(Methods and attributes are exemplary and does not reflect the full class implementation)*
|
||||
|
||||
### Key design
|
||||
|
||||
#### Build of forms
|
||||
|
@ -28,6 +24,20 @@ In order to let icingaweb create a submit button for you (which is required for
|
|||
method) you have to call the *setSubmitLabel($label)* method, which will add a
|
||||
Zend_Form_Element_Submit element to your form.
|
||||
|
||||
#### Client side behaviour
|
||||
|
||||
A few methods in our Form implementation don't affect the rendering, but the behaviour of a form.
|
||||
|
||||
* **Automatic submission of fields via *\Icinga\Web\Form::enableAutoSubmit(array $forms)***
|
||||
All form element ids passed in the $forms array will cause a submission of the form when changed. This normally
|
||||
doesn't cause validation errors, as the form is not really seen as submitted by the 'isSubmittedAndValid', but allows
|
||||
you to update your form when necessary. For example, when you have a select box whose selection affects which form elements
|
||||
should be shown, you can use `$form->enableAutoSubmit(array('myForm'))`.
|
||||
* **User confirmation when discarding forms.** When a user wants to leave the current page despite having unsaved changes,
|
||||
a popup will appear and asks the user if he **really** wants to leave. This is implemented by the *app/form* componenent
|
||||
and enabled by default. If you don't want this, you can call *setIgnoreChangeDiscarding($bool)* on your form.
|
||||
|
||||
|
||||
#### Calling is *isSubmittedAndValid()*
|
||||
|
||||
*isSubmittedAndValid()* is used to check whether the form is ready to be processed or not.
|
||||
|
@ -40,7 +50,6 @@ If the form has been updated, but not submitted (for example, because the a butt
|
|||
some fields in the form) the form is repopulated but not validated at this time. is SubmittedAndValid() returns false
|
||||
in this case, but no errors are added to the created form.
|
||||
|
||||
|
||||
In order to be able to use isSubmittedAndValid, you have to define a submitbutton in the form.
|
||||
This is done with the *setSubmitLabel(string)* function, with the first parameter being the
|
||||
label set to the submit button.
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
On top of the elements provided by the Zend Framework, Icinga 2 Web ships its own to offer additional functionality.
|
||||
The following is a list of these classes, as well as descriptions of the functionality they offer.
|
||||
|
||||
## DateTimePicker
|
||||
## Elements
|
||||
|
||||
### DateTimePicker
|
||||
|
||||
`Icinga\Web\Form\Element\DateTimePicker` represents a control that allows the user to select date/time and to
|
||||
display the date and time with a user specified format. Internally the element returns the input as Unix timestamp after
|
||||
|
@ -63,4 +65,74 @@ on the timezone set by the user.
|
|||
)
|
||||
)
|
||||
|
||||
## Validators
|
||||
|
||||
### WritablePathValidator
|
||||
|
||||
This *Icinga\Web\Form\Validator\WritablePathValidator* validator tests a given (string-)input for being a valid writable
|
||||
path. Normally it just tests for an existing, writable path but when setRequireExistence() is called, the path must
|
||||
exist on form submission and be writable.
|
||||
|
||||
**Example usage of writablePathValidator
|
||||
|
||||
use \Icinga\Web\Form\Validator\WritablePathValidator;
|
||||
$txtLogPath = new Zend_Form_Element_Text(
|
||||
array(
|
||||
'name' => 'logging_app_target',
|
||||
'label' => 'Application Log Path',
|
||||
'helptext' => 'The logfile to write the icingaweb debug logs to.'
|
||||
. 'The webserver must be able to write at this location',
|
||||
'required' => true,
|
||||
'value' => $logging->get('target', '/var/log/icingaweb.log')
|
||||
)
|
||||
);
|
||||
$txtLogPath->addValidator(new WritablePathValidator());
|
||||
|
||||
|
||||
### DateTimeValidator
|
||||
|
||||
The *Icinga\Web\Form\Validator\DateTimeValidator* validator allows you to validate an input against a set of datetime
|
||||
patterns. On successful validation, it either gives a valid pattern via getValidPattern, or null if the entered time
|
||||
is a timestamp. The above DateTimePicker utilizes this validator and should be used instead of directly using the validator.
|
||||
|
||||
|
||||
## Decorators
|
||||
|
||||
### ConditionalHidden Decorator
|
||||
|
||||
The `Icinga\Web\Form\Decorator\ConditionalHidden` allows you to hide a form element with the 'conditional' attribute for
|
||||
users that don't have JavaScript enabled (the form is rendered in a \<noscript> tag when conditional is 1). Users with
|
||||
javascript won't see the elements, users with javascript will see it. This is useful in a lot of cases to allow icingaweb
|
||||
to be fully functional without JavaScript: Forms can show only sensible forms for most users (and, for example hide the
|
||||
debug log filepath input when debugging is disabled) and automatically reload the form as soon as the forms should be
|
||||
shown (e.g. when the debug checkbox is clicked), while users with text-browsers or javascript disabled see all forms,
|
||||
but can only fill out the ones relative or them.
|
||||
|
||||
**Example use of ConditionalHidden**
|
||||
|
||||
use Icinga\Web\Form\Decorator\ConditionalHidden;
|
||||
|
||||
$textLoggingDebugPath = new Zend_Form_Element_Text(array(
|
||||
'name' => 'logging_debug_target',
|
||||
'label' => 'Debug Log Path',
|
||||
'required' => $this->shouldDisplayDebugLog($debug),
|
||||
'condition' => $this->shouldDisplayDebugLog($debug), // 1 if displayed, otherwise 0
|
||||
'value' => getLogPath,
|
||||
'helptext' => 'Set the path to the debug log'
|
||||
))
|
||||
$textLoggingDebugPath->addDecorator(new ConditionalHidden());
|
||||
$form->addElement($textLoggingDebugPath);
|
||||
|
||||
### HelpText Decorator ###
|
||||
|
||||
The `Icinga\Web\Form\Decorator\HelpText` decorator allows you to use the 'helptext' property and renders this text in
|
||||
a consistent ways across the application. It is automatically added by our Form implementation, so you can just use
|
||||
the 'helptext' property in your form elements.
|
||||
|
||||
|
||||
### BootstrapForm Decorator
|
||||
|
||||
`Icinga\Web\Form\Decorator\BoostrapForm` is the decorator we use for our forms.
|
||||
It causes the forms to be rendered in a bootstrap friendly manner instead of the \<dd> \<dt> encapsulated way Zend normally
|
||||
renders the forms. You usually don't have to work with this decorator as our Form implementation automatically uses it,
|
||||
but it's always good to know why forms look how they look.
|
|
@ -256,7 +256,7 @@ class Monitoring_ListController extends ActionController
|
|||
->from($view, $cols)
|
||||
->applyRequest($this->_request);
|
||||
$this->handleFormatRequest($query);
|
||||
return $query;
|
||||
return $query->paginate();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,8 +13,7 @@ function formatDateString($self,$dateString){
|
|||
$d = new DateTime($dateString);
|
||||
return $self->util()->showTime($d->getTimestamp());
|
||||
}
|
||||
$paginator = $downtimes->paginate();
|
||||
$downtimes = $downtimes->fetchAll();
|
||||
|
||||
?>
|
||||
|
||||
<div data-icinga-component="app/mainDetailGrid">
|
||||
|
@ -23,7 +22,7 @@ $downtimes = $downtimes->fetchAll();
|
|||
</div>
|
||||
<div>
|
||||
<?= $this->paginationControl(
|
||||
$paginator,
|
||||
$downtimes,
|
||||
null,
|
||||
array(
|
||||
'mixedPagination.phtml',
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?php
|
||||
$hosts = $this->hosts->paginate();
|
||||
$viewHelper = $this->getHelper('MonitoringState');
|
||||
?>
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<?= $this->tabs->render($this); ?>
|
||||
|
||||
<?php
|
||||
$hosts = $this->hosts->paginate();
|
||||
$viewHelper = $this->getHelper('MonitoringState');
|
||||
?>
|
||||
|
||||
|
|
|
@ -5,9 +5,6 @@
|
|||
?>
|
||||
<div data-icinga-component="app/mainDetailGrid">
|
||||
<?= $this->sortControl->render($this); ?>
|
||||
<?php
|
||||
$notifications = $this->notifications->paginate();
|
||||
?>
|
||||
|
||||
<?= $this->paginationControl($notifications, null, null, array('preserve' => $this->preserve)); ?>
|
||||
<table class="table table-condensed" >
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?php
|
||||
$paginator = $services->paginate();
|
||||
$viewHelper = $this->getHelper('MonitoringState');
|
||||
?>
|
||||
|
||||
|
@ -9,7 +8,7 @@ $viewHelper = $this->getHelper('MonitoringState');
|
|||
</div>
|
||||
<?php return; endif ?>
|
||||
|
||||
<?= $this->paginationControl($paginator, null, null, array('preserve' => $this->preserve)) ?>
|
||||
<?= $this->paginationControl($services, null, null, array('preserve' => $this->preserve)) ?>
|
||||
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
|
@ -21,7 +20,7 @@ $viewHelper = $this->getHelper('MonitoringState');
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($services->fetchAll() as $service): ?>
|
||||
<?php foreach ($services as $service): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<div>
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
<?= $this->tabs->render($this); ?>
|
||||
|
||||
<?php
|
||||
$paginator = $services->paginate();
|
||||
$viewHelper = $this->getHelper('MonitoringState');
|
||||
?>
|
||||
<div data-icinga-component="app/mainDetailGrid">
|
||||
|
||||
<?= $this->sortControl->render($this); ?>
|
||||
<?= $this->paginationControl($paginator, null, null, array('preserve' => $this->preserve)) ?>
|
||||
<?= $this->paginationControl($this->services, null, null, array('preserve' => $this->preserve)) ?>
|
||||
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
|
@ -20,7 +19,7 @@ $viewHelper = $this->getHelper('MonitoringState');
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($services->fetchAll() as $service): ?>
|
||||
<?php foreach ($services as $service): ?>
|
||||
<?php
|
||||
$serviceLink = $this->href('monitoring/show/service',array(
|
||||
'host' => $service->host_name,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
|
||||
$hostgroupLinkList = array();
|
||||
if (!empty($this->hostgroups)) {
|
||||
foreach ($this->hostgroups as $name => $alias) {
|
||||
|
|
|
@ -109,7 +109,7 @@ define(['jquery', 'logging', 'icinga/componentRegistry'], function ($, log, regi
|
|||
type,
|
||||
el,
|
||||
function(cmp) {
|
||||
var id = registry.add(cmp, el.id, type);
|
||||
var id = registry.add(cmp, type);
|
||||
registry.markActive(id);
|
||||
el.id = id;
|
||||
finalize();
|
||||
|
|
|
@ -112,10 +112,8 @@ define(['jquery'], function($) {
|
|||
*
|
||||
* @returns {*|Array}
|
||||
*/
|
||||
this.add = function(cmp, id, type) {
|
||||
if (!id){
|
||||
id = self.getId(cmp) || createId();
|
||||
}
|
||||
this.add = function(cmp, type) {
|
||||
var id = createId();
|
||||
components[id] = {
|
||||
cmp: cmp,
|
||||
type: type,
|
||||
|
|
|
@ -26,9 +26,23 @@
|
|||
*/
|
||||
// {{{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'],
|
||||
function($, logger, componentLoader, URI) {
|
||||
"use strict";
|
||||
|
||||
var Icinga;
|
||||
|
||||
/**
|
||||
* Enumeration of possible container types
|
||||
*
|
||||
* @type {{GENERIC: string, MAIN: string, DETAIL: string}}
|
||||
*/
|
||||
var CONTAINER_TYPES = {
|
||||
'GENERIC' : 'generic',
|
||||
'MAIN' : 'icingamain',
|
||||
'DETAIL': 'icingadetail'
|
||||
};
|
||||
|
||||
/**
|
||||
* Static reference to the main container, populated on the first 'getMainContainer' call
|
||||
*
|
||||
|
@ -60,17 +74,6 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
|
|||
*/
|
||||
var Container = function(target) {
|
||||
|
||||
/**
|
||||
* Enumeration of possible container types
|
||||
*
|
||||
* @type {{GENERIC: string, MAIN: string, DETAIL: string}}
|
||||
*/
|
||||
var CONTAINER_TYPES = {
|
||||
'GENERIC' : 'generic',
|
||||
'MAIN' : 'icingamain',
|
||||
'DETAIL': 'icingadetail'
|
||||
};
|
||||
|
||||
/**
|
||||
* Set to true when no history changes should be made
|
||||
*
|
||||
|
@ -113,8 +116,14 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
|
|||
} else {
|
||||
this.containerType = CONTAINER_TYPES.GENERIC;
|
||||
}
|
||||
this.containerDom.attr('data-icinga-href', this.getContainerHref());
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the window without the hostname
|
||||
*
|
||||
* @returns {string} path with query, search and hash
|
||||
*/
|
||||
var getWindowLocationWithoutHost = function() {
|
||||
return window.location.pathname + window.location.search + window.location.hash;
|
||||
};
|
||||
|
@ -126,9 +135,9 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
|
|||
*
|
||||
* @returns {string} The Url of the main container
|
||||
*/
|
||||
var getMainContainerHrefFromUrl = function() {
|
||||
var getMainContainerHrefFromUrl = function(baseUrl) {
|
||||
// main has the url without the icingadetail part
|
||||
var href = URI(getWindowLocationWithoutHost());
|
||||
var href = URI(getWindowLocationWithoutHost(baseUrl));
|
||||
href.removeQuery('detail');
|
||||
return href.href();
|
||||
};
|
||||
|
@ -141,8 +150,8 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
|
|||
*
|
||||
* @returns {string|undefined} The Url of the detail container or undefined if no detail container is active
|
||||
*/
|
||||
var getDetailContainerHrefFromUrl = function() {
|
||||
var location = new URI(getWindowLocationWithoutHost());
|
||||
var getDetailContainerHrefFromUrl = function(baseUrl) {
|
||||
var location = new URI(baseUrl);
|
||||
var href = URI.parseQuery(location.query()).detail;
|
||||
if (!href) {
|
||||
return;
|
||||
|
@ -165,17 +174,18 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
|
|||
*
|
||||
* @returns {String|undefined} The Url of the container or undefined if the container has no Url set
|
||||
*/
|
||||
this.getContainerHref = function() {
|
||||
this.getContainerHref = function(baseUrl) {
|
||||
baseUrl = baseUrl || getWindowLocationWithoutHost();
|
||||
switch (this.containerType) {
|
||||
case CONTAINER_TYPES.MAIN:
|
||||
return getMainContainerHrefFromUrl();
|
||||
return getMainContainerHrefFromUrl(baseUrl);
|
||||
case CONTAINER_TYPES.DETAIL:
|
||||
return getDetailContainerHrefFromUrl();
|
||||
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(getWindowLocationWithoutHost()).href();
|
||||
return URI(baseUrl).href();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -187,8 +197,8 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
|
|||
*
|
||||
* @returns {URI} The modified URI.js containing the new main and the current detail link
|
||||
*/
|
||||
var setMainContainerHref = function(url) {
|
||||
var detail = getDetailContainerHrefFromUrl();
|
||||
var setMainContainerHref = function(url, baseUrl) {
|
||||
var detail = getDetailContainerHrefFromUrl(baseUrl);
|
||||
if (detail) {
|
||||
url.addQuery('detail', detail);
|
||||
}
|
||||
|
@ -202,8 +212,8 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
|
|||
*
|
||||
* @returns {URI} The modified URI.js containing the new detail and the current main link
|
||||
*/
|
||||
var setDetailContainerHref = function(url) {
|
||||
var location = new URI(window.location.href);
|
||||
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);
|
||||
|
@ -219,23 +229,27 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
|
|||
* 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) {
|
||||
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);
|
||||
windowUrl = setMainContainerHref(url, baseUrl);
|
||||
containerUrl = windowUrl.clone().removeQuery('detail');
|
||||
break;
|
||||
case CONTAINER_TYPES.DETAIL:
|
||||
windowUrl = setDetailContainerHref(url);
|
||||
windowUrl = setDetailContainerHref(url, baseUrl);
|
||||
containerUrl = url;
|
||||
break;
|
||||
case CONTAINER_TYPES.GENERIC:
|
||||
containerUrl = url;
|
||||
windowUrl = baseUrl;
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -244,56 +258,10 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
|
|||
} else {
|
||||
this.containerDom.removeAttr('data-icinga-href');
|
||||
}
|
||||
if (!this.freezeHistory) {
|
||||
History.pushState({container: this.containerDom.attr('id')}, document.title, windowUrl.href());
|
||||
}
|
||||
return windowUrl;
|
||||
|
||||
return windowUrl.href();
|
||||
};
|
||||
|
||||
/**
|
||||
* Synchronize the container with the currently active window Url
|
||||
*
|
||||
* This is called mostly after changes in the history and makes sure the container contains the same content
|
||||
* as the Url refers to. If the Url is the same as the Url in the container (the one in the data-icinga-href
|
||||
* attribute), the container will be untouched, otherwise it's content and data-icinga-href attribute will be
|
||||
* updated with the Url from the window.
|
||||
*/
|
||||
this.syncWithCurrentUrl = function() {
|
||||
if (this.containerType === CONTAINER_TYPES.GENERIC) {
|
||||
return; // generic containers would require this method to be specialised
|
||||
}
|
||||
// Catch initial page loading: Here no data-icinga-href is set and no url is given, so we're safe to ignore this
|
||||
if (typeof this.containerDom.attr('data-icinga-href') === 'undefined' &&
|
||||
typeof this.getContainerHref() === 'undefined') {
|
||||
return;
|
||||
}
|
||||
// This is the case when an detail is removed on history back
|
||||
if (typeof this.getContainerHref() === 'undefined' && typeof this.containerDom.attr('data-icinga-href') !== 'undefined') {
|
||||
this.containerDom.removeAttr('data-icinga-href');
|
||||
this.containerDom.empty();
|
||||
this.hideDetail();
|
||||
logger.debug("Hiding detail panel on Url change");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!URI(this.getContainerHref()).equals(this.containerDom.attr('data-icinga-href'))) {
|
||||
logger.debug(
|
||||
"Applying URL change for ", this.containerType,
|
||||
"from", this.getContainerHref(),
|
||||
"to", this.containerDom.attr('data-icinga-href')
|
||||
);
|
||||
|
||||
if (typeof this.containerDom.attr('data-icinga-href') === 'undefined') { // container is empty now
|
||||
//this.replaceDom('');
|
||||
} else {
|
||||
this.freezeHistory = true;
|
||||
this.replaceDomFromUrl(this.getContainerHref());
|
||||
this.freezeHistory = false;
|
||||
}
|
||||
} else {
|
||||
logger.debug("No action performed on Url change, same Url for ", this.containerType);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the provided url, stop all pending requests for this container and call replaceDom for the returned html
|
||||
|
@ -303,26 +271,7 @@ 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) {
|
||||
if (!Modernizr.history) {
|
||||
window.location.href = this.updateContainerHref(url);
|
||||
}
|
||||
|
||||
this.updateContainerHref(url);
|
||||
var scope = this;
|
||||
if (this.containerDom.pending) {
|
||||
this.containerDom.pending.abort();
|
||||
}
|
||||
this.containerDom.pending = $.ajax({
|
||||
url: url,
|
||||
success: function(domNodes) {
|
||||
scope.replaceDom(domNodes);
|
||||
},
|
||||
error: function(response) {
|
||||
if (response.status === 401) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
Icinga.replaceBodyFromUrl(this.updateContainerHref(url));
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -340,8 +289,7 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
|
|||
* @see registerOnUpdate
|
||||
*/
|
||||
this.replaceDom = function(domNodes, keepLayout) {
|
||||
var newDom = $('#icingamain', domNodes);
|
||||
this.containerDom.empty().append(newDom.children());
|
||||
this.containerDom.empty().append(domNodes);
|
||||
this.containerDom.trigger('updated', [domNodes]);
|
||||
componentLoader.load();
|
||||
if (!keepLayout) {
|
||||
|
@ -351,6 +299,8 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Register a method to be called when this container is updated
|
||||
*
|
||||
|
@ -461,7 +411,7 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
|
|||
* 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', function(ev) {
|
||||
$('body').on('click', '#icingamain, #icingadetail', function(ev) {
|
||||
var targetEl = ev.target || ev.toElement || ev.relatedTarget;
|
||||
if (targetEl.tagName.toLowerCase() !== 'a') {
|
||||
return true;
|
||||
|
@ -470,12 +420,27 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
|
|||
if (Container.isExternalLink($(targetEl).attr('href'))) {
|
||||
return true;
|
||||
} else {
|
||||
Container.getMainContainer().replaceDomFromUrl(URI($(targetEl).attr('href')).href());
|
||||
var container = new Container($(targetEl));
|
||||
// detail links render to main by default;
|
||||
Icinga.replaceBodyFromUrl(URI($(targetEl).attr('href')).href());
|
||||
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the icinga object into the Container class
|
||||
*
|
||||
* This can't be done via requirejs as we would end up in circular references
|
||||
*
|
||||
* @param {Icinga} icingaObj The Icinga object to use for reloading
|
||||
*/
|
||||
Container.setIcinga = function(icingaObj) {
|
||||
Icinga = icingaObj;
|
||||
};
|
||||
|
||||
return Container;
|
||||
});
|
|
@ -97,13 +97,14 @@ define(['jquery'], function($) {
|
|||
form.change(function(changed) {
|
||||
if ($(changed.target).attr('data-icinga-form-autosubmit')) {
|
||||
form.clearModificationFlag();
|
||||
form.submit();
|
||||
} else {
|
||||
form.setModificationFlag();
|
||||
}
|
||||
});
|
||||
// submissions should clear the modification flag
|
||||
form.submit(function() {
|
||||
form.clearModificationFlag()
|
||||
form.clearModificationFlag();
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -127,7 +128,12 @@ define(['jquery'], function($) {
|
|||
*/
|
||||
return function(targetForm) {
|
||||
var form = getFormObject(targetForm);
|
||||
|
||||
|
||||
registerFormEventHandler(form);
|
||||
registerLeaveConfirmationHandler(form);
|
||||
|
||||
// Remove DOM level onchange, we registered proper jQuery listeners for them
|
||||
$('[data-icinga-form-autosubmit]').removeAttr('onchange');
|
||||
};
|
||||
});
|
|
@ -25,8 +25,8 @@
|
|||
* @author Icinga Development Team <info@icinga.org>
|
||||
*/
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
define(['components/app/container', 'jquery', 'logging', 'icinga/util/async', 'URIjs/URI', 'URIjs/URITemplate'],
|
||||
function(Container, $, logger, async, URI) {
|
||||
define(['components/app/container', 'jquery', 'logging', 'URIjs/URI', 'URIjs/URITemplate'],
|
||||
function(Container, $, logger, URI) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
|
@ -120,7 +120,6 @@ function(Container, $, logger, async, URI) {
|
|||
*/
|
||||
this.registerTableLinks = function(domContext) {
|
||||
domContext = domContext || contentNode;
|
||||
this.container.disableClickHandler();
|
||||
|
||||
$('tbody tr', domContext).on('click', function(ev) {
|
||||
var targetEl = ev.target || ev.toElement || ev.relatedTarget;
|
||||
|
@ -148,18 +147,27 @@ function(Container, $, logger, async, URI) {
|
|||
* current container
|
||||
*/
|
||||
this.registerControls = function() {
|
||||
controlForms.on('submit', (function(evt) {
|
||||
// append the form's parameters to the current container href
|
||||
var form = $(evt.currentTarget);
|
||||
var url = URI(this.container.getContainerHref());
|
||||
|
||||
controlForms.on('submit', function(evt) {
|
||||
var form = $(this);
|
||||
var container = (new Container(this));
|
||||
var url = URI(container.getContainerHref());
|
||||
url.search(URI.parseQuery(form.serialize()));
|
||||
// reload this container
|
||||
this.container.replaceDomFromUrl(url);
|
||||
container.replaceDomFromUrl(url.href());
|
||||
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
return false;
|
||||
}).bind(this));
|
||||
|
||||
});
|
||||
$('.pagination li a', contentNode).on('click', function() {
|
||||
Container.hideDetail();
|
||||
});
|
||||
};
|
||||
|
||||
var getSelectedRows = function() {
|
||||
return $('a[href="' + Container.getDetailContainer().getContainerHref() + '"]', determineContentTable()).
|
||||
parentsUntil('table', 'tr');
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -167,9 +175,7 @@ function(Container, $, logger, async, URI) {
|
|||
*/
|
||||
this.syncSelectionWithDetail = function() {
|
||||
$('tr', contentNode).removeClass('active');
|
||||
var selection = $('a[href="' + Container.getDetailContainer().getContainerHref() + '"]', contentNode).
|
||||
parentsUntil('table', 'tr');
|
||||
selection.addClass('active');
|
||||
getSelectedRows().addClass('active');
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -184,10 +190,8 @@ function(Container, $, logger, async, URI) {
|
|||
*/
|
||||
this.construct = function(target) {
|
||||
this.container = new Container(target);
|
||||
logger.debug("Registering table events for ", this.container.containerType);
|
||||
controlForms = determineControlForms();
|
||||
contentNode = determineContentTable();
|
||||
|
||||
this.registerControls();
|
||||
this.registerTableLinks();
|
||||
this.registerHistoryChanges();
|
||||
|
|
|
@ -29,11 +29,10 @@
|
|||
define([
|
||||
'jquery',
|
||||
'logging',
|
||||
'icinga/util/async',
|
||||
'icinga/componentLoader',
|
||||
'components/app/container',
|
||||
'URIjs/URI'
|
||||
], function ($, log, async, components, Container, URI) {
|
||||
], function ($, log, components, Container, URI) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
|
@ -45,7 +44,9 @@ define([
|
|||
|
||||
var initialize = function () {
|
||||
components.load();
|
||||
ignoreHistoryChanges = true;
|
||||
registerGenericHistoryHandler();
|
||||
ignoreHistoryChanges = false;
|
||||
log.debug("Initialization finished");
|
||||
|
||||
};
|
||||
|
@ -60,15 +61,8 @@ define([
|
|||
if (ignoreHistoryChanges) {
|
||||
return;
|
||||
}
|
||||
log.debug(URI(History.getState().url).relativeTo(lastUrl).href());
|
||||
var relativeURLPart = URI(History.getState().url).relativeTo(lastUrl).href();
|
||||
if (relativeURLPart !== "" && relativeURLPart[0] === '?' ) {
|
||||
// same controller, different parameters, so only update the container
|
||||
Container.getMainContainer().syncWithCurrentUrl();
|
||||
Container.getDetailContainer().syncWithCurrentUrl();
|
||||
} else {
|
||||
gotoUrl(History.getState().url);
|
||||
}
|
||||
|
||||
gotoUrl(History.getState().url);
|
||||
lastUrl = URI(window.location.href);
|
||||
});
|
||||
};
|
||||
|
@ -78,15 +72,18 @@ define([
|
|||
if (typeof document.body.pending !== 'undefined') {
|
||||
document.body.pending.abort();
|
||||
}
|
||||
$.ajax({
|
||||
if (typeof href === 'string') {
|
||||
href = URI(href);
|
||||
}
|
||||
document.body.pending = $.ajax({
|
||||
success: function(domNodes) {
|
||||
$('body').empty().append(jQuery.parseHTML(domNodes));
|
||||
ignoreHistoryChanges = true;
|
||||
History.pushState({}, document.title, href);
|
||||
$('body').empty().append($(domNodes));
|
||||
History.pushState({}, document.title, href.href());
|
||||
ignoreHistoryChanges = false;
|
||||
components.load();
|
||||
},
|
||||
url: href
|
||||
url: href.href()
|
||||
});
|
||||
|
||||
return false;
|
||||
|
@ -110,11 +107,11 @@ define([
|
|||
});
|
||||
}
|
||||
$(document).ready(initialize.bind(this));
|
||||
return {
|
||||
|
||||
components: components,
|
||||
gotoUrl: gotoUrl
|
||||
};
|
||||
Container.setIcinga(this);
|
||||
this.components = components;
|
||||
this.replaceBodyFromUrl = gotoUrl;
|
||||
};
|
||||
|
||||
|
||||
return new Icinga();
|
||||
});
|
||||
|
|
|
@ -22,7 +22,7 @@ requirejs.config({
|
|||
|
||||
define(['jquery', 'history'], function ($) {
|
||||
|
||||
requirejs(['bootstrap', 'jqueryPlugins/wookmark'], function() {
|
||||
requirejs(['bootstrap'], function() {
|
||||
requirejs(['datetimepicker']);
|
||||
});
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ describe('Component loader', function() {
|
|||
});
|
||||
});
|
||||
|
||||
it('Component load with user-defined id', function() {
|
||||
xit('Component load with user-defined id', function() {
|
||||
setUp();
|
||||
addComponent('app/component2','some-id');
|
||||
|
||||
|
|
|
@ -73,8 +73,11 @@ describe('The container component', function() {
|
|||
* Test dom selectors and instance creation
|
||||
*/
|
||||
it('should provide access to the main and detail component', function() {
|
||||
requireNew('icinga/components/container.js');
|
||||
createDOM();
|
||||
rjsmock.registerDependencies({
|
||||
'URIjs/URI' : URI
|
||||
});
|
||||
requireNew('icinga/components/container.js');
|
||||
var Container = rjsmock.getDefine();
|
||||
should.exist(Container.getMainContainer().containerDom, 'Assert that the main container has an DOM attached');
|
||||
should.exist(Container.getDetailContainer().containerDom, 'Assert that the detail container has an DOM attached');
|
||||
|
@ -94,62 +97,31 @@ describe('The container component', function() {
|
|||
requireNew('icinga/components/container.js');
|
||||
createDOM();
|
||||
var Container = rjsmock.getDefine();
|
||||
Container.getMainContainer().updateContainerHref('/some/other/url?test');
|
||||
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');
|
||||
|
||||
window.location.href.should.equal(
|
||||
url.should.equal(
|
||||
'/some/other/url?test',
|
||||
'Assert the main container updating the url correctly');
|
||||
|
||||
Container.getDetailContainer().updateContainerHref('/some/detail/url?test');
|
||||
url = Container.getDetailContainer().updateContainerHref('/some/detail/url?test');
|
||||
window.setWindowUrl(url);
|
||||
|
||||
Container.getDetailContainer().containerDom.attr('data-icinga-href').should.equal('/some/detail/url?test');
|
||||
window.location.href.should.equal(
|
||||
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'
|
||||
);
|
||||
|
||||
Container.getMainContainer().updateContainerHref('/some/other2/url?test=test');
|
||||
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');
|
||||
window.location.href.should.equal(
|
||||
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'
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Test synchronization with Url
|
||||
*/
|
||||
it('should be able to sync correctly with the current url if the URL changed', function() {
|
||||
rjsmock.registerDependencies({
|
||||
'URIjs/URI' : URI,
|
||||
'icinga/componentLoader' : {
|
||||
load: function() {}
|
||||
}
|
||||
});
|
||||
requireNew('icinga/components/container.js');
|
||||
createDOM();
|
||||
|
||||
var Container = rjsmock.getDefine();
|
||||
var containerModified = false;
|
||||
|
||||
Container.getMainContainer().updateContainerHref('/my/test/url?test=1');
|
||||
Container.getMainContainer().registerOnUpdate(function() {
|
||||
containerModified = true;
|
||||
});
|
||||
|
||||
window.setWindowUrl('/my/test/url?test=2');
|
||||
Container.getMainContainer().syncWithCurrentUrl();
|
||||
Container.getMainContainer().containerDom.attr('data-icinga-href').should.equal('/my/test/url?test=2');
|
||||
containerModified.should.equal(true);
|
||||
containerModified = false;
|
||||
|
||||
Container.getMainContainer().syncWithCurrentUrl();
|
||||
// URL hasn't changed, so this should not return true
|
||||
containerModified.should.equal(false);
|
||||
|
||||
window.setWindowUrl('/my/test/url?test=2&detail=test');
|
||||
Container.getMainContainer().syncWithCurrentUrl();
|
||||
// URL is not modified for main container, so this should not return true
|
||||
containerModified.should.equal(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,7 +30,7 @@ describe('Component registry',function() {
|
|||
cleanTestDom();
|
||||
});
|
||||
|
||||
it('Existing ids are preserved', function() {
|
||||
xit('Existing ids are preserved', function() {
|
||||
setUp();
|
||||
|
||||
registry.add({}, 'user-defined-id', null).should.equal('user-defined-id');
|
||||
|
@ -41,10 +41,6 @@ describe('Component registry',function() {
|
|||
it('Components are correctly added to the library', function() {
|
||||
setUp();
|
||||
|
||||
var cmp1 = { component: "cmp1" };
|
||||
registry.add(cmp1, 'user-defined-id', null);
|
||||
registry.getById('user-defined-id').should.equal(cmp1);
|
||||
|
||||
var cmp2 = { component: "cmp2" };
|
||||
registry.add(cmp2, null, null);
|
||||
registry.getById('icinga-component-0').should.equal(cmp2);
|
||||
|
@ -52,7 +48,10 @@ describe('Component registry',function() {
|
|||
cleanTestDom();
|
||||
});
|
||||
|
||||
it('getId(component) should return the components assigned id.', function() {
|
||||
/**
|
||||
* Not supported anymore
|
||||
*/
|
||||
xit('getId(component) should return the components assigned id.', function() {
|
||||
setUp();
|
||||
|
||||
var cmp1 = { component: "cmp1" };
|
||||
|
@ -72,13 +71,13 @@ describe('Component registry',function() {
|
|||
setUp();
|
||||
|
||||
var cmp1 = { component: "some/type" };
|
||||
registry.add(cmp1, null, 'some/type');
|
||||
registry.add(cmp1,'some/type');
|
||||
|
||||
var cmp2 = { component: "some/type" };
|
||||
registry.add(cmp2, null, "some/type");
|
||||
registry.add(cmp2, "some/type");
|
||||
|
||||
var cmp3 = { component: "other/type" };
|
||||
registry.add(cmp3, null, "other/type");
|
||||
registry.add(cmp3, "other/type");
|
||||
|
||||
var cmps = registry.getByType('some/type');
|
||||
cmps.length.should.equal(2);
|
||||
|
|
|
@ -21,6 +21,11 @@ var URI = require('URIjs');
|
|||
var states = [];
|
||||
|
||||
|
||||
/**
|
||||
* Api for setting the window URL
|
||||
*
|
||||
* @param {string} url The new url to use for window.location
|
||||
*/
|
||||
window.setWindowUrl = function(url) {
|
||||
var url = URI(url);
|
||||
window.location.protocol = url.protocol();
|
||||
|
|
Loading…
Reference in New Issue