From ef2f332869efc71e5ec5481f497b3d6d1c69b595 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Fri, 19 Sep 2014 15:48:44 +0200 Subject: [PATCH 01/10] Do not refresh a container when form input was changed or a form element is focused Listen for changes in form elements and abort all reloads that contain a form with at least one changed form element. Do not refresh containers that contain a focused form element, except of elements with autofocus, to preserve form elements with a dropdown. Only focus autofocus elements when there is currently no other selection. refs #7146 refs #5537 fixes #7162 --- library/Icinga/Web/JavaScript.php | 3 +- public/js/icinga/behavior/form.js | 104 ++++++++++++++++++++++++++++++ public/js/icinga/events.js | 5 +- public/js/icinga/loader.js | 20 ++++-- public/js/icinga/logger.js | 2 +- 5 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 public/js/icinga/behavior/form.js diff --git a/library/Icinga/Web/JavaScript.php b/library/Icinga/Web/JavaScript.php index 6aa36d608..6224b5b82 100644 --- a/library/Icinga/Web/JavaScript.php +++ b/library/Icinga/Web/JavaScript.php @@ -26,7 +26,8 @@ class JavaScript 'js/icinga/behavior/tooltip.js', 'js/icinga/behavior/sparkline.js', 'js/icinga/behavior/tristate.js', - 'js/icinga/behavior/navigation.js' + 'js/icinga/behavior/navigation.js', + 'js/icinga/behavior/form.js' ); protected static $vendorFiles = array( diff --git a/public/js/icinga/behavior/form.js b/public/js/icinga/behavior/form.js new file mode 100644 index 000000000..d289183d6 --- /dev/null +++ b/public/js/icinga/behavior/form.js @@ -0,0 +1,104 @@ +// {{{ICINGA_LICENSE_HEADER}}} +// {{{ICINGA_LICENSE_HEADER}}} + +/** + * Controls behavior of form elements, depending reload and + */ +(function(Icinga, $) { + + "use strict"; + + Icinga.Behaviors = Icinga.Behaviors || {}; + + var Form = function (icinga) { + Icinga.EventListener.call(this, icinga); + this.on('keyup change', 'form input', this.onChange, this); + + // store the modification state of all input fields + this.inputs = {}; + }; + Form.prototype = new Icinga.EventListener(); + + /** + * @param evt + */ + Form.prototype.onChange = function (evt) { + var el = evt.target; + var form = evt.data.self.uniqueFormName($(el).closest('form')[0] || {}); + evt.data.self.inputs[form] = evt.data.self.inputs[form] || {}; + if (el.value !== '') { + evt.data.self.inputs[form][el.name] = true; + } else { + evt.data.self.inputs[form][el.name] = false; + } + }; + + /** + * Try to generate an unique form name using the action + * and the name of the given form element + * + * @param form {HTMLFormElement} The + * @returns {String} The unique name + */ + Form.prototype.uniqueFormName = function(form) + { + return (form.name || 'undefined') + '.' + (form.action || 'undefined'); + }; + + /** + * Mutates the HTML before it is placed in the DOM after a reload + * + * @param content {String} The content to be rendered + * @param $container {jQuery} The target container where the html will be rendered in + * @param action {String} The action-url that caused the reload + * @param autorefresh {Boolean} Whether the rendering is due to an autoRefresh + * + * @returns {string|NULL} The content to be rendered, or NULL, when nothing should be changed + */ + Form.prototype.renderHook = function(content, $container, action, autorefresh) { + var origFocus = document.activeElement; + var containerId = $container.attr('id'); + var icinga = this.icinga; + var self = this.icinga.behaviors.form; + var changed = false; + $container.find('form').each(function () { + var form = self.uniqueFormName(this); + if (autorefresh) { + // check if an element in this container was changed + $(this).find('input').each(function () { + var name = this.name; + if (self.inputs[form] && self.inputs[form][name]) { + icinga.logger.debug( + 'form input: ' + form + '.' + name + ' was changed and aborts reload...' + ); + changed = true; + } + }); + } else { + // user-triggered reload, forget all changes to forms in this container + self.inputs[form] = null; + } + }); + if (changed) { + return null; + } + if ( + // is the focus among the elements to be replaced? + $container.has(origFocus).length && + // is an autorefresh + autorefresh && + + // and has focus + $(origFocus).length && + !$(origFocus).hasClass('autofocus') && + $(origFocus).closest('form').length + ) { + icinga.logger.debug('Not changing content for ' + containerId + ' form has focus'); + return null; + } + return content; + }; + + Icinga.Behaviors.Form = Form; + +}) (Icinga, jQuery); diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js index f226836cc..9c889dfa8 100644 --- a/public/js/icinga/events.js +++ b/public/js/icinga/events.js @@ -75,8 +75,9 @@ } }); - $('input.autofocus', el).focus(); - + if (document.activeElement === document.body) { + $('input.autofocus', el).focus(); + } var searchField = $('#menu input.search', el); // Remember initial search field value if any if (searchField.length && searchField.val().length) { diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js index 9b6c2bd00..9c3feab6e 100644 --- a/public/js/icinga/loader.js +++ b/public/js/icinga/loader.js @@ -661,6 +661,7 @@ // Container update happens here var scrollPos = false; var self = this; + var origFocus = document.activeElement; var containerId = $container.attr('id'); if (typeof containerId !== 'undefined') { if (autorefresh) { @@ -670,13 +671,18 @@ } } - var origFocus = document.activeElement; - if ( - // Do not reload menu when search field has content - (containerId === 'menu' && $(origFocus).length && $(origFocus).val().length) - // TODO: remove once #7146 is solved - || (containerId !== 'menu' && typeof containerId !== 'undefined' && autorefresh && origFocus && $(origFocus).closest('form').length && $container.has($(origFocus)) && $(origFocus).closest('#' + containerId).length && ! $(origFocus).hasClass('autosubmit'))) { - this.icinga.logger.debug('Not changing content for ', containerId, ' form has focus'); + var discard = false; + $.each(self.icinga.behaviors, function(name, behavior) { + if (behavior.renderHook) { + var changed = behavior.renderHook(content, $container, action, autorefresh); + if (!changed) { + discard = true; + } else { + content = changed; + } + } + }); + if (discard) { return; } diff --git a/public/js/icinga/logger.js b/public/js/icinga/logger.js index 1bdc2d718..07135e007 100644 --- a/public/js/icinga/logger.js +++ b/public/js/icinga/logger.js @@ -37,7 +37,7 @@ /** * Raise or lower current log level * - * Messages blow this threshold will be silently discarded + * Messages below this threshold will be silently discarded */ setLevel: function (level) { if ('undefined' !== typeof this.numericLevel(level)) { From dc05b2e933f32feb81c5cd24c1bbbbb27b997dba Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Mon, 15 Sep 2014 11:38:36 +0200 Subject: [PATCH 02/10] Transform TimelineIntervalBox into a generic ListBox Add options to control the changed url parameter and the displayed label to make it usable in generic cases. --- .../controllers/TimelineController.php | 8 +++-- ...{TimelineIntervalBox.php => SelectBox.php} | 29 ++++++++++++++----- 2 files changed, 27 insertions(+), 10 deletions(-) rename modules/monitoring/library/Monitoring/Web/Widget/{TimelineIntervalBox.php => SelectBox.php} (77%) diff --git a/modules/monitoring/application/controllers/TimelineController.php b/modules/monitoring/application/controllers/TimelineController.php index 2da29a0b9..7b4ab139f 100644 --- a/modules/monitoring/application/controllers/TimelineController.php +++ b/modules/monitoring/application/controllers/TimelineController.php @@ -12,7 +12,7 @@ use Icinga\Util\DateTimeFactory; use Icinga\Module\Monitoring\Controller; use Icinga\Module\Monitoring\Timeline\TimeLine; use Icinga\Module\Monitoring\Timeline\TimeRange; -use Icinga\Module\Monitoring\Web\Widget\TimelineIntervalBox; +use Icinga\Module\Monitoring\Web\Widget\SelectBox; use Icinga\Module\Monitoring\DataView\EventHistory as EventHistoryView; class Monitoring_TimelineController extends Controller @@ -85,7 +85,7 @@ class Monitoring_TimelineController extends Controller */ private function setupIntervalBox() { - $box = new TimelineIntervalBox( + $box = new SelectBox( 'intervalBox', array( '4h' => t('4 Hours'), @@ -93,7 +93,9 @@ class Monitoring_TimelineController extends Controller '1w' => t('One week'), '1m' => t('One month'), '1y' => t('One year') - ) + ), + t('TimeLine interval'), + 'interval' ); $box->applyRequest($this->getRequest()); $this->view->intervalBox = $box; diff --git a/modules/monitoring/library/Monitoring/Web/Widget/TimelineIntervalBox.php b/modules/monitoring/library/Monitoring/Web/Widget/SelectBox.php similarity index 77% rename from modules/monitoring/library/Monitoring/Web/Widget/TimelineIntervalBox.php rename to modules/monitoring/library/Monitoring/Web/Widget/SelectBox.php index 307fffba8..244a5d88e 100644 --- a/modules/monitoring/library/Monitoring/Web/Widget/TimelineIntervalBox.php +++ b/modules/monitoring/library/Monitoring/Web/Widget/SelectBox.php @@ -8,10 +8,7 @@ use Icinga\Web\Form; use Icinga\Web\Request; use Icinga\Web\Widget\AbstractWidget; -/** - * @todo Might be better if this is a generic selection widget. - */ -class TimelineIntervalBox extends AbstractWidget +class SelectBox extends AbstractWidget { /** * The name of the form that will be created @@ -27,6 +24,20 @@ class TimelineIntervalBox extends AbstractWidget */ private $values; + /** + * The label displayed next to the select box + * + * @var string + */ + private $label; + + /** + * The name of the url parameter to set + * + * @var string + */ + private $parameter; + /** * A request object used for initial form population * @@ -39,11 +50,15 @@ class TimelineIntervalBox extends AbstractWidget * * @param string $name The name of the form that will be created * @param array $values An array containing all intervals with their associated labels + * @param string $label The label displayed next to the select box + * @param string $param The request parameter name to set */ - public function __construct($name, array $values) + public function __construct($name, array $values, $label = 'Select', $param = 'selection') { $this->name = $name; $this->values = $values; + $this->label = $label; + $this->parameter = $param; } /** @@ -88,9 +103,9 @@ class TimelineIntervalBox extends AbstractWidget $form->setName($this->name); $form->addElement( 'select', - 'interval', + $this->parameter, array( - 'label' => 'Timeline Interval', + 'label' => $this->label, 'multiOptions' => $this->values, 'class' => 'autosubmit' ) From 65203fddcf1cf63191d99715dd3ed156658d0a8d Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Tue, 16 Sep 2014 15:58:24 +0200 Subject: [PATCH 03/10] Add additional state history filters for time interval and event type --- .../Web/Widget/Chart/HistoryColorGrid.php | 48 ++++++--- .../controllers/ListController.php | 100 ++++++++++++++++-- .../scripts/list/statehistorysummary.phtml | 75 ++++++++++--- 3 files changed, 186 insertions(+), 37 deletions(-) diff --git a/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php b/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php index 974e02060..5595cabce 100644 --- a/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php +++ b/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php @@ -28,29 +28,33 @@ class HistoryColorGrid extends AbstractWidget { private $maxValue = 1; private $start = null; - private $end = null; private $data = array(); private $color; - public function __construct($color = '#51e551') { + public function __construct($color = '#51e551', $start = null, $end = null) { $this->setColor($color); + if (isset($start)) { + $this->start = $this->tsToDateStr($start); + } + if (isset($end)) { + $this->end = $this->tsToDateStr($end); + } } /** * Set the displayed data-set * - * @param $data array The values to display. - * properties for each entry: + * @param $events array The history events to display as an array of arrays: * value: The value to display * caption: The caption on mouse-over * url: The url to open on click. */ - public function setData(array $data) + public function setData(array $events) { - $this->data = $data; + $this->data = $events; $start = time(); $end = time(); foreach ($this->data as $entry) { @@ -68,8 +72,12 @@ class HistoryColorGrid extends AbstractWidget { $start = $time; } } - $this->start = $this->tsToDateStr($start); - $this->end = $this->tsToDateStr($end); + if (!isset($this->start)) { + $this->start = $this->tsToDateStr($start); + } + if (!isset($this->end)) { + $this->end = $this->tsToDateStr($end); + } } /** @@ -130,13 +138,14 @@ class HistoryColorGrid extends AbstractWidget { { $weeks = $grid['weeks']; $months = $grid['months']; + $years = $grid['years']; $html = ''; $html .= ''; $old = -1; - foreach ($months as $month) { + foreach ($months as $week => $month) { if ($old !== $month) { $old = $month; - $txt = $this->monthName($month); + $txt = $this->monthName($month, $years[$week]); } else { $txt = ''; } @@ -157,6 +166,7 @@ class HistoryColorGrid extends AbstractWidget { */ private function renderVertical($grid) { + $years = $grid['years']; $weeks = $grid['weeks']; $months = $grid['months']; $html = '
'; @@ -176,7 +186,7 @@ class HistoryColorGrid extends AbstractWidget { } if ($old !== $months[$index]) { $old = $months[$index]; - $txt = $this->monthName($old); + $txt = $this->monthName($old, $years[$index]); } else { $txt = ''; } @@ -220,6 +230,7 @@ class HistoryColorGrid extends AbstractWidget { $weeks = array(array()); $week = 0; $months = array(); + $years = array(); $start = strtotime($this->start); $year = intval(date('Y', $start)); $month = intval(date('n', $start)); @@ -232,6 +243,7 @@ class HistoryColorGrid extends AbstractWidget { $date = $this->toDateStr($day, $month, $year); $weeks[0][$weekday] = $date; + $years[0] = $year; $months[0] = $month; while ($date !== $this->end) { $day++; @@ -242,6 +254,7 @@ class HistoryColorGrid extends AbstractWidget { // PRESENT => The last day of week determines the month if ($this->weekFlow === self::CAL_GROW_INTO_PRESENT) { $months[$week] = $month; + $years[$week] = $year; } $week++; } @@ -257,21 +270,25 @@ class HistoryColorGrid extends AbstractWidget { // PAST => The first day of each week determines the month if ($this->weekFlow === self::CAL_GROW_INTO_PAST) { $months[$week] = $month; + $years[$week] = $year; } } $date = $this->toDateStr($day, $month, $year); $weeks[$week][$weekday] = $date; }; + $years[$week] = $year; $months[$week] = $month; if ($this->weekFlow == self::CAL_GROW_INTO_PAST) { return array( 'weeks' => array_reverse($weeks), - 'months' => array_reverse($months) + 'months' => array_reverse($months), + 'years' => array_reverse($years) ); } return array( 'weeks' => $weeks, - 'months' => $months + 'months' => $months, + 'years' => $years ); } @@ -282,9 +299,10 @@ class HistoryColorGrid extends AbstractWidget { * * @return string The */ - private function monthName($month) + private function monthName($month, $year) { - $dt = DateTimeFactory::create('2000-' . $month . '-01'); + // TODO: find a way to render years without messing up the layout + $dt = DateTimeFactory::create($year . '-' . $month . '-01'); return $dt->format('M'); } diff --git a/modules/monitoring/application/controllers/ListController.php b/modules/monitoring/application/controllers/ListController.php index bd0bb047f..259dbef02 100644 --- a/modules/monitoring/application/controllers/ListController.php +++ b/modules/monitoring/application/controllers/ListController.php @@ -14,6 +14,7 @@ use Icinga\Web\Widget\FilterBox; use Icinga\Web\Widget\Chart\HistoryColorGrid; use Icinga\Data\Filter\Filter; use Icinga\Web\Widget; +use Icinga\Module\Monitoring\Web\Widget\SelectBox; class Monitoring_ListController extends Controller { @@ -359,15 +360,100 @@ class Monitoring_ListController extends Controller public function statehistorysummaryAction() { - $this->addTitleTab('statehistorysummary', 'Critical Events'); + $this->view->from = $this->params->shift('from', '3 months ago'); + $this->addTitleTab('statehistorysummary', 'State Summary'); + $selections = array( + 'critical' => array( + 'column' => 'cnt_critical', + 'filter' => Filter::matchAll( + Filter::expression('object_type', '=', 'service'), + Filter::expression('state', '=', '2') + ), + 'tooltip' => t('%d critical events on %s'), + 'color' => '#ff5566', 'opacity' => '0.9' + ), + 'warning' => array( + 'column' => 'cnt_warning', + 'filter' => Filter::matchAll( + Filter::expression('object_type', '=', 'service'), + Filter::expression('state', '=', '1') + ), + 'tooltip' => t('%d warning events on %s'), + 'color' => '#ffaa44', 'opacity' => '1.0' + ), + 'unknown' => array( + 'column' => 'cnt_unknown', + 'filter' => Filter::matchAll( + Filter::expression('object_type', '=', 'service'), + Filter::expression('state', '=', '3') + ), + 'tooltip' => t('%d unknown events on %s'), + 'color' => '#cc77ff', 'opacity' => '0.7' + ), + 'ok' => array( + 'column' => 'cnt_ok', + 'filter' => Filter::matchAll( + Filter::expression('object_type', '=', 'service'), + Filter::expression('state', '=', '0') + ), + 'tooltip' => t('%d ok events on %s'), + 'color' => '#49DF96', 'opacity' => '0.55' + ) + ); + + $eventBox = new SelectBox( + 'statehistoryfilter', + array( + 'critical' => t('Critical'), + 'warning' => t('Warning'), + 'unknown' => t('Unknown'), + 'ok' => t('Ok') + ), + t('Events'), + 'event' + ); + $eventBox->applyRequest($this->getRequest()); + + $orientationBox = new SelectBox( + 'orientation', + array( + '0' => t('Vertical'), + '1' => t('Horizontal') + ), + t('Orientation'), + 'horizontal' + ); + $orientationBox->applyRequest($this->getRequest()); + + $intervalBox = new SelectBox( + 'from', + array( + '3 months ago' => t('3 Months'), + '4 months ago' => t('4 Months'), + '8 months ago' => t('8 Months'), + '12 months ago' => t('1 Year'), + '24 months ago' => t('2 Years') + ), + t('Interval'), + 'from' + ); + $intervalBox->applyRequest($this->getRequest()); + + $eventtype = $this->params->shift('event', 'critical'); + $orientation = $this->params->shift('horizontal', 0) ? 'horizontal' : 'vertical'; + $selection = $selections[$eventtype]; + $query = $this->backend->select()->from( 'stateHistorySummary', - array('day', 'cnt_critical') - )->getQuery()->order('day'); - $query->limit(365); - $this->view->summary = $query->fetchAll(); - $this->view->grid = new HistoryColorGrid(); - $this->handleFormatRequest($query); + array('day', $selection['column']) + ); + $this->applyFilters($query); + $this->view->orientationBox = $orientationBox; + $this->view->eventBox = $eventBox; + $this->view->selection = $selection; + $this->view->orientation = $orientation; + $this->view->summary = $query->getQuery()->fetchAll(); + $this->view->intervalBox = $intervalBox; } public function contactgroupsAction() diff --git a/modules/monitoring/application/views/scripts/list/statehistorysummary.phtml b/modules/monitoring/application/views/scripts/list/statehistorysummary.phtml index 55c41ffcb..b5a0b3fdb 100644 --- a/modules/monitoring/application/views/scripts/list/statehistorysummary.phtml +++ b/modules/monitoring/application/views/scripts/list/statehistorysummary.phtml @@ -1,34 +1,47 @@ + + + +
+ tabs->render($this); ?> +
+
+ + + +
+
+ -?>
-tabs ?> -

History - Critical Events

-
setColor('#f05060'); +$from = strtotime($from); +$to = time(); $data = array(); if (count($summary) === 0) { - echo t('No history entry matching the filter'); + echo t('No events in the selected time period.'); } foreach ($summary as $entry) { $day = $entry->day; - $value = $entry->cnt_critical; - $caption = $value . ' ' . - t('critical events on ') . $this->dateFormat()->formatDate(strtotime($day)); + $value = $entry->{$selection['column']}; + $caption = sprintf( + $selection['tooltip'], + $value, + $this->dateFormat()->formatDate(strtotime($day)) + ); $filter = Filter::matchAll( Filter::expression('timestamp', '<', strtotime($day . ' 23:59:59')), Filter::expression('timestamp', '>', strtotime($day . ' 00:00:00')), - Filter::expression('object_type', '=', 'service'), - Filter::expression('state', '=', '2'), Filter::matchAny( Filter::expression('type', '=', 'hard_state'), Filter::expression('type', '=', 'hard_state') - ) + ), + $selection['filter'] ); $data[$day] = array( 'value' => $value, @@ -36,7 +49,39 @@ foreach ($summary as $entry) { 'url' => $this->href('monitoring/list/eventhistory?' . $filter->toQueryString()) ); } -$grid->setData($data); + +$f = new DateTime(); +$f->setTimestamp($from); +$t = new DateTime(); +$t->setTimestamp($to); +$diff = $t->diff($f); +$step = 124; + +for ($i = 0; $i < $diff->days; $i += $step) { + $end = clone $f; + if ($diff->days - $i > $step) { + // full range, move last day to next chunk + $end->add(new DateInterval('P' . ($step - 1) . 'D')); + } else { + // include last day + $end->add(new DateInterval('P' . ($diff->days - $i) . 'D')); + } + $grid = new HistoryColorGrid(null, $f->getTimestamp(), $end->getTimestamp()); + $grid->setColor($selection['color']); + $grid->opacity = $selection['opacity']; + $grid->orientation = $orientation; + $grid->setData($data); + $grids[] = $grid; + + $f->add(new DateInterval('P' . $step . 'D')); +} ?> - +
+ $grid) { ?> +
+ + orientation === 'horizontal' ? '
' : '' ?> +
+ +
From 6f1cb6f1f3acd723b7bc47b7057aed052e462778 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Tue, 16 Sep 2014 16:00:01 +0200 Subject: [PATCH 04/10] Adjust brightness for different event type colors --- .../Web/Widget/Chart/HistoryColorGrid.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php b/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php index 5595cabce..1a9c20acb 100644 --- a/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php +++ b/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php @@ -29,10 +29,9 @@ class HistoryColorGrid extends AbstractWidget { private $start = null; private $end = null; - private $data = array(); - private $color; + public $opacity = 1.0; public function __construct($color = '#51e551', $start = null, $end = null) { $this->setColor($color); @@ -90,6 +89,16 @@ class HistoryColorGrid extends AbstractWidget { $this->color = $color; } + /** + * Set the used opacity + * + * @param $opacity + */ + public function setOpacity($opacity) + { + $this->opacity = $opacity; + } + /** * Calculate the color to display for the given value. * @@ -115,13 +124,14 @@ class HistoryColorGrid extends AbstractWidget { if (array_key_exists($day, $this->data)) { $entry = $this->data[$day]; return' '; } else { return ''; } From 7385be06eaec30b6b5357cef6847b81eb7571494 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Wed, 17 Sep 2014 18:21:32 +0200 Subject: [PATCH 05/10] Move SelectBoxes into form --- .../controllers/ListController.php | 95 ++++--------------- .../scripts/list/statehistorysummary.phtml | 73 ++++++++++---- 2 files changed, 76 insertions(+), 92 deletions(-) diff --git a/modules/monitoring/application/controllers/ListController.php b/modules/monitoring/application/controllers/ListController.php index 259dbef02..d315ffe1b 100644 --- a/modules/monitoring/application/controllers/ListController.php +++ b/modules/monitoring/application/controllers/ListController.php @@ -15,6 +15,7 @@ use Icinga\Web\Widget\Chart\HistoryColorGrid; use Icinga\Data\Filter\Filter; use Icinga\Web\Widget; use Icinga\Module\Monitoring\Web\Widget\SelectBox; +use Icinga\Module\Monitoring\Form\StatehistoryForm; class Monitoring_ListController extends Controller { @@ -360,59 +361,17 @@ class Monitoring_ListController extends Controller public function statehistorysummaryAction() { - $this->view->from = $this->params->shift('from', '3 months ago'); $this->addTitleTab('statehistorysummary', 'State Summary'); - $selections = array( - 'critical' => array( - 'column' => 'cnt_critical', - 'filter' => Filter::matchAll( - Filter::expression('object_type', '=', 'service'), - Filter::expression('state', '=', '2') - ), - 'tooltip' => t('%d critical events on %s'), - 'color' => '#ff5566', 'opacity' => '0.9' - ), - 'warning' => array( - 'column' => 'cnt_warning', - 'filter' => Filter::matchAll( - Filter::expression('object_type', '=', 'service'), - Filter::expression('state', '=', '1') - ), - 'tooltip' => t('%d warning events on %s'), - 'color' => '#ffaa44', 'opacity' => '1.0' - ), - 'unknown' => array( - 'column' => 'cnt_unknown', - 'filter' => Filter::matchAll( - Filter::expression('object_type', '=', 'service'), - Filter::expression('state', '=', '3') - ), - 'tooltip' => t('%d unknown events on %s'), - 'color' => '#cc77ff', 'opacity' => '0.7' - ), - 'ok' => array( - 'column' => 'cnt_ok', - 'filter' => Filter::matchAll( - Filter::expression('object_type', '=', 'service'), - Filter::expression('state', '=', '0') - ), - 'tooltip' => t('%d ok events on %s'), - 'color' => '#49DF96', 'opacity' => '0.55' - ) - ); - $eventBox = new SelectBox( - 'statehistoryfilter', - array( - 'critical' => t('Critical'), - 'warning' => t('Warning'), - 'unknown' => t('Unknown'), - 'ok' => t('Ok') - ), - t('Events'), - 'event' - ); - $eventBox->applyRequest($this->getRequest()); + $form = new StatehistoryForm(); + $form->setEnctype(Zend_Form::ENCTYPE_URLENCODED); + $form->setMethod('get'); + $form->setTokenDisabled(); + $form->setRequest($this->getRequest()); + $form->buildForm(); + $this->view->form = $form; + + $orientation = $this->params->shift('horizontal', 0) ? 'horizontal' : 'vertical'; $orientationBox = new SelectBox( 'orientation', @@ -425,35 +384,20 @@ class Monitoring_ListController extends Controller ); $orientationBox->applyRequest($this->getRequest()); - $intervalBox = new SelectBox( - 'from', - array( - '3 months ago' => t('3 Months'), - '4 months ago' => t('4 Months'), - '8 months ago' => t('8 Months'), - '12 months ago' => t('1 Year'), - '24 months ago' => t('2 Years') - ), - t('Interval'), - 'from' - ); - $intervalBox->applyRequest($this->getRequest()); - - $eventtype = $this->params->shift('event', 'critical'); - $orientation = $this->params->shift('horizontal', 0) ? 'horizontal' : 'vertical'; - $selection = $selections[$eventtype]; - $query = $this->backend->select()->from( 'stateHistorySummary', - array('day', $selection['column']) + array('day', $form->getValue('state')) ); + $this->params->shift('objecttype'); + $this->params->shift('from'); + $this->params->shift('to'); + $this->params->shift('state'); + $this->params->shift('btn_submit'); $this->applyFilters($query); - $this->view->orientationBox = $orientationBox; - $this->view->eventBox = $eventBox; - $this->view->selection = $selection; - $this->view->orientation = $orientation; $this->view->summary = $query->getQuery()->fetchAll(); - $this->view->intervalBox = $intervalBox; + $this->view->column = $form->getValue('state'); + $this->view->orientationBox = $orientationBox; + $this->view->orientation = $orientation; } public function contactgroupsAction() @@ -735,6 +679,7 @@ class Monitoring_ListController extends Controller 'hosts', 'services', 'eventhistory', + 'statehistorysummary', 'notifications' ))) { $tabs->extend(new OutputFormat())->extend(new DashboardAction()); diff --git a/modules/monitoring/application/views/scripts/list/statehistorysummary.phtml b/modules/monitoring/application/views/scripts/list/statehistorysummary.phtml index b5a0b3fdb..b34fa23b0 100644 --- a/modules/monitoring/application/views/scripts/list/statehistorysummary.phtml +++ b/modules/monitoring/application/views/scripts/list/statehistorysummary.phtml @@ -9,9 +9,7 @@ use Icinga\Web\Widget\Chart\HistoryColorGrid; tabs->render($this); ?>

- - - +
@@ -19,34 +17,75 @@ use Icinga\Web\Widget\Chart\HistoryColorGrid;
array( + 'tooltip' => t('%d ok states on %s'), + 'color' => '#49DF96', + 'opacity' => '0.55' + ), + 'cnt_down_hard' => array( + 'tooltip' => t('%d ok states on %s'), + 'color' => '#ff5566', + 'opacity' => '0.7' + ), + 'cnt_unreachable_hard' => array( + 'tooltip' => t('%d ok states on %s'), + 'color' => '#77AAFF', + 'opacity' => '0.55' + ), + 'cnt_critical_hard' => array( + 'tooltip' => t('%d critical states on %s'), + 'color' => '#ff5566', + 'opacity' => '0.9' + ), + + 'cnt_warning_hard' => array( + 'tooltip' => t('%d warning states on %s'), + 'color' => '#ffaa44', + 'opacity' => '1.0' + ), + 'cnt_unknown_hard' => array( + 'tooltip' => t('%d unknown states on %s'), + 'color' => '#cc77ff', + 'opacity' => '0.7' + ), + 'cnt_ok' => array( + 'tooltip' => t('%d ok states on %s'), + 'color' => '#49DF96', + 'opacity' => '0.55' + ) +); + +$from = intval($form->getValue('from', strtotime('3 months ago'))); +$to = intval($form->getValue('to', time())); + +// don't display more than ten years, or else this will get really slow +if ($to - $from > 315360000) { + $from = $to - 315360000; +} $data = array(); if (count($summary) === 0) { - echo t('No events in the selected time period.'); + echo t('No state changes in the selected time period.'); } foreach ($summary as $entry) { $day = $entry->day; - $value = $entry->{$selection['column']}; + $value = $entry->$column; $caption = sprintf( - $selection['tooltip'], + $settings[$column]['tooltip'], $value, $this->dateFormat()->formatDate(strtotime($day)) ); - $filter = Filter::matchAll( + $linkFilter = Filter::matchAll( Filter::expression('timestamp', '<', strtotime($day . ' 23:59:59')), Filter::expression('timestamp', '>', strtotime($day . ' 00:00:00')), - Filter::matchAny( - Filter::expression('type', '=', 'hard_state'), - Filter::expression('type', '=', 'hard_state') - ), - $selection['filter'] + $form->getFilter(), + $filter ); $data[$day] = array( 'value' => $value, 'caption' => $caption, - 'url' => $this->href('monitoring/list/eventhistory?' . $filter->toQueryString()) + 'url' => $this->href('monitoring/list/eventhistory?' . $linkFilter->toQueryString()) ); } @@ -67,8 +106,8 @@ for ($i = 0; $i < $diff->days; $i += $step) { $end->add(new DateInterval('P' . ($diff->days - $i) . 'D')); } $grid = new HistoryColorGrid(null, $f->getTimestamp(), $end->getTimestamp()); - $grid->setColor($selection['color']); - $grid->opacity = $selection['opacity']; + $grid->setColor($settings[$column]['color']); + $grid->opacity = $settings[$column]['opacity']; $grid->orientation = $orientation; $grid->setData($data); $grids[] = $grid; From 44e5fe7e128188bd1b5c63f831f6e9c4cb3280cc Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Wed, 17 Sep 2014 18:24:20 +0200 Subject: [PATCH 06/10] Join Host and Servicegroups for extended filtering --- .../Ido/Query/StateHistorySummaryQuery.php | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/StateHistorySummaryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StateHistorySummaryQuery.php index cdaee0697..9004d5250 100644 --- a/modules/monitoring/library/Monitoring/Backend/Ido/Query/StateHistorySummaryQuery.php +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StateHistorySummaryQuery.php @@ -10,6 +10,7 @@ class StateHistorySummaryQuery extends IdoQuery 'statehistory' => array( 'day' => 'DATE(sh.state_time)', 'cnt_events' => 'COUNT(*)', + 'objecttype_id' => 'sho.objecttype_id', 'cnt_up' => 'SUM(CASE WHEN sho.objecttype_id = 1 AND sh.state = 0 THEN 1 ELSE 0 END)', 'cnt_down_hard' => 'SUM(CASE WHEN sho.objecttype_id = 1 AND sh.state = 1 AND state_type = 1 THEN 1 ELSE 0 END)', 'cnt_down' => 'SUM(CASE WHEN sho.objecttype_id = 1 AND sh.state = 1 THEN 1 ELSE 0 END)', @@ -23,6 +24,19 @@ class StateHistorySummaryQuery extends IdoQuery 'cnt_warning' => 'SUM(CASE WHEN sho.objecttype_id = 2 AND sh.state = 1 THEN 1 ELSE 0 END)', 'cnt_warning_hard' => 'SUM(CASE WHEN sho.objecttype_id = 2 AND sh.state = 1 AND state_type = 1 THEN 1 ELSE 0 END)', 'cnt_ok' => 'SUM(CASE WHEN sho.objecttype_id = 2 AND sh.state = 0 THEN 1 ELSE 0 END)', + 'host' => 'sho.name1 COLLATE latin1_general_ci', + 'service' => 'sho.name2 COLLATE latin1_general_ci', + 'host_name' => 'sho.name1 COLLATE latin1_general_ci', + 'service_description' => 'sho.name2 COLLATE latin1_general_ci', + 'timestamp' => 'UNIX_TIMESTAMP(sh.state_time)' + ), + + 'servicegroups' => array( + 'servicegroup' => 'sgo.name1' + ), + + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1' ) ); @@ -35,8 +49,42 @@ class StateHistorySummaryQuery extends IdoQuery array('sho' => $this->prefix . 'objects'), 'sh.object_id = sho.object_id AND sho.is_active = 1', array() - )->where('sh.state_time >= ?', '2013-11-20 00:00:00') + ) ->group('DATE(sh.state_time)'); $this->joinedVirtualTables = array('statehistory' => true); } + + protected function joinHostgroups() + { + $this->select->join( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = sho.object_id', + array() + )->join( + array('hgs' => $this->prefix . 'hostgroups'), + 'hgm.hostgroup_id = hgs.hostgroup_id', + array() + )->join( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hgs.hostgroup_object_id', + array() + ); + } + + protected function joinServicegroups() + { + $this->select->join( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = sho.object_id', + array() + )->join( + array('sgs' => $this->prefix . 'servicegroups'), + 'sgm.servicegroup_id = sgs.servicegroup_id', + array() + )->join( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sgs.servicegroup_object_id', + array() + ); + } } From dc6562b97e7adb25e6b55751630eaebe16c98f0c Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Wed, 17 Sep 2014 18:30:15 +0200 Subject: [PATCH 07/10] Add missing StateHistoryForm --- .../application/forms/StatehistoryForm.php | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 modules/monitoring/application/forms/StatehistoryForm.php diff --git a/modules/monitoring/application/forms/StatehistoryForm.php b/modules/monitoring/application/forms/StatehistoryForm.php new file mode 100644 index 000000000..629fcfc1d --- /dev/null +++ b/modules/monitoring/application/forms/StatehistoryForm.php @@ -0,0 +1,155 @@ +getValue('objecttype', 'hosts') === 'hosts') { + $objectTypeFilter = Filter::expression('object_type', '=', 'host'); + } else { + $objectTypeFilter = Filter::expression('object_type', '=', 'service'); + } + + $states = array( + 'cnt_down_hard' => Filter::expression('state', '=', '1'), + 'cnt_unreachable_hard' => Filter::expression('state', '=', '2'), + 'cnt_up' => Filter::expression('state', '=', '0'), + 'cnt_critical_hard' => Filter::expression('state', '=', '2'), + 'cnt_warning_hard' => Filter::expression('state', '=', '1'), + 'cnt_unknown_hard' => Filter::expression('state', '=', '3'), + 'cnt_ok' => Filter::expression('state', '=', '0') + ); + $stateFilter = $states[$this->getValue('state', 'cnt_critical_hard')]; + return Filter::matchAll($baseFilter, $objectTypeFilter, $stateFilter); + } + + public function relativeMonth($month) + { + $timestamp = strtotime('3 months ago'); + } + + /** + * Create the confirmation form + * + * @see Form::create() + */ + public function create() + { + $this->addElement( + 'select', + 'from', + array( + 'label' => t('From'), + 'value' => $this->getRequest()->getParam('from', strtotime('3 months ago')), + 'multiOptions' => array( + strtotime('midnight 3 months ago') => t('3 Months'), + strtotime('midnight 4 months ago') => t('4 Months'), + strtotime('midnight 8 months ago') => t('8 Months'), + strtotime('midnight 12 months ago') => t('1 Year'), + strtotime('midnight 24 months ago') => t('2 Years') + ), + 'class' => 'autosubmit' + ) + ); + $this->addElement( + 'select', + 'to', + array( + 'label' => t('To'), + 'value' => $this->getRequest()->getParam('to', time()), + 'multiOptions' => array( + time() => t('Today') + ), + 'class' => 'autosubmit' + ) + ); + + $objectType = $this->getRequest()->getParam('objecttype', 'services'); + $this->addElement( + 'select', + 'objecttype', + array( + 'label' => t('Object type'), + 'value' => $objectType, + 'multiOptions' => array( + 'services' => t('Services'), + 'hosts' => t('Hosts') + ), + 'class' => 'autosubmit' + ) + ); + if ($objectType === 'services') { + $serviceState = $this->getRequest()->getParam('state', 'cnt_critical_hard'); + if (in_array($serviceState, array('cnt_down_hard', 'cnt_unreachable', 'cnt_up'))) { + $serviceState = 'cnt_critical_hard'; + } + $this->addElement( + 'select', + 'state', + array( + 'label' => t('State'), + 'value' => $serviceState, + 'multiOptions' => array( + 'cnt_critical_hard' => t('Critical'), + 'cnt_warning_hard' => t('Warning'), + 'cnt_unknown_hard' => t('Unknown'), + 'cnt_ok' => t('Ok') + ), + 'class' => 'autosubmit' + ) + ); + } else { + $hostState = $this->getRequest()->getParam('state', 'cnt_down_hard'); + if (in_array($hostState, array('cnt_ok', 'cnt_critical_hard', 'cnt_warning', 'cnt_unknown'))) { + $hostState = 'cnt_down_hard'; + } + $this->addElement( + 'select', + 'state', + array( + 'label' => t('State'), + 'value' => $hostState, + 'multiOptions' => array( + 'cnt_up' => t('Up'), + 'cnt_down_hard' => t('Down'), + 'cnt_unreachable' => t('Unreachable') + ), + 'class' => 'autosubmit' + ) + ); + } + + $this->enableAutoSubmit(array('from', 'objecttype', 'state')); + $this->addElement( + 'button', + 'btn_submit', + array( + 'type' => 'submit', + 'escape' => false, + 'value' => '1', + 'class' => 'btn btn-cta btn-common', + 'label' => t('Apply') + ) + ); + } +} From 4ce71ef2f2dba00731add882d24c88316545c9ec Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Thu, 18 Sep 2014 09:20:13 +0200 Subject: [PATCH 08/10] Display a tooltip on days without events --- library/Icinga/Web/Widget/Chart/HistoryColorGrid.php | 2 +- public/css/icinga/widgets.less | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php b/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php index 1a9c20acb..bec7db181 100644 --- a/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php +++ b/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php @@ -131,7 +131,7 @@ class HistoryColorGrid extends AbstractWidget { '> '; } else { return 'calculateColor(0) . '; ' . ' opacity: ' . $this->opacity . ';" ' . 'title="No entries for ' . $day . '" ' . '>'; } diff --git a/public/css/icinga/widgets.less b/public/css/icinga/widgets.less index f546443c4..9fe42c899 100644 --- a/public/css/icinga/widgets.less +++ b/public/css/icinga/widgets.less @@ -19,7 +19,7 @@ table.historycolorgrid td { margin: 1em; } -table.historycolorgrid td.hover { +table.historycolorgrid td:hover { opacity: 0.5; } From 6c8f324dd49cb49add41d468dd9d190f1bf18a54 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Thu, 18 Sep 2014 09:24:50 +0200 Subject: [PATCH 09/10] Add filter widget to StateHistorySummary and fix some minor bugs fixes #6979 --- .../Web/Widget/Chart/HistoryColorGrid.php | 2 +- .../application/forms/StatehistoryForm.php | 15 +++++------ .../scripts/list/statehistorysummary.phtml | 27 +++++++++++-------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php b/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php index bec7db181..56de318e8 100644 --- a/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php +++ b/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php @@ -121,7 +121,7 @@ class HistoryColorGrid extends AbstractWidget { */ private function renderDay($day) { - if (array_key_exists($day, $this->data)) { + if (array_key_exists($day, $this->data) && $this->data[$day]['value'] > 0) { $entry = $this->data[$day]; return'%s', + $css, + $this->dateTime->format('d.m.Y - H:i:s'), + $format + ); + } +} diff --git a/library/Icinga/Web/View/helpers/format.php b/library/Icinga/Web/View/helpers/format.php index 0d902acfd..a203f2fb3 100644 --- a/library/Icinga/Web/View/helpers/format.php +++ b/library/Icinga/Web/View/helpers/format.php @@ -44,3 +44,7 @@ $this->addHelperFunction('prefixedTimeUntil', function ($timestamp, $ucfirst = f Format::prefixedTimeUntil($timestamp, $ucfirst) ); }); + +$this->addHelperFunction('dateTimeRenderer', function ($dateTimeOrTimestamp, $future = false) { + return DateTimeRenderer::create($dateTimeOrTimestamp, $future); +}); diff --git a/modules/monitoring/application/views/scripts/list/notifications.phtml b/modules/monitoring/application/views/scripts/list/notifications.phtml index ee414f502..826254fd8 100644 --- a/modules/monitoring/application/views/scripts/list/notifications.phtml +++ b/modules/monitoring/application/views/scripts/list/notifications.phtml @@ -44,7 +44,14 @@ foreach ($notifications as $notification): } ?>
- +
timeSince($notification->notification_start_time) ?> + dateTimeRenderer($notification->notification_start_time)->render( + $this->translate('on %s', 'datetime'), + $this->translate('at %s', 'time'), + $this->translate('%s ago', 'timespan') + ); + ?> + service ?> on host ?> diff --git a/public/js/icinga/ui.js b/public/js/icinga/ui.js index ef5e43f70..28ce353e1 100644 --- a/public/js/icinga/ui.js +++ b/public/js/icinga/ui.js @@ -543,25 +543,39 @@ refreshTimeSince: function () { $('.timesince').each(function (idx, el) { - var m = el.innerHTML.match(/^(.*?)(-?\d+)m\s(-?\d+)s/); + + // todo remove after replace timeSince + var mp = el.innerHTML.match(/^(.*?)(-?\d+)d\s(-?\d+)h/); + if (mp !== null) { + return true; + } + + var m = el.innerHTML.match(/^(.*?)(-?\d+)(.+\s)(-?\d+)(.+)/); if (m !== null) { var nm = parseInt(m[2]); - var ns = parseInt(m[3]); + var ns = parseInt(m[4]); if (ns < 59) { ns++; } else { ns = 0; nm++; } - $(el).html(m[1] + nm + 'm ' + ns + 's'); + $(el).html(m[1] + nm + m[3] + ns + m[5]); } }); $('.timeuntil').each(function (idx, el) { - var m = el.innerHTML.match(/^(.*?)(-?\d+)m\s(-?\d+)s/); + + // todo remove after replace timeUntil + var mp = el.innerHTML.match(/^(.*?)(-?\d+)d\s(-?\d+)h/); + if (mp !== null) { + return true; + } + + var m = el.innerHTML.match(/^(.*?)(-?\d+)(.+\s)(-?\d+)(.+)/); if (m !== null) { var nm = parseInt(m[2]); - var ns = parseInt(m[3]); + var ns = parseInt(m[4]); var signed = ''; var sec = 0; @@ -589,7 +603,7 @@ nm = Math.floor(sec/60); ns = sec - nm * 60; - $(el).html(m[1] + signed + nm + 'm ' + ns + 's'); + $(el).html(m[1] + signed + nm + m[3] + ns + m[5]); } }); }, diff --git a/test/php/library/Icinga/Web/View/DateTimeRendererTest.php b/test/php/library/Icinga/Web/View/DateTimeRendererTest.php new file mode 100644 index 000000000..e45d3fdfe --- /dev/null +++ b/test/php/library/Icinga/Web/View/DateTimeRendererTest.php @@ -0,0 +1,102 @@ +assertInstanceOf( + 'Icinga\Web\View\DateTimeRenderer', + $dt, + 'Dashboard::create() could not create DateTimeRenderer' + ); + } + + /** + * @depends testWhetherCreateCreatesDateTimeRenderer + */ + public function testWhetherIsDateTimeReturnsRightType() + { + $dateTime = new DateTime('+1 day'); + $dt = DateTimeRenderer::create($dateTime); + + $this->assertTrue( + $dt->isDateTime(), + 'Dashboard::isDateTime() returns wrong type' + ); + } + + /** + * @depends testWhetherCreateCreatesDateTimeRenderer + */ + public function testWhetherIsTimeReturnsRightType() + { + $dateTime = new DateTime('+1 hour'); + $dt = DateTimeRenderer::create($dateTime); + + $this->assertTrue( + $dt->isTime(), + 'Dashboard::isTime() returns wrong type' + ); + } + + /** + * @depends testWhetherCreateCreatesDateTimeRenderer + */ + public function testWhetherIsTimespanReturnsRightType() + { + $dateTime = new DateTime('+1 minute'); + $dt = DateTimeRenderer::create($dateTime); + + $this->assertTrue( + $dt->isTimespan(), + 'Dashboard::isTimespan() returns wrong type' + ); + } + + /** + * @depends testWhetherCreateCreatesDateTimeRenderer + */ + public function testWhetherNormalizeReturnsNormalizedDateTime() + { + $dateTime = time(); + $dt = DateTimeRenderer::normalize($dateTime); + + $this->assertInstanceOf( + 'DateTime', + $dt, + 'Dashboard::normalize() returns wrong instance' + ); + } + + /** + * @depends testWhetherCreateCreatesDateTimeRenderer + */ + public function testWhetherRenderReturnsRightText() + { + $dateTime = new DateTime('+1 minute'); + $dt = DateTimeRenderer::create($dateTime); + + $text = $dt->render( + '#1 The service is down since %s', + '#2 The service is down since %s o\'clock.', + '#3 The service is down for %s.' + ); + + $this->assertRegExp( + '/#3/', + $text, + 'Dashboard::render() returns wrong text' + ); + } +}