summaryrefslogtreecommitdiffstats
path: root/application/controllers
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--application/controllers/CommandTransportController.php155
-rw-r--r--application/controllers/CommentController.php72
-rw-r--r--application/controllers/CommentsController.php200
-rw-r--r--application/controllers/ConfigController.php63
-rw-r--r--application/controllers/DowntimeController.php84
-rw-r--r--application/controllers/DowntimesController.php206
-rw-r--r--application/controllers/ErrorController.php97
-rw-r--r--application/controllers/EventController.php71
-rw-r--r--application/controllers/HealthController.php115
-rw-r--r--application/controllers/HistoryController.php138
-rw-r--r--application/controllers/HostController.php288
-rw-r--r--application/controllers/HostgroupController.php89
-rw-r--r--application/controllers/HostgroupsController.php121
-rw-r--r--application/controllers/HostsController.php240
-rw-r--r--application/controllers/MigrateController.php120
-rw-r--r--application/controllers/NotificationsController.php134
-rw-r--r--application/controllers/ServiceController.php239
-rw-r--r--application/controllers/ServicegroupController.php95
-rw-r--r--application/controllers/ServicegroupsController.php120
-rw-r--r--application/controllers/ServicesController.php438
-rw-r--r--application/controllers/TacticalController.php96
-rw-r--r--application/controllers/UserController.php48
-rw-r--r--application/controllers/UsergroupController.php48
-rw-r--r--application/controllers/UsergroupsController.php97
-rw-r--r--application/controllers/UsersController.php99
25 files changed, 3473 insertions, 0 deletions
diff --git a/application/controllers/CommandTransportController.php b/application/controllers/CommandTransportController.php
new file mode 100644
index 0000000..b0b4c32
--- /dev/null
+++ b/application/controllers/CommandTransportController.php
@@ -0,0 +1,155 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Application\Config;
+use Icinga\Data\Filter\Filter;
+use Icinga\Forms\ConfirmRemovalForm;
+use Icinga\Module\Icingadb\Command\Transport\CommandTransportConfig;
+use Icinga\Module\Icingadb\Forms\ApiTransportForm;
+use Icinga\Module\Icingadb\Widget\ItemList\CommandTransportList;
+use Icinga\Web\Notification;
+use ipl\Html\HtmlString;
+use ipl\Web\Widget\ButtonLink;
+
+class CommandTransportController extends ConfigController
+{
+ public function init()
+ {
+ $this->assertPermission('config/modules');
+ }
+
+ public function indexAction()
+ {
+ $list = new CommandTransportList((new CommandTransportConfig())->select());
+
+ $this->addControl(
+ (new ButtonLink(
+ t('Create Command Transport'),
+ 'icingadb/command-transport/add',
+ 'plus'
+ ))->setBaseTarget('_next')
+ );
+
+ $this->addContent($list);
+
+ $this->mergeTabs($this->Module()->getConfigTabs());
+ $this->getTabs()->disableLegacyExtensions();
+ $this->setTitle($this->getTabs()
+ ->activate('command-transports')
+ ->getActiveTab()
+ ->getLabel());
+ }
+
+ public function showAction()
+ {
+ $transportName = $this->params->getRequired('name');
+
+ $transportConfig = (new CommandTransportConfig())
+ ->select()
+ ->where('name', $transportName)
+ ->fetchRow();
+ if ($transportConfig === false) {
+ $this->httpNotFound(t('Unknown transport'));
+ }
+
+ $form = new ApiTransportForm();
+ $form->populate((array) $transportConfig);
+ $form->on(ApiTransportForm::ON_SUCCESS, function (ApiTransportForm $form) use ($transportName) {
+ (new CommandTransportConfig())->update(
+ 'transport',
+ $form->getValues(),
+ Filter::where('name', $transportName)
+ );
+
+ Notification::success(sprintf(t('Updated command transport "%s" successfully'), $transportName));
+
+ $this->redirectNow('icingadb/command-transport');
+ });
+
+ $form->handleRequest(ServerRequest::fromGlobals());
+
+ $this->addContent($form);
+
+ $this->addTitleTab($this->translate('Command Transport: %s'), $transportName);
+ $this->getTabs()->disableLegacyExtensions();
+ }
+
+ public function addAction()
+ {
+ $form = new ApiTransportForm();
+ $form->on(ApiTransportForm::ON_SUCCESS, function (ApiTransportForm $form) {
+ (new CommandTransportConfig())->insert('transport', $form->getValues());
+
+ Notification::success(t('Created command transport successfully'));
+
+ $this->redirectNow('icingadb/command-transport');
+ });
+
+ $form->handleRequest(ServerRequest::fromGlobals());
+
+ $this->addContent($form);
+
+ $this->addTitleTab($this->translate('Add Command Transport'));
+ $this->getTabs()->disableLegacyExtensions();
+ }
+
+ public function removeAction()
+ {
+ $transportName = $this->params->getRequired('name');
+
+ $form = new ConfirmRemovalForm();
+ $form->setAttrib('style', 'text-align:center;');
+ $form->setOnSuccess(function () use ($transportName) {
+ (new CommandTransportConfig())->delete(
+ 'transport',
+ Filter::where('name', $transportName)
+ );
+
+ Notification::success(sprintf(t('Removed command transport "%s" successfully'), $transportName));
+
+ $this->redirectNow('icingadb/command-transport');
+ });
+
+ $form->handleRequest();
+
+ $this->addContent(HtmlString::create($form->render()));
+
+ $this->setTitle($this->translate('Remove Command Transport: %s'), $transportName);
+ $this->getTabs()->disableLegacyExtensions();
+ }
+
+ public function sortAction()
+ {
+ $transportName = $this->params->getRequired('name');
+ $newPosition = (int) $this->params->getRequired('pos');
+
+ $config = $this->Config('commandtransports');
+ if (! $config->hasSection($transportName)) {
+ $this->httpNotFound(t('Unknown transport'));
+ }
+
+ if ($newPosition < 0 || $newPosition > $config->count()) {
+ $this->httpBadRequest(t('Position out of bounds'));
+ }
+
+ $transports = $config->getConfigObject()->toArray();
+ $transportNames = array_keys($transports);
+
+ array_splice($transportNames, array_search($transportName, $transportNames, true), 1);
+ array_splice($transportNames, $newPosition, 0, [$transportName]);
+
+ $sortedTransports = [];
+ foreach ($transportNames as $name) {
+ $sortedTransports[$name] = $transports[$name];
+ }
+
+ $newConfig = Config::fromArray($sortedTransports);
+ $newConfig->saveIni($config->getConfigFile());
+
+ $this->redirectNow('icingadb/command-transport');
+ }
+}
diff --git a/application/controllers/CommentController.php b/application/controllers/CommentController.php
new file mode 100644
index 0000000..b184d6b
--- /dev/null
+++ b/application/controllers/CommentController.php
@@ -0,0 +1,72 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Icingadb\Common\CommandActions;
+use Icinga\Module\Icingadb\Common\Links;
+use Icinga\Module\Icingadb\Model\Comment;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\Detail\CommentDetail;
+use Icinga\Module\Icingadb\Widget\ItemList\CommentList;
+use ipl\Stdlib\Filter;
+use ipl\Web\Url;
+
+class CommentController extends Controller
+{
+ use CommandActions;
+
+ /** @var Comment The comment object */
+ protected $comment;
+
+ public function init()
+ {
+ $this->addTitleTab(t('Comment'));
+
+ $name = $this->params->getRequired('name');
+
+ $query = Comment::on($this->getDb())->with([
+ 'host',
+ 'host.state',
+ 'service',
+ 'service.state',
+ 'service.host',
+ 'service.host.state'
+ ]);
+ $query->filter(Filter::equal('comment.name', $name));
+
+ $this->applyRestrictions($query);
+
+ $comment = $query->first();
+ if ($comment === null) {
+ throw new NotFoundError(t('Comment not found'));
+ }
+
+ $this->comment = $comment;
+ }
+
+ public function indexAction()
+ {
+ $this->addControl((new CommentList([$this->comment]))
+ ->setViewMode('minimal')
+ ->setDetailActionsDisabled()
+ ->setCaptionDisabled()
+ ->setNoSubjectLink());
+
+ $this->addContent((new CommentDetail($this->comment))->setTicketLinkEnabled());
+
+ $this->setAutorefreshInterval(10);
+ }
+
+ protected function fetchCommandTargets(): array
+ {
+ return [$this->comment];
+ }
+
+ protected function getCommandTargetsUrl(): Url
+ {
+ return Links::comment($this->comment);
+ }
+}
diff --git a/application/controllers/CommentsController.php b/application/controllers/CommentsController.php
new file mode 100644
index 0000000..298fae5
--- /dev/null
+++ b/application/controllers/CommentsController.php
@@ -0,0 +1,200 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Module\Icingadb\Common\Links;
+use Icinga\Module\Icingadb\Forms\Command\Object\DeleteCommentForm;
+use Icinga\Module\Icingadb\Model\Comment;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\ItemList\CommentList;
+use Icinga\Module\Icingadb\Widget\ShowMore;
+use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
+use ipl\Web\Control\LimitControl;
+use ipl\Web\Control\SortControl;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Url;
+
+class CommentsController extends Controller
+{
+ public function indexAction()
+ {
+ $this->addTitleTab(t('Comments'));
+ $compact = $this->view->compact;
+
+ $db = $this->getDb();
+
+ $comments = Comment::on($db)->with([
+ 'host',
+ 'host.state',
+ 'service',
+ 'service.host',
+ 'service.host.state',
+ 'service.state'
+ ]);
+
+ $this->handleSearchRequest($comments);
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($comments);
+ $sortControl = $this->createSortControl(
+ $comments,
+ [
+ 'comment.entry_time desc' => t('Entry Time'),
+ 'host.display_name' => t('Host'),
+ 'service.display_name' => t('Service'),
+ 'comment.author' => t('Author'),
+ 'comment.expire_time desc' => t('Expire Time')
+ ]
+ );
+ $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl);
+ $searchBar = $this->createSearchBar($comments, [
+ $limitControl->getLimitParam(),
+ $sortControl->getSortParam(),
+ $viewModeSwitcher->getViewModeParam()
+ ]);
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ $this->filter($comments, $filter);
+
+ $comments->peekAhead($compact);
+
+ yield $this->export($comments);
+
+ $this->addControl($paginationControl);
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($viewModeSwitcher);
+ $this->addControl($searchBar);
+ $continueWith = $this->createContinueWith(Links::commentsDetails(), $searchBar);
+
+ $results = $comments->execute();
+
+ $this->addContent((new CommentList($results))->setViewMode($viewModeSwitcher->getViewMode()));
+
+ if ($compact) {
+ $this->addContent(
+ (new ShowMore($results, Url::fromRequest()->without(['showCompact', 'limit', 'view'])))
+ ->setBaseTarget('_next')
+ ->setAttribute('title', sprintf(
+ t('Show all %d comments'),
+ $comments->count()
+ ))
+ );
+ }
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate($continueWith);
+ }
+
+ $this->setAutorefreshInterval(10);
+ }
+
+ public function deleteAction()
+ {
+ $this->setTitle(t('Remove Comments'));
+
+ $db = $this->getDb();
+
+ $comments = Comment::on($db)->with([
+ 'host',
+ 'host.state',
+ 'service',
+ 'service.host',
+ 'service.host.state',
+ 'service.state'
+ ]);
+
+ $this->filter($comments);
+
+ $form = (new DeleteCommentForm())
+ ->setObjects($comments)
+ ->setRedirectUrl(Links::comments()->getAbsoluteUrl())
+ ->on(DeleteCommentForm::ON_SUCCESS, function ($form) {
+ // This forces the column to reload nearly instantly after the redirect
+ // and ensures the effect of the command is visible to the user asap
+ $this->getResponse()->setAutoRefreshInterval(1);
+
+ $this->redirectNow($form->getRedirectUrl());
+ })
+ ->handleRequest(ServerRequest::fromGlobals());
+
+ $this->addContent($form);
+ }
+
+ public function detailsAction()
+ {
+ $this->addTitleTab(t('Comments'));
+
+ $db = $this->getDb();
+
+ $comments = Comment::on($db)->with([
+ 'host',
+ 'host.state',
+ 'service',
+ 'service.host',
+ 'service.host.state',
+ 'service.state'
+ ]);
+
+ $comments->limit(3)->peekAhead();
+
+ $this->filter($comments);
+
+ yield $this->export($comments);
+
+ $rs = $comments->execute();
+
+ $this->addControl((new CommentList($rs))->setViewMode('minimal'));
+
+ $this->addControl(new ShowMore(
+ $rs,
+ Links::comments()->setQueryString(QueryString::render($this->getFilter())),
+ sprintf(t('Show all %d comments'), $comments->count())
+ ));
+
+ $this->addContent(
+ (new DeleteCommentForm())
+ ->setObjects($comments)
+ ->setAction(
+ Links::commentsDelete()
+ ->setQueryString(QueryString::render($this->getFilter()))
+ ->getAbsoluteUrl()
+ )
+ );
+ }
+
+ public function completeAction()
+ {
+ $suggestions = new ObjectSuggestions();
+ $suggestions->setModel(Comment::class);
+ $suggestions->forRequest(ServerRequest::fromGlobals());
+ $this->getDocument()->add($suggestions);
+ }
+
+ public function searchEditorAction()
+ {
+ $editor = $this->createSearchEditor(Comment::on($this->getDb()), [
+ LimitControl::DEFAULT_LIMIT_PARAM,
+ SortControl::DEFAULT_SORT_PARAM,
+ ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM
+ ]);
+
+ $this->getDocument()->add($editor);
+ $this->setTitle(t('Adjust Filter'));
+ }
+}
diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php
new file mode 100644
index 0000000..0d64fbf
--- /dev/null
+++ b/application/controllers/ConfigController.php
@@ -0,0 +1,63 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use Icinga\Application\Config;
+use Icinga\Module\Icingadb\Forms\DatabaseConfigForm;
+use Icinga\Module\Icingadb\Forms\RedisConfigForm;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Web\Form;
+use Icinga\Web\Widget\Tab;
+use Icinga\Web\Widget\Tabs;
+use ipl\Html\HtmlString;
+
+class ConfigController extends Controller
+{
+// public function init()
+// {
+// $this->assertPermission('config/modules');
+//
+// parent::init();
+// }
+
+ public function databaseAction()
+ {
+ $form = (new DatabaseConfigForm())
+ ->setIniConfig(Config::module('icingadb'));
+
+ $form->handleRequest();
+
+ $this->mergeTabs($this->Module()->getConfigTabs()->activate('database'));
+
+ $this->addFormToContent($form);
+ }
+
+ public function redisAction()
+ {
+ $form = (new RedisConfigForm())
+ ->setIniConfig($this->Config());
+
+ $form->handleRequest();
+
+ $this->mergeTabs($this->Module()->getConfigTabs()->activate('redis'));
+
+ $this->addFormToContent($form);
+ }
+
+ protected function addFormToContent(Form $form)
+ {
+ $this->addContent(new HtmlString($form->render()));
+ }
+
+ protected function mergeTabs(Tabs $tabs): self
+ {
+ /** @var Tab $tab */
+ foreach ($tabs->getTabs() as $tab) {
+ $this->tabs->add($tab->getName(), $tab);
+ }
+
+ return $this;
+ }
+}
diff --git a/application/controllers/DowntimeController.php b/application/controllers/DowntimeController.php
new file mode 100644
index 0000000..a0a7fa0
--- /dev/null
+++ b/application/controllers/DowntimeController.php
@@ -0,0 +1,84 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Icingadb\Common\CommandActions;
+use Icinga\Module\Icingadb\Common\Links;
+use Icinga\Module\Icingadb\Model\Downtime;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\Detail\DowntimeDetail;
+use Icinga\Module\Icingadb\Widget\ItemList\DowntimeList;
+use ipl\Stdlib\Filter;
+use ipl\Web\Url;
+
+class DowntimeController extends Controller
+{
+ use CommandActions;
+
+ /** @var Downtime */
+ protected $downtime;
+
+ public function init()
+ {
+ $this->addTitleTab(t('Downtime'));
+
+ $name = $this->params->getRequired('name');
+
+ $query = Downtime::on($this->getDb())->with([
+ 'host',
+ 'host.state',
+ 'service',
+ 'service.state',
+ 'service.host',
+ 'service.host.state',
+ 'parent',
+ 'parent.host',
+ 'parent.host.state',
+ 'parent.service',
+ 'parent.service.state',
+ 'triggered_by',
+ 'triggered_by.host',
+ 'triggered_by.host.state',
+ 'triggered_by.service',
+ 'triggered_by.service.state'
+ ]);
+ $query->filter(Filter::equal('downtime.name', $name));
+
+ $this->applyRestrictions($query);
+
+ $downtime = $query->first();
+ if ($downtime === null) {
+ throw new NotFoundError(t('Downtime not found'));
+ }
+
+ $this->downtime = $downtime;
+ }
+
+ public function indexAction()
+ {
+ $detail = new DowntimeDetail($this->downtime);
+
+ $this->addControl((new DowntimeList([$this->downtime]))
+ ->setViewMode('minimal')
+ ->setDetailActionsDisabled()
+ ->setCaptionDisabled()
+ ->setNoSubjectLink());
+
+ $this->addContent($detail);
+
+ $this->setAutorefreshInterval(10);
+ }
+
+ protected function fetchCommandTargets(): array
+ {
+ return [$this->downtime];
+ }
+
+ protected function getCommandTargetsUrl(): Url
+ {
+ return Links::downtime($this->downtime);
+ }
+}
diff --git a/application/controllers/DowntimesController.php b/application/controllers/DowntimesController.php
new file mode 100644
index 0000000..89fdfc8
--- /dev/null
+++ b/application/controllers/DowntimesController.php
@@ -0,0 +1,206 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Module\Icingadb\Common\Links;
+use Icinga\Module\Icingadb\Forms\Command\Object\DeleteDowntimeForm;
+use Icinga\Module\Icingadb\Model\Downtime;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\ItemList\DowntimeList;
+use Icinga\Module\Icingadb\Widget\ShowMore;
+use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
+use ipl\Web\Control\LimitControl;
+use ipl\Web\Control\SortControl;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Url;
+
+class DowntimesController extends Controller
+{
+ public function indexAction()
+ {
+ $this->addTitleTab(t('Downtimes'));
+ $compact = $this->view->compact;
+
+ $db = $this->getDb();
+
+ $downtimes = Downtime::on($db)->with([
+ 'host',
+ 'host.state',
+ 'service',
+ 'service.host',
+ 'service.host.state',
+ 'service.state'
+ ]);
+
+ $this->handleSearchRequest($downtimes);
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($downtimes);
+ $sortControl = $this->createSortControl(
+ $downtimes,
+ [
+ 'downtime.is_in_effect desc, downtime.start_time desc' => t('Is In Effect'),
+ 'downtime.entry_time' => t('Entry Time'),
+ 'host.display_name' => t('Host'),
+ 'service.display_name' => t('Service'),
+ 'downtime.author' => t('Author'),
+ 'downtime.start_time desc' => t('Start Time'),
+ 'downtime.end_time desc' => t('End Time'),
+ 'downtime.scheduled_start_time desc' => t('Scheduled Start Time'),
+ 'downtime.scheduled_end_time desc' => t('Scheduled End Time'),
+ 'downtime.duration desc' => t('Duration')
+ ]
+ );
+ $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl);
+ $searchBar = $this->createSearchBar($downtimes, [
+ $limitControl->getLimitParam(),
+ $sortControl->getSortParam(),
+ $viewModeSwitcher->getViewModeParam()
+ ]);
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ $this->filter($downtimes, $filter);
+
+ $downtimes->peekAhead($compact);
+
+ yield $this->export($downtimes);
+
+ $this->addControl($paginationControl);
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($viewModeSwitcher);
+ $this->addControl($searchBar);
+ $continueWith = $this->createContinueWith(Links::downtimesDetails(), $searchBar);
+
+ $results = $downtimes->execute();
+
+ $this->addContent((new DowntimeList($results))->setViewMode($viewModeSwitcher->getViewMode()));
+
+ if ($compact) {
+ $this->addContent(
+ (new ShowMore($results, Url::fromRequest()->without(['showCompact', 'limit', 'view'])))
+ ->setBaseTarget('_next')
+ ->setAttribute('title', sprintf(
+ t('Show all %d downtimes'),
+ $downtimes->count()
+ ))
+ );
+ }
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate($continueWith);
+ }
+
+ $this->setAutorefreshInterval(10);
+ }
+
+ public function deleteAction()
+ {
+ $this->setTitle(t('Cancel Downtimes'));
+
+ $db = $this->getDb();
+
+ $downtimes = Downtime::on($db)->with([
+ 'host',
+ 'host.state',
+ 'service',
+ 'service.host',
+ 'service.host.state',
+ 'service.state'
+ ]);
+
+ $this->filter($downtimes);
+
+ $form = (new DeleteDowntimeForm())
+ ->setObjects($downtimes)
+ ->setRedirectUrl(Links::downtimes()->getAbsoluteUrl())
+ ->on(DeleteDowntimeForm::ON_SUCCESS, function ($form) {
+ // This forces the column to reload nearly instantly after the redirect
+ // and ensures the effect of the command is visible to the user asap
+ $this->getResponse()->setAutoRefreshInterval(1);
+
+ $this->redirectNow($form->getRedirectUrl());
+ })
+ ->handleRequest(ServerRequest::fromGlobals());
+
+ $this->addContent($form);
+ }
+
+ public function detailsAction()
+ {
+ $this->addTitleTab(t('Downtimes'));
+
+ $db = $this->getDb();
+
+ $downtimes = Downtime::on($db)->with([
+ 'host',
+ 'host.state',
+ 'service',
+ 'service.host',
+ 'service.host.state',
+ 'service.state'
+ ]);
+
+ $downtimes->limit(3)->peekAhead();
+
+ $this->filter($downtimes);
+
+ yield $this->export($downtimes);
+
+ $rs = $downtimes->execute();
+
+ $this->addControl((new DowntimeList($rs))->setViewMode('minimal'));
+
+ $this->addControl(new ShowMore(
+ $rs,
+ Links::downtimes()->setQueryString(QueryString::render($this->getFilter())),
+ sprintf(t('Show all %d downtimes'), $downtimes->count())
+ ));
+
+ $this->addContent(
+ (new DeleteDowntimeForm())
+ ->setObjects($downtimes)
+ ->setAction(
+ Links::downtimesDelete()
+ ->setQueryString(QueryString::render($this->getFilter()))
+ ->getAbsoluteUrl()
+ )
+ );
+ }
+
+ public function completeAction()
+ {
+ $suggestions = new ObjectSuggestions();
+ $suggestions->setModel(Downtime::class);
+ $suggestions->forRequest(ServerRequest::fromGlobals());
+ $this->getDocument()->add($suggestions);
+ }
+
+ public function searchEditorAction()
+ {
+ $editor = $this->createSearchEditor(Downtime::on($this->getDb()), [
+ LimitControl::DEFAULT_LIMIT_PARAM,
+ SortControl::DEFAULT_SORT_PARAM,
+ ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM
+ ]);
+
+ $this->getDocument()->add($editor);
+ $this->setTitle(t('Adjust Filter'));
+ }
+}
diff --git a/application/controllers/ErrorController.php b/application/controllers/ErrorController.php
new file mode 100644
index 0000000..38621c0
--- /dev/null
+++ b/application/controllers/ErrorController.php
@@ -0,0 +1,97 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use Icinga\Controllers\ErrorController as IcingaErrorController;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+use ipl\Web\Layout\Content;
+use ipl\Web\Layout\Controls;
+use ipl\Web\Url;
+use ipl\Web\Widget\Link;
+use ipl\Web\Widget\Tabs;
+
+class ErrorController extends IcingaErrorController
+{
+ /** @var HtmlDocument */
+ protected $document;
+
+ /** @var Controls */
+ protected $controls;
+
+ /** @var Content */
+ protected $content;
+
+ /** @var Tabs */
+ protected $tabs;
+
+ protected function prepareInit()
+ {
+ $this->document = new HtmlDocument();
+ $this->document->setSeparator("\n");
+ $this->controls = new Controls();
+ $this->content = new Content();
+ $this->tabs = new Tabs();
+
+ $this->controls->setTabs($this->tabs);
+ $this->view->document = $this->document;
+ }
+
+ public function postDispatch()
+ {
+ $this->tabs->add(uniqid(), [
+ 'active' => true,
+ 'label' => $this->view->title,
+ 'url' => $this->getRequest()->getUrl()
+ ]);
+
+ if (! $this->content->isEmpty()) {
+ $this->document->prepend($this->content);
+ }
+
+ if (! $this->view->compact && ! $this->controls->isEmpty()) {
+ $this->document->prepend($this->controls);
+ }
+
+ parent::postDispatch();
+ }
+
+ protected function postDispatchXhr()
+ {
+ parent::postDispatchXhr();
+ $this->getResponse()->setHeader('X-Icinga-Module', $this->getModuleName(), true);
+ }
+
+ public function errorAction()
+ {
+ $error = $this->getParam('error_handler');
+ $exception = $error->exception;
+ /** @var \Exception $exception */
+
+ $message = $exception->getMessage();
+ if (substr($message, 0, 27) !== 'Cannot load resource config') {
+ $this->forward('error', 'error', 'default');
+ return;
+ } else {
+ $this->setParam('error_handler', null);
+ }
+
+ // TODO: Find a native way for ipl-html to support enriching text with html
+ $heading = Html::tag('h2', t('Database not configured'));
+ $intro = Html::tag('p', ['data-base-target' => '_next'], Html::sprintf(
+ 'You seem to not have configured a resource for Icinga DB yet. Please %s and then tell Icinga DB Web %s.',
+ new Link(
+ Html::tag('strong', 'create one'),
+ Url::fromPath('config/resource')
+ ),
+ new Link(
+ Html::tag('strong', 'which one it is'),
+ Url::fromPath('icingadb/config/database')
+ )
+ ));
+
+ $this->content->add([$heading, $intro]);
+ }
+}
diff --git a/application/controllers/EventController.php b/application/controllers/EventController.php
new file mode 100644
index 0000000..7108606
--- /dev/null
+++ b/application/controllers/EventController.php
@@ -0,0 +1,71 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use ArrayObject;
+use Icinga\Module\Icingadb\Model\History;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\Detail\EventDetail;
+use Icinga\Module\Icingadb\Widget\ItemList\HistoryList;
+use ipl\Orm\ResultSet;
+use ipl\Stdlib\Filter;
+
+class EventController extends Controller
+{
+ /** @var History */
+ protected $event;
+
+ public function init()
+ {
+ $this->addTitleTab(t('Event'));
+
+ $id = $this->params->getRequired('id');
+
+ $query = History::on($this->getDb())
+ ->with([
+ 'host',
+ 'host.state',
+ 'service',
+ 'service.state',
+ 'comment',
+ 'downtime',
+ 'downtime.parent',
+ 'downtime.parent.host',
+ 'downtime.parent.host.state',
+ 'downtime.parent.service',
+ 'downtime.parent.service.state',
+ 'downtime.triggered_by',
+ 'downtime.triggered_by.host',
+ 'downtime.triggered_by.host.state',
+ 'downtime.triggered_by.service',
+ 'downtime.triggered_by.service.state',
+ 'flapping',
+ 'notification',
+ 'acknowledgement',
+ 'state'
+ ])
+ ->filter(Filter::equal('id', hex2bin($id)));
+
+ $this->applyRestrictions($query);
+
+ $event = $query->first();
+ if ($event === null) {
+ $this->httpNotFound(t('Event not found'));
+ }
+
+ $this->event = $event;
+ }
+
+ public function indexAction()
+ {
+ $this->addControl((new HistoryList(new ResultSet(new ArrayObject([$this->event]))))
+ ->setViewMode('minimal')
+ ->setPageSize(1)
+ ->setCaptionDisabled()
+ ->setNoSubjectLink()
+ ->setDetailActionsDisabled());
+ $this->addContent((new EventDetail($this->event))->setTicketLinkEnabled());
+ }
+}
diff --git a/application/controllers/HealthController.php b/application/controllers/HealthController.php
new file mode 100644
index 0000000..52ba220
--- /dev/null
+++ b/application/controllers/HealthController.php
@@ -0,0 +1,115 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Module\Icingadb\Command\Instance\ToggleInstanceFeatureCommand;
+use Icinga\Module\Icingadb\Common\Links;
+use Icinga\Module\Icingadb\Forms\Command\Instance\ToggleInstanceFeaturesForm;
+use Icinga\Module\Icingadb\Model\HoststateSummary;
+use Icinga\Module\Icingadb\Model\Instance;
+use Icinga\Module\Icingadb\Model\ServicestateSummary;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\Health;
+use ipl\Web\Widget\VerticalKeyValue;
+use ipl\Html\Html;
+use ipl\Web\Url;
+
+class HealthController extends Controller
+{
+ public function indexAction()
+ {
+ $this->addTitleTab(t('Health'));
+
+ $db = $this->getDb();
+
+ $instance = Instance::on($db)->with(['endpoint']);
+ $hoststateSummary = HoststateSummary::on($db);
+ $servicestateSummary = ServicestateSummary::on($db);
+
+ $this->applyRestrictions($hoststateSummary);
+ $this->applyRestrictions($servicestateSummary);
+
+ yield $this->export($instance, $hoststateSummary, $servicestateSummary);
+
+ $instance = $instance->first();
+
+ if ($instance === null) {
+ $this->addContent(Html::tag('p', t(
+ 'It seems that Icinga DB is not running.'
+ . ' Make sure Icinga DB is running and writing into the database.'
+ )));
+
+ return;
+ }
+
+ $hoststateSummary = $hoststateSummary->first();
+ $servicestateSummary = $servicestateSummary->first();
+
+ $this->content->addAttributes(['class' => 'monitoring-health']);
+
+ $this->addContent(new Health($instance));
+ $this->addContent(Html::tag('section', ['class' => 'check-summary'], [
+ Html::tag('div', ['class' => 'col'], [
+ Html::tag('h3', t('Host Checks')),
+ Html::tag('div', ['class' => 'col-content'], [
+ new VerticalKeyValue(
+ t('Active'),
+ $hoststateSummary->hosts_active_checks_enabled
+ ),
+ new VerticalKeyValue(
+ t('Passive'),
+ $hoststateSummary->hosts_passive_checks_enabled
+ )
+ ])
+ ]),
+ Html::tag('div', ['class' => 'col'], [
+ Html::tag('h3', t('Service Checks')),
+ Html::tag('div', ['class' => 'col-content'], [
+ new VerticalKeyValue(
+ t('Active'),
+ $servicestateSummary->services_active_checks_enabled
+ ),
+ new VerticalKeyValue(
+ t('Passive'),
+ $servicestateSummary->services_passive_checks_enabled
+ )
+ ])
+ ])
+ ]));
+
+ $featureCommands = Html::tag(
+ 'section',
+ ['class' => 'instance-commands'],
+ Html::tag('h2', t('Feature Commands'))
+ );
+ $toggleInstanceFeaturesCommandForm = new ToggleInstanceFeaturesForm([
+ ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS =>
+ $instance->icinga2_active_host_checks_enabled,
+ ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS =>
+ $instance->icinga2_active_service_checks_enabled,
+ ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS =>
+ $instance->icinga2_event_handlers_enabled,
+ ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION =>
+ $instance->icinga2_flap_detection_enabled,
+ ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS =>
+ $instance->icinga2_notifications_enabled,
+ ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA =>
+ $instance->icinga2_performance_data_enabled
+ ]);
+ $toggleInstanceFeaturesCommandForm->setObjects([$instance]);
+ $toggleInstanceFeaturesCommandForm->on(ToggleInstanceFeaturesForm::ON_SUCCESS, function () {
+ $this->getResponse()->setAutoRefreshInterval(1);
+
+ $this->redirectNow(Url::fromPath('icingadb/health')->getAbsoluteUrl());
+ });
+ $toggleInstanceFeaturesCommandForm->handleRequest(ServerRequest::fromGlobals());
+
+ $featureCommands->add($toggleInstanceFeaturesCommandForm);
+ $this->addContent($featureCommands);
+
+ $this->setAutorefreshInterval(30);
+ }
+}
diff --git a/application/controllers/HistoryController.php b/application/controllers/HistoryController.php
new file mode 100644
index 0000000..b4fc6df
--- /dev/null
+++ b/application/controllers/HistoryController.php
@@ -0,0 +1,138 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Module\Icingadb\Model\History;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\ItemList\HistoryList;
+use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
+use ipl\Stdlib\Filter;
+use ipl\Web\Control\LimitControl;
+use ipl\Web\Control\SortControl;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Url;
+
+class HistoryController extends Controller
+{
+ public function indexAction()
+ {
+ $this->addTitleTab(t('History'));
+ $compact = $this->view->compact; // TODO: Find a less-legacy way..
+
+ $preserveParams = [
+ LimitControl::DEFAULT_LIMIT_PARAM,
+ SortControl::DEFAULT_SORT_PARAM,
+ ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM
+ ];
+
+ $db = $this->getDb();
+
+ $history = History::on($db)->with([
+ 'host',
+ 'host.state',
+ 'service',
+ 'service.state',
+ 'comment',
+ 'downtime',
+ 'flapping',
+ 'notification',
+ 'acknowledgement',
+ 'state'
+ ]);
+
+ $before = $this->params->shift('before', time());
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($history);
+ $sortControl = $this->createSortControl(
+ $history,
+ [
+ 'history.event_time desc' => t('Event Time')
+ ]
+ );
+ $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl, true);
+ $searchBar = $this->createSearchBar($history, $preserveParams);
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ $history->peekAhead();
+
+ $page = $paginationControl->getCurrentPageNumber();
+
+ if ($page > 1 && ! $compact) {
+ $history->limit($page * $limitControl->getLimit());
+ }
+
+ $history->filter(Filter::lessThanOrEqual('event_time', $before));
+ $this->filter($history, $filter);
+
+ $history->getWith()['history.host']->setJoinType('LEFT');
+ $history->filter(Filter::any(
+ // Because of LEFT JOINs, make sure we'll fetch history entries only for items which still exist:
+ Filter::like('host.id', '*'),
+ Filter::like('service.id', '*')
+ ));
+
+ yield $this->export($history);
+
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($viewModeSwitcher);
+ $this->addControl($searchBar);
+
+ $url = Url::fromRequest()->onlyWith($preserveParams);
+ $url->setQueryString(QueryString::render($filter) . '&' . $url->getParams()->toString());
+
+ $historyList = (new HistoryList($history->execute()))
+ ->setPageSize($limitControl->getLimit())
+ ->setViewMode($viewModeSwitcher->getViewMode())
+ ->setLoadMoreUrl($url->setParam('before', $before));
+ if ($compact) {
+ $historyList->setPageNumber($page);
+ }
+
+ if ($compact && $page > 1) {
+ $this->document->addFrom($historyList);
+ } else {
+ $this->addContent($historyList);
+ }
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate();
+ }
+ }
+
+ public function completeAction()
+ {
+ $suggestions = new ObjectSuggestions();
+ $suggestions->setModel(History::class);
+ $suggestions->forRequest(ServerRequest::fromGlobals());
+ $this->getDocument()->add($suggestions);
+ }
+
+ public function searchEditorAction()
+ {
+ $editor = $this->createSearchEditor(History::on($this->getDb()), [
+ LimitControl::DEFAULT_LIMIT_PARAM,
+ SortControl::DEFAULT_SORT_PARAM,
+ ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM
+ ]);
+
+ $this->getDocument()->add($editor);
+ $this->setTitle(t('Adjust Filter'));
+ }
+}
diff --git a/application/controllers/HostController.php b/application/controllers/HostController.php
new file mode 100644
index 0000000..ae5944d
--- /dev/null
+++ b/application/controllers/HostController.php
@@ -0,0 +1,288 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Icingadb\Command\Object\GetObjectCommand;
+use Icinga\Module\Icingadb\Command\Transport\CommandTransport;
+use Icinga\Module\Icingadb\Common\CommandActions;
+use Icinga\Module\Icingadb\Common\HostLinks;
+use Icinga\Module\Icingadb\Common\Links;
+use Icinga\Module\Icingadb\Hook\TabHook\HookActions;
+use Icinga\Module\Icingadb\Model\History;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Icingadb\Model\ServicestateSummary;
+use Icinga\Module\Icingadb\Redis\VolatileStateResults;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\Detail\HostDetail;
+use Icinga\Module\Icingadb\Widget\Detail\HostInspectionDetail;
+use Icinga\Module\Icingadb\Widget\Detail\HostMetaInfo;
+use Icinga\Module\Icingadb\Widget\Detail\QuickActions;
+use Icinga\Module\Icingadb\Widget\ItemList\HostList;
+use Icinga\Module\Icingadb\Widget\ItemList\HistoryList;
+use Icinga\Module\Icingadb\Widget\ItemList\ServiceList;
+use ipl\Stdlib\Filter;
+use ipl\Web\Url;
+use ipl\Web\Widget\Tabs;
+
+class HostController extends Controller
+{
+ use CommandActions;
+ use HookActions;
+
+ /** @var Host The host object */
+ protected $host;
+
+ public function init()
+ {
+ $name = $this->params->getRequired('name');
+
+ $query = Host::on($this->getDb())->with(['state', 'icon_image']);
+ $query
+ ->setResultSetClass(VolatileStateResults::class)
+ ->filter(Filter::equal('host.name', $name));
+
+ $this->applyRestrictions($query);
+
+ /** @var Host $host */
+ $host = $query->first();
+ if ($host === null) {
+ throw new NotFoundError(t('Host not found'));
+ }
+
+ $this->host = $host;
+ $this->loadTabsForObject($host);
+
+ $this->setTitleTab($this->getRequest()->getActionName());
+ $this->setTitle($host->display_name);
+ }
+
+ public function indexAction()
+ {
+ $serviceSummary = ServicestateSummary::on($this->getDb());
+ $serviceSummary->filter(Filter::equal('service.host_id', $this->host->id));
+
+ $this->applyRestrictions($serviceSummary);
+
+ if ($this->host->state->is_overdue) {
+ $this->controls->addAttributes(['class' => 'overdue']);
+ }
+
+ $this->addControl((new HostList([$this->host]))
+ ->setViewMode('objectHeader')
+ ->setDetailActionsDisabled()
+ ->setNoSubjectLink());
+ $this->addControl(new HostMetaInfo($this->host));
+ $this->addControl(new QuickActions($this->host));
+
+ $this->addContent(new HostDetail($this->host, $serviceSummary->first()));
+
+ $this->setAutorefreshInterval(10);
+ }
+
+ public function sourceAction()
+ {
+ $this->assertPermission('icingadb/object/show-source');
+ $apiResult = (new CommandTransport())->send((new GetObjectCommand())->setObject($this->host));
+
+ if ($this->host->state->is_overdue) {
+ $this->controls->addAttributes(['class' => 'overdue']);
+ }
+
+ $this->addControl((new HostList([$this->host]))
+ ->setViewMode('objectHeader')
+ ->setDetailActionsDisabled()
+ ->setNoSubjectLink());
+ $this->addContent(new HostInspectionDetail(
+ $this->host,
+ reset($apiResult)
+ ));
+ }
+
+ public function historyAction()
+ {
+ $compact = $this->view->compact; // TODO: Find a less-legacy way..
+
+ if ($this->host->state->is_overdue) {
+ $this->controls->addAttributes(['class' => 'overdue']);
+ }
+
+ $db = $this->getDb();
+
+ $history = History::on($db)->with([
+ 'host',
+ 'host.state',
+ 'comment',
+ 'downtime',
+ 'flapping',
+ 'notification',
+ 'acknowledgement',
+ 'state'
+ ]);
+
+ $history->filter(Filter::all(
+ Filter::equal('history.host_id', $this->host->id),
+ Filter::unlike('history.service_id', '*')
+ ));
+
+ $before = $this->params->shift('before', time());
+ $url = Url::fromRequest()->setParams(clone $this->params);
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($history);
+ $sortControl = $this->createSortControl(
+ $history,
+ [
+ 'history.event_time desc' => t('Event Time')
+ ]
+ );
+ $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl, true);
+
+ $history->peekAhead();
+
+ $page = $paginationControl->getCurrentPageNumber();
+
+ if ($page > 1 && ! $compact) {
+ $history->limit($page * $limitControl->getLimit());
+ }
+
+ $history->filter(Filter::lessThanOrEqual('event_time', $before));
+
+ yield $this->export($history);
+
+ $this->addControl((new HostList([$this->host]))
+ ->setViewMode('objectHeader')
+ ->setDetailActionsDisabled()
+ ->setNoSubjectLink());
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($viewModeSwitcher);
+
+ $historyList = (new HistoryList($history->execute()))
+ ->setViewMode($viewModeSwitcher->getViewMode())
+ ->setPageSize($limitControl->getLimit())
+ ->setLoadMoreUrl($url->setParam('before', $before));
+
+ if ($compact) {
+ $historyList->setPageNumber($page);
+ }
+
+ if ($compact && $page > 1) {
+ $this->document->addFrom($historyList);
+ } else {
+ $this->addContent($historyList);
+ }
+ }
+
+ public function servicesAction()
+ {
+ if ($this->host->state->is_overdue) {
+ $this->controls->addAttributes(['class' => 'overdue']);
+ }
+
+ $db = $this->getDb();
+
+ $services = Service::on($db)->with([
+ 'state',
+ 'state.last_comment',
+ 'icon_image',
+ 'host',
+ 'host.state'
+ ]);
+ $services
+ ->setResultSetClass(VolatileStateResults::class)
+ ->filter(Filter::equal('host.id', $this->host->id));
+
+ $this->applyRestrictions($services);
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($services);
+ $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl);
+ $sortControl = $this->createSortControl(
+ $services,
+ [
+ 'service.display_name' => t('Name'),
+ 'service.state.severity desc' => t('Severity'),
+ 'service.state.soft_state' => t('Current State'),
+ 'service.state.last_state_change desc' => t('Last State Change')
+ ]
+ );
+
+ yield $this->export($services);
+
+ $serviceList = (new ServiceList($services))
+ ->setViewMode($viewModeSwitcher->getViewMode());
+
+ $this->addControl((new HostList([$this->host]))
+ ->setViewMode('objectHeader')
+ ->setDetailActionsDisabled()
+ ->setNoSubjectLink());
+ $this->addControl($paginationControl);
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($viewModeSwitcher);
+
+ $this->addContent($serviceList);
+
+ $this->setAutorefreshInterval(10);
+ }
+
+ protected function createTabs(): Tabs
+ {
+ $tabs = $this->getTabs()
+ ->add('index', [
+ 'label' => t('Host'),
+ 'url' => Links::host($this->host)
+ ])
+ ->add('services', [
+ 'label' => t('Services'),
+ 'url' => HostLinks::services($this->host)
+ ])
+ ->add('history', [
+ 'label' => t('History'),
+ 'url' => HostLinks::history($this->host)
+ ]);
+
+ if ($this->hasPermission('icingadb/object/show-source')) {
+ $tabs->add('source', [
+ 'label' => t('Source'),
+ 'url' => Links::hostSource($this->host)
+ ]);
+ }
+
+ foreach ($this->loadAdditionalTabs() as $name => $tab) {
+ $tabs->add($name, $tab + ['urlParams' => ['name' => $this->host->name]]);
+ }
+
+ return $tabs;
+ }
+
+ protected function setTitleTab(string $name)
+ {
+ $tab = $this->createTabs()->get($name);
+
+ if ($tab !== null) {
+ $tab->setActive();
+
+ $this->setTitle($tab->getLabel());
+ }
+ }
+
+ protected function fetchCommandTargets(): array
+ {
+ return [$this->host];
+ }
+
+ protected function getCommandTargetsUrl(): Url
+ {
+ return Links::host($this->host);
+ }
+
+ protected function getDefaultTabControls(): array
+ {
+ return [(new HostList([$this->host]))->setDetailActionsDisabled()->setNoSubjectLink()];
+ }
+}
diff --git a/application/controllers/HostgroupController.php b/application/controllers/HostgroupController.php
new file mode 100644
index 0000000..978489d
--- /dev/null
+++ b/application/controllers/HostgroupController.php
@@ -0,0 +1,89 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\Hostgroupsummary;
+use Icinga\Module\Icingadb\Redis\VolatileStateResults;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\ItemList\HostList;
+use Icinga\Module\Icingadb\Widget\ItemList\HostgroupList;
+use ipl\Html\Html;
+use ipl\Stdlib\Filter;
+use ipl\Web\Widget\HorizontalKeyValue;
+
+class HostgroupController extends Controller
+{
+ /** @var Hostgroupsummary The host group object */
+ protected $hostgroup;
+
+ public function init()
+ {
+ $this->assertRouteAccess('hostgroups');
+
+ $this->addTitleTab(t('Host Group'));
+
+ $name = $this->params->getRequired('name');
+
+ $query = Hostgroupsummary::on($this->getDb());
+
+ foreach ($query->getUnions() as $unionPart) {
+ $unionPart->filter(Filter::equal('hostgroup.name', $name));
+ }
+
+ $this->applyRestrictions($query);
+
+ $hostgroup = $query->first();
+ if ($hostgroup === null) {
+ throw new NotFoundError(t('Host group not found'));
+ }
+
+ $this->hostgroup = $hostgroup;
+ $this->setTitle($hostgroup->display_name);
+ }
+
+ public function indexAction()
+ {
+ $db = $this->getDb();
+
+ $hosts = Host::on($db)->with(['state', 'state.last_comment', 'icon_image']);
+ $hosts
+ ->setResultSetClass(VolatileStateResults::class)
+ ->filter(Filter::equal('hostgroup.id', $this->hostgroup->id));
+ $this->applyRestrictions($hosts);
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($hosts);
+ $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl);
+
+ $hostList = (new HostList($hosts->execute()))
+ ->setViewMode($viewModeSwitcher->getViewMode());
+
+ yield $this->export($hosts);
+
+ // ICINGAWEB_EXPORT_FORMAT is not set yet and $this->format is inaccessible, yeah...
+ if ($this->getRequest()->getParam('format') === 'pdf') {
+ $this->addContent((new HostgroupList([$this->hostgroup]))
+ ->setViewMode('minimal')
+ ->setDetailActionsDisabled()
+ ->setNoSubjectLink());
+ $this->addContent(Html::tag('h2', null, t('Hosts')));
+ } else {
+ $this->addControl((new HostgroupList([$this->hostgroup]))
+ ->setViewMode('minimal')
+ ->setDetailActionsDisabled()
+ ->setNoSubjectLink());
+ }
+
+ $this->addControl($paginationControl);
+ $this->addControl($viewModeSwitcher);
+ $this->addControl($limitControl);
+
+ $this->addContent($hostList);
+
+ $this->setAutorefreshInterval(10);
+ }
+}
diff --git a/application/controllers/HostgroupsController.php b/application/controllers/HostgroupsController.php
new file mode 100644
index 0000000..f510cf4
--- /dev/null
+++ b/application/controllers/HostgroupsController.php
@@ -0,0 +1,121 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Module\Icingadb\Model\Hostgroup;
+use Icinga\Module\Icingadb\Model\Hostgroupsummary;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\ItemList\HostgroupList;
+use Icinga\Module\Icingadb\Widget\ShowMore;
+use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
+use ipl\Web\Control\LimitControl;
+use ipl\Web\Control\SortControl;
+use ipl\Web\Url;
+
+class HostgroupsController extends Controller
+{
+ public function init()
+ {
+ parent::init();
+
+ $this->assertRouteAccess();
+ }
+
+ public function indexAction()
+ {
+ $this->addTitleTab(t('Host Groups'));
+ $compact = $this->view->compact;
+
+ $db = $this->getDb();
+
+ $hostgroups = Hostgroupsummary::on($db);
+
+ $this->handleSearchRequest($hostgroups);
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($hostgroups);
+ $sortControl = $this->createSortControl(
+ $hostgroups,
+ [
+ 'display_name' => t('Name'),
+ 'hosts_severity desc' => t('Severity'),
+ 'hosts_total desc' => t('Total Hosts'),
+ 'services_total desc' => t('Total Services')
+ ]
+ );
+ $searchBar = $this->createSearchBar($hostgroups, [
+ $limitControl->getLimitParam(),
+ $sortControl->getSortParam()
+ ]);
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ $this->filter($hostgroups, $filter);
+
+ $hostgroups->peekAhead($compact);
+
+ yield $this->export($hostgroups);
+
+ $this->addControl($paginationControl);
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($searchBar);
+
+ $results = $hostgroups->execute();
+
+ $this->addContent(
+ (new HostgroupList($results))->setBaseFilter($filter)
+ );
+
+ if ($compact) {
+ $this->addContent(
+ (new ShowMore($results, Url::fromRequest()->without(['showCompact', 'limit', 'view'])))
+ ->setBaseTarget('_next')
+ ->setAttribute('title', sprintf(
+ t('Show all %d hostgroups'),
+ $hostgroups->count()
+ ))
+ );
+ }
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate();
+ }
+
+ $this->setAutorefreshInterval(30);
+ }
+
+ public function completeAction()
+ {
+ $suggestions = new ObjectSuggestions();
+ $suggestions->setModel(Hostgroup::class);
+ $suggestions->forRequest(ServerRequest::fromGlobals());
+ $this->getDocument()->add($suggestions);
+ }
+
+ public function searchEditorAction()
+ {
+ $editor = $this->createSearchEditor(Hostgroupsummary::on($this->getDb()), [
+ LimitControl::DEFAULT_LIMIT_PARAM,
+ SortControl::DEFAULT_SORT_PARAM,
+ ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM
+ ]);
+
+ $this->getDocument()->add($editor);
+ $this->setTitle(t('Adjust Filter'));
+ }
+}
diff --git a/application/controllers/HostsController.php b/application/controllers/HostsController.php
new file mode 100644
index 0000000..eaf804a
--- /dev/null
+++ b/application/controllers/HostsController.php
@@ -0,0 +1,240 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Module\Icingadb\Common\CommandActions;
+use Icinga\Module\Icingadb\Common\Links;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\HoststateSummary;
+use Icinga\Module\Icingadb\Redis\VolatileStateResults;
+use Icinga\Module\Icingadb\Util\FeatureStatus;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\Detail\MultiselectQuickActions;
+use Icinga\Module\Icingadb\Widget\Detail\ObjectsDetail;
+use Icinga\Module\Icingadb\Widget\ItemList\HostList;
+use Icinga\Module\Icingadb\Widget\HostStatusBar;
+use Icinga\Module\Icingadb\Widget\ItemTable\HostItemTable;
+use Icinga\Module\Icingadb\Widget\ShowMore;
+use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
+use ipl\Orm\Query;
+use ipl\Stdlib\Filter;
+use ipl\Web\Control\LimitControl;
+use ipl\Web\Control\SortControl;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Url;
+
+class HostsController extends Controller
+{
+ use CommandActions;
+
+ public function indexAction()
+ {
+ $this->addTitleTab(t('Hosts'));
+ $compact = $this->view->compact;
+
+ $db = $this->getDb();
+
+ $hosts = Host::on($db)->with(['state', 'icon_image', 'state.last_comment']);
+ $hosts->getWith()['host.state']->setJoinType('INNER');
+ $hosts->setResultSetClass(VolatileStateResults::class);
+
+ $this->handleSearchRequest($hosts);
+
+ $summary = null;
+ if (! $compact) {
+ $summary = HoststateSummary::on($db);
+ }
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($hosts);
+ $sortControl = $this->createSortControl(
+ $hosts,
+ [
+ 'host.display_name' => t('Name'),
+ 'host.state.severity desc' => t('Severity'),
+ 'host.state.soft_state' => t('Current State'),
+ 'host.state.last_state_change desc' => t('Last State Change')
+ ]
+ );
+ $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl);
+ $columns = $this->createColumnControl($hosts, $viewModeSwitcher);
+
+ $searchBar = $this->createSearchBar($hosts, [
+ $limitControl->getLimitParam(),
+ $sortControl->getSortParam(),
+ $viewModeSwitcher->getViewModeParam(),
+ 'columns'
+ ]);
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ $hosts->peekAhead($compact);
+
+ $this->filter($hosts, $filter);
+ if (isset($summary)) {
+ $this->filter($summary, $filter);
+ yield $this->export($hosts, $summary);
+ } else {
+ yield $this->export($hosts);
+ }
+
+ $this->addControl($paginationControl);
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($viewModeSwitcher);
+ $this->addControl($searchBar);
+ $continueWith = $this->createContinueWith(Links::hostsDetails(), $searchBar);
+
+ $results = $hosts->execute();
+
+ if ($viewModeSwitcher->getViewMode() === 'tabular') {
+ $hostList = (new HostItemTable($results, HostItemTable::applyColumnMetaData($hosts, $columns)))
+ ->setSort($sortControl->getSort());
+ } else {
+ $hostList = (new HostList($results))
+ ->setViewMode($viewModeSwitcher->getViewMode());
+ }
+
+ $this->addContent($hostList);
+
+ if ($compact) {
+ $this->addContent(
+ (new ShowMore($results, Url::fromRequest()->without(['showCompact', 'limit', 'view'])))
+ ->setBaseTarget('_next')
+ ->setAttribute('title', sprintf(
+ t('Show all %d hosts'),
+ $hosts->count()
+ ))
+ );
+ } else {
+ $this->addFooter((new HostStatusBar($summary->first()))->setBaseFilter($filter));
+ }
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate($continueWith);
+ }
+
+ $this->setAutorefreshInterval(10);
+ }
+
+ public function detailsAction()
+ {
+ $this->addTitleTab(t('Hosts'));
+
+ $db = $this->getDb();
+
+ $hosts = Host::on($db)->with(['state', 'icon_image']);
+ $hosts->setResultSetClass(VolatileStateResults::class);
+ $summary = HoststateSummary::on($db)->with(['state']);
+
+ $this->filter($hosts);
+ $this->filter($summary);
+
+ $hosts->limit(3);
+ $hosts->peekAhead();
+
+ yield $this->export($hosts, $summary);
+
+ $results = $hosts->execute();
+ $summary = $summary->first();
+
+ $downtimes = Host::on($db)->with(['downtime']);
+ $downtimes->getWith()['host.downtime']->setJoinType('INNER');
+ $this->filter($downtimes);
+ $summary->downtimes_total = $downtimes->count();
+
+ $comments = Host::on($db)->with(['comment']);
+ $comments->getWith()['host.comment']->setJoinType('INNER');
+ // TODO: This should be automatically done by the model/resolver and added as ON condition
+ $comments->filter(Filter::equal('comment.object_type', 'host'));
+ $this->filter($comments);
+ $summary->comments_total = $comments->count();
+
+ $this->addControl(
+ (new HostList($results))
+ ->setViewMode('minimal')
+ );
+ $this->addControl(new ShowMore(
+ $results,
+ Links::hosts()->setQueryString(QueryString::render($this->getFilter())),
+ sprintf(t('Show all %d hosts'), $hosts->count())
+ ));
+ $this->addControl(
+ (new MultiselectQuickActions('host', $summary))
+ ->setBaseFilter($this->getFilter())
+ );
+
+ $this->addContent(
+ (new ObjectsDetail('host', $summary, $hosts))
+ ->setBaseFilter($this->getFilter())
+ );
+ }
+
+ 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()), [
+ LimitControl::DEFAULT_LIMIT_PARAM,
+ SortControl::DEFAULT_SORT_PARAM,
+ ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM,
+ 'columns'
+ ]);
+
+ $this->getDocument()->add($editor);
+ $this->setTitle(t('Adjust Filter'));
+ }
+
+ protected function fetchCommandTargets(): Query
+ {
+ $db = $this->getDb();
+
+ $hosts = Host::on($db)->with('state');
+ $hosts->setResultSetClass(VolatileStateResults::class);
+
+ switch ($this->getRequest()->getActionName()) {
+ case 'acknowledge':
+ $hosts->filter(Filter::equal('state.is_problem', 'y'))
+ ->filter(Filter::equal('state.is_acknowledged', 'n'));
+
+ break;
+ }
+
+ $this->filter($hosts);
+
+ return $hosts;
+ }
+
+ protected function getCommandTargetsUrl(): Url
+ {
+ return Links::hostsDetails()->setQueryString(QueryString::render($this->getFilter()));
+ }
+
+ protected function getFeatureStatus()
+ {
+ $summary = HoststateSummary::on($this->getDb());
+ $this->filter($summary);
+
+ return new FeatureStatus('host', $summary->first());
+ }
+}
diff --git a/application/controllers/MigrateController.php b/application/controllers/MigrateController.php
new file mode 100644
index 0000000..395319a
--- /dev/null
+++ b/application/controllers/MigrateController.php
@@ -0,0 +1,120 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use Exception;
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Application\Hook;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Icingadb\Compat\UrlMigrator;
+use Icinga\Module\Icingadb\Forms\SetAsBackendForm;
+use Icinga\Module\Icingadb\Hook\IcingadbSupportHook;
+use Icinga\Module\Icingadb\Web\Controller;
+use ipl\Html\HtmlString;
+use ipl\Web\Url;
+
+class MigrateController extends Controller
+{
+ public function monitoringUrlAction()
+ {
+ $this->assertHttpMethod('post');
+ if (! $this->getRequest()->isApiRequest()) {
+ $this->httpBadRequest('No API request');
+ }
+
+ if (
+ ! preg_match('/([^;]*);?/', $this->getRequest()->getHeader('Content-Type'), $matches)
+ || $matches[1] !== 'application/json'
+ ) {
+ $this->httpBadRequest('No JSON content');
+ }
+
+ $urls = $this->getRequest()->getPost();
+
+ $result = [];
+ $errors = [];
+ foreach ($urls as $urlString) {
+ $url = Url::fromPath($urlString);
+ if (UrlMigrator::isSupportedUrl($url)) {
+ try {
+ $urlString = UrlMigrator::transformUrl($url)->getAbsoluteUrl();
+ } catch (Exception $e) {
+ $errors[$urlString] = [
+ IcingaException::describe($e),
+ IcingaException::getConfidentialTraceAsString($e)
+ ];
+ $urlString = false;
+ }
+ }
+
+ $result[] = $urlString;
+ }
+
+ $response = $this->getResponse()->json();
+ if (empty($errors)) {
+ $response->setSuccessData($result);
+ } else {
+ $response->setFailData([
+ 'result' => $result,
+ 'errors' => $errors
+ ]);
+ }
+
+ $response->sendResponse();
+ }
+
+ public function checkboxStateAction()
+ {
+ $this->assertHttpMethod('get');
+
+ $form = new SetAsBackendForm();
+ $form->setAction(Url::fromPath('icingadb/migrate/checkbox-submit')->getAbsoluteUrl());
+
+ $this->getDocument()->addHtml($form);
+ }
+
+ public function checkboxSubmitAction()
+ {
+ $this->assertHttpMethod('post');
+ $this->addPart(HtmlString::create('"bogus"'), 'Behavior:Migrate');
+
+ (new SetAsBackendForm())->handleRequest(ServerRequest::fromGlobals());
+ }
+
+ public function backendSupportAction()
+ {
+ $this->assertHttpMethod('post');
+ if (! $this->getRequest()->isApiRequest()) {
+ $this->httpBadRequest('No API request');
+ }
+
+ if (
+ ! preg_match('/([^;]*);?/', $this->getRequest()->getHeader('Content-Type'), $matches)
+ || $matches[1] !== 'application/json'
+ ) {
+ $this->httpBadRequest('No JSON content');
+ }
+
+ $supportList = [];
+ foreach (Hook::all('Icingadb/IcingadbSupport') as $hook) {
+ /** @var IcingadbSupportHook $hook */
+ $supportList[$hook->getModule()->getName()] = $hook->supportsIcingaDb();
+ }
+
+ $moduleSupportStates = [];
+ foreach ($this->getRequest()->getPost() as $moduleName) {
+ if (isset($supportList[$moduleName])) {
+ $moduleSupportStates[] = $supportList[$moduleName];
+ } else {
+ $moduleSupportStates[] = false;
+ }
+ }
+
+ $this->getResponse()
+ ->json()
+ ->setSuccessData($moduleSupportStates)
+ ->sendResponse();
+ }
+}
diff --git a/application/controllers/NotificationsController.php b/application/controllers/NotificationsController.php
new file mode 100644
index 0000000..1c8a5ff
--- /dev/null
+++ b/application/controllers/NotificationsController.php
@@ -0,0 +1,134 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Module\Icingadb\Model\NotificationHistory;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\ItemList\NotificationList;
+use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
+use ipl\Sql\Sql;
+use ipl\Stdlib\Filter;
+use ipl\Web\Control\LimitControl;
+use ipl\Web\Control\SortControl;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Url;
+
+class NotificationsController extends Controller
+{
+ public function indexAction()
+ {
+ $this->addTitleTab(t('Notifications'));
+ $compact = $this->view->compact;
+
+ $preserveParams = [
+ LimitControl::DEFAULT_LIMIT_PARAM,
+ SortControl::DEFAULT_SORT_PARAM,
+ ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM
+ ];
+
+ $db = $this->getDb();
+
+ $notifications = NotificationHistory::on($db)->with([
+ 'history',
+ 'host',
+ 'host.state',
+ 'service',
+ 'service.state'
+ ]);
+
+ $this->handleSearchRequest($notifications);
+ $before = $this->params->shift('before', time());
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($notifications);
+ $sortControl = $this->createSortControl(
+ $notifications,
+ [
+ 'notification_history.send_time desc' => t('Send Time')
+ ]
+ );
+ $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl, true);
+ $searchBar = $this->createSearchBar($notifications, $preserveParams);
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ $notifications->peekAhead();
+
+ $page = $paginationControl->getCurrentPageNumber();
+
+ if ($page > 1 && ! $compact) {
+ $notifications->limit($page * $limitControl->getLimit());
+ }
+
+ $notifications->filter(Filter::lessThanOrEqual('send_time', $before));
+ $this->filter($notifications, $filter);
+ $notifications->filter(Filter::any(
+ // Make sure we'll fetch service history entries only for services which still exist
+ Filter::unlike('service_id', '*'),
+ Filter::like('history.service.id', '*')
+ ));
+
+ yield $this->export($notifications);
+
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($viewModeSwitcher);
+ $this->addControl($searchBar);
+
+ $url = Url::fromRequest()->onlyWith($preserveParams);
+ $url->setQueryString(QueryString::render($filter) . '&' . $url->getParams()->toString());
+
+ $notificationList = (new NotificationList($notifications->execute()))
+ ->setPageSize($limitControl->getLimit())
+ ->setViewMode($viewModeSwitcher->getViewMode())
+ ->setLoadMoreUrl($url->setParam('before', $before));
+
+ if ($compact) {
+ $notificationList->setPageNumber($page);
+ }
+
+ if ($compact && $page > 1) {
+ $this->document->addFrom($notificationList);
+ } else {
+ $this->addContent($notificationList);
+ }
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate();
+ }
+ }
+
+ public function completeAction()
+ {
+ $suggestions = new ObjectSuggestions();
+ $suggestions->setModel(NotificationHistory::class);
+ $suggestions->forRequest(ServerRequest::fromGlobals());
+ $this->getDocument()->add($suggestions);
+ }
+
+ public function searchEditorAction()
+ {
+ $editor = $this->createSearchEditor(NotificationHistory::on($this->getDb()), [
+ LimitControl::DEFAULT_LIMIT_PARAM,
+ SortControl::DEFAULT_SORT_PARAM,
+ ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM
+ ]);
+
+ $this->getDocument()->add($editor);
+ $this->setTitle(t('Adjust Filter'));
+ }
+}
diff --git a/application/controllers/ServiceController.php b/application/controllers/ServiceController.php
new file mode 100644
index 0000000..2933d93
--- /dev/null
+++ b/application/controllers/ServiceController.php
@@ -0,0 +1,239 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Icingadb\Command\Object\GetObjectCommand;
+use Icinga\Module\Icingadb\Command\Transport\CommandTransport;
+use Icinga\Module\Icingadb\Common\CommandActions;
+use Icinga\Module\Icingadb\Common\Links;
+use Icinga\Module\Icingadb\Common\ServiceLinks;
+use Icinga\Module\Icingadb\Hook\TabHook\HookActions;
+use Icinga\Module\Icingadb\Model\History;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Icingadb\Redis\VolatileStateResults;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\Detail\QuickActions;
+use Icinga\Module\Icingadb\Widget\Detail\ServiceDetail;
+use Icinga\Module\Icingadb\Widget\Detail\ServiceInspectionDetail;
+use Icinga\Module\Icingadb\Widget\Detail\ServiceMetaInfo;
+use Icinga\Module\Icingadb\Widget\ItemList\HistoryList;
+use Icinga\Module\Icingadb\Widget\ItemList\ServiceList;
+use ipl\Stdlib\Filter;
+use ipl\Web\Url;
+
+class ServiceController extends Controller
+{
+ use CommandActions;
+ use HookActions;
+
+ /** @var Service The service object */
+ protected $service;
+
+ public function init()
+ {
+ $name = $this->params->getRequired('name');
+ $hostName = $this->params->getRequired('host.name');
+
+ $query = Service::on($this->getDb())->with([
+ 'state',
+ 'icon_image',
+ 'host',
+ 'host.state'
+ ]);
+ $query
+ ->setResultSetClass(VolatileStateResults::class)
+ ->filter(Filter::all(
+ Filter::equal('service.name', $name),
+ Filter::equal('host.name', $hostName)
+ ));
+
+ $this->applyRestrictions($query);
+
+ /** @var Service $service */
+ $service = $query->first();
+ if ($service === null) {
+ throw new NotFoundError(t('Service not found'));
+ }
+
+ $this->service = $service;
+ $this->loadTabsForObject($service);
+
+ $this->setTitleTab($this->getRequest()->getActionName());
+ $this->setTitle(
+ t('%s on %s', '<service> on <host>'),
+ $service->display_name,
+ $service->host->display_name
+ );
+ }
+
+ public function indexAction()
+ {
+ if ($this->service->state->is_overdue) {
+ $this->controls->addAttributes(['class' => 'overdue']);
+ }
+
+ $this->addControl((new ServiceList([$this->service]))
+ ->setViewMode('objectHeader')
+ ->setDetailActionsDisabled()
+ ->setNoSubjectLink());
+ $this->addControl(new ServiceMetaInfo($this->service));
+ $this->addControl(new QuickActions($this->service));
+
+ $this->addContent(new ServiceDetail($this->service));
+
+ $this->setAutorefreshInterval(10);
+ }
+
+ public function sourceAction()
+ {
+ $this->assertPermission('icingadb/object/show-source');
+ $apiResult = (new CommandTransport())->send((new GetObjectCommand())->setObject($this->service));
+
+ if ($this->service->state->is_overdue) {
+ $this->controls->addAttributes(['class' => 'overdue']);
+ }
+
+ $this->addControl((new ServiceList([$this->service]))
+ ->setViewMode('objectHeader')
+ ->setDetailActionsDisabled()
+ ->setNoSubjectLink());
+ $this->addContent(new ServiceInspectionDetail(
+ $this->service,
+ reset($apiResult)
+ ));
+ }
+
+ public function historyAction()
+ {
+ $compact = $this->view->compact; // TODO: Find a less-legacy way..
+
+ if ($this->service->state->is_overdue) {
+ $this->controls->addAttributes(['class' => 'overdue']);
+ }
+
+ $db = $this->getDb();
+
+ $history = History::on($db)->with([
+ 'host',
+ 'host.state',
+ 'service',
+ 'service.state',
+ 'comment',
+ 'downtime',
+ 'flapping',
+ 'notification',
+ 'acknowledgement',
+ 'state'
+ ]);
+ $history->filter(Filter::all(
+ Filter::equal('history.host_id', $this->service->host_id),
+ Filter::equal('history.service_id', $this->service->id)
+ ));
+
+ $before = $this->params->shift('before', time());
+ $url = Url::fromRequest()->setParams(clone $this->params);
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($history);
+ $sortControl = $this->createSortControl(
+ $history,
+ [
+ 'history.event_time desc' => t('Event Time')
+ ]
+ );
+ $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl, true);
+
+ $history->peekAhead();
+
+ $page = $paginationControl->getCurrentPageNumber();
+
+ if ($page > 1 && ! $compact) {
+ $history->limit($page * $limitControl->getLimit());
+ }
+
+ $history->filter(Filter::lessThanOrEqual('event_time', $before));
+
+ yield $this->export($history);
+
+ $this->addControl((new ServiceList([$this->service]))
+ ->setViewMode('objectHeader')
+ ->setDetailActionsDisabled()
+ ->setNoSubjectLink());
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($viewModeSwitcher);
+
+ $historyList = (new HistoryList($history->execute()))
+ ->setViewMode($viewModeSwitcher->getViewMode())
+ ->setPageSize($limitControl->getLimit())
+ ->setLoadMoreUrl($url->setParam('before', $before));
+
+ if ($compact) {
+ $historyList->setPageNumber($page);
+ }
+
+ if ($compact && $page > 1) {
+ $this->document->addFrom($historyList);
+ } else {
+ $this->addContent($historyList);
+ }
+ }
+
+ protected function createTabs()
+ {
+ $tabs = $this->getTabs()
+ ->add('index', [
+ 'label' => t('Service'),
+ 'url' => Links::service($this->service, $this->service->host)
+ ])
+ ->add('history', [
+ 'label' => t('History'),
+ 'url' => ServiceLinks::history($this->service, $this->service->host)
+ ]);
+
+ if ($this->hasPermission('icingadb/object/show-source')) {
+ $tabs->add('source', [
+ 'label' => t('Source'),
+ 'url' => Links::serviceSource($this->service, $this->service->host)
+ ]);
+ }
+
+ foreach ($this->loadAdditionalTabs() as $name => $tab) {
+ $tabs->add($name, $tab + ['urlParams' => [
+ 'name' => $this->service->name,
+ 'host.name' => $this->service->host->name
+ ]]);
+ }
+
+ return $tabs;
+ }
+
+ protected function setTitleTab(string $name)
+ {
+ $tab = $this->createTabs()->get($name);
+
+ if ($tab !== null) {
+ $tab->setActive();
+
+ $this->setTitle($tab->getLabel());
+ }
+ }
+
+ protected function fetchCommandTargets(): array
+ {
+ return [$this->service];
+ }
+
+ protected function getCommandTargetsUrl(): Url
+ {
+ return Links::service($this->service, $this->service->host);
+ }
+
+ protected function getDefaultTabControls(): array
+ {
+ return [(new ServiceList([$this->service]))->setDetailActionsDisabled()->setNoSubjectLink()];
+ }
+}
diff --git a/application/controllers/ServicegroupController.php b/application/controllers/ServicegroupController.php
new file mode 100644
index 0000000..89fb829
--- /dev/null
+++ b/application/controllers/ServicegroupController.php
@@ -0,0 +1,95 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Icingadb\Model\ServicegroupSummary;
+use Icinga\Module\Icingadb\Redis\VolatileStateResults;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\ItemList\ServiceList;
+use Icinga\Module\Icingadb\Widget\ItemList\ServicegroupList;
+use ipl\Html\Html;
+use ipl\Stdlib\Filter;
+
+class ServicegroupController extends Controller
+{
+ /** @var ServicegroupSummary The service group object */
+ protected $servicegroup;
+
+ public function init()
+ {
+ $this->assertRouteAccess('servicegroups');
+
+ $this->addTitleTab(t('Service Group'));
+
+ $name = $this->params->getRequired('name');
+
+ $query = ServicegroupSummary::on($this->getDb());
+
+ foreach ($query->getUnions() as $unionPart) {
+ $unionPart->filter(Filter::equal('servicegroup.name', $name));
+ }
+
+ $this->applyRestrictions($query);
+
+ $servicegroup = $query->first();
+ if ($servicegroup === null) {
+ throw new NotFoundError(t('Service group not found'));
+ }
+
+ $this->servicegroup = $servicegroup;
+ $this->setTitle($servicegroup->display_name);
+ }
+
+ public function indexAction()
+ {
+ $db = $this->getDb();
+
+ $services = Service::on($db)->with([
+ 'state',
+ 'state.last_comment',
+ 'icon_image',
+ 'host',
+ 'host.state'
+ ]);
+ $services
+ ->setResultSetClass(VolatileStateResults::class)
+ ->filter(Filter::equal('servicegroup.id', $this->servicegroup->id));
+
+ $this->applyRestrictions($services);
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($services);
+ $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl);
+
+ $serviceList = (new ServiceList($services->execute()))
+ ->setViewMode($viewModeSwitcher->getViewMode());
+
+ yield $this->export($services);
+
+ // ICINGAWEB_EXPORT_FORMAT is not set yet and $this->format is inaccessible, yeah...
+ if ($this->getRequest()->getParam('format') === 'pdf') {
+ $this->addContent((new ServicegroupList([$this->servicegroup]))
+ ->setViewMode('minimal')
+ ->setDetailActionsDisabled()
+ ->setNoSubjectLink());
+ $this->addContent(Html::tag('h2', null, t('Services')));
+ } else {
+ $this->addControl((new ServicegroupList([$this->servicegroup]))
+ ->setViewMode('minimal')
+ ->setDetailActionsDisabled()
+ ->setNoSubjectLink());
+ }
+
+ $this->addControl($paginationControl);
+ $this->addControl($viewModeSwitcher);
+ $this->addControl($limitControl);
+
+ $this->addContent($serviceList);
+
+ $this->setAutorefreshInterval(10);
+ }
+}
diff --git a/application/controllers/ServicegroupsController.php b/application/controllers/ServicegroupsController.php
new file mode 100644
index 0000000..76c48fb
--- /dev/null
+++ b/application/controllers/ServicegroupsController.php
@@ -0,0 +1,120 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Module\Icingadb\Model\Servicegroup;
+use Icinga\Module\Icingadb\Model\ServicegroupSummary;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\ItemList\ServicegroupList;
+use Icinga\Module\Icingadb\Widget\ShowMore;
+use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
+use ipl\Web\Control\LimitControl;
+use ipl\Web\Control\SortControl;
+use ipl\Web\Url;
+
+class ServicegroupsController extends Controller
+{
+ public function init()
+ {
+ parent::init();
+
+ $this->assertRouteAccess();
+ }
+
+ public function indexAction()
+ {
+ $this->addTitleTab(t('Service Groups'));
+ $compact = $this->view->compact;
+
+ $db = $this->getDb();
+
+ $servicegroups = ServicegroupSummary::on($db);
+
+ $this->handleSearchRequest($servicegroups);
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($servicegroups);
+ $sortControl = $this->createSortControl(
+ $servicegroups,
+ [
+ 'display_name' => t('Name'),
+ 'services_severity desc' => t('Severity'),
+ 'services_total desc' => t('Total Services')
+ ]
+ );
+ $searchBar = $this->createSearchBar($servicegroups, [
+ $limitControl->getLimitParam(),
+ $sortControl->getSortParam()
+ ]);
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ $this->filter($servicegroups, $filter);
+
+ $servicegroups->peekAhead($compact);
+
+ yield $this->export($servicegroups);
+
+ $this->addControl($paginationControl);
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($searchBar);
+
+ $results = $servicegroups->execute();
+
+ $this->addContent(
+ (new ServicegroupList($results))->setBaseFilter($filter)
+ );
+
+ if ($compact) {
+ $this->addContent(
+ (new ShowMore($results, Url::fromRequest()->without(['showCompact', 'limit', 'view'])))
+ ->setBaseTarget('_next')
+ ->setAttribute('title', sprintf(
+ t('Show all %d servicegroups'),
+ $servicegroups->count()
+ ))
+ );
+ }
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate();
+ }
+
+ $this->setAutorefreshInterval(30);
+ }
+
+ public function completeAction()
+ {
+ $suggestions = new ObjectSuggestions();
+ $suggestions->setModel(Servicegroup::class);
+ $suggestions->forRequest(ServerRequest::fromGlobals());
+ $this->getDocument()->add($suggestions);
+ }
+
+ public function searchEditorAction()
+ {
+ $editor = $this->createSearchEditor(ServicegroupSummary::on($this->getDb()), [
+ LimitControl::DEFAULT_LIMIT_PARAM,
+ SortControl::DEFAULT_SORT_PARAM,
+ ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM
+ ]);
+
+ $this->getDocument()->add($editor);
+ $this->setTitle(t('Adjust Filter'));
+ }
+}
diff --git a/application/controllers/ServicesController.php b/application/controllers/ServicesController.php
new file mode 100644
index 0000000..9ee9474
--- /dev/null
+++ b/application/controllers/ServicesController.php
@@ -0,0 +1,438 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Module\Icingadb\Common\CommandActions;
+use Icinga\Module\Icingadb\Common\Links;
+use Icinga\Module\Icingadb\Data\PivotTable;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Icingadb\Model\ServicestateSummary;
+use Icinga\Module\Icingadb\Redis\VolatileStateResults;
+use Icinga\Module\Icingadb\Util\FeatureStatus;
+use Icinga\Module\Icingadb\Web\Control\ProblemToggle;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\Detail\MultiselectQuickActions;
+use Icinga\Module\Icingadb\Widget\Detail\ObjectsDetail;
+use Icinga\Module\Icingadb\Widget\ItemList\ServiceList;
+use Icinga\Module\Icingadb\Widget\ItemTable\ServiceItemTable;
+use Icinga\Module\Icingadb\Widget\ServiceStatusBar;
+use Icinga\Module\Icingadb\Widget\ShowMore;
+use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
+use ipl\Html\HtmlString;
+use ipl\Orm\Query;
+use ipl\Stdlib\Filter;
+use ipl\Web\Control\LimitControl;
+use ipl\Web\Control\SortControl;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Url;
+
+class ServicesController extends Controller
+{
+ use CommandActions;
+
+ public function indexAction()
+ {
+ $this->addTitleTab(t('Services'));
+ $compact = $this->view->compact;
+
+ $db = $this->getDb();
+
+ $services = Service::on($db)->with([
+ 'state',
+ 'state.last_comment',
+ 'host',
+ 'host.state',
+ 'icon_image'
+ ]);
+ $services->getWith()['service.state']->setJoinType('INNER');
+ $services->setResultSetClass(VolatileStateResults::class);
+
+ $this->handleSearchRequest($services);
+
+ $summary = null;
+ if (! $compact) {
+ $summary = ServicestateSummary::on($db);
+ }
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($services);
+ $sortControl = $this->createSortControl(
+ $services,
+ [
+ 'service.display_name' => t('Name'),
+ 'service.state.severity desc' => t('Severity'),
+ 'service.state.soft_state' => t('Current State'),
+ 'service.state.last_state_change desc' => t('Last State Change'),
+ 'host.display_name' => t('Host')
+ ]
+ );
+ $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl);
+ $columns = $this->createColumnControl($services, $viewModeSwitcher);
+
+ $searchBar = $this->createSearchBar($services, [
+ $limitControl->getLimitParam(),
+ $sortControl->getSortParam(),
+ $viewModeSwitcher->getViewModeParam(),
+ 'columns'
+ ]);
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ $services->peekAhead($compact);
+
+ $this->filter($services, $filter);
+ if (! $compact) {
+ $this->filter($summary, $filter);
+ yield $this->export($services, $summary);
+ } else {
+ yield $this->export($services);
+ }
+
+ $this->addControl($paginationControl);
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($viewModeSwitcher);
+ $this->addControl($searchBar);
+ $continueWith = $this->createContinueWith(Links::servicesDetails(), $searchBar);
+
+ $results = $services->execute();
+
+ if ($viewModeSwitcher->getViewMode() === 'tabular') {
+ $serviceList = (new ServiceItemTable($results, ServiceItemTable::applyColumnMetaData($services, $columns)))
+ ->setSort($sortControl->getSort());
+ } else {
+ $serviceList = (new ServiceList($results))
+ ->setViewMode($viewModeSwitcher->getViewMode());
+ }
+
+ $this->addContent($serviceList);
+
+ if ($compact) {
+ $this->addContent(
+ (new ShowMore($results, Url::fromRequest()->without(['showCompact', 'limit', 'view'])))
+ ->setBaseTarget('_next')
+ ->setAttribute('title', sprintf(
+ t('Show all %d services'),
+ $services->count()
+ ))
+ );
+ } else {
+ $this->addFooter((new ServiceStatusBar($summary->first()))->setBaseFilter($filter));
+ }
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate($continueWith);
+ }
+
+ $this->setAutorefreshInterval(10);
+ }
+
+ public function detailsAction()
+ {
+ $this->addTitleTab(t('Services'));
+
+ $db = $this->getDb();
+
+ $services = Service::on($db)->with([
+ 'state',
+ 'icon_image',
+ 'host',
+ 'host.state'
+ ]);
+ $services->setResultSetClass(VolatileStateResults::class);
+ $summary = ServicestateSummary::on($db)->with(['state']);
+
+ $this->filter($services);
+ $this->filter($summary);
+
+ $services->limit(3);
+ $services->peekAhead();
+
+ yield $this->export($services, $summary);
+
+ $results = $services->execute();
+ $summary = $summary->first();
+
+ $downtimes = Service::on($db)->with(['downtime']);
+ $downtimes->getWith()['service.downtime']->setJoinType('INNER');
+ $this->filter($downtimes);
+ $summary->downtimes_total = $downtimes->count();
+
+ $comments = Service::on($db)->with(['comment']);
+ $comments->getWith()['service.comment']->setJoinType('INNER');
+ // TODO: This should be automatically done by the model/resolver and added as ON condition
+ $comments->filter(Filter::equal('comment.object_type', 'service'));
+ $this->filter($comments);
+ $summary->comments_total = $comments->count();
+
+ $this->addControl(
+ (new ServiceList($results))
+ ->setViewMode('minimal')
+ );
+ $this->addControl(new ShowMore(
+ $results,
+ Links::services()->setQueryString(QueryString::render($this->getFilter())),
+ sprintf(t('Show all %d services'), $services->count())
+ ));
+ $this->addControl(
+ (new MultiselectQuickActions('service', $summary))
+ ->setBaseFilter($this->getFilter())
+ );
+
+ $this->addContent(
+ (new ObjectsDetail('service', $summary, $services))
+ ->setBaseFilter($this->getFilter())
+ );
+ }
+
+ 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()), [
+ LimitControl::DEFAULT_LIMIT_PARAM,
+ SortControl::DEFAULT_SORT_PARAM,
+ ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM,
+ 'columns'
+ ]);
+
+ $this->getDocument()->add($editor);
+ $this->setTitle(t('Adjust Filter'));
+ }
+
+ public function gridAction()
+ {
+ $db = $this->getDb();
+ $this->addTitleTab(t('Service Grid'));
+
+ $query = Service::on($db)->with([
+ 'state',
+ 'host',
+ 'host.state'
+ ]);
+ $query->setResultSetClass(VolatileStateResults::class);
+
+ $this->handleSearchRequest($query);
+
+ $this->params->shift('page'); // Handled by PivotTable internally
+ $this->params->shift('limit'); // Handled by PivotTable internally
+ $flipped = $this->params->shift('flipped', false);
+
+ $problemToggle = $this->createProblemToggle();
+ $sortControl = $this->createSortControl($query, [
+ 'service.display_name' => t('Service Name'),
+ 'host.display_name' => t('Host Name'),
+ ])->setDefault('service.display_name');
+ $searchBar = $this->createSearchBar($query, [
+ LimitControl::DEFAULT_LIMIT_PARAM,
+ $sortControl->getSortParam(),
+ 'flipped',
+ 'page',
+ 'problems'
+ ]);
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ $this->filter($query, $filter);
+
+ $this->addControl($problemToggle);
+ $this->addControl($sortControl);
+ $this->addControl($searchBar);
+ $continueWith = $this->createContinueWith(Links::servicesDetails(), $searchBar);
+
+ $pivotFilter = $problemToggle->isChecked() ?
+ Filter::equal('service.state.is_problem', 'y') : null;
+
+ $columns = [
+ 'id',
+ 'host.id',
+ 'host_name' => 'host.name',
+ 'host_display_name' => 'host.display_name',
+ 'name' => 'service.name',
+ 'display_name' => 'service.display_name',
+ 'service.state.is_handled',
+ 'service.state.output',
+ 'service.state.soft_state'
+ ];
+
+ if ($flipped) {
+ $pivot = (new PivotTable($query, 'host_name', 'name', $columns))
+ ->setXAxisFilter($pivotFilter)
+ ->setYAxisFilter($pivotFilter ? clone $pivotFilter : null)
+ ->setXAxisHeader('host_display_name')
+ ->setYAxisHeader('display_name');
+ } else {
+ $pivot = (new PivotTable($query, 'name', 'host_name', $columns))
+ ->setXAxisFilter($pivotFilter)
+ ->setYAxisFilter($pivotFilter ? clone $pivotFilter : null)
+ ->setXAxisHeader('display_name')
+ ->setYAxisHeader('host_display_name');
+ }
+
+
+ $this->view->horizontalPaginator = $pivot->paginateXAxis();
+ $this->view->verticalPaginator = $pivot->paginateYAxis();
+ list($pivotData, $pivotHeader) = $pivot->toArray();
+ $this->view->pivotData = $pivotData;
+ $this->view->pivotHeader = $pivotHeader;
+
+ /** Preserve filter and params in view links (the `BaseFilter` implementation for view scripts -.-) */
+ $this->view->baseUrl = $this->getRequest()->getUrl()
+ ->onlyWith([
+ LimitControl::DEFAULT_LIMIT_PARAM,
+ $sortControl->getSortParam(),
+ 'flipped',
+ 'page',
+ 'problems'
+ ]);
+ $preservedParams = $this->view->baseUrl->getParams();
+ $this->view->baseUrl->setQueryString(QueryString::render($filter));
+ foreach ($preservedParams->toArray(false) as $name => $value) {
+ if (is_int($name)) {
+ $name = $value;
+ $value = true;
+ }
+
+ $this->view->baseUrl->getParams()->addEncoded($name, $value);
+ }
+
+ $searchBar->setEditorUrl(Url::fromPath(
+ "icingadb/services/grid-search-editor"
+ )->setParams($preservedParams));
+
+ $this->view->controls = $this->controls;
+
+ if ($flipped) {
+ $this->getHelper('viewRenderer')->setScriptAction('grid-flipped');
+ }
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ // TODO: Everything up to addContent() (inclusive) can be removed once the grid is a widget
+ $this->view->controls = ''; // Relevant controls are transmitted separately
+ $viewRenderer = $this->getHelper('viewRenderer');
+ $viewRenderer->postDispatch();
+ $viewRenderer->setNoRender(false);
+
+ $content = trim($this->getResponse());
+ $this->getResponse()->clearBody($viewRenderer->getResponseSegment());
+
+ $this->addContent(HtmlString::create(substr($content, strpos($content, '>') + 1, -6)));
+
+ $this->sendMultipartUpdate($continueWith);
+ }
+
+ $this->setAutorefreshInterval(30);
+ }
+
+ public function gridSearchEditorAction()
+ {
+ $editor = $this->createSearchEditor(
+ Service::on($this->getDb()),
+ Url::fromPath('icingadb/services/grid'),
+ [
+ LimitControl::DEFAULT_LIMIT_PARAM,
+ SortControl::DEFAULT_SORT_PARAM,
+ 'flipped',
+ 'page',
+ 'problems'
+ ]
+ );
+
+ $this->getDocument()->add($editor);
+ $this->setTitle(t('Adjust Filter'));
+ }
+
+ protected function fetchCommandTargets(): Query
+ {
+ $db = $this->getDb();
+
+ $services = Service::on($db)->with([
+ 'state',
+ 'host',
+ 'host.state'
+ ]);
+ $services->setResultSetClass(VolatileStateResults::class);
+
+ switch ($this->getRequest()->getActionName()) {
+ case 'acknowledge':
+ $services->filter(Filter::equal('state.is_problem', 'y'))
+ ->filter(Filter::equal('state.is_acknowledged', 'n'));
+
+ break;
+ }
+
+ $this->filter($services);
+
+ return $services;
+ }
+
+ protected function getCommandTargetsUrl(): Url
+ {
+ return Links::servicesDetails()->setQueryString(QueryString::render($this->getFilter()));
+ }
+
+ protected function getFeatureStatus()
+ {
+ $summary = ServicestateSummary::on($this->getDb());
+ $this->filter($summary);
+
+ return new FeatureStatus('service', $summary->first());
+ }
+
+ protected function prepareSearchFilter(Query $query, string $search, Filter\Any $filter)
+ {
+ if ($this->params->shift('_hostFilterOnly', false)) {
+ $filter->add(Filter::like('host.name_ci', "*$search*"));
+ } else {
+ parent::prepareSearchFilter($query, $search, $filter);
+ }
+ }
+
+ public function createProblemToggle(): ProblemToggle
+ {
+ $filter = $this->params->shift('problems');
+
+ $problemToggle = new ProblemToggle($filter);
+ $problemToggle->setIdProtector([$this->getRequest(), 'protectId']);
+
+ $problemToggle->on(ProblemToggle::ON_SUCCESS, function (ProblemToggle $form) {
+ if (! $form->getElement('problems')->isChecked()) {
+ $this->redirectNow(Url::fromRequest()->remove('problems'));
+ } else {
+ $this->redirectNow(Url::fromRequest()->setParams($this->params->add('problems')));
+ }
+ })->handleRequest(ServerRequest::fromGlobals());
+
+ return $problemToggle;
+ }
+}
diff --git a/application/controllers/TacticalController.php b/application/controllers/TacticalController.php
new file mode 100644
index 0000000..582b046
--- /dev/null
+++ b/application/controllers/TacticalController.php
@@ -0,0 +1,96 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Module\Icingadb\Model\HoststateSummary;
+use Icinga\Module\Icingadb\Model\ServicestateSummary;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\HostSummaryDonut;
+use Icinga\Module\Icingadb\Widget\ServiceSummaryDonut;
+use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
+use ipl\Orm\Query;
+use ipl\Stdlib\Filter;
+use ipl\Web\Control\LimitControl;
+use ipl\Web\Control\SortControl;
+
+class TacticalController extends Controller
+{
+ public function indexAction()
+ {
+ $this->addTitleTab(t('Tactical Overview'));
+
+ $db = $this->getDb();
+
+ $hoststateSummary = HoststateSummary::on($db);
+ $servicestateSummary = ServicestateSummary::on($db);
+
+ $this->handleSearchRequest($servicestateSummary);
+
+ $searchBar = $this->createSearchBar($servicestateSummary);
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ $this->filter($hoststateSummary, $filter);
+ $this->filter($servicestateSummary, $filter);
+
+ yield $this->export($hoststateSummary, $servicestateSummary);
+
+ $this->addControl($searchBar);
+
+ $this->addContent(
+ (new HostSummaryDonut($hoststateSummary->first()))
+ ->setBaseFilter($filter)
+ );
+
+ $this->addContent(
+ (new ServiceSummaryDonut($servicestateSummary->first()))
+ ->setBaseFilter($filter)
+ );
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate();
+ }
+
+ $this->setAutorefreshInterval(10);
+ }
+
+ public function completeAction()
+ {
+ $suggestions = new ObjectSuggestions();
+ $suggestions->setModel(ServicestateSummary::class);
+ $suggestions->forRequest(ServerRequest::fromGlobals());
+ $this->getDocument()->add($suggestions);
+ }
+
+ public function searchEditorAction()
+ {
+ $editor = $this->createSearchEditor(ServicestateSummary::on($this->getDb()), [
+ LimitControl::DEFAULT_LIMIT_PARAM,
+ SortControl::DEFAULT_SORT_PARAM,
+ ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM
+ ]);
+
+ $this->getDocument()->add($editor);
+ $this->setTitle(t('Adjust Filter'));
+ }
+
+ protected function prepareSearchFilter(Query $query, string $search, Filter\Any $filter)
+ {
+ parent::prepareSearchFilter($query, $search, $filter);
+
+ $filter->add(Filter::like('host.name_ci', "*$search*"));
+ }
+}
diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php
new file mode 100644
index 0000000..751bb38
--- /dev/null
+++ b/application/controllers/UserController.php
@@ -0,0 +1,48 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Icingadb\Model\User;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\Detail\UserDetail;
+use Icinga\Module\Icingadb\Widget\ItemList\UserList;
+use ipl\Stdlib\Filter;
+
+class UserController extends Controller
+{
+ /** @var User The user object */
+ protected $user;
+
+ public function init()
+ {
+ $this->assertRouteAccess('users');
+
+ $this->addTitleTab(t('User'));
+
+ $name = $this->params->getRequired('name');
+
+ $query = User::on($this->getDb());
+ $query->filter(Filter::equal('user.name', $name));
+
+ $this->applyRestrictions($query);
+
+ $user = $query->first();
+ if ($user === null) {
+ throw new NotFoundError(t('User not found'));
+ }
+
+ $this->user = $user;
+ $this->setTitle($user->display_name);
+ }
+
+ public function indexAction()
+ {
+ $this->addControl((new UserList([$this->user]))->setNoSubjectLink()->setDetailActionsDisabled());
+ $this->addContent(new UserDetail($this->user));
+
+ $this->setAutorefreshInterval(10);
+ }
+}
diff --git a/application/controllers/UsergroupController.php b/application/controllers/UsergroupController.php
new file mode 100644
index 0000000..e1d4ae2
--- /dev/null
+++ b/application/controllers/UsergroupController.php
@@ -0,0 +1,48 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Icingadb\Model\Usergroup;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\Detail\UsergroupDetail;
+use Icinga\Module\Icingadb\Widget\ItemList\UsergroupList;
+use ipl\Stdlib\Filter;
+
+class UsergroupController extends Controller
+{
+ /** @var Usergroup The usergroup object */
+ protected $usergroup;
+
+ public function init()
+ {
+ $this->assertRouteAccess('usergroups');
+
+ $this->addTitleTab(t('User Group'));
+
+ $name = $this->params->getRequired('name');
+
+ $query = Usergroup::on($this->getDb());
+ $query->filter(Filter::equal('usergroup.name', $name));
+
+ $this->applyRestrictions($query);
+
+ $usergroup = $query->first();
+ if ($usergroup === null) {
+ throw new NotFoundError(t('User group not found'));
+ }
+
+ $this->usergroup = $usergroup;
+ $this->setTitle($usergroup->display_name);
+ }
+
+ public function indexAction()
+ {
+ $this->addControl((new UsergroupList([$this->usergroup]))->setNoSubjectLink()->setDetailActionsDisabled());
+ $this->addContent(new UsergroupDetail($this->usergroup));
+
+ $this->setAutorefreshInterval(10);
+ }
+}
diff --git a/application/controllers/UsergroupsController.php b/application/controllers/UsergroupsController.php
new file mode 100644
index 0000000..3f0ea11
--- /dev/null
+++ b/application/controllers/UsergroupsController.php
@@ -0,0 +1,97 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Module\Icingadb\Model\Usergroup;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\ItemList\UsergroupList;
+use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
+use ipl\Web\Control\LimitControl;
+use ipl\Web\Control\SortControl;
+
+class UsergroupsController extends Controller
+{
+ public function init()
+ {
+ parent::init();
+
+ $this->assertRouteAccess();
+ }
+
+ public function indexAction()
+ {
+ $this->addTitleTab(t('User Groups'));
+
+ $db = $this->getDb();
+
+ $usergroups = Usergroup::on($db);
+
+ $this->handleSearchRequest($usergroups);
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($usergroups);
+ $sortControl = $this->createSortControl(
+ $usergroups,
+ [
+ 'usergroup.display_name' => t('Name')
+ ]
+ );
+ $searchBar = $this->createSearchBar($usergroups, [
+ $limitControl->getLimitParam(),
+ $sortControl->getSortParam()
+ ]);
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ $this->filter($usergroups, $filter);
+
+ yield $this->export($usergroups);
+
+ $this->addControl($paginationControl);
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($searchBar);
+
+ $this->addContent(new UsergroupList($usergroups));
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate();
+ }
+
+ $this->setAutorefreshInterval(10);
+ }
+
+ public function completeAction()
+ {
+ $suggestions = new ObjectSuggestions();
+ $suggestions->setModel(Usergroup::class);
+ $suggestions->forRequest(ServerRequest::fromGlobals());
+ $this->getDocument()->add($suggestions);
+ }
+
+ public function searchEditorAction()
+ {
+ $editor = $this->createSearchEditor(Usergroup::on($this->getDb()), [
+ LimitControl::DEFAULT_LIMIT_PARAM,
+ SortControl::DEFAULT_SORT_PARAM,
+ ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM
+ ]);
+
+ $this->getDocument()->add($editor);
+ $this->setTitle(t('Adjust Filter'));
+ }
+}
diff --git a/application/controllers/UsersController.php b/application/controllers/UsersController.php
new file mode 100644
index 0000000..ef75e89
--- /dev/null
+++ b/application/controllers/UsersController.php
@@ -0,0 +1,99 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Module\Icingadb\Model\User;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+use Icinga\Module\Icingadb\Web\Controller;
+use Icinga\Module\Icingadb\Widget\ItemList\UserList;
+use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
+use ipl\Web\Control\LimitControl;
+use ipl\Web\Control\SortControl;
+
+class UsersController extends Controller
+{
+ public function init()
+ {
+ parent::init();
+
+ $this->assertRouteAccess();
+ }
+
+ public function indexAction()
+ {
+ $this->addTitleTab(t('Users'));
+
+ $db = $this->getDb();
+
+ $users = User::on($db);
+
+ $this->handleSearchRequest($users);
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($users);
+ $sortControl = $this->createSortControl(
+ $users,
+ [
+ 'user.display_name' => t('Name'),
+ 'user.email' => t('Email'),
+ 'user.pager' => t('Pager Address / Number')
+ ]
+ );
+ $searchBar = $this->createSearchBar($users, [
+ $limitControl->getLimitParam(),
+ $sortControl->getSortParam()
+ ]);
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ $this->filter($users, $filter);
+
+ yield $this->export($users);
+
+ $this->addControl($paginationControl);
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($searchBar);
+
+ $this->addContent(new UserList($users));
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate();
+ }
+
+ $this->setAutorefreshInterval(10);
+ }
+
+ public function completeAction()
+ {
+ $suggestions = new ObjectSuggestions();
+ $suggestions->setModel(User::class);
+ $suggestions->forRequest(ServerRequest::fromGlobals());
+ $this->getDocument()->add($suggestions);
+ }
+
+ public function searchEditorAction()
+ {
+ $editor = $this->createSearchEditor(User::on($this->getDb()), [
+ LimitControl::DEFAULT_LIMIT_PARAM,
+ SortControl::DEFAULT_SORT_PARAM,
+ ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM
+ ]);
+
+ $this->getDocument()->add($editor);
+ $this->setTitle(t('Adjust Filter'));
+ }
+}