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;
|
||||
|
||||
use Icinga\Application\Icinga;
|
||||
use Icinga\Forms\AcknowledgeApplicationStateMessageForm;
|
||||
use Icinga\Web\Announcement\AnnouncementCookie;
|
||||
use Icinga\Web\Announcement\AnnouncementIniRepository;
|
||||
use Icinga\Web\Controller;
|
||||
use Icinga\Web\Session;
|
||||
use Icinga\Web\Widget;
|
||||
|
||||
/**
|
||||
* @TODO(el): https://dev.icinga.com/issues/10646
|
||||
|
@ -16,10 +17,14 @@ class ApplicationStateController extends Controller
|
|||
{
|
||||
protected $requiresAuthentication = false;
|
||||
|
||||
public function init()
|
||||
{
|
||||
$this->_helper->layout->disableLayout();
|
||||
$this->_helper->viewRenderer->setNoRender(true);
|
||||
}
|
||||
|
||||
public function indexAction()
|
||||
{
|
||||
$this->_helper->layout()->disableLayout();
|
||||
|
||||
if ($this->Auth()->isAuthenticated()) {
|
||||
if (isset($_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);
|
||||
|
||||
(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(
|
||||
'text',
|
||||
'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')) {
|
||||
$this->addElement(
|
||||
'checkbox',
|
||||
|
|
|
@ -64,4 +64,5 @@ if ($this->layout()->inlineLayout) {
|
|||
}
|
||||
}
|
||||
?></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>
|
||||
|
|
|
@ -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,46 +6,73 @@
|
|||
margin: 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;
|
||||
content: "\e811";
|
||||
font-family: 'ifont';
|
||||
left: 1em;
|
||||
margin-top: -1em;
|
||||
padding: 0.3em;
|
||||
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
left: 1.25em;
|
||||
}
|
||||
|
||||
> li {
|
||||
border-bottom: 1px solid @gray-lighter;
|
||||
padding: 1em 3em;
|
||||
position: relative;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: @icinga-blue;
|
||||
}
|
||||
}
|
||||
|
||||
> li .message {
|
||||
.message {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
> li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.acknowledge-announcement-control {
|
||||
.acknowledge-announcement-control,
|
||||
.application-state-acknowledge-message-control {
|
||||
background: none;
|
||||
border: none;
|
||||
display: block;
|
||||
margin-top: -0.6em;
|
||||
margin-top: -0.75em;
|
||||
|
||||
position: absolute;
|
||||
right: 1em;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-link {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
var ApplicationState = function (icinga) {
|
||||
Icinga.EventListener.call(this, icinga);
|
||||
this.on('rendered', this.onRendered, this);
|
||||
this.on('rendered', '#application-state-summary', this.onSummaryRendered, this);
|
||||
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, jQuery);
|
||||
|
|
Loading…
Reference in New Issue