summaryrefslogtreecommitdiffstats
path: root/application
diff options
context:
space:
mode:
Diffstat (limited to 'application')
-rw-r--r--application/clicommands/Icinga2Command.php206
-rw-r--r--application/controllers/ConfigController.php30
-rw-r--r--application/controllers/GraphController.php172
-rw-r--r--application/controllers/HostsController.php112
-rw-r--r--application/controllers/ListController.php192
-rw-r--r--application/controllers/MonitoringGraphController.php155
-rw-r--r--application/controllers/ServicesController.php115
-rw-r--r--application/forms/Config/AdvancedForm.php95
-rw-r--r--application/forms/Config/BackendForm.php59
-rw-r--r--application/forms/TimeRangePicker/CommonForm.php196
-rw-r--r--application/forms/TimeRangePicker/CustomForm.php246
-rw-r--r--application/views/scripts/config/advanced.phtml7
-rw-r--r--application/views/scripts/config/backend.phtml7
-rw-r--r--application/views/scripts/list/hosts.phtml67
-rw-r--r--application/views/scripts/list/services.phtml77
-rw-r--r--application/views/scripts/test/apache.phtml12
-rw-r--r--application/views/scripts/test/cpu.phtml28
17 files changed, 1776 insertions, 0 deletions
diff --git a/application/clicommands/Icinga2Command.php b/application/clicommands/Icinga2Command.php
new file mode 100644
index 0000000..816e063
--- /dev/null
+++ b/application/clicommands/Icinga2Command.php
@@ -0,0 +1,206 @@
+<?php
+
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Graphite\Clicommands;
+
+use Icinga\Cli\Command;
+use Icinga\Module\Graphite\Graphing\GraphingTrait;
+use Icinga\Module\Graphite\Graphing\Template;
+use Icinga\Module\Graphite\Util\MacroTemplate;
+use Icinga\Module\Graphite\Web\Widget\Graphs;
+
+class Icinga2Command extends Command
+{
+ use GraphingTrait;
+
+ /**
+ * Generate Icinga 2 host and service config based on the present graph templates
+ *
+ * The generated (fictive) monitored objects' checks yield random perfdata to be
+ * written to Graphite as expected by the present graph templates of this module.
+ * The generated Icinga 2 config can be used to simulate graphs generated based
+ * on the graph templates.
+ *
+ * icingacli graphite icinga2 config
+ */
+ public function configAction()
+ {
+ $icinga2CfgObjPrefix = 'IW2_graphite_demo';
+ $obscuredCheckCommandCustomVar = Graphs::getObscuredCheckCommandCustomVar();
+
+ $result = [
+ <<<EOT
+object CheckCommand "$icinga2CfgObjPrefix" {
+ command = [ "/usr/bin/printf" ]
+ arguments = {
+ "%s" = {{
+ var res = " |"
+ for (label => max in macro("\$$icinga2CfgObjPrefix\$")) {
+ res += " '" + label + "'=" + (random() % max) + ";" + (max * 0.8) + ";" + (max * 0.9) + ";0;" + max
+ }
+ res
+ }}
+ }
+}
+EOT
+ ,
+ <<<EOT
+object HostGroup "$icinga2CfgObjPrefix" {
+ assign where host.vars.$icinga2CfgObjPrefix
+}
+EOT
+ ,
+ <<<EOT
+object ServiceGroup "$icinga2CfgObjPrefix" {
+ assign where service.vars.$icinga2CfgObjPrefix
+}
+EOT
+ ,
+ <<<EOT
+object Host "{$icinga2CfgObjPrefix}_doesntmatchanycheckcommand" {
+ check_command = "$icinga2CfgObjPrefix"
+ check_interval = 30s
+ vars.$obscuredCheckCommandCustomVar = "doesntmatchanycheckcommand"
+ vars.$icinga2CfgObjPrefix = {
+ "dummy1" = 100
+ "dummy2" = 100
+ "dummy3" = 100
+ "dummy4" = 100
+ }
+}
+EOT
+ ,
+ <<<EOT
+apply Service "{$icinga2CfgObjPrefix}_doesntmatchanycheckcommand" {
+ assign where host.vars.$icinga2CfgObjPrefix
+ check_command = "$icinga2CfgObjPrefix"
+ check_interval = 30s
+ vars.$obscuredCheckCommandCustomVar = "doesntmatchanycheckcommand"
+ vars.$icinga2CfgObjPrefix = {
+ "dummy1" = 100
+ "dummy2" = 100
+ "dummy3" = 100
+ "dummy4" = 100
+ }
+}
+EOT
+ ];
+
+ foreach (static::getAllTemplates()->getAllTemplates() as $checkCommand => $templates) {
+ $perfdata = [];
+
+ foreach ($templates as $templateName => $template) {
+ /** @var Template $template */
+
+ $urlParams = $template->getUrlParams();
+
+ switch (isset($urlParams['yUnitSystem']) ? $urlParams['yUnitSystem']->resolve([]) : 'none') {
+ case 'si':
+ case 'binary':
+ $max = 42000000;
+ break;
+
+ case 'sec':
+ case 'msec':
+ $max = 82800;
+ break;
+
+ default:
+ $max = 100;
+ }
+
+ foreach ($template->getCurves() as $curveName => $curve) {
+ /** @var MacroTemplate $metricFilter */
+ $metricFilter = $curve[0];
+
+ $macros = array_flip($metricFilter->getMacros());
+ $service = isset($macros['service_name_template']);
+
+ foreach ($macros as & $macro) {
+ $macro = ['dummy1', 'dummy2', 'dummy3', 'dummy4'];
+ }
+
+ $macros['host_name_template'] = [''];
+ $macros['service_name_template'] = [''];
+
+ foreach ($this->cartesianProduct($macros) as $macroValues) {
+ if (
+ preg_match(
+ '/\A\.[^.]+\.(.+)\.[^.]+\z/',
+ $metricFilter->resolve($macroValues),
+ $match
+ )
+ ) {
+ $perfdata[$match[1]] = $max;
+ }
+ }
+ }
+ }
+
+ $monObj = $service
+ ? [
+ "apply Service \"{$icinga2CfgObjPrefix}_{$checkCommand}\" {",
+ " assign where host.vars.$icinga2CfgObjPrefix"
+ ]
+ : ["object Host \"{$icinga2CfgObjPrefix}_{$checkCommand}\" {"];
+
+ $monObj[] = " check_command = \"$icinga2CfgObjPrefix\"";
+ $monObj[] = ' check_interval = 30s';
+ $monObj[] = " vars.$obscuredCheckCommandCustomVar = \"$checkCommand\"";
+ $monObj[] = " vars.$icinga2CfgObjPrefix = {";
+
+ foreach ($perfdata as $label => $max) {
+ $monObj[] = " \"$label\" = $max";
+ }
+
+ $monObj[] = ' }';
+ $monObj[] = '}';
+
+ $result[] = implode("\n", $monObj);
+ }
+
+ echo implode("\n\n", $result) . "\n";
+ }
+
+ /**
+ * Generate the cartesian product of the given array
+ *
+ * [
+ * 'a' => ['b', 'c'],
+ * 'd' => ['e', 'f']
+ * ]
+ *
+ * [
+ * ['a' => 'b', 'd' => 'e'],
+ * ['a' => 'b', 'd' => 'f'],
+ * ['a' => 'c', 'd' => 'e'],
+ * ['a' => 'c', 'd' => 'f']
+ * ]
+ *
+ * @param array[] $input
+ *
+ * @return array[]
+ */
+ protected function cartesianProduct(array &$input)
+ {
+ $results = [[]];
+
+ foreach ($input as $key => & $values) {
+ $nextStep = [];
+
+ foreach ($results as & $result) {
+ foreach ($values as $value) {
+ $nextStep[] = array_merge($result, [$key => $value]);
+ }
+ }
+ unset($result);
+
+ $results = & $nextStep;
+ unset($nextStep);
+ }
+ unset($values);
+
+ return $results;
+ }
+}
diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php
new file mode 100644
index 0000000..f627e36
--- /dev/null
+++ b/application/controllers/ConfigController.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Icinga\Module\Graphite\Controllers;
+
+use Icinga\Module\Graphite\Forms\Config\AdvancedForm;
+use Icinga\Module\Graphite\Forms\Config\BackendForm;
+use Icinga\Web\Controller;
+
+class ConfigController extends Controller
+{
+ public function init()
+ {
+ $this->assertPermission('config/modules');
+ parent::init();
+ }
+
+ public function backendAction()
+ {
+ $this->view->form = $form = new BackendForm();
+ $form->setIniConfig($this->Config())->handleRequest();
+ $this->view->tabs = $this->Module()->getConfigTabs()->activate('backend');
+ }
+
+ public function advancedAction()
+ {
+ $this->view->form = $form = new AdvancedForm();
+ $form->setIniConfig($this->Config())->handleRequest();
+ $this->view->tabs = $this->Module()->getConfigTabs()->activate('advanced');
+ }
+}
diff --git a/application/controllers/GraphController.php b/application/controllers/GraphController.php
new file mode 100644
index 0000000..c8dc7db
--- /dev/null
+++ b/application/controllers/GraphController.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace Icinga\Module\Graphite\Controllers;
+
+use Icinga\Exception\Http\HttpBadRequestException;
+use Icinga\Exception\Http\HttpNotFoundException;
+use Icinga\Module\Graphite\Graphing\GraphingTrait;
+use Icinga\Module\Graphite\Util\IcingadbUtils;
+use Icinga\Module\Graphite\Web\Widget\Graphs;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Web\Controller;
+use Icinga\Web\UrlParams;
+use ipl\Orm\Model;
+use ipl\Stdlib\Filter;
+
+class GraphController extends Controller
+{
+ use GraphingTrait;
+
+ /**
+ * The URL parameters for the graph
+ *
+ * @var string[]
+ */
+ protected $graphParamsNames = [
+ 'start', 'end',
+ 'width', 'height',
+ 'legend',
+ 'template', 'default_template',
+ 'bgcolor', 'fgcolor',
+ 'majorGridLineColor', 'minorGridLineColor'
+ ];
+
+ /**
+ * The URL parameters for metrics filtering
+ *
+ * @var UrlParams
+ */
+ protected $filterParams;
+
+ /**
+ * The URL parameters for the graph
+ *
+ * @var string[]
+ */
+ protected $graphParams = [];
+
+ public function init()
+ {
+ parent::init();
+
+ $this->filterParams = clone $this->getRequest()->getUrl()->getParams();
+
+ foreach ($this->graphParamsNames as $paramName) {
+ $this->graphParams[$paramName] = $this->filterParams->shift($paramName);
+ }
+ }
+
+ public function serviceAction()
+ {
+ $hostName = $this->filterParams->getRequired('host.name');
+ $serviceName = $this->filterParams->getRequired('service.name');
+ $icingadbUtils = IcingadbUtils::getInstance();
+ $query = Service::on($icingadbUtils->getDb())
+ ->with('state')
+ ->with('host');
+
+ $query->filter(Filter::all(
+ Filter::equal('service.name', $serviceName),
+ Filter::equal('service.host.name', $hostName)
+ ));
+
+ $icingadbUtils->applyRestrictions($query);
+
+ /** @var Service $service */
+ $service = $query->first();
+
+ if ($service === null) {
+ throw new HttpNotFoundException($this->translate('No such service'));
+ }
+
+ $checkCommandColumn = $service->vars[Graphs::getObscuredCheckCommandCustomVar()] ?? null;
+
+ $this->supplyImage(
+ $service,
+ $service->checkcommand_name,
+ $checkCommandColumn
+ );
+ }
+
+ public function hostAction()
+ {
+ $hostName = $this->filterParams->getRequired('host.name');
+ $icingadbUtils = IcingadbUtils::getInstance();
+ $query = Host::on($icingadbUtils->getDb())->with('state');
+ $query->filter(Filter::equal('host.name', $hostName));
+
+ $icingadbUtils->applyRestrictions($query);
+
+ /** @var Host $host */
+ $host = $query->first();
+
+ if ($host === null) {
+ throw new HttpNotFoundException($this->translate('No such host'));
+ }
+
+ $checkCommandColumn = $host->vars[Graphs::getObscuredCheckCommandCustomVar()] ?? null;
+
+ $this->supplyImage(
+ $host,
+ $host->checkcommand_name,
+ $checkCommandColumn
+ );
+ }
+
+ /**
+ * Do all monitored object type independent actions
+ *
+ * @param Model $object The object to render the graphs for
+ * @param string $checkCommand The check command of the object we supply an image for
+ * @param string|null $obscuredCheckCommand The "real" check command (if any) of the object we
+ * display graphs for
+ */
+ protected function supplyImage($object, $checkCommand, $obscuredCheckCommand)
+ {
+ if (isset($this->graphParams['default_template'])) {
+ $urlParam = 'default_template';
+ $templates = $this->getAllTemplates()->getDefaultTemplates();
+ } else {
+ $urlParam = 'template';
+ $templates = $this->getAllTemplates()->getTemplates(
+ $obscuredCheckCommand === null ? $checkCommand : $obscuredCheckCommand
+ );
+ }
+
+ if (! isset($templates[$this->graphParams[$urlParam]])) {
+ throw new HttpNotFoundException($this->translate('No such template'));
+ }
+
+ $charts = $templates[$this->graphParams[$urlParam]]->getCharts(
+ static::getMetricsDataSource(),
+ $object,
+ array_map('rawurldecode', $this->filterParams->toArray(false))
+ );
+
+ switch (count($charts)) {
+ case 0:
+ throw new HttpNotFoundException($this->translate('No such graph'));
+
+ case 1:
+ $charts[0]
+ ->setFrom($this->graphParams['start'])
+ ->setUntil($this->graphParams['end'])
+ ->setWidth($this->graphParams['width'])
+ ->setHeight($this->graphParams['height'])
+ ->setBackgroundColor($this->graphParams['bgcolor'])
+ ->setForegroundColor($this->graphParams['fgcolor'])
+ ->setMajorGridLineColor($this->graphParams['majorGridLineColor'])
+ ->setMinorGridLineColor($this->graphParams['minorGridLineColor'])
+ ->setShowLegend((bool) $this->graphParams['legend'])
+ ->serveImage($this->getResponse());
+
+ // not falling through, serveImage exits
+ default:
+ throw new HttpBadRequestException('%s', $this->translate(
+ 'Graphite Web yields more than one metric for the given filter.'
+ . ' Please specify a more precise filter.'
+ ));
+ }
+ }
+}
diff --git a/application/controllers/HostsController.php b/application/controllers/HostsController.php
new file mode 100644
index 0000000..f77281a
--- /dev/null
+++ b/application/controllers/HostsController.php
@@ -0,0 +1,112 @@
+<?php
+
+/* Icinga Graphite Web | (c) 2022 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Graphite\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Module\Graphite\Web\Controller\IcingadbGraphiteController;
+use Icinga\Module\Graphite\Web\Controller\TimeRangePickerTrait;
+use Icinga\Module\Graphite\Web\Widget\IcingadbGraphs;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+use Icinga\Web\Url;
+use ipl\Html\HtmlString;
+use ipl\Stdlib\Filter;
+use ipl\Web\Control\LimitControl;
+use ipl\Web\Control\SortControl;
+
+class HostsController extends IcingadbGraphiteController
+{
+ use TimeRangePickerTrait;
+
+ public function indexAction()
+ {
+ if (! $this->useIcingadbAsBackend) {
+ $params = urldecode($this->params->get('legacyParams'));
+ $this->redirectNow(Url::fromPath('graphite/list/hosts')->setQueryString($params));
+ }
+
+ // shift graph params to avoid exception
+ $graphRange = $this->params->shift('graph_range');
+ $baseFilter = $graphRange ? Filter::equal('graph_range', $graphRange) : null;
+ foreach ($this->graphParams as $param) {
+ $this->params->shift($param);
+ }
+
+ $this->addTitleTab(t('Hosts'));
+
+ $db = $this->getDb();
+
+ $hosts = Host::on($db)->with('state');
+ $hosts->filter(Filter::like('state.performance_data', '*'));
+
+ $this->applyRestrictions($hosts);
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($hosts);
+ $sortControl = $this->createSortControl($hosts, ['host.display_name' => t('Hostname')]);
+
+ $searchBar = $this->createSearchBar(
+ $hosts,
+ array_merge(
+ [$limitControl->getLimitParam(), $sortControl->getSortParam()],
+ $this->graphParams
+ )
+ );
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ $hosts->filter($filter);
+
+ $this->addControl($paginationControl);
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($searchBar);
+ $this->handleTimeRangePickerRequest();
+ $this->addControl(HtmlString::create($this->renderTimeRangePicker($this->view)));
+
+ $this->addContent(
+ (new IcingadbGraphs($hosts->execute()))
+ ->setBaseFilter($baseFilter)
+ );
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate();
+ }
+
+ $this->setAutorefreshInterval(30);
+ }
+
+ public function completeAction()
+ {
+ $suggestions = new ObjectSuggestions();
+ $suggestions->setModel(Host::class);
+ $suggestions->forRequest(ServerRequest::fromGlobals());
+ $this->getDocument()->add($suggestions);
+ }
+
+ public function searchEditorAction()
+ {
+ $editor = $this->createSearchEditor(
+ Host::on($this->getDb()),
+ array_merge(
+ [LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM],
+ $this->graphParams
+ )
+ );
+
+ $this->getDocument()->add($editor);
+ $this->setTitle(t('Adjust Filter'));
+ }
+}
diff --git a/application/controllers/ListController.php b/application/controllers/ListController.php
new file mode 100644
index 0000000..46d8321
--- /dev/null
+++ b/application/controllers/ListController.php
@@ -0,0 +1,192 @@
+<?php
+
+namespace Icinga\Module\Graphite\Controllers;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Graphite\Util\TimeRangePickerTools;
+use Icinga\Module\Graphite\Web\Controller\MonitoringAwareController;
+use Icinga\Module\Graphite\Web\Controller\TimeRangePickerTrait;
+use Icinga\Module\Icingadb\Compat\UrlMigrator;
+use Icinga\Module\Monitoring\DataView\DataView;
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\MenuAction;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+use ipl\Web\Filter\QueryString;
+
+class ListController extends MonitoringAwareController
+{
+ use TimeRangePickerTrait;
+
+ public function init()
+ {
+ parent::init();
+ $this->getTabs()
+ ->extend(new OutputFormat([OutputFormat::TYPE_CSV, OutputFormat::TYPE_JSON]))
+ ->extend(new DashboardAction())
+ ->extend(new MenuAction());
+ }
+
+ public function hostsAction()
+ {
+ if ($this->useIcingadbAsBackend) {
+ $legacyParams = urlencode($this->params->toString());
+ $params = QueryString::render(
+ UrlMigrator::transformFilter(
+ QueryString::parse($this->params->toString())
+ )
+ );
+
+ $url = Url::fromPath('graphite/hosts')
+ ->setQueryString($params);
+
+ if ($legacyParams) {
+ $url->setParam('legacyParams', $legacyParams);
+ }
+
+ $this->redirectNow($url);
+ }
+
+ $this->addTitleTab(
+ 'hosts',
+ mt('monitoring', 'Hosts'),
+ mt('monitoring', 'List hosts')
+ );
+
+ $hostsQuery = $this->applyMonitoringRestriction(
+ $this->backend->select()->from('hoststatus', ['host_name'])
+ );
+
+ $hostsQuery->applyFilter(Filter::expression('host_perfdata', '!=', ''));
+
+ $this->view->baseUrl = $baseUrl = Url::fromPath('monitoring/host/show');
+ TimeRangePickerTools::copyAllRangeParameters(
+ $baseUrl->getParams(),
+ $this->getRequest()->getUrl()->getParams()
+ );
+
+ $this->filterQuery($hostsQuery);
+ $this->setupPaginationControl($hostsQuery);
+ $this->setupLimitControl();
+ $this->setupSortControl(['host_display_name' => mt('monitoring', 'Hostname')], $hostsQuery);
+
+ $hosts = [];
+ foreach ($hostsQuery->peekAhead($this->view->compact) as $host) {
+ $host = new Host($this->backend, $host->host_name);
+ $host->fetch();
+ $hosts[] = $host;
+ }
+
+ $this->handleTimeRangePickerRequest();
+ $this->view->timeRangePicker = $this->renderTimeRangePicker($this->view);
+ $this->view->hosts = $hosts;
+ $this->view->hasMoreHosts = ! $this->view->compact && $hostsQuery->hasMore();
+
+ $this->setAutorefreshInterval(30);
+ }
+
+ public function servicesAction()
+ {
+ if ($this->useIcingadbAsBackend) {
+ $legacyParams = urlencode($this->params->toString());
+ $params = QueryString::render(
+ UrlMigrator::transformFilter(
+ QueryString::parse($this->params->toString())
+ )
+ );
+
+ $url = Url::fromPath('graphite/services')
+ ->setQueryString($params);
+
+ if ($legacyParams) {
+ $url->setParam('legacyParams', $legacyParams);
+ }
+
+ $this->redirectNow($url);
+ }
+
+ $this->addTitleTab(
+ 'services',
+ mt('monitoring', 'Services'),
+ mt('monitoring', 'List services')
+ );
+
+ $servicesQuery = $this->applyMonitoringRestriction(
+ $this->backend->select()->from('servicestatus', ['host_name', 'service_description'])
+ );
+
+ $servicesQuery->applyFilter(Filter::expression('service_perfdata', '!=', ''));
+
+ $this->view->hostBaseUrl = $hostBaseUrl = Url::fromPath('monitoring/host/show');
+ TimeRangePickerTools::copyAllRangeParameters(
+ $hostBaseUrl->getParams(),
+ $this->getRequest()->getUrl()->getParams()
+ );
+
+ $this->view->serviceBaseUrl = $serviceBaseUrl = Url::fromPath('monitoring/service/show');
+ TimeRangePickerTools::copyAllRangeParameters(
+ $serviceBaseUrl->getParams(),
+ $this->getRequest()->getUrl()->getParams()
+ );
+
+ $this->filterQuery($servicesQuery);
+ $this->setupPaginationControl($servicesQuery);
+ $this->setupLimitControl();
+ $this->setupSortControl([
+ 'service_display_name' => mt('monitoring', 'Service Name'),
+ 'host_display_name' => mt('monitoring', 'Hostname')
+ ], $servicesQuery);
+
+ $services = [];
+ foreach ($servicesQuery->peekAhead($this->view->compact) as $service) {
+ $service = new Service($this->backend, $service->host_name, $service->service_description);
+ $service->fetch();
+ $services[] = $service;
+ }
+
+ $this->handleTimeRangePickerRequest();
+ $this->view->timeRangePicker = $this->renderTimeRangePicker($this->view);
+ $this->view->services = $services;
+ $this->view->hasMoreServices = ! $this->view->compact && $servicesQuery->hasMore();
+
+ $this->setAutorefreshInterval(30);
+ }
+
+ /**
+ * Apply filters on a DataView
+ *
+ * @param DataView $dataView The DataView to apply filters on
+ */
+ protected function filterQuery(DataView $dataView)
+ {
+ $this->setupFilterControl(
+ $dataView,
+ null,
+ null,
+ array_merge(
+ ['format', 'stateType', 'addColumns', 'problems', 'graphs_limit'],
+ TimeRangePickerTools::getAllRangeParameters()
+ )
+ );
+ $this->handleFormatRequest($dataView);
+ }
+
+ /**
+ * Add title tab
+ *
+ * @param string $action
+ * @param string $title
+ * @param string $tip
+ */
+ protected function addTitleTab($action, $title, $tip)
+ {
+ $this->getTabs()->add($action, [
+ 'title' => $tip,
+ 'label' => $title,
+ 'url' => Url::fromRequest(),
+ 'active' => true
+ ]);
+ }
+}
diff --git a/application/controllers/MonitoringGraphController.php b/application/controllers/MonitoringGraphController.php
new file mode 100644
index 0000000..583c859
--- /dev/null
+++ b/application/controllers/MonitoringGraphController.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Icinga\Module\Graphite\Controllers;
+
+use Icinga\Exception\Http\HttpBadRequestException;
+use Icinga\Exception\Http\HttpNotFoundException;
+use Icinga\Module\Graphite\Graphing\GraphingTrait;
+use Icinga\Module\Graphite\Web\Controller\MonitoringAwareController;
+use Icinga\Module\Graphite\Web\Widget\Graphs;
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Web\UrlParams;
+
+class MonitoringGraphController extends MonitoringAwareController
+{
+ use GraphingTrait;
+
+ /**
+ * The URL parameters for the graph
+ *
+ * @var string[]
+ */
+ protected $graphParamsNames = [
+ 'start', 'end',
+ 'width', 'height',
+ 'legend',
+ 'template', 'default_template',
+ 'bgcolor', 'fgcolor',
+ 'majorGridLineColor', 'minorGridLineColor'
+ ];
+
+ /**
+ * The URL parameters for metrics filtering
+ *
+ * @var UrlParams
+ */
+ protected $filterParams;
+
+ /**
+ * The URL parameters for the graph
+ *
+ * @var string[]
+ */
+ protected $graphParams = [];
+
+ public function init()
+ {
+ parent::init();
+
+ $this->filterParams = clone $this->getRequest()->getUrl()->getParams();
+
+ foreach ($this->graphParamsNames as $paramName) {
+ $this->graphParams[$paramName] = $this->filterParams->shift($paramName);
+ }
+ }
+
+ public function hostAction()
+ {
+ $hostName = $this->filterParams->getRequired('host.name');
+ $checkCommandColumn = '_host_' . Graphs::getObscuredCheckCommandCustomVar();
+ $host = $this->applyMonitoringRestriction(
+ $this->backend->select()->from('hoststatus', ['host_check_command', $checkCommandColumn])
+ )
+ ->where('host_name', $hostName)
+ ->limit(1) // just to be sure to save a few CPU cycles
+ ->fetchRow();
+
+ if ($host === false) {
+ throw new HttpNotFoundException('%s', $this->translate('No such host'));
+ }
+
+ $this->supplyImage(new Host($this->backend, $hostName), $host->host_check_command, $host->$checkCommandColumn);
+ }
+
+ public function serviceAction()
+ {
+ $hostName = $this->filterParams->getRequired('host.name');
+ $serviceName = $this->filterParams->getRequired('service.name');
+ $checkCommandColumn = '_service_' . Graphs::getObscuredCheckCommandCustomVar();
+ $service = $this->applyMonitoringRestriction(
+ $this->backend->select()->from('servicestatus', ['service_check_command', $checkCommandColumn])
+ )
+ ->where('host_name', $hostName)
+ ->where('service_description', $serviceName)
+ ->limit(1) // just to be sure to save a few CPU cycles
+ ->fetchRow();
+
+ if ($service === false) {
+ throw new HttpNotFoundException('%s', $this->translate('No such service'));
+ }
+
+ $this->supplyImage(
+ new Service($this->backend, $hostName, $serviceName),
+ $service->service_check_command,
+ $service->$checkCommandColumn
+ );
+ }
+
+ /**
+ * Do all monitored object type independend actions
+ *
+ * @param MonitoredObject $object The object to render the graphs for
+ * @param string $checkCommand The check command of the object we supply an image for
+ * @param string|null $obscuredCheckCommand The "real" check command (if any) of the object we
+ * display graphs for
+ */
+ protected function supplyImage($object, $checkCommand, $obscuredCheckCommand)
+ {
+ if (isset($this->graphParams['default_template'])) {
+ $urlParam = 'default_template';
+ $templates = $this->getAllTemplates()->getDefaultTemplates();
+ } else {
+ $urlParam = 'template';
+ $templates = $this->getAllTemplates()->getTemplates(
+ $obscuredCheckCommand === null ? $checkCommand : $obscuredCheckCommand
+ );
+ }
+
+ if (! isset($templates[$this->graphParams[$urlParam]])) {
+ throw new HttpNotFoundException($this->translate('No such template'));
+ }
+
+ $charts = $templates[$this->graphParams[$urlParam]]->getCharts(
+ static::getMetricsDataSource(),
+ $object,
+ array_map('rawurldecode', $this->filterParams->toArray(false))
+ );
+
+ switch (count($charts)) {
+ case 0:
+ throw new HttpNotFoundException($this->translate('No such graph'));
+
+ case 1:
+ $charts[0]
+ ->setFrom($this->graphParams['start'])
+ ->setUntil($this->graphParams['end'])
+ ->setWidth($this->graphParams['width'])
+ ->setHeight($this->graphParams['height'])
+ ->setBackgroundColor($this->graphParams['bgcolor'])
+ ->setForegroundColor($this->graphParams['fgcolor'])
+ ->setMajorGridLineColor($this->graphParams['majorGridLineColor'])
+ ->setMinorGridLineColor($this->graphParams['minorGridLineColor'])
+ ->setShowLegend((bool) $this->graphParams['legend'])
+ ->serveImage($this->getResponse());
+
+ // not falling through, serveImage exits
+ default:
+ throw new HttpBadRequestException('%s', $this->translate(
+ 'Graphite Web yields more than one metric for the given filter.'
+ . ' Please specify a more precise filter.'
+ ));
+ }
+ }
+}
diff --git a/application/controllers/ServicesController.php b/application/controllers/ServicesController.php
new file mode 100644
index 0000000..212ad1f
--- /dev/null
+++ b/application/controllers/ServicesController.php
@@ -0,0 +1,115 @@
+<?php
+
+namespace Icinga\Module\Graphite\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Module\Graphite\Web\Controller\IcingadbGraphiteController;
+use Icinga\Module\Graphite\Web\Controller\TimeRangePickerTrait;
+use Icinga\Module\Graphite\Web\Widget\IcingadbGraphs;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+use Icinga\Web\Url;
+use ipl\Html\HtmlString;
+use ipl\Stdlib\Filter;
+use ipl\Web\Control\LimitControl;
+use ipl\Web\Control\SortControl;
+
+class ServicesController extends IcingadbGraphiteController
+{
+ use TimeRangePickerTrait;
+
+ public function indexAction()
+ {
+ if (! $this->useIcingadbAsBackend) {
+ $params = urldecode($this->params->get('legacyParams'));
+ $this->redirectNow(Url::fromPath('graphite/list/services')->setQueryString($params));
+ }
+
+ // shift graph params to avoid exception
+ $graphRange = $this->params->shift('graph_range');
+ $baseFilter = $graphRange ? Filter::equal('graph_range', $graphRange) : null;
+ foreach ($this->graphParams as $param) {
+ $this->params->shift($param);
+ }
+
+ $this->addTitleTab(t('Services'));
+
+ $db = $this->getDb();
+
+ $services = Service::on($db)
+ ->with('state')
+ ->with('host');
+ $services->filter(Filter::like('state.performance_data', '*'));
+
+ $this->applyRestrictions($services);
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($services);
+ $sortControl = $this->createSortControl($services, [
+ 'service.display_name' => t('Servicename'),
+ 'host.display_name' => t('Hostname')
+ ]);
+
+ $searchBar = $this->createSearchBar(
+ $services,
+ array_merge(
+ [$limitControl->getLimitParam(), $sortControl->getSortParam()],
+ $this->graphParams
+ )
+ );
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ $services->filter($filter);
+
+ $this->addControl($paginationControl);
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($searchBar);
+ $this->handleTimeRangePickerRequest();
+ $this->addControl(HtmlString::create($this->renderTimeRangePicker($this->view)));
+
+ $this->addContent(
+ (new IcingadbGraphs($services->execute()))
+ ->setBaseFilter($baseFilter)
+ );
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate();
+ }
+
+ $this->setAutorefreshInterval(30);
+ }
+
+ public function completeAction()
+ {
+ $suggestions = new ObjectSuggestions();
+ $suggestions->setModel(Service::class);
+ $suggestions->forRequest(ServerRequest::fromGlobals());
+ $this->getDocument()->add($suggestions);
+ }
+
+ public function searchEditorAction()
+ {
+ $editor = $this->createSearchEditor(
+ Service::on($this->getDb()),
+ array_merge(
+ [LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM],
+ $this->graphParams
+ )
+ );
+
+ $this->getDocument()->add($editor);
+ $this->setTitle(t('Adjust Filter'));
+ }
+}
diff --git a/application/forms/Config/AdvancedForm.php b/application/forms/Config/AdvancedForm.php
new file mode 100644
index 0000000..1fc196b
--- /dev/null
+++ b/application/forms/Config/AdvancedForm.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Icinga\Module\Graphite\Forms\Config;
+
+use Icinga\Forms\ConfigForm;
+use Icinga\Module\Graphite\Web\Form\Validator\MacroTemplateValidator;
+use Zend_Validate_Regex;
+
+class AdvancedForm extends ConfigForm
+{
+ public function init()
+ {
+ $this->setName('form_config_graphite_advanced');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ public function createElements(array $formData)
+ {
+ $this->addElements([
+ [
+ 'number',
+ 'ui_default_time_range',
+ [
+ 'label' => $this->translate('Default time range'),
+ 'description' => $this->translate('The default time range for graphs'),
+ 'min' => 0,
+ 'value' => 1
+ ]
+ ],
+ [
+ 'select',
+ 'ui_default_time_range_unit',
+ [
+ 'label' => $this->translate('Default time range unit'),
+ 'description' => $this->translate('The above range\'s unit'),
+ 'multiOptions' => [
+ 'minutes' => $this->translate('Minutes'),
+ 'hours' => $this->translate('Hours'),
+ 'days' => $this->translate('Days'),
+ 'weeks' => $this->translate('Weeks'),
+ 'months' => $this->translate('Months'),
+ 'years' => $this->translate('Years')
+ ],
+ 'value' => 'hours'
+ ]
+ ],
+ [
+ 'checkbox',
+ 'ui_disable_no_graphs_found',
+ [
+ 'label' => $this->translate('Disable "no graphs found"'),
+ 'description' => $this->translate(
+ 'If no graphs were found for a monitored object, just display nothing at all'
+ ),
+ ]
+ ],
+ [
+ 'text',
+ 'icinga_graphite_writer_host_name_template',
+ [
+ 'label' => $this->translate('Host name template'),
+ 'description' => $this->translate(
+ 'The value of your Icinga 2 GraphiteWriter\'s'
+ . ' attribute host_name_template (if specified)'
+ ),
+ 'validators' => [new MacroTemplateValidator()]
+ ]
+ ],
+ [
+ 'text',
+ 'icinga_graphite_writer_service_name_template',
+ [
+ 'label' => $this->translate('Service name template'),
+ 'description' => $this->translate(
+ 'The value of your Icinga 2 GraphiteWriter\'s'
+ . ' attribute service_name_template (if specified)'
+ ),
+ 'validators' => [new MacroTemplateValidator()]
+ ]
+ ],
+ [
+ 'text',
+ 'icinga_customvar_obscured_check_command',
+ [
+ 'label' => $this->translate('Obscured check command custom variable'),
+ 'description' => $this->translate(
+ 'The Icinga custom variable with the "actual" check command obscured'
+ . ' by e.g. check_by_ssh (defaults to check_command)'
+ ),
+ 'validators' => [new Zend_Validate_Regex('/\A\w*\z/')]
+ ]
+ ]
+ ]);
+ }
+}
diff --git a/application/forms/Config/BackendForm.php b/application/forms/Config/BackendForm.php
new file mode 100644
index 0000000..90e0af2
--- /dev/null
+++ b/application/forms/Config/BackendForm.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Icinga\Module\Graphite\Forms\Config;
+
+use Icinga\Forms\ConfigForm;
+use Icinga\Module\Graphite\Web\Form\Validator\HttpUserValidator;
+
+class BackendForm extends ConfigForm
+{
+ public function init()
+ {
+ $this->setName('form_config_graphite_backend');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ public function createElements(array $formData)
+ {
+ $this->addElements([
+ [
+ 'text',
+ 'graphite_url',
+ [
+ 'required' => true,
+ 'label' => $this->translate('Graphite Web URL'),
+ 'description' => $this->translate('URL to your Graphite Web'),
+ 'validators' => ['UrlValidator']
+ ]
+ ],
+ [
+ 'text',
+ 'graphite_user',
+ [
+ 'label' => $this->translate('Graphite Web user'),
+ 'description' => $this->translate(
+ 'A user with access to your Graphite Web via HTTP basic authentication'
+ ),
+ 'validators' => [new HttpUserValidator()]
+ ]
+ ],
+ [
+ 'password',
+ 'graphite_password',
+ [
+ 'renderPassword' => true,
+ 'label' => $this->translate('Graphite Web password'),
+ 'description' => $this->translate('The above user\'s password')
+ ]
+ ],
+ [
+ 'checkbox',
+ 'graphite_insecure',
+ [
+ 'label' => $this->translate('Connect insecurely'),
+ 'description' => $this->translate('Check this to not verify the remote\'s TLS certificate')
+ ]
+ ]
+ ]);
+ }
+}
diff --git a/application/forms/TimeRangePicker/CommonForm.php b/application/forms/TimeRangePicker/CommonForm.php
new file mode 100644
index 0000000..21e2096
--- /dev/null
+++ b/application/forms/TimeRangePicker/CommonForm.php
@@ -0,0 +1,196 @@
+<?php
+
+namespace Icinga\Module\Graphite\Forms\TimeRangePicker;
+
+use Icinga\Module\Graphite\Util\TimeRangePickerTools;
+use Icinga\Web\Form;
+use Zend_Form_Element_Select;
+
+class CommonForm extends Form
+{
+ /**
+ * 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[]
+ */
+ protected $rangeFactors = [
+ 'minutes' => 60,
+ 'hours' => 3600,
+ 'days' => 86400,
+ 'weeks' => 604800,
+ 'months' => 2592000,
+ 'years' => 31557600
+ ];
+
+ /**
+ * The elements' default values
+ *
+ * @var string[]|null
+ */
+ protected $defaultFormData;
+
+ public function init()
+ {
+ $this->setName('form_timerangepickercommon_graphite');
+ $this->setAttrib('data-base-target', '_self');
+ $this->setAttrib('class', 'icinga-form icinga-controls inline');
+ }
+
+ 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(TimeRangePickerTools::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,
+ 'title' => $description,
+ 'autosubmit' => true
+ ]);
+
+ $decorators = $element->getDecorators();
+ $element->setDecorators([
+ 'Zend_Form_Decorator_ViewHelper' => $decorators['Zend_Form_Decorator_ViewHelper']
+ ]);
+
+ return $element;
+ }
+
+ /**
+ * Set this form's elements' default values based on the redirect URL's parameters
+ */
+ protected function urlToForm()
+ {
+ $params = $this->getRedirectUrl()->getParams();
+ $seconds = TimeRangePickerTools::getRelativeSeconds($params);
+
+ if (
+ $seconds === null
+ && count(array_intersect_key(
+ $params->toArray(false),
+ array_keys(TimeRangePickerTools::getAllRangeParameters())
+ )) === 0
+ ) {
+ $seconds = TimeRangePickerTools::getDefaultRelativeTimeRange();
+ }
+
+ 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(TimeRangePickerTools::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(
+ TimeRangePickerTools::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..89b5833
--- /dev/null
+++ b/application/forms/TimeRangePicker/CustomForm.php
@@ -0,0 +1,246 @@
+<?php
+
+namespace Icinga\Module\Graphite\Forms\TimeRangePicker;
+
+use DateInterval;
+use DateTime;
+use DateTimeZone;
+use Icinga\Module\Graphite\Util\TimeRangePickerTools;
+use Icinga\Module\Graphite\Web\Form\Decorator\Proxy;
+use Icinga\Web\Form;
+
+class CustomForm extends Form
+{
+ /**
+ * @var string
+ */
+ protected $dateTimeFormat = 'Y-m-d\TH:i';
+
+ /**
+ * @var string
+ */
+ protected $timestamp = '/^(?:0|-?[1-9]\d*)$/';
+
+ /**
+ * Right now
+ *
+ * @var DateTime
+ */
+ protected $now;
+
+ public function init()
+ {
+ $this->setName('form_timerangepickercustom_graphite');
+ $this->setAttrib('data-base-target', '_self');
+ }
+
+ public function createElements(array $formData)
+ {
+ $this->addElements([
+ [
+ 'date',
+ 'start_date',
+ [
+ 'placeholder' => 'YYYY-MM-DD',
+ 'label' => $this->translate('Start'),
+ 'description' => $this->translate('Start of the date/time range')
+ ]
+ ],
+ [
+ 'time',
+ 'start_time',
+ [
+ 'placeholder' => 'HH:MM',
+ 'label' => $this->translate('Start'),
+ 'description' => $this->translate('Start of the date/time range')
+ ]
+ ],
+ [
+ 'date',
+ 'end_date',
+ [
+ 'placeholder' => 'YYYY-MM-DD',
+ 'label' => $this->translate('End'),
+ 'description' => $this->translate('End of the date/time range')
+ ]
+ ],
+ [
+ 'time',
+ 'end_time',
+ [
+ 'placeholder' => 'HH:MM',
+ '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 addSubmitButton()
+ {
+ $result = parent::addSubmitButton();
+
+ $this->getElement('btn_submit')->class = 'flyover-toggle';
+
+ return $result;
+ }
+
+ public function onSuccess()
+ {
+ $start = $this->formToUrl('start', '00:00');
+ $end = $this->formToUrl('end', '23:59', 'PT59S');
+ if ($start > $end) {
+ $absoluteRangeParameters = TimeRangePickerTools::getAbsoluteRangeParameters();
+ $this->getRedirectUrl()->getParams()
+ ->set($absoluteRangeParameters['start'], $end)
+ ->set($absoluteRangeParameters['end'], $start);
+ }
+
+ $this->getRedirectUrl()->remove(TimeRangePickerTools::getRelativeRangeParameter());
+ }
+
+ /**
+ * 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 = TimeRangePickerTools::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(new DateTimeZone(date_default_timezone_get()))
+ ->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 = TimeRangePickerTools::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 = TimeRangePickerTools::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)
+ );
+
+ 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 now}
+ *
+ * @return DateTime
+ */
+ public function getNow()
+ {
+ if ($this->now === null) {
+ $this->now = new DateTime();
+ }
+
+ return $this->now;
+ }
+
+ /**
+ * Set {@link now}
+ *
+ * @param DateTime $now
+ *
+ * @return $this
+ */
+ public function setNow($now)
+ {
+ $this->now = $now;
+ return $this;
+ }
+}
diff --git a/application/views/scripts/config/advanced.phtml b/application/views/scripts/config/advanced.phtml
new file mode 100644
index 0000000..ab47cdb
--- /dev/null
+++ b/application/views/scripts/config/advanced.phtml
@@ -0,0 +1,7 @@
+<div class="controls">
+ <?= /** @var \Icinga\Web\Widget\Tabs $tabs */ $tabs ?>
+</div>
+
+<div class="content">
+ <?= /** @var \Icinga\Module\Graphite\Forms\Config\AdvancedForm $form */ $form ?>
+</div>
diff --git a/application/views/scripts/config/backend.phtml b/application/views/scripts/config/backend.phtml
new file mode 100644
index 0000000..7750f3c
--- /dev/null
+++ b/application/views/scripts/config/backend.phtml
@@ -0,0 +1,7 @@
+<div class="controls">
+ <?= /** @var \Icinga\Web\Widget\Tabs $tabs */ $tabs ?>
+</div>
+
+<div class="content">
+ <?= /** @var \Icinga\Module\Graphite\Forms\Config\BackendForm $form */ $form ?>
+</div>
diff --git a/application/views/scripts/list/hosts.phtml b/application/views/scripts/list/hosts.phtml
new file mode 100644
index 0000000..ce0e37c
--- /dev/null
+++ b/application/views/scripts/list/hosts.phtml
@@ -0,0 +1,67 @@
+<?php
+
+use Icinga\Module\Graphite\Web\Widget\Graphs\Host;
+use Icinga\Web\Url;
+
+/** @var \Icinga\Web\View $this */
+/** @var \Icinga\Web\Widget\FilterEditor $filterEditor */
+/** @var \Icinga\Module\Monitoring\Object\Host[] $hosts */
+/** @var bool $hasMoreHosts */
+/** @var \Icinga\Web\Url $baseUrl */
+
+if (! $compact): ?>
+<div class="controls">
+ <?= $tabs ?>
+ <?= $paginator ?>
+ <div class="sort-controls-container">
+ <?= $limiter ?>
+ <?= $sortBox ?>
+ </div>
+ <?= $filterEditor ?>
+ <?= $timeRangePicker ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php
+if (! empty($hosts)) {
+ echo '<div class="graphite-graph-color-registry"></div>';
+ echo '<div class="grid">';
+ foreach ($hosts as $host) {
+ $hostGraphs = (string) (new Host($host))->setPreloadDummy()->handleRequest();
+
+ if ($hostGraphs !== '') {
+ echo '<div class="grid-item">'
+ . '<h2>'
+ . $this->qlink(
+ $host->host_name === $host->host_display_name
+ ? $host->host_display_name
+ : $host->host_display_name . ' (' . $this->escape($host->host_name) . ')',
+ $baseUrl->with(['host' => $host->host_name]),
+ null,
+ ['data-base-target' => '_next']
+ )
+ . '</h2>'
+ . $hostGraphs
+ . '</div>';
+ }
+ }
+
+ if ($hasMoreHosts) {
+ echo '<div class="action-links">'
+ . $this->qlink(
+ mt('monitoring', 'Show More'),
+ $this->url()->without(array('view', 'limit')),
+ null,
+ [
+ 'class' => 'action-link',
+ 'data-base-target' => '_next'
+ ]
+ )
+ . '</div>';
+ }
+ echo '</div>';
+} else {
+ echo '<p>' . $this->escape(mt('monitoring', 'No hosts found matching the filter.')) . '</p>';
+}
+?>
+</div>
diff --git a/application/views/scripts/list/services.phtml b/application/views/scripts/list/services.phtml
new file mode 100644
index 0000000..90ca03c
--- /dev/null
+++ b/application/views/scripts/list/services.phtml
@@ -0,0 +1,77 @@
+<?php
+
+use Icinga\Module\Graphite\Web\Widget\Graphs\Service;
+use Icinga\Web\Url;
+
+/** @var \Icinga\Web\View $this */
+/** @var \Icinga\Web\Widget\FilterEditor $filterEditor */
+/** @var \Icinga\Module\Monitoring\Object\Service[] $services */
+/** @var bool $hasMoreServices */
+/** @var \Icinga\Web\Url $hostBaseUrl */
+/** @var \Icinga\Web\Url $serviceBaseUrl */
+
+if (! $compact): ?>
+<div class="controls">
+ <?= $tabs ?>
+ <?= $paginator ?>
+ <div class="sort-controls-container">
+ <?= $limiter ?>
+ <?= $sortBox ?>
+ </div>
+ <?= $filterEditor ?>
+ <?= $timeRangePicker ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php
+if (! empty($services)) {
+ echo '<div class="graphite-graph-color-registry"></div>';
+ echo '<div class="grid">';
+ foreach ($services as $service) {
+ echo '<div class="grid-item">'
+ . '<h2>'
+ . $this->qlink(
+ $service->host_name === $service->host_display_name
+ ? $service->host_display_name
+ : $service->host_display_name . ' (' . $this->escape($service->host_name) . ')',
+ $hostBaseUrl->with(['host' => $service->host_name]),
+ null,
+ ['data-base-target' => '_next']
+ )
+ . '&#58; '
+ . $this->qlink(
+ $service->service_description === $service->service_display_name
+ ? $service->service_display_name
+ : $service->service_display_name . ' (' . $this->escape($service->service_description) . ')',
+ $serviceBaseUrl->with([
+ 'host' => $service->host_name,
+ 'service' => $service->service_description
+ ]),
+ null,
+ ['data-base-target' => '_next']
+ )
+ . '</h2>';
+
+ echo (new Service($service))->setPreloadDummy()->handleRequest();
+ echo '</div>';
+ }
+
+ if ($hasMoreServices) {
+ echo '<div class="action-links">'
+ . $this->qlink(
+ mt('monitoring', 'Show More'),
+ $this->url()->without(array('view', 'limit')),
+ null,
+ [
+ 'class' => 'action-link',
+ 'data-base-target' => '_next'
+ ]
+ )
+ . '</div>';
+ }
+ echo '</div>';
+} else {
+ echo '<p>' . $this->escape(mt('monitoring', 'No services found matching the filter.')) . '</p>';
+}
+?>
+</div>
diff --git a/application/views/scripts/test/apache.phtml b/application/views/scripts/test/apache.phtml
new file mode 100644
index 0000000..069ccbe
--- /dev/null
+++ b/application/views/scripts/test/apache.phtml
@@ -0,0 +1,12 @@
+<div class="controls">
+<?= $this->tabs ?>
+</div>
+
+<div class="content">
+<?php foreach ($this->images as $base => $img): ?>
+<div style="width: 260px; float: left; margin-right: 5px;">
+<h3><?= $this->escape($base) ?></h3>
+<img src="<?= $img ?>" />
+</div>
+<?php endforeach ?>
+</div>
diff --git a/application/views/scripts/test/cpu.phtml b/application/views/scripts/test/cpu.phtml
new file mode 100644
index 0000000..495e315
--- /dev/null
+++ b/application/views/scripts/test/cpu.phtml
@@ -0,0 +1,28 @@
+<?php
+$maxCnt = 0;
+foreach ($this->images as $base => $cpus) {
+ $maxCnt = max($maxCnt, count($cpus));
+}
+?>
+<div class="controls">
+<?= $this->tabs ?>
+<h1>CPUs</h1>
+</div>
+<div class="content">
+<table style="width: 100%;">
+<tr>
+ <th style="width: 15em;">&nbsp;</th>
+ <th>CPUs</th>
+</tr>
+<?php foreach ($this->images as $base => $cpus): ?>
+<tr>
+<th style="vertical-align: top; text-align: right; padding-right: 2em;"><?= $this->escape($base) ?></th>
+<td>
+<?php foreach ($cpus as $num => $img): ?>
+<div style="width: 53px; float: left;"><img src="<?= $img ?>" /><!--<br />CPU <?= $num ?>--></div>
+<?php endforeach ?>
+</td>
+</tr>
+<?php endforeach ?>
+</table>
+</div>