Merge pull request #3516 from Icinga/feature/application-state-hook
Application state hook
This commit is contained in:
commit
b88c6b0a6e
|
@ -3,11 +3,12 @@
|
||||||
|
|
||||||
namespace Icinga\Controllers;
|
namespace Icinga\Controllers;
|
||||||
|
|
||||||
use Icinga\Application\Icinga;
|
use Icinga\Forms\AcknowledgeApplicationStateMessageForm;
|
||||||
use Icinga\Web\Announcement\AnnouncementCookie;
|
use Icinga\Web\Announcement\AnnouncementCookie;
|
||||||
use Icinga\Web\Announcement\AnnouncementIniRepository;
|
use Icinga\Web\Announcement\AnnouncementIniRepository;
|
||||||
use Icinga\Web\Controller;
|
use Icinga\Web\Controller;
|
||||||
use Icinga\Web\Session;
|
use Icinga\Web\Session;
|
||||||
|
use Icinga\Web\Widget;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @TODO(el): https://dev.icinga.com/issues/10646
|
* @TODO(el): https://dev.icinga.com/issues/10646
|
||||||
|
@ -16,10 +17,14 @@ class ApplicationStateController extends Controller
|
||||||
{
|
{
|
||||||
protected $requiresAuthentication = false;
|
protected $requiresAuthentication = false;
|
||||||
|
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
$this->_helper->layout->disableLayout();
|
||||||
|
$this->_helper->viewRenderer->setNoRender(true);
|
||||||
|
}
|
||||||
|
|
||||||
public function indexAction()
|
public function indexAction()
|
||||||
{
|
{
|
||||||
$this->_helper->layout()->disableLayout();
|
|
||||||
|
|
||||||
if ($this->Auth()->isAuthenticated()) {
|
if ($this->Auth()->isAuthenticated()) {
|
||||||
if (isset($_COOKIE['icingaweb2-session'])) {
|
if (isset($_COOKIE['icingaweb2-session'])) {
|
||||||
$last = (int) $_COOKIE['icingaweb2-session'];
|
$last = (int) $_COOKIE['icingaweb2-session'];
|
||||||
|
@ -59,6 +64,31 @@ class ApplicationStateController extends Controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->setAutorefreshInterval(60);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function summaryAction()
|
||||||
|
{
|
||||||
|
if ($this->Auth()->isAuthenticated()) {
|
||||||
|
$this->getResponse()->setBody((string) Widget::create('ApplicationStateMessages'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setAutorefreshInterval(60);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function acknowledgeMessageAction()
|
||||||
|
{
|
||||||
|
if (! $this->Auth()->isAuthenticated()) {
|
||||||
|
$this->getResponse()
|
||||||
|
->setHttpResponseCode(401)
|
||||||
|
->sendHeaders();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertHttpMethod('POST');
|
||||||
|
|
||||||
$this->getResponse()->setHeader('X-Icinga-Container', 'ignore', true);
|
$this->getResponse()->setHeader('X-Icinga-Container', 'ignore', true);
|
||||||
|
|
||||||
|
(new AcknowledgeApplicationStateMessageForm())->handleRequest();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Forms;
|
||||||
|
|
||||||
|
use Icinga\Application\Hook\ApplicationStateHook;
|
||||||
|
use Icinga\Web\ApplicationStateCookie;
|
||||||
|
use Icinga\Web\Form;
|
||||||
|
use Icinga\Web\Url;
|
||||||
|
|
||||||
|
class AcknowledgeApplicationStateMessageForm extends Form
|
||||||
|
{
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
$this->setAction(Url::fromPath('application-state/acknowledge-message'));
|
||||||
|
$this->setAttrib('class', 'form-inline application-state-acknowledge-message-control');
|
||||||
|
$this->setRedirectUrl('application-state/summary');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addSubmitButton()
|
||||||
|
{
|
||||||
|
$this->addElement(
|
||||||
|
'button',
|
||||||
|
'btn_submit',
|
||||||
|
[
|
||||||
|
'class' => 'link-button spinner',
|
||||||
|
'decorators' => [
|
||||||
|
'ViewHelper',
|
||||||
|
['HtmlTag', ['tag' => 'div', 'class' => 'control-group form-controls']]
|
||||||
|
],
|
||||||
|
'escape' => false,
|
||||||
|
'ignore' => true,
|
||||||
|
'label' => $this->getView()->icon('cancel'),
|
||||||
|
'title' => $this->translate('Acknowledge message'),
|
||||||
|
'type' => 'submit'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createElements(array $formData = [])
|
||||||
|
{
|
||||||
|
$this->addElements(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'hidden',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'required' => true,
|
||||||
|
'validators' => ['NotEmpty'],
|
||||||
|
'decorators' => ['ViewHelper']
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onSuccess()
|
||||||
|
{
|
||||||
|
$cookie = new ApplicationStateCookie();
|
||||||
|
|
||||||
|
$ack = $cookie->getAcknowledgedMessages();
|
||||||
|
$ack[] = $this->getValue('id');
|
||||||
|
|
||||||
|
$active = ApplicationStateHook::getAllMessages();
|
||||||
|
|
||||||
|
$cookie->setAcknowledgedMessages(array_keys(array_intersect_key($active, array_flip($ack))));
|
||||||
|
|
||||||
|
$this->getResponse()->setCookie($cookie);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,6 +43,20 @@ class ApplicationConfigForm extends Form
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$this->addElement(
|
||||||
|
'checkbox',
|
||||||
|
'global_show_application_state_messages',
|
||||||
|
array(
|
||||||
|
'required' => true,
|
||||||
|
'value' => true,
|
||||||
|
'label' => $this->translate('Show Application State Messages'),
|
||||||
|
'description' => $this->translate(
|
||||||
|
"Set whether to show application state messages."
|
||||||
|
. " This can also be set in a user's preferences."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
$this->addElement(
|
$this->addElement(
|
||||||
'text',
|
'text',
|
||||||
'global_module_path',
|
'global_module_path',
|
||||||
|
|
|
@ -233,6 +233,23 @@ class PreferenceForm extends Form
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$this->addElement(
|
||||||
|
'select',
|
||||||
|
'show_application_state_messages',
|
||||||
|
array(
|
||||||
|
'required' => true,
|
||||||
|
'label' => $this->translate('Show application state messages'),
|
||||||
|
'description' => $this->translate('Whether to show application state messages.'),
|
||||||
|
'multiOptions' => [
|
||||||
|
'system' => (bool) Config::app()->get('global', 'show_application_state_messages', true)
|
||||||
|
? $this->translate('System (Yes)')
|
||||||
|
: $this->translate('System (No)'),
|
||||||
|
1 => $this->translate('Yes'),
|
||||||
|
0 => $this->translate('No')],
|
||||||
|
'value' => 'system'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (Auth::getInstance()->hasPermission('application/stacktraces')) {
|
if (Auth::getInstance()->hasPermission('application/stacktraces')) {
|
||||||
$this->addElement(
|
$this->addElement(
|
||||||
'checkbox',
|
'checkbox',
|
||||||
|
|
|
@ -64,4 +64,5 @@ if ($this->layout()->inlineLayout) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?></ul>
|
?></ul>
|
||||||
|
<div id="application-state-summary" class="container" data-icinga-url="<?= $this->url('application-state/summary') ?>" data-last-update="-1" data-icinga-refresh="60"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Application\Hook;
|
||||||
|
|
||||||
|
use Icinga\Application\Hook;
|
||||||
|
use Icinga\Application\Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application state hook base class
|
||||||
|
*/
|
||||||
|
abstract class ApplicationStateHook
|
||||||
|
{
|
||||||
|
const ERROR = 'error';
|
||||||
|
|
||||||
|
private $messages = [];
|
||||||
|
|
||||||
|
final public function hasMessages()
|
||||||
|
{
|
||||||
|
return ! empty($this->messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
final public function getMessages()
|
||||||
|
{
|
||||||
|
return $this->messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an error message
|
||||||
|
*
|
||||||
|
* The timestamp of the message is used for deduplication and thus must refer to the time when the error first
|
||||||
|
* occurred. Don't use {@link time()} here!
|
||||||
|
*
|
||||||
|
* @param string $id ID of the message. The ID must be prefixed with the module name
|
||||||
|
* @param int $timestamp Timestamp when the error first occurred
|
||||||
|
* @param string $message Error message
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
final public function addError($id, $timestamp, $message)
|
||||||
|
{
|
||||||
|
$id = trim($id);
|
||||||
|
$timestamp = (int) $timestamp;
|
||||||
|
|
||||||
|
if (! strlen($id)) {
|
||||||
|
throw new \InvalidArgumentException('ID expected.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $timestamp) {
|
||||||
|
throw new \InvalidArgumentException('Timestamp expected.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->messages[sha1($id . $timestamp)] = [self::ERROR, $timestamp, $message];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override this method in order to provide application state messages
|
||||||
|
*/
|
||||||
|
abstract public function collectMessages();
|
||||||
|
|
||||||
|
final public static function getAllMessages()
|
||||||
|
{
|
||||||
|
$messages = [];
|
||||||
|
|
||||||
|
if (! Hook::has('ApplicationState')) {
|
||||||
|
return $messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Hook::all('ApplicationState') as $hook) {
|
||||||
|
/** @var self $hook */
|
||||||
|
try {
|
||||||
|
$hook->collectMessages();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Logger::error(
|
||||||
|
"Failed to collect messages from hook '%s'. An error occurred: %s",
|
||||||
|
get_class($hook),
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hook->hasMessages()) {
|
||||||
|
$messages += $hook->getMessages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $messages;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Web;
|
||||||
|
|
||||||
|
use Icinga\Application\Logger;
|
||||||
|
use Icinga\Authentication\Auth;
|
||||||
|
use Icinga\Exception\Json\JsonDecodeException;
|
||||||
|
use Icinga\Util\Json;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle acknowledged application state messages via cookie
|
||||||
|
*/
|
||||||
|
class ApplicationStateCookie extends Cookie
|
||||||
|
{
|
||||||
|
/** @var array */
|
||||||
|
protected $acknowledgedMessages = [];
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct('icingaweb2-application-state');
|
||||||
|
|
||||||
|
$this->setExpire(2147483648);
|
||||||
|
|
||||||
|
if (isset($_COOKIE['icingaweb2-application-state'])) {
|
||||||
|
try {
|
||||||
|
$cookie = Json::decode($_COOKIE['icingaweb2-application-state'], true);
|
||||||
|
} catch (JsonDecodeException $e) {
|
||||||
|
Logger::error(
|
||||||
|
"Can't decode the application state cookie of user '%s'. An error occurred: %s",
|
||||||
|
Auth::getInstance()->getUser()->getUsername(),
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($cookie['acknowledged-messages'])) {
|
||||||
|
$this->setAcknowledgedMessages($cookie['acknowledged-messages']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the acknowledged messages
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getAcknowledgedMessages()
|
||||||
|
{
|
||||||
|
return $this->acknowledgedMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the acknowledged messages
|
||||||
|
*
|
||||||
|
* @param array $acknowledged
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setAcknowledgedMessages(array $acknowledged)
|
||||||
|
{
|
||||||
|
$this->acknowledgedMessages = $acknowledged;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getValue()
|
||||||
|
{
|
||||||
|
return Json::encode([
|
||||||
|
'acknowledged-messages' => $this->getAcknowledgedMessages()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Web\Widget;
|
||||||
|
|
||||||
|
use Icinga\Application\Config;
|
||||||
|
use Icinga\Application\Hook\ApplicationStateHook;
|
||||||
|
use Icinga\Authentication\Auth;
|
||||||
|
use Icinga\Forms\AcknowledgeApplicationStateMessageForm;
|
||||||
|
use Icinga\Web\ApplicationStateCookie;
|
||||||
|
use Icinga\Web\Helper\HtmlPurifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render application state messages
|
||||||
|
*/
|
||||||
|
class ApplicationStateMessages extends AbstractWidget
|
||||||
|
{
|
||||||
|
protected function getMessages()
|
||||||
|
{
|
||||||
|
$cookie = new ApplicationStateCookie();
|
||||||
|
|
||||||
|
$acked = array_flip($cookie->getAcknowledgedMessages());
|
||||||
|
$messages = ApplicationStateHook::getAllMessages();
|
||||||
|
|
||||||
|
$active = array_diff_key($messages, $acked);
|
||||||
|
|
||||||
|
return $active;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected function getPurifier()
|
||||||
|
{
|
||||||
|
return new HtmlPurifier(['HTML.Allowed' => 'b,a[href|target],i,*[class]']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
$enabled = Auth::getInstance()
|
||||||
|
->getUser()
|
||||||
|
->getPreferences()
|
||||||
|
->getValue('icingaweb', 'show_application_state_messages', 'system');
|
||||||
|
|
||||||
|
if ($enabled === 'system') {
|
||||||
|
$enabled = Config::app()->get('global', 'show_application_state_messages', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! (bool) $enabled) {
|
||||||
|
return '<div style="display: none;"></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$active = $this->getMessages();
|
||||||
|
|
||||||
|
if (empty($active)) {
|
||||||
|
// Force container update on XHR
|
||||||
|
return '<div style="display: none;"></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$purifier = $this->getPurifier();
|
||||||
|
|
||||||
|
$html = '<div>';
|
||||||
|
|
||||||
|
reset($active);
|
||||||
|
|
||||||
|
$id = key($active);
|
||||||
|
$spec = current($active);
|
||||||
|
$message = array_pop($spec); // We don't use state and timestamp here
|
||||||
|
|
||||||
|
|
||||||
|
$ackForm = new AcknowledgeApplicationStateMessageForm();
|
||||||
|
$ackForm->populate(['id' => $id]);
|
||||||
|
|
||||||
|
$html .= $purifier->purify($message) . $ackForm;
|
||||||
|
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Module\Monitoring\ProvidedHook;
|
||||||
|
|
||||||
|
use Icinga\Application\Hook\ApplicationStateHook;
|
||||||
|
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
|
||||||
|
|
||||||
|
class ApplicationState extends ApplicationStateHook
|
||||||
|
{
|
||||||
|
public function collectMessages()
|
||||||
|
{
|
||||||
|
$backend = MonitoringBackend::instance();
|
||||||
|
|
||||||
|
$programStatus = $backend
|
||||||
|
->select()
|
||||||
|
->from(
|
||||||
|
'programstatus',
|
||||||
|
['is_currently_running', 'status_update_time']
|
||||||
|
)
|
||||||
|
->fetchRow();
|
||||||
|
|
||||||
|
if ($programStatus === false || ! (bool) $programStatus->is_currently_running) {
|
||||||
|
$message = sprintf(
|
||||||
|
mt('monitoring', "Monitoring backend '%s' is not running."),
|
||||||
|
$backend->getName()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->addError('monitoring/backend-down', $programStatus->status_update_time, $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,45 +6,72 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
> li:before {
|
> li {
|
||||||
|
border-bottom: 1px solid @gray-lighter;
|
||||||
|
line-height: 1.5em;
|
||||||
|
padding: 0.5em 1em 0.5em 3em;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:before {
|
||||||
color: @icinga-blue;
|
color: @icinga-blue;
|
||||||
content: "\e811";
|
content: "\e811";
|
||||||
font-family: 'ifont';
|
font-family: 'ifont';
|
||||||
left: 1em;
|
|
||||||
margin-top: -1em;
|
|
||||||
padding: 0.3em;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
text-align: center;
|
left: 1.25em;
|
||||||
top: 50%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> li {
|
&:last-child {
|
||||||
border-bottom: 1px solid @gray-lighter;
|
border-bottom: none;
|
||||||
padding: 1em 3em;
|
}
|
||||||
position: relative;
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: @icinga-blue;
|
color: @icinga-blue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
> li .message {
|
.message {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
> li:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.acknowledge-announcement-control {
|
.acknowledge-announcement-control,
|
||||||
|
.application-state-acknowledge-message-control {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: -0.6em;
|
margin-top: -0.75em;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 1em;
|
right: 1em;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-state-acknowledge-message-control .link-button {
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&:hover .icon-cancel {
|
||||||
|
color: @icinga-blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#application-state-summary > div {
|
||||||
|
background-color: @color-critical;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.5em;
|
||||||
|
padding: 0.5em 1em 0.5em 3em;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "\e84d";
|
||||||
|
font-family: 'ifont';
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 1.25em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
var ApplicationState = function (icinga) {
|
var ApplicationState = function (icinga) {
|
||||||
Icinga.EventListener.call(this, icinga);
|
Icinga.EventListener.call(this, icinga);
|
||||||
this.on('rendered', this.onRendered, this);
|
this.on('rendered', this.onRendered, this);
|
||||||
|
this.on('rendered', '#application-state-summary', this.onSummaryRendered, this);
|
||||||
this.icinga = icinga;
|
this.icinga = icinga;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -31,6 +32,13 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ApplicationState.prototype.onSummaryRendered = function(e) {
|
||||||
|
var height = $(this).height();
|
||||||
|
|
||||||
|
$('#sidebar').css('bottom', height);
|
||||||
|
$('#main').css('bottom', height);
|
||||||
|
};
|
||||||
|
|
||||||
Icinga.Behaviors.ApplicationState = ApplicationState;
|
Icinga.Behaviors.ApplicationState = ApplicationState;
|
||||||
|
|
||||||
})(Icinga, jQuery);
|
})(Icinga, jQuery);
|
||||||
|
|
Loading…
Reference in New Issue