From d18d05ccee3b0f5e93f2008634923b141c84d0ef Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Mon, 9 Jul 2018 15:33:20 +0200 Subject: [PATCH 1/3] Introduce ApplicationStateHook refs #2835 --- .../ApplicationStateController.php | 36 +++++++- ...AcknowledgeApplicationStateMessageForm.php | 75 ++++++++++++++++ application/layouts/scripts/body.phtml | 1 + .../scripts/application-state/index.phtml | 0 .../Application/Hook/ApplicationStateHook.php | 90 +++++++++++++++++++ library/Icinga/Web/ApplicationStateCookie.php | 74 +++++++++++++++ .../Web/Widget/ApplicationStateMessages.php | 63 +++++++++++++ public/css/icinga/widgets.less | 79 ++++++++++------ .../js/icinga/behavior/application-state.js | 8 ++ 9 files changed, 397 insertions(+), 29 deletions(-) create mode 100644 application/forms/AcknowledgeApplicationStateMessageForm.php delete mode 100644 application/views/scripts/application-state/index.phtml create mode 100644 library/Icinga/Application/Hook/ApplicationStateHook.php create mode 100644 library/Icinga/Web/ApplicationStateCookie.php create mode 100644 library/Icinga/Web/Widget/ApplicationStateMessages.php diff --git a/application/controllers/ApplicationStateController.php b/application/controllers/ApplicationStateController.php index 7d7b2ed75..711eea694 100644 --- a/application/controllers/ApplicationStateController.php +++ b/application/controllers/ApplicationStateController.php @@ -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(); } } diff --git a/application/forms/AcknowledgeApplicationStateMessageForm.php b/application/forms/AcknowledgeApplicationStateMessageForm.php new file mode 100644 index 000000000..6fe0dd37c --- /dev/null +++ b/application/forms/AcknowledgeApplicationStateMessageForm.php @@ -0,0 +1,75 @@ +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; + } +} diff --git a/application/layouts/scripts/body.phtml b/application/layouts/scripts/body.phtml index 2464db4a2..2a9ec2fa7 100644 --- a/application/layouts/scripts/body.phtml +++ b/application/layouts/scripts/body.phtml @@ -64,4 +64,5 @@ if ($this->layout()->inlineLayout) { } } ?> +
diff --git a/application/views/scripts/application-state/index.phtml b/application/views/scripts/application-state/index.phtml deleted file mode 100644 index e69de29bb..000000000 diff --git a/library/Icinga/Application/Hook/ApplicationStateHook.php b/library/Icinga/Application/Hook/ApplicationStateHook.php new file mode 100644 index 000000000..be973feaa --- /dev/null +++ b/library/Icinga/Application/Hook/ApplicationStateHook.php @@ -0,0 +1,90 @@ +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; + } +} diff --git a/library/Icinga/Web/ApplicationStateCookie.php b/library/Icinga/Web/ApplicationStateCookie.php new file mode 100644 index 000000000..e40c17bb5 --- /dev/null +++ b/library/Icinga/Web/ApplicationStateCookie.php @@ -0,0 +1,74 @@ +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() + ]); + } +} diff --git a/library/Icinga/Web/Widget/ApplicationStateMessages.php b/library/Icinga/Web/Widget/ApplicationStateMessages.php new file mode 100644 index 000000000..f79133f1b --- /dev/null +++ b/library/Icinga/Web/Widget/ApplicationStateMessages.php @@ -0,0 +1,63 @@ +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() + { + $active = $this->getMessages(); + + if (empty($active)) { + // Force container update on XHR + return '
'; + } + + $purifier = $this->getPurifier(); + + $html = '
'; + + 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 .= '
'; + + return $html; + } +} diff --git a/public/css/icinga/widgets.less b/public/css/icinga/widgets.less index 10144cd0f..45761f97b 100644 --- a/public/css/icinga/widgets.less +++ b/public/css/icinga/widgets.less @@ -6,45 +6,72 @@ margin: 0; padding: 0; - > li:before { - color: @icinga-blue; - content: "\e811"; - font-family: 'ifont'; - left: 1em; - margin-top: -1em; - padding: 0.3em; - position: absolute; - text-align: center; - top: 50%; - } - > li { border-bottom: 1px solid @gray-lighter; - padding: 1em 3em; + line-height: 1.5em; + padding: 0.5em 1em 0.5em 3em; + position: relative; + &:before { + color: @icinga-blue; + content: "\e811"; + font-family: 'ifont'; + + position: absolute; + left: 1.25em; + } + + &:last-child { + border-bottom: none; + } + a { color: @icinga-blue; } - } - > li .message { - display: inline-block; - vertical-align: middle; + .message { + display: inline-block; + vertical-align: middle; + } } +} - > li:last-child { - border-bottom: none; +.acknowledge-announcement-control, +.application-state-acknowledge-message-control { + background: none; + border: none; + display: block; + 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'; - .acknowledge-announcement-control { - background: none; - border: none; - display: block; - margin-top: -0.6em; position: absolute; - right: 1em; - top: 50%; + left: 1.25em; } } diff --git a/public/js/icinga/behavior/application-state.js b/public/js/icinga/behavior/application-state.js index 7cd655b44..a38848ab8 100644 --- a/public/js/icinga/behavior/application-state.js +++ b/public/js/icinga/behavior/application-state.js @@ -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); From 47405127d083caa217ebc5004db71ce760eb6473 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Mon, 9 Jul 2018 15:59:27 +0200 Subject: [PATCH 2/3] Add config to hide/show pplication state messages refs #2835 --- .../Config/General/ApplicationConfigForm.php | 14 ++++++++++++++ application/forms/PreferenceForm.php | 17 +++++++++++++++++ .../Web/Widget/ApplicationStateMessages.php | 15 +++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/application/forms/Config/General/ApplicationConfigForm.php b/application/forms/Config/General/ApplicationConfigForm.php index 6a1689316..f09d39b87 100644 --- a/application/forms/Config/General/ApplicationConfigForm.php +++ b/application/forms/Config/General/ApplicationConfigForm.php @@ -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', diff --git a/application/forms/PreferenceForm.php b/application/forms/PreferenceForm.php index 80108516a..836befadb 100644 --- a/application/forms/PreferenceForm.php +++ b/application/forms/PreferenceForm.php @@ -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', diff --git a/library/Icinga/Web/Widget/ApplicationStateMessages.php b/library/Icinga/Web/Widget/ApplicationStateMessages.php index f79133f1b..e2ba7ecdd 100644 --- a/library/Icinga/Web/Widget/ApplicationStateMessages.php +++ b/library/Icinga/Web/Widget/ApplicationStateMessages.php @@ -3,7 +3,9 @@ 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; @@ -33,6 +35,19 @@ class ApplicationStateMessages extends AbstractWidget 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 '
'; + } + $active = $this->getMessages(); if (empty($active)) { From 45468b7a8e0f7a4535a1f8987ef65e9ac5c73cb4 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Mon, 9 Jul 2018 16:00:50 +0200 Subject: [PATCH 3/3] Introduce app state for the monitoring module refs #2835 --- .../ProvidedHook/ApplicationState.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 modules/monitoring/library/Monitoring/ProvidedHook/ApplicationState.php diff --git a/modules/monitoring/library/Monitoring/ProvidedHook/ApplicationState.php b/modules/monitoring/library/Monitoring/ProvidedHook/ApplicationState.php new file mode 100644 index 000000000..4e2e61c93 --- /dev/null +++ b/modules/monitoring/library/Monitoring/ProvidedHook/ApplicationState.php @@ -0,0 +1,32 @@ +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); + } + } +}