Merge branch 'feature/add-time-picker-33' into next

fixes #33
This commit is contained in:
Alexander A. Klimov 2017-09-07 13:26:54 +02:00
commit a53216aba5
6 changed files with 726 additions and 0 deletions

View file

@ -0,0 +1,189 @@
<?php
namespace Icinga\Module\Graphite\Forms\TimeRangePicker;
use Icinga\Web\Form;
use Zend_Form_Decorator_HtmlTag;
use Zend_Form_Element_Select;
class CommonForm extends Form
{
use TimeRangePickerTrait;
/**
* The selectable units with themselves in seconds
*
* One month equals 30 days and one year equals 365.25 days. This should cover enough cases.
*
* @var int[string]
*/
protected $rangeFactors = [
'minutes' => 60,
'hours' => 3600,
'days' => 86400,
'weeks' => 604800,
'months' => 2592000,
'years' => 31557600
];
/**
* The elements' default values
*
* @var string[string]|null
*/
protected $defaultFormData;
public function init()
{
$this->setName('form_timerangepickercommon_graphite');
}
public function createElements(array $formData)
{
$this->addElements([
$this->createSelect(
'minutes',
$this->translate('Minutes'),
$this->translate('Show the last … minutes'),
[5, 10, 15, 30, 45],
$this->translate('%d minute'),
$this->translate('%d minutes')
),
$this->createSelect(
'hours',
$this->translate('Hours'),
$this->translate('Show the last … hours'),
[1, 2, 3, 6, 12, 18],
$this->translate('%d hour'),
$this->translate('%d hours')
),
$this->createSelect(
'days',
$this->translate('Days'),
$this->translate('Show the last … days'),
range(1, 6),
$this->translate('%d day'),
$this->translate('%d days')
),
$this->createSelect(
'weeks',
$this->translate('Weeks'),
$this->translate('Show the last … weeks'),
range(1, 4),
$this->translate('%d week'),
$this->translate('%d weeks')
),
$this->createSelect(
'months',
$this->translate('Months'),
$this->translate('Show the last … months'),
[1, 2, 3, 6, 9],
$this->translate('%d month'),
$this->translate('%d months')
),
$this->createSelect(
'years',
$this->translate('Years'),
$this->translate('Show the last … years'),
range(1, 3),
$this->translate('%d year'),
$this->translate('%d years')
)
]);
$this->urlToForm();
$this->defaultFormData = $this->getValues();
}
public function onSuccess()
{
$this->formToUrl();
$this->getRedirectUrl()->remove(array_values(static::getAbsoluteRangeParameters()));
}
/**
* Create a common range picker for a specific time unit
*
* @param string $name
* @param string $label
* @param string $description
* @param int[] $options
* @param string $singular
* @param string $plural
*
* @return Zend_Form_Element_Select
*/
protected function createSelect($name, $label, $description, array $options, $singular, $plural)
{
$multiOptions = ['' => $label];
foreach ($options as $option) {
$multiOptions[$option] = sprintf($option === 1 ? $singular : $plural, $option);
}
$element = $this->createElement('select', $name, [
'label' => $label,
'description' => $description,
'multiOptions' => $multiOptions,
'autosubmit' => true
]);
$decorators = $element->getDecorators();
$element->setDecorators([
'Zend_Form_Decorator_ViewHelper' => $decorators['Zend_Form_Decorator_ViewHelper'],
'Zend_Form_Decorator_HtmlTag' => new Zend_Form_Decorator_HtmlTag([
'tag' => 'span',
'title' => $description
])
]);
return $element;
}
/**
* Set this form's elements' default values based on the redirect URL's parameters
*/
protected function urlToForm()
{
$params = $this->getRedirectUrl()->getParams();
$seconds = $this->getRelativeSeconds($params);
if ($seconds !== null) {
if ($seconds !== false) {
foreach ($this->rangeFactors as $unit => $factor) {
/** @var Zend_Form_Element_Select $element */
$element = $this->getElement($unit);
$options = $element->getMultiOptions();
unset($options['']);
foreach ($options as $option => $_) {
if ($seconds === $option * $factor) {
$element->setValue((string) $option);
return;
}
}
}
}
$params->remove(static::getRelativeRangeParameter());
}
}
/**
* Change the redirect URL's parameters based on this form's elements' values
*/
protected function formToUrl()
{
$formData = $this->getValues();
foreach ($this->rangeFactors as $unit => $factor) {
if ($formData[$unit] !== '' && $formData[$unit] !== $this->defaultFormData[$unit]) {
$this->getRedirectUrl()->setParam(
static::getRelativeRangeParameter(),
(string) ((int) $formData[$unit] * $factor)
);
return;
}
}
}
}

View file

@ -0,0 +1,273 @@
<?php
namespace Icinga\Module\Graphite\Forms\TimeRangePicker;
use DateInterval;
use DateTime;
use DateTimeZone;
use Icinga\Module\Graphite\Web\Form\Decorator\Proxy;
use Icinga\Util\TimezoneDetect;
use Icinga\Web\Form;
class CustomForm extends Form
{
use TimeRangePickerTrait;
/**
* @var string
*/
protected $dateTimeFormat = 'Y-m-d\TH:i';
/**
* @var string
*/
protected $timestamp = '/^(?:0|-?[1-9]\d*)$/';
/**
* The time zone of all dates and times
*
* @var DateTimeZone
*/
protected $timeZone;
/**
* Right now
*
* @var DateTime
*/
protected $now;
public function init()
{
$this->setName('form_timerangepickercustom_graphite');
}
public function createElements(array $formData)
{
$this->addElements([
[
'date',
'start_date',
[
'label' => $this->translate('Start'),
'description' => $this->translate('Start of the date/time range')
]
],
[
'time',
'start_time',
[
'label' => $this->translate('Start'),
'description' => $this->translate('Start of the date/time range')
]
],
[
'date',
'end_date',
[
'label' => $this->translate('End'),
'description' => $this->translate('End of the date/time range')
]
],
[
'time',
'end_time',
[
'label' => $this->translate('End'),
'description' => $this->translate('End of the date/time range')
]
]
]);
$this->groupDateTime('start');
$this->groupDateTime('end');
$this->setSubmitLabel($this->translate('Update'));
$this->urlToForm('start', $this->getRelativeTimestamp());
$this->urlToForm('end');
}
public function onSuccess()
{
$start = $this->formToUrl('start', '00:00');
$end = $this->formToUrl('end', '23:59', 'PT59S');
if ($start > $end) {
$absoluteRangeParameters = static::getAbsoluteRangeParameters();
$this->getRedirectUrl()->getParams()
->set($absoluteRangeParameters['start'], $end)
->set($absoluteRangeParameters['end'], $start);
}
$this->getRedirectUrl()->remove(
[static::getRelativeRangeParameter(), static::getRangeCustomizationParameter()]
);
}
/**
* Add display group for a date and a time input belonging together
*
* @param string $part Either 'start' or 'end'
*/
protected function groupDateTime($part)
{
$this->addDisplayGroup(["{$part}_date", "{$part}_time"], $part);
$group = $this->getDisplayGroup($part);
foreach ($group->getElements() as $element) {
/** @var \Zend_Form_Element $element */
$elementDecorators = $element->getDecorators();
$element->setDecorators([
'Zend_Form_Decorator_ViewHelper' => $elementDecorators['Zend_Form_Decorator_ViewHelper']
]);
}
$decorators = [];
foreach ($elementDecorators as $key => $decorator) {
if ($key === 'Zend_Form_Decorator_ViewHelper') {
$decorators['Zend_Form_Decorator_FormElements'] = $group->getDecorators()['Zend_Form_Decorator_FormElements'];
} else {
$decorators[$key] = (new Proxy())->setActualDecorator($decorator->setElement($element));
}
}
$group->setDecorators($decorators);
}
/**
* Set this form's elements' default values based on the redirect URL's parameters
*
* @param string $part Either 'start' or 'end'
* @param int $defaultTimestamp Fallback
*/
protected function urlToForm($part, $defaultTimestamp = null)
{
$params = $this->getRedirectUrl()->getParams();
$absoluteRangeParameters = static::getAbsoluteRangeParameters();
$timestamp = $params->get($absoluteRangeParameters[$part], $defaultTimestamp);
if ($timestamp !== null) {
if (preg_match($this->timestamp, $timestamp)) {
list($date, $time) = explode(
'T',
DateTime::createFromFormat('U', $timestamp)
->setTimezone($this->getTimeZone())
->format($this->dateTimeFormat)
);
$this->getElement("{$part}_date")->setValue($date);
$this->getElement("{$part}_time")->setValue($time);
} else {
$params->remove($absoluteRangeParameters[$part]);
}
}
}
/**
* Get the relative range start (if any) set by {@link CommonForm}
*
* @return int|null
*/
protected function getRelativeTimestamp()
{
$seconds = $this->getRelativeSeconds($this->getRedirectUrl()->getParams());
return is_int($seconds) ? $this->getNow()->getTimestamp() - $seconds : null;
}
/**
* Change the redirect URL's parameters based on this form's elements' values
*
* @param string $part Either 'start' or 'end'
* @param string $defaultTime Default if no time given
* @param string $addInterval Add this interval to the result
*
* @return int|null The updated timestamp (if any)
*/
protected function formToUrl($part, $defaultTime, $addInterval = null)
{
$date = $this->getValue("{$part}_date");
$time = $this->getValue("{$part}_time");
$params = $this->getRedirectUrl()->getParams();
$absoluteRangeParameters = static::getAbsoluteRangeParameters();
if ($date === '' && $time === '') {
$params->remove($absoluteRangeParameters[$part]);
} else {
$dateTime = DateTime::createFromFormat(
$this->dateTimeFormat,
($date === '' ? $this->getNow()->format('Y-m-d') : $date)
. 'T' . ($time === '' ? $defaultTime : $time),
$this->getTimeZone()
);
if ($dateTime === false) {
$params->remove($absoluteRangeParameters[$part]);
} else {
if ($addInterval !== null) {
$dateTime->add(new DateInterval($addInterval));
}
$params->set($absoluteRangeParameters[$part], $dateTime->format('U'));
return $dateTime->getTimestamp();
}
}
}
/**
* Get {@link timeZone}
*
* @return DateTimeZone
*/
public function getTimeZone()
{
if ($this->timeZone === null) {
$timezoneDetect = new TimezoneDetect();
$this->timeZone = new DateTimeZone(
$timezoneDetect->success() ? $timezoneDetect->getTimezoneName() : date_default_timezone_get()
);
}
return $this->timeZone;
}
/**
* Set {@link timeZone}
*
* @param DateTimeZone $timeZone
*
* @return $this
*/
public function setTimeZone(DateTimeZone $timeZone)
{
$this->timeZone = $timeZone;
return $this;
}
/**
* Get {@link now}
*
* @return DateTime
*/
public function getNow()
{
if ($this->now === null) {
$this->now = (new DateTime())->setTimezone($this->getTimeZone());
}
return $this->now;
}
/**
* Set {@link now}
*
* @param DateTime $now
*
* @return $this
*/
public function setNow($now)
{
$this->now = $now;
return $this;
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace Icinga\Module\Graphite\Forms\TimeRangePicker;
use Icinga\Web\UrlParams;
trait TimeRangePickerTrait
{
/**
* @return string
*/
public static function getRelativeRangeParameter()
{
return 'graph_range';
}
/**
* @return string[string]
*/
public static function getAbsoluteRangeParameters()
{
return ['start' => 'graph_start', 'end' => 'graph_end'];
}
/**
* @return string
*/
public static function getRangeCustomizationParameter()
{
return 'graph_range_custom';
}
/**
* @return string[]
*/
public static function getAllRangeParameters()
{
return array_values(array_merge([static::getRelativeRangeParameter()], static::getAbsoluteRangeParameters()));
}
/**
* @return string[]
*/
public static function getAllParameters()
{
return array_values(array_merge(static::getAllRangeParameters(), [static::getRangeCustomizationParameter()]));
}
/**
* Extract the relative time range (if any) from the given URL parameters
*
* @param UrlParams $params
*
* @return bool|int|null
*/
protected function getRelativeSeconds(UrlParams $params)
{
$seconds = $params->get(static::getRelativeRangeParameter());
if ($seconds === null) {
return null;
}
return preg_match('/^(?:0|[1-9]\d*)$/', $seconds) ? (int) $seconds : false;
}
}

View file

@ -0,0 +1,129 @@
<?php
namespace Icinga\Module\Graphite\Web\Controller;
use Icinga\Module\Graphite\Forms\TimeRangePicker\CommonForm;
use Icinga\Module\Graphite\Forms\TimeRangePicker\CustomForm;
use Icinga\Module\Graphite\Forms\TimeRangePicker\TimeRangePickerTrait as TimeRangePicker;
use Icinga\Web\Request;
use Icinga\Web\Url;
use Icinga\Web\View;
trait TimeRangePickerTrait
{
/**
* @var CommonForm
*/
protected $timeRangePickerCommonForm;
/**
* @var CustomForm
*/
protected $timeRangePickerCustomForm;
/**
* Process the given request using the forms
*
* @param Request $request The request to be processed
*
* @return Request The request supposed to be processed
*/
protected function handleTimeRangePickerRequest(Request $request = null)
{
$this->getTimeRangePickerCommonForm()->handleRequest($request);
return $this->getTimeRangePickerCustomForm()->handleRequest($request);
}
/**
* Render all needed forms and links
*
* @param View $view
*
* @return string
*/
protected function renderTimeRangePicker(View $view)
{
$result = $this->getTimeRangePickerCommonForm();
$url = Url::fromRequest();
$relevantParams = TimeRangePicker::getAllRangeParameters();
foreach ($relevantParams as $param) {
if ($url->hasParam($param)) {
$result .= $view->qlink(
$view->translate('Clear', 'TimeRangePicker'),
$url->without($relevantParams),
null,
['class' => 'button-link']
);
break;
}
}
if ($url->hasParam(TimeRangePicker::getRangeCustomizationParameter())) {
$result .= $this->getTimeRangePickerCustomForm();
} else {
$result .= $view->qlink(
$view->translate('Custom', 'TimeRangePicker'),
$url->with(TimeRangePicker::getRangeCustomizationParameter(), '1'),
null,
['class' => 'button-link']
);
}
return '<div class="timerangepicker-forms">' . $result . '</div>';
}
/**
* Get {@link timeRangePickerCommonForm}
*
* @return CommonForm
*/
public function getTimeRangePickerCommonForm()
{
if ($this->timeRangePickerCommonForm === null) {
$this->timeRangePickerCommonForm = new CommonForm();
}
return $this->timeRangePickerCommonForm;
}
/**
* Set {@link timeRangePickerCommonForm}
*
* @param CommonForm $timeRangePickerCommonForm
*
* @return $this
*/
public function setTimeRangePickerCommonForm(CommonForm $timeRangePickerCommonForm)
{
$this->timeRangePickerCommonForm = $timeRangePickerCommonForm;
return $this;
}
/**
* Get {@link timeRangePickerCustomForm}
*
* @return CustomForm
*/
public function getTimeRangePickerCustomForm()
{
if ($this->timeRangePickerCustomForm === null) {
$this->timeRangePickerCustomForm = new CustomForm();
}
return $this->timeRangePickerCustomForm;
}
/**
* Set {@link timeRangePickerCustomForm}
*
* @param CustomForm $timeRangePickerCustomForm
*
* @return $this
*/
public function setTimeRangePickerCustomForm(CustomForm $timeRangePickerCustomForm)
{
$this->timeRangePickerCustomForm = $timeRangePickerCustomForm;
return $this;
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Icinga\Module\Graphite\Web\Form\Decorator;
use Zend_Form_Decorator_Abstract;
use Zend_Form_Decorator_Interface;
/**
* Wrap a decorator and use it only for rendering
*/
class Proxy extends Zend_Form_Decorator_Abstract
{
/**
* The actual decorator being proxied
*
* @var Zend_Form_Decorator_Interface
*/
protected $actualDecorator;
public function render($content)
{
return $this->actualDecorator->render($content);
}
/**
* Get {@link actualDecorator}
*
* @return Zend_Form_Decorator_Interface
*/
public function getActualDecorator()
{
return $this->actualDecorator;
}
/**
* Set {@link actualDecorator}
*
* @param Zend_Form_Decorator_Interface $actualDecorator
*
* @return $this
*/
public function setActualDecorator($actualDecorator)
{
$this->actualDecorator = $actualDecorator;
return $this;
}
}

View file

@ -49,3 +49,26 @@ ul.legend {
}
}
}
form[name=form_timerangepickercommon_graphite] select {
width: 7.5em;
margin-right: 0.25em;
}
form[name=form_timerangepickercustom_graphite] {
input[type=date] {
width: 12.5em;
}
input[type=time] {
width: 7.5em;
}
}
.timerangepicker-forms {
padding: 0.25em;
> * {
margin: 0.25em;
}
}