diff --git a/application/forms/TimeRangePicker/CommonForm.php b/application/forms/TimeRangePicker/CommonForm.php new file mode 100644 index 0000000..c00ce65 --- /dev/null +++ b/application/forms/TimeRangePicker/CommonForm.php @@ -0,0 +1,189 @@ + 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; + } + } + } +} diff --git a/application/forms/TimeRangePicker/CustomForm.php b/application/forms/TimeRangePicker/CustomForm.php new file mode 100644 index 0000000..a90834c --- /dev/null +++ b/application/forms/TimeRangePicker/CustomForm.php @@ -0,0 +1,273 @@ +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; + } +} diff --git a/application/forms/TimeRangePicker/TimeRangePickerTrait.php b/application/forms/TimeRangePicker/TimeRangePickerTrait.php new file mode 100644 index 0000000..26577a5 --- /dev/null +++ b/application/forms/TimeRangePicker/TimeRangePickerTrait.php @@ -0,0 +1,65 @@ + '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; + } +} diff --git a/library/Graphite/Web/Controller/TimeRangePickerTrait.php b/library/Graphite/Web/Controller/TimeRangePickerTrait.php new file mode 100644 index 0000000..79a6619 --- /dev/null +++ b/library/Graphite/Web/Controller/TimeRangePickerTrait.php @@ -0,0 +1,129 @@ +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 '